Freigeben über


Asynchrone Programmierszenarios

Wenn Ihr Code E/A-gebundene Szenarien implementiert, um Netzwerkdatenanforderungen, Datenbankzugriff oder Dateisystemlese-/Schreibvorgänge zu unterstützen, ist die asynchrone Programmierung der beste Ansatz. Sie können auch asynchronen Code für CPU-gebundene Szenarien wie teure Berechnungen schreiben.

C# verfügt über ein asynchrones Programmiermodell auf Sprachebene, mit dem Sie auf einfache Weise asynchronen Code schreiben können, ohne rückrufe zu müssen oder einer Bibliothek entsprechen zu müssen, die Asynchronie unterstützt. Das Modell folgt dem sogenannten Aufgabenbasierten asynchronen Muster (TAP).The model follows what is known as the Task-based asynchron pattern (TAP).

Erkunden des asynchronen Programmiermodells

Die Task Objekte stellen Task<T> den Kern der asynchronen Programmierung dar. Diese Objekte werden verwendet, um asynchrone Vorgänge zu modellieren, indem sie die async Schlüsselwörter await unterstützen. In den meisten Fällen ist das Modell für I/O-gebundene und CPU-gebundene Szenarien relativ einfach. Innerhalb einer async Methode:

  • I/O-gebundener Code startet einen Vorgang, der durch ein Task Oder Task<T> Objekt innerhalb der async Methode dargestellt wird.
  • CPU-gebundener Code startet einen Vorgang in einem Hintergrundthread mit der Task.Run Methode.

In beiden Fällen stellt ein aktiver Task Vorgang einen asynchronen Vorgang dar, der möglicherweise nicht abgeschlossen ist.

Das Schlüsselwort await ist sozusagen der Zauberstab. Sie liefert die Steuerung für den Aufrufer der Methode, die den await Ausdruck enthält, und ermöglicht letztendlich, dass die Benutzeroberfläche reaktionsfähig ist oder ein Dienst elastisch ist. Obwohl es Möglichkeiten gibt , asynchronen Code zu nähern als die Verwendung der async Ausdrücke await und Ausdrücke, konzentriert sich dieser Artikel auf die Konstrukte auf Sprachebene.

Hinweis

Einige Beispiele in diesem Artikel verwenden die System.Net.Http.HttpClient Klasse zum Herunterladen von Daten aus einem Webdienst. Im Beispielcode ist das s_httpClient Objekt ein statisches Feld der Typklasse Program :

private static readonly HttpClient s_httpClient = new();

Weitere Informationen finden Sie im vollständigen Beispielcode am Ende dieses Artikels.

Überprüfen der zugrunde liegenden Konzepte

Wenn Sie die asynchrone Programmierung in Ihrem C#-Code implementieren, wandelt der Compiler Ihr Programm in einen Zustandsautomaten um. Mit diesem Konstrukt werden verschiedene Vorgänge und Der Zustand in Ihrem Code nachverfolgt, z. B. die Ausführung, wenn der Code einen await Ausdruck erreicht, und die Ausführung fortsetzen, wenn ein Hintergrundauftrag abgeschlossen ist.

In Bezug auf die Informatiktheorie ist die asynchrone Programmierung eine Implementierung des Zusagemodells von Asynchronie.

Im asynchronen Programmiermodell gibt es mehrere wichtige Konzepte, die Sie verstehen sollten:

  • Sie können asynchronen Code sowohl für I/O-gebundenen als auch für CPU-gebundenen Code verwenden, die Implementierung ist jedoch unterschiedlich.
  • Asynchroner Code verwendet Task<T> und Task Objekte als Konstrukte zum Modellieren der Ausführung im Hintergrund.
  • Das async Schlüsselwort deklariert eine Methode als asynchrone Methode, mit der Sie das await Schlüsselwort im Methodentext verwenden können.
  • Wenn Sie das await Schlüsselwort anwenden, hält der Code die aufrufende Methode an und gibt die Kontrolle zurück an den Aufrufer, bis die Aufgabe abgeschlossen ist.
  • Sie können den await Ausdruck nur in einer asynchronen Methode verwenden.

E/A-gebundenes Beispiel: Herunterladen von Daten aus dem Webdienst

Wenn der Benutzer in diesem Beispiel eine Schaltfläche auswählt, lädt die App Daten aus einem Webdienst herunter. Sie möchten den UI-Thread für die App während des Downloadvorgangs nicht blockieren. Der folgende Code führt diese Aufgabe aus:

s_downloadButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI as the request
    // from the web service is happening.
    //
    // The UI thread is now free to perform other work.
    var stringData = await s_httpClient.GetStringAsync(URL);
    DoSomethingWithData(stringData);
};

Der Code gibt die Absicht (Daten asynchron herunterladen) an, ohne durch Interaktion mit Task-Objekten vereitelt zu werden.

CPU-gebundenes Beispiel: Ausführen der Spielberechnung

Im nächsten Beispiel verursacht ein mobiles Spiel als Reaktion auf ein Schaltflächenereignis Schäden an mehreren Agents auf dem Bildschirm. Das Ausführen der Schadensberechnung kann teuer sein. Das Ausführen der Berechnung im UI-Thread kann während der Berechnung zu Anzeige- und UI-Interaktionsproblemen führen.

Die beste Möglichkeit zum Behandeln der Aufgabe besteht darin, einen Hintergrundthread zu starten, um die Arbeit mit der Task.Run Methode abzuschließen. Der Vorgang liefert mithilfe eines await Ausdrucks. Der Vorgang wird fortgesetzt, wenn die Aufgabe abgeschlossen ist. Mit diesem Ansatz kann die Benutzeroberfläche reibungslos ausgeführt werden, während die Arbeit im Hintergrund abgeschlossen ist.

static DamageResult CalculateDamageDone()
{
    return new DamageResult()
    {
        // Code omitted:
        //
        // Does an expensive calculation and returns
        // the result of that calculation.
    };
}

s_calculateButton.Clicked += async (o, e) =>
{
    // This line will yield control to the UI while CalculateDamageDone()
    // performs its work. The UI thread is free to perform other work.
    var damageResult = await Task.Run(() => CalculateDamageDone());
    DisplayDamage(damageResult);
};

Der Code drückt eindeutig die Absicht des Schaltflächenereignisses Clicked aus. Es ist nicht erforderlich, einen Hintergrundthread manuell zu verwalten, und die Aufgabe wird nicht blockiert.

Erkennen von CPU-gebundenen und E/A-gebundenen Szenarien

In den vorherigen Beispielen wird die Verwendung des Modifizierers und await des async Ausdrucks für I/O-gebundene und CPU-gebundene Arbeit veranschaulicht. Ein Beispiel für jedes Szenario zeigt, wie sich der Code je nach dem Gebundenen des Vorgangs unterscheidet. Um sich auf Ihre Implementierung vorzubereiten, müssen Sie verstehen, wie Sie ermitteln können, wann ein Vorgang I/O-gebunden oder CPU-gebunden ist. Ihre Implementierungsauswahl kann sich erheblich auf die Leistung Ihres Codes auswirken und möglicherweise zu fehlverwendenden Konstrukten führen.

Es gibt zwei hauptfragen, die Sie behandeln müssen, bevor Sie Code schreiben:

Frage Szenario Implementierung
Sollte der Code auf ein Ergebnis oder eine Aktion warten, z. B. Daten aus einer Datenbank? E/A-gebunden Verwenden Sie den async Modifizierer und await Ausdruck ohne die Task.Run Methode.

Vermeiden Sie die Verwendung der parallelen Aufgabenbibliothek.
Sollte der Code eine teure Berechnung ausführen? CPU-gebunden Verwenden Sie den async Modifizierer und await Ausdruck, aber lassen Sie die Arbeit an einem anderen Thread mit der Task.Run Methode aus. Bei diesem Ansatz geht es um Probleme mit der CPU-Reaktionsfähigkeit.

Wenn die Arbeit für Parallelität und Konkurrenz geeignet ist, erwägen Sie auch die Verwendung der Task Parallel Library.

Messen Sie immer die Ausführung des Codes. Möglicherweise stellen Sie fest, dass Ihre CPU-gebundene Arbeit nicht kostspielig genug ist, verglichen mit dem Aufwand von Kontextschaltern beim Multithreading. Jede Wahl hat Kompromisse. Wählen Sie den richtigen Kompromiss für Ihre Situation aus.

Erkunden weiterer Beispiele

Die Beispiele in diesem Abschnitt veranschaulichen verschiedene Möglichkeiten, asynchronen Code in C# zu schreiben. Sie decken einige Szenarien ab, die sie möglicherweise treffen.

Extrahieren von Daten aus einem Netzwerk

Der folgende Code lädt HTML aus einer bestimmten URL herunter und zählt, wie oft die Zeichenfolge ".NET" im HTML-Code auftritt. Der Code verwendet ASP.NET zum Definieren einer Web-API-Controllermethode, die die Aufgabe ausführt und die Anzahl zurückgibt.

Hinweis

Wenn Sie eine HTML-Analyse im Produktionscode durchführen möchten, nutzen Sie dafür nicht die regulären Ausdrücke. Verwenden Sie stattdessen eine Analysebibliothek.

[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCount(string URL)
{
    // Suspends GetDotNetCount() to allow the caller (the web server)
    // to accept another request, rather than blocking on this one.
    var html = await s_httpClient.GetStringAsync(URL);
    return Regex.Matches(html, @"\.NET").Count;
}

Sie können ähnlichen Code für eine universelle Windows-App schreiben und die Zählaufgabe ausführen, nachdem eine Schaltfläche gedrückt wurde:

private readonly HttpClient _httpClient = new HttpClient();

private async void OnSeeTheDotNetsButtonClick(object sender, RoutedEventArgs e)
{
    // Capture the task handle here so we can await the background task later.
    var getDotNetFoundationHtmlTask = _httpClient.GetStringAsync("https://dotnetfoundation.org");

    // Any other work on the UI thread can be done here, such as enabling a Progress Bar.
    // It's important to do the extra work here before the "await" call,
    // so the user sees the progress bar before execution of this method is yielded.
    NetworkProgressBar.IsEnabled = true;
    NetworkProgressBar.Visibility = Visibility.Visible;

    // The await operator suspends OnSeeTheDotNetsButtonClick(), returning control to its caller.
    // This action is what allows the app to be responsive and not block the UI thread.
    var html = await getDotNetFoundationHtmlTask;
    int count = Regex.Matches(html, @"\.NET").Count;

    DotNetCountLabel.Text = $"Number of .NETs on dotnetfoundation.org: {count}";

    NetworkProgressBar.IsEnabled = false;
    NetworkProgressBar.Visibility = Visibility.Collapsed;
}

Warten auf das Abschließen mehrerer Tasks

In einigen Szenarien muss der Code mehrere Datenabschnitte gleichzeitig abrufen. Die Task APIs stellen Methoden bereit, mit denen Sie asynchronen Code schreiben können, der eine nicht blockierte Wartezeit für mehrere Hintergrundaufträge ausführt:

Das folgende Beispiel zeigt, wie Sie Objektdaten für eine Gruppe von userId Objekten abrufen User können.

private static async Task<User> GetUserAsync(int userId)
{
    // Code omitted:
    //
    // Given a user Id {userId}, retrieves a User object corresponding
    // to the entry in the database with {userId} as its Id.

    return await Task.FromResult(new User() { id = userId });
}

private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
{
    var getUserTasks = new List<Task<User>>();
    foreach (int userId in userIds)
    {
        getUserTasks.Add(GetUserAsync(userId));
    }

    return await Task.WhenAll(getUserTasks);
}

Sie können diesen Code prägnant schreiben, indem Sie LINQ verwenden:

private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
{
    var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
    return await Task.WhenAll(getUserTasks);
}

Obwohl Sie weniger Code mit LINQ schreiben, achten Sie beim Mischen von LINQ mit asynchronen Code. LINQ verwendet verzögerte (oder faule) Ausführung. Asynchrone Aufrufe erfolgen nicht sofort wie in einer foreach Schleife, es sei denn, Sie erzwingen, dass die generierte Sequenz mit einem Aufruf der .ToList() Methode durchlaufen .ToArray() wird. In diesem Beispiel wird die Enumerable.ToArray Methode verwendet, um die Abfrage eifrig auszuführen und die Ergebnisse in einem Array zu speichern. Durch diesen Ansatz wird die Anweisung erzwungen, die id => GetUserAsync(id) Aufgabe auszuführen und zu initiieren.

Überlegungen zur asynchronen Programmierung

Bei der asynchronen Programmierung sind mehrere Details zu beachten, die unerwartetes Verhalten verhindern können.

Verwenden von await inside async() method body

Wenn Sie den async Modifizierer verwenden, sollten Sie einen oder await mehrere Ausdrücke in den Methodentext einschließen. Wenn der Compiler keinen Ausdruck findet await , führt die Methode nicht zum Ertrag. Obwohl der Compiler eine Warnung generiert, wird der Code weiterhin kompiliert, und der Compiler führt die Methode aus. Der zustandsautomat, der vom C#-Compiler für die asynchrone Methode generiert wird, kann nichts erreichen, sodass der gesamte Prozess sehr ineffizient ist.

Hinzufügen von "Async"-Suffix zu asynchronen Methodennamen

Die .NET-Formatkonvention besteht darin, das Suffix "Async" allen asynchronen Methodennamen hinzuzufügen. Dieser Ansatz hilft dabei, synchrone und asynchrone Methoden einfacher zu unterscheiden. Bestimmte Methoden, die nicht explizit von Ihrem Code aufgerufen werden (z. B. Ereignishandler oder Webcontrollermethoden), gelten in diesem Szenario nicht unbedingt. Da diese Elemente nicht explizit von Ihrem Code aufgerufen werden, ist die Verwendung expliziter Benennungen nicht so wichtig.

"async void" nur von Ereignishandlern zurückgeben

Ereignishandler müssen Rückgabetypen deklarieren void und können diese nicht wie andere Methoden verwenden oder zurückgebenTask.Task<T> Wenn Sie asynchrone Ereignishandler schreiben, müssen Sie den async Modifizierer für eine void rückgabende Methode für die Handler verwenden. Andere Implementierungen der async void zurückgegebenen Methoden folgen nicht dem TAP-Modell und können Herausforderungen darstellen:

  • Ausnahmen, die in einer async void Methode ausgelöst werden, können nicht außerhalb dieser Methode abgefangen werden.
  • async void Methoden sind schwierig zu testen
  • async void Methoden können negative Nebenwirkungen verursachen, wenn der Aufrufer nicht erwartet, dass sie asynchron sind.

Verwenden Sie Vorsicht bei asynchronen Lambdas in LINQ

Es ist wichtig, Vorsicht zu verwenden, wenn Sie asynchrone Lambdas in LINQ-Ausdrücken implementieren. Lambda-Ausdrücke in LINQ verwenden verzögerte Ausführung, was bedeutet, dass der Code zu einem unerwarteten Zeitpunkt ausgeführt werden kann. Die Einführung von Blockierungsaufgaben in dieses Szenario kann leicht zu einem Deadlock führen, wenn der Code nicht ordnungsgemäß geschrieben wurde. Darüber hinaus kann die Schachtelung von asynchronen Code auch die Ursache für die Ausführung des Codes erschweren. Async und LINQ sind leistungsfähig, aber diese Techniken sollten so sorgfältig und deutlich wie möglich verwendet werden.

Ertrag für Vorgänge auf nicht blockierende Weise

Wenn Ihr Programm das Ergebnis einer Aufgabe benötigt, schreiben Sie Code, der den await Ausdruck auf nicht blockierte Weise implementiert. Das Blockieren des aktuellen Threads als Mittel, synchron zu warten, bis ein Task Element abgeschlossen ist, kann zu Deadlocks und blockierten Kontextthreads führen. Dieser Programmieransatz kann eine komplexere Fehlerbehandlung erfordern. In der folgenden Tabelle finden Sie Anleitungen dazu, wie der Zugriff von Vorgängen auf nicht blockierte Weise erfolgt:

Aufgabenszenario Aktueller Code Ersetzen durch "await"
Abrufen des Ergebnisses einer Hintergrundaufgabe Task.Wait oder Task.Result await
Fortfahren, wenn eine Aufgabe abgeschlossen ist Task.WaitAny await Task.WhenAny
Fortfahren, wenn alle Aufgaben abgeschlossen sind Task.WaitAll await Task.WhenAll
Nach einiger Zeit fortfahren Thread.Sleep await Task.Delay

Erwägen Sie die Verwendung des ValueTask-Typs.

Wenn eine asynchrone Methode ein Task Objekt zurückgibt, können Leistungsengpässe in bestimmten Pfaden eingeführt werden. Da Task es sich um einen Verweistyp handelt, wird ein Task Objekt vom Heap zugewiesen. Wenn eine mit dem async Modifizierer deklarierte Methode ein zwischengespeichertes Ergebnis zurückgibt oder synchron abgeschlossen wird, können die zusätzlichen Zuordnungen erhebliche Zeitkosten in leistungskritischen Codeabschnitten anfallen. Dieses Szenario kann kostspielig werden, wenn die Zuordnungen in engen Schleifen auftreten. Weitere Informationen finden Sie unter Generalisierte asynchrone Rückgabetypen.

Verstehen, wann ConfigureAwait(false) festgelegt werden soll

Entwickler fragen häufig, wann der Task.ConfigureAwait(Boolean) boolesche Wert verwendet werden soll. Diese API ermöglicht es einer Task Instanz, den Kontext für den Zustandscomputer zu konfigurieren, der einen beliebigen await Ausdruck implementiert. Wenn der boolesche Wert nicht ordnungsgemäß festgelegt ist, kann die Leistung beeinträchtigt oder Deadlocks auftreten. Weitere Informationen finden Sie unter ConfigureAwait FAQ.

Schreiben von weniger zustandsbehafteten Code

Vermeiden Sie das Schreiben von Code, der vom Status globaler Objekte oder der Ausführung bestimmter Methoden abhängt. Seien Sie stattdessen nur abhängig von Rückgabewerten der Methoden. Es gibt viele Vorteile beim Schreiben von Code, der weniger zustandsbehaftet ist:

  • Einfachere Gründe für Code
  • Einfacheres Testen von Code
  • Einfacheres Kombinieren von asynchronen und synchronen Code
  • In der Lage, Rennbedingungen im Code zu vermeiden
  • Einfaches Koordinieren von asynchronen Code, der von Rückgabewerten abhängt
  • (Bonus) Dies eignet sich gut für abhängigkeitsbasierte Einfügungen im Code

Ein empfohlenes Ziel ist das vollständige oder nahezu vollständige Erreichen referenzieller Transparenz in Ihrem Code. Dieser Ansatz führt zu einer vorhersehbaren, testbaren und wartungsfähigen Codebasis.

Überprüfen des vollständigen Beispiels

Der folgende Code stellt das vollständige Beispiel dar, das in der Program.cs Beispieldatei verfügbar ist.

using System.Text.RegularExpressions;
using System.Windows;
using Microsoft.AspNetCore.Mvc;

class Button
{
    public Func<object, object, Task>? Clicked
    {
        get;
        internal set;
    }
}

class DamageResult
{
    public int Damage
    {
        get { return 0; }
    }
}

class User
{
    public bool isEnabled
    {
        get;
        set;
    }

    public int id
    {
        get;
        set;
    }
}

public class Program
{
    private static readonly Button s_downloadButton = new();
    private static readonly Button s_calculateButton = new();

    private static readonly HttpClient s_httpClient = new();

    private static readonly IEnumerable<string> s_urlList = new string[]
    {
            "https://learn.microsoft.com",
            "https://learn.microsoft.com/aspnet/core",
            "https://learn.microsoft.com/azure",
            "https://learn.microsoft.com/azure/devops",
            "https://learn.microsoft.com/dotnet",
            "https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio",
            "https://learn.microsoft.com/education",
            "https://learn.microsoft.com/shows/net-core-101/what-is-net",
            "https://learn.microsoft.com/enterprise-mobility-security",
            "https://learn.microsoft.com/gaming",
            "https://learn.microsoft.com/graph",
            "https://learn.microsoft.com/microsoft-365",
            "https://learn.microsoft.com/office",
            "https://learn.microsoft.com/powershell",
            "https://learn.microsoft.com/sql",
            "https://learn.microsoft.com/surface",
            "https://dotnetfoundation.org",
            "https://learn.microsoft.com/visualstudio",
            "https://learn.microsoft.com/windows",
            "https://learn.microsoft.com/maui"
    };

    private static void Calculate()
    {
        // <PerformGameCalculation>
        static DamageResult CalculateDamageDone()
        {
            return new DamageResult()
            {
                // Code omitted:
                //
                // Does an expensive calculation and returns
                // the result of that calculation.
            };
        }

        s_calculateButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI while CalculateDamageDone()
            // performs its work. The UI thread is free to perform other work.
            var damageResult = await Task.Run(() => CalculateDamageDone());
            DisplayDamage(damageResult);
        };
        // </PerformGameCalculation>
    }

    private static void DisplayDamage(DamageResult damage)
    {
        Console.WriteLine(damage.Damage);
    }

    private static void Download(string URL)
    {
        // <UnblockingDownload>
        s_downloadButton.Clicked += async (o, e) =>
        {
            // This line will yield control to the UI as the request
            // from the web service is happening.
            //
            // The UI thread is now free to perform other work.
            var stringData = await s_httpClient.GetStringAsync(URL);
            DoSomethingWithData(stringData);
        };
        // </UnblockingDownload>
    }

    private static void DoSomethingWithData(object stringData)
    {
        Console.WriteLine($"Displaying data: {stringData}");
    }

    // <GetUsersForDataset>
    private static async Task<User> GetUserAsync(int userId)
    {
        // Code omitted:
        //
        // Given a user Id {userId}, retrieves a User object corresponding
        // to the entry in the database with {userId} as its Id.

        return await Task.FromResult(new User() { id = userId });
    }

    private static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds)
    {
        var getUserTasks = new List<Task<User>>();
        foreach (int userId in userIds)
        {
            getUserTasks.Add(GetUserAsync(userId));
        }

        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDataset>

    // <GetUsersForDatasetByLINQ>
    private static async Task<User[]> GetUsersAsyncByLINQ(IEnumerable<int> userIds)
    {
        var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
        return await Task.WhenAll(getUserTasks);
    }
    // </GetUsersForDatasetByLINQ>

    // <ExtractDataFromNetwork>
    [HttpGet, Route("DotNetCount")]
    static public async Task<int> GetDotNetCount(string URL)
    {
        // Suspends GetDotNetCount() to allow the caller (the web server)
        // to accept another request, rather than blocking on this one.
        var html = await s_httpClient.GetStringAsync(URL);
        return Regex.Matches(html, @"\.NET").Count;
    }
    // </ExtractDataFromNetwork>

    static async Task Main()
    {
        Console.WriteLine("Application started.");

        Console.WriteLine("Counting '.NET' phrase in websites...");
        int total = 0;
        foreach (string url in s_urlList)
        {
            var result = await GetDotNetCount(url);
            Console.WriteLine($"{url}: {result}");
            total += result;
        }
        Console.WriteLine("Total: " + total);

        Console.WriteLine("Retrieving User objects with list of IDs...");
        IEnumerable<int> ids = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
        var users = await GetUsersAsync(ids);
        foreach (User? user in users)
        {
            Console.WriteLine($"{user.id}: isEnabled={user.isEnabled}");
        }

        Console.WriteLine("Application ending.");
    }
}

// Example output:
//
// Application started.
// Counting '.NET' phrase in websites...
// https://learn.microsoft.com: 0
// https://learn.microsoft.com/aspnet/core: 57
// https://learn.microsoft.com/azure: 1
// https://learn.microsoft.com/azure/devops: 2
// https://learn.microsoft.com/dotnet: 83
// https://learn.microsoft.com/dotnet/desktop/wpf/get-started/create-app-visual-studio: 31
// https://learn.microsoft.com/education: 0
// https://learn.microsoft.com/shows/net-core-101/what-is-net: 42
// https://learn.microsoft.com/enterprise-mobility-security: 0
// https://learn.microsoft.com/gaming: 0
// https://learn.microsoft.com/graph: 0
// https://learn.microsoft.com/microsoft-365: 0
// https://learn.microsoft.com/office: 0
// https://learn.microsoft.com/powershell: 0
// https://learn.microsoft.com/sql: 0
// https://learn.microsoft.com/surface: 0
// https://dotnetfoundation.org: 16
// https://learn.microsoft.com/visualstudio: 0
// https://learn.microsoft.com/windows: 0
// https://learn.microsoft.com/maui: 6
// Total: 238
// Retrieving User objects with list of IDs...
// 1: isEnabled= False
// 2: isEnabled= False
// 3: isEnabled= False
// 4: isEnabled= False
// 5: isEnabled= False
// 6: isEnabled= False
// 7: isEnabled= False
// 8: isEnabled= False
// 9: isEnabled= False
// 0: isEnabled= False
// Application ending.