Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
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
OderTask<T>
Objekt innerhalb derasync
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>
undTask
Objekte als Konstrukte zum Modellieren der Ausführung im Hintergrund. - Das
async
Schlüsselwort deklariert eine Methode als asynchrone Methode, mit der Sie dasawait
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:
- Task.WhenAll-Methode
- Task.WhenAny-Methode
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.