Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Se o código implementar cenários associados a E/S para dar suporte a solicitações de dados de rede, acesso ao banco de dados ou leitura/gravações do sistema de arquivos, a programação assíncrona será a melhor abordagem. Você também pode escrever código assíncrono para cenários associados à CPU, como cálculos caros.
O C# tem um modelo de programação assíncrona no nível da linguagem que permite que você escreva facilmente código assíncrono sem precisar fazer malabarismo com retornos de chamada ou em conformidade com uma biblioteca que dê suporte à assíncrona. O modelo segue o que é conhecido como TAP (padrão assíncrono baseado em tarefa).
Explorar o modelo de programação assíncrona
Os Task objetos e Task<T> representam o núcleo da programação assíncrona. Esses objetos são usados para modelar operações assíncronas por meio do suporte às palavras-chave async e await. Na maioria dos casos, o modelo é bastante simples para cenários associados a E/S e CPU. Dentro de um método async:
-
O código associado a E/S inicia uma operação representada por um
TaskouTask<T>objeto dentro doasyncmétodo. - O código associado à CPU inicia uma operação em um thread em segundo plano com o método Task.Run.
Em ambos os casos, um ativo Task representa uma operação assíncrona que pode não ser concluída.
É na palavra-chave await que a mágica acontece. Ele gera controle ao chamador do método que contém a await expressão e, por fim, permite que a interface do usuário seja responsiva ou um serviço seja elástico. Embora existam maneiras de abordar código assíncrono além do uso das expressões async e await, este artigo se concentra nos construtos ao nível da linguagem.
Observação
Alguns exemplos apresentados neste artigo usam a System.Net.Http.HttpClient classe para baixar dados de um serviço Web. No código de exemplo, o s_httpClient objeto é um campo estático da classe de tipo Program :
private static readonly HttpClient s_httpClient = new();
Para obter mais informações, consulte o código de exemplo completo no final deste artigo.
Examinar conceitos subjacentes
Quando você implementa a programação assíncrona no código C#, o compilador transforma seu programa em um computador de estado. Esse constructo rastreia várias operações e estado em seu código, como gerar execução quando o código atinge uma await expressão e retomar a execução quando um trabalho em segundo plano é concluído.
Em termos de teoria da ciência da computação, a programação assíncrona é uma implementação do modelo Promise de assíncrona.
No modelo de programação assíncrona, há vários conceitos importantes para entender:
- Você pode usar código assíncrono para código associado a E/S e CPU, mas a implementação é diferente.
- O código assíncrono usa
Task<T>eTaskobjetos como constructos para modelar o trabalho em execução em segundo plano. - A
asyncpalavra-chave declara um método como um método assíncrono, que permite que você use aawaitpalavra-chave no corpo do método. - Quando você aplica a
awaitpalavra-chave, o código suspende o método de chamada e retorna o controle ao chamador até que a tarefa seja concluída. - Você só pode usar a
awaitexpressão em um método assíncrono.
Exemplo vinculado à E/S: baixar dados de um serviço Web
Neste exemplo, quando o usuário seleciona um botão, o aplicativo baixa dados de um serviço Web. Você não deseja bloquear o thread da interface do usuário do aplicativo durante o processo de download. O código a seguir realiza esta tarefa:
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);
};
O código expressa a intenção (baixar alguns dados de forma assíncrona) sem se prender à interação com objetos Task.
Exemplo limitado pela CPU: executar o cálculo do jogo
No exemplo a seguir, um jogo móvel causa danos a vários agentes na tela em resposta a um evento de botão. Executar o cálculo de danos pode ser caro. Executar o cálculo no thread da interface do usuário pode causar problemas de exibição e interação com a interface do usuário durante o cálculo.
A melhor maneira de lidar com a tarefa é começar um thread de segundo plano usando o método Task.Run para concluir o trabalho. A operação é realizada usando uma expressão await. A operação é retomada quando a tarefa é concluída. Essa abordagem permite que a interface do usuário seja executada sem problemas enquanto o trabalho é concluído em segundo plano.
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);
};
O código expressa claramente a intenção do evento de botão Clicked . Não é necessário o gerenciamento manual de uma thread em segundo plano e ele conclui a tarefa de maneira não bloqueante.
Reconhecer cenários associados à CPU e associados à E/S
Os exemplos anteriores demonstram como usar o modificador async e a expressão await para trabalhos limitados por E/S e CPU. Um exemplo para cada cenário mostra como o código é diferente com base no local em que a operação está associada. Para se preparar para sua implementação, você precisa entender como identificar quando uma operação está associada a E/S ou associada à CPU. Sua escolha de implementação pode afetar muito o desempenho do código e potencialmente levar a construções de uso indevido.
Há duas perguntas principais a serem abordadas antes de escrever qualquer código:
| Pergunta | Cenário | Implementação |
|---|---|---|
| O código deve aguardar um resultado ou ação, como dados de um banco de dados? | Limite de E/S | Use o modificador async e a expressão awaitsem o método Task.Run. Evite usar a Biblioteca Paralela de Tarefas. |
| O código deve executar uma computação cara? | Limitado pela CPU | Use o modificador async e expressão await, mas desencadeie o trabalho em outra linha de execução com o método Task.Run. Essa abordagem aborda preocupações com a capacidade de resposta da CPU. Se o trabalho for adequado para a simultaneidade e paralelismo, você também deverá considerar o uso da Biblioteca de paralelismo de tarefas. |
Sempre meça a execução do código. Você pode descobrir que seu trabalho associado à CPU não é caro o suficiente em comparação com a sobrecarga de alternâncias de contexto ao fazer multithreading. Todas as opções têm compensações. Escolha a compensação correta para sua situação.
Explorar outros exemplos
Os exemplos nesta seção demonstram várias maneiras de escrever código assíncrono em C#. Eles abrangem alguns cenários que você pode encontrar.
Extrair dados de uma rede
O código a seguir baixa HTML de uma determinada URL e conta o número de vezes que a cadeia de caracteres ".NET" ocorre no HTML. O código usa ASP.NET para definir um método de controlador de API Web, que executa a tarefa e retorna a contagem.
Observação
Se você pretende fazer análise de HTML no código de produção, não use expressões regulares. Use uma biblioteca de análise.
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCountAsync(string URL)
{
// Suspends GetDotNetCountAsync() 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;
}
Você pode escrever um código semelhante para um Aplicativo Universal do Windows e executar a tarefa de contagem depois que um botão for pressionado.
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;
}
Aguardar a conclusão de várias tarefas
Em alguns cenários, o código precisa recuperar várias partes de dados simultaneamente. As Task APIs fornecem métodos que permitem que você escreva código assíncrono que executa uma espera sem bloqueio em vários trabalhos em segundo plano:
- método Task.WhenAll
- método Task.WhenAny
O exemplo a seguir mostra como você pode capturar dados de User objeto para um conjunto de userId objetos.
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);
}
Você pode escrever esse código de forma mais sucinta usando LINQ:
private static async Task<User[]> GetUsersByLINQAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
Embora você escreva menos código usando LINQ, tenha cuidado ao misturar LINQ com código assíncrono. O LINQ usa a execução adiada (ou lenta), o que significa que, sem avaliação imediata, as chamadas assíncronas não acontecem até que a sequência seja enumerada.
O exemplo anterior é correto e seguro, pois usa o Enumerable.ToArray método para avaliar imediatamente a consulta LINQ e armazenar as tarefas em uma matriz. Essa abordagem garante que as id => GetUserAsync(id) chamadas sejam executadas imediatamente e todas as tarefas comecem simultaneamente, assim como a abordagem de foreach loop. Sempre use Enumerable.ToArray ou Enumerable.ToList ao criar tarefas com LINQ para garantir a execução imediata e a execução simultânea da tarefa. Aqui está um exemplo que demonstra como usar ToList()Task.WhenAny para processar tarefas conforme elas são concluídas:
private static async Task ProcessTasksAsTheyCompleteAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToList();
while (getUserTasks.Count > 0)
{
Task<User> completedTask = await Task.WhenAny(getUserTasks);
getUserTasks.Remove(completedTask);
User user = await completedTask;
Console.WriteLine($"Processed user {user.id}");
}
}
Neste exemplo, ToList() cria uma lista que dá suporte à Remove() operação, permitindo que você remova dinamicamente as tarefas concluídas. Esse padrão é particularmente útil quando você deseja lidar com os resultados assim que eles estiverem disponíveis, em vez de esperar que todas as tarefas sejam concluídas.
Embora você escreva menos código usando LINQ, tenha cuidado ao misturar LINQ com código assíncrono. O LINQ usa a execução adiada (ou lenta). As chamadas assíncronas não ocorrem imediatamente como ocorrem em um foreach loop, a menos que você force a sequência gerada a iterar com uma chamada para o método .ToList() ou .ToArray().
Você pode escolher entre Enumerable.ToArray e Enumerable.ToList com base em seu cenário:
- Use
ToArray()quando você planeja processar todas as tarefas em conjunto, como comTask.WhenAll. As matrizes são eficientes para cenários em que o tamanho da coleção é fixo. - Use
ToList()quando precisar gerenciar tarefas dinamicamente, como quandoTask.WhenAnyvocê pode remover tarefas concluídas da coleção conforme elas forem concluídas.
Examinar considerações sobre programação assíncrona
Com a programação assíncrona, há vários detalhes a serem considerados que podem impedir um comportamento inesperado.
Usar await dentro do corpo do método async()
Ao usar o async modificador, você deve incluir uma ou mais await expressões no corpo do método. Se o compilador não encontrar uma await expressão, o método não produzirá. Embora o compilador gere um aviso, o código ainda é compilado e o compilador executa o método. O computador de estado gerado pelo compilador C# para o método assíncrono não realiza nada, portanto, todo o processo é altamente ineficiente.
Adicionar sufixo "Async" a nomes de método assíncrono
A convenção de estilo .NET é adicionar o sufixo "Assíncrono" a todos os nomes de método assíncrono. Essa abordagem ajuda a diferenciar mais facilmente entre métodos síncronos e assíncronos. Determinados métodos que não são explicitamente chamados pelo código (como manipuladores de eventos ou métodos de controlador web) não se aplicam necessariamente nesse cenário. Como esses itens não são explicitamente chamados pelo código, o uso de nomenclatura explícita não é tão importante.
Exibir "async void" somente de manipuladores de eventos
Os manipuladores de eventos devem declarar void tipos de retorno e não podem usar ou retornar Task e Task<T> objetos como outros métodos fazem. Ao escrever manipuladores de eventos assíncronos, você precisa usar o async modificador em um método que retorne void para os manipuladores. Outras implementações de métodos de async void retorno não seguem o modelo TAP e podem apresentar desafios:
- Exceções geradas em um
async voidmétodo não podem ser capturadas fora desse método -
async voidmétodos são difíceis de testar -
async voidmétodos podem causar efeitos colaterais negativos se quem os invoca não espera que eles sejam assíncronos
Tenha cuidado com lambdas assíncronas no LINQ
É importante ter cuidado ao implementar lambdas assíncronas em expressões LINQ. As expressões Lambda no LINQ usam a execução adiada, o que significa que o código pode ser executado em um momento inesperado. A introdução de tarefas de bloqueio nesse cenário pode facilmente resultar em um deadlock, se o código não for gravado corretamente. Além disso, o aninhamento de código assíncrono também pode dificultar a ponderação a respeito da execução do código. Técnicas Async e LINQ são poderosas, mas devem ser utilizadas juntas da forma mais cuidadosa e clara possível.
Suspender para tarefas de maneira não bloqueadora
Se o programa precisar do resultado de uma tarefa, escreva o código que implemente a await expressão de maneira não bloqueante. Bloquear o thread atual como um meio de aguardar de forma síncrona para que um item de Task seja concluído pode resultar em deadlocks e threads de contexto bloqueados. Essa abordagem de programação pode exigir tratamento de erros mais complexo. A tabela a seguir fornece diretrizes sobre como acessar resultados de tarefas de maneira não bloqueante.
| Cenário de tarefa | Código atual | Substituir por 'await' |
|---|---|---|
| Recuperar o resultado de uma tarefa em segundo plano |
Task.Wait ou Task.Result |
await |
| Continuar quando qualquer tarefa for concluída | Task.WaitAny |
await Task.WhenAny |
| Continuar quando todas as tarefas forem concluídas | Task.WaitAll |
await Task.WhenAll |
| Continuar após algum tempo | Thread.Sleep |
await Task.Delay |
Considerar o uso do tipo ValueTask
Quando um método assíncrono retorna um Task objeto, gargalos de desempenho podem ser introduzidos em determinados caminhos. Como Task é um tipo de referência, um Task objeto é alocado do heap. Se um método declarado com o async modificador retornar um resultado armazenado em cache ou for concluído de forma síncrona, as alocações extras poderão acumular custos de tempo significativos em seções críticas de desempenho do código. Esse cenário pode se tornar caro quando as alocações ocorrem em loops apertados. Para obter mais informações, consulte Tipos de retorno assíncronos generalizados.
Entender quando definir ConfigureAwait(false)
Os desenvolvedores geralmente perguntam sobre quando usar o Task.ConfigureAwait(Boolean) booliano. Essa API permite que uma Task instância configure o contexto para o computador de estado que implementa qualquer await expressão. Quando o booliano não está definido corretamente, o desempenho pode degradar ou deadlocks podem ocorrer. Para obter mais informações, consulte As perguntas frequentes sobre ConfigureAwait.
Gravar código sem estado
Evite escrever código que dependa do estado dos objetos globais ou da execução de determinados métodos. Em vez disso, depender apenas dos valores retornados dos métodos. Há muitos benefícios ao escrever código menos dependente de estado.
- Mais fácil de raciocinar sobre código
- Mais fácil de testar código
- Mais simples de misturar código assíncrono e síncrono
- Capaz de evitar condições de corrida no código
- Simples de coordenar o código assíncrono que depende de valores de retorno
- (Bônus) Funciona bem com a injeção de dependência no código
Uma meta recomendada é alcançar a Transparência referencial completa ou quase completa em seu código. Essa abordagem resulta em uma base de código previsível, testável e mantenedível.
Acesso síncrono a operações assíncronas
Em cenários, talvez seja necessário bloquear operações assíncronas quando a await palavra-chave não estiver disponível em toda a pilha de chamadas. Essa situação ocorre em bases de código herdadas ou ao integrar métodos assíncronos a APIs síncronas que não podem ser alteradas.
Aviso
O bloqueio síncrono em operações assíncronas pode levar a deadlocks e deve ser evitado sempre que possível. A solução preferencial é usar async/await em toda a pilha de chamadas.
Quando você deve bloquear de forma síncrona em um Task, aqui estão as abordagens disponíveis, listadas da maioria para a menos preferida:
Use GetAwaiter(). GetResult()
O GetAwaiter().GetResult() padrão geralmente é a abordagem preferencial quando você deve bloquear de forma síncrona:
// When you cannot use await
Task<string> task = GetDataAsync();
string result = task.GetAwaiter().GetResult();
Esta abordagem:
- Preserva a exceção original sem embrulhá-la em um
AggregateException. - Bloqueia o thread atual até que a tarefa seja concluída.
- Ainda carrega risco de deadlock se não for usado com cuidado.
Usar Task.Run para cenários complexos
Para cenários complexos em que você precisa isolar o trabalho assíncrono:
// Offload to thread pool to avoid context deadlocks
string result = Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();
Este padrão:
- Executa o método assíncrono em um thread do pool de threads.
- Pode ajudar a evitar alguns cenários de deadlock.
- Adiciona sobrecarga agendando o trabalho para o pool de threads.
Usar Wait() e Result
Você pode usar uma abordagem de bloqueio chamando Wait() e Result. No entanto, essa abordagem é desencorajada porque encapsula exceções em AggregateException.
Task<string> task = GetDataAsync();
task.Wait();
string result = task.Result;
Problemas com Wait() e Result:
- As exceções são encapsuladas
AggregateException, tornando o tratamento de erros mais complexo. - Maior risco de deadlock.
- Intenção menos clara no código.
Considerações adicionais
- Prevenção de deadlock: tenha cuidado especialmente em aplicativos de interface do usuário ou ao usar um contexto de sincronização.
- Impacto no desempenho: o bloqueio de threads reduz a escalabilidade.
- Tratamento de exceção: testar cenários de erro cuidadosamente, pois o comportamento de exceção difere entre padrões.
Para obter diretrizes mais detalhadas sobre os desafios e considerações de wrappers síncronos para métodos assíncronos, consulte Devo expor wrappers síncronos para métodos assíncronos?.
Examinar o exemplo completo
O código a seguir representa o exemplo completo, que está disponível no arquivo de exemplo Program.cs .
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()
{
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);
};
}
private static void DisplayDamage(DamageResult damage)
{
Console.WriteLine(damage.Damage);
}
private static void Download(string URL)
{
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);
};
}
private static void DoSomethingWithData(object stringData)
{
Console.WriteLine($"Displaying data: {stringData}");
}
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);
}
private static async Task<User[]> GetUsersByLINQAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
private static async Task ProcessTasksAsTheyCompleteAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToList();
while (getUserTasks.Count > 0)
{
Task<User> completedTask = await Task.WhenAny(getUserTasks);
getUserTasks.Remove(completedTask);
User user = await completedTask;
Console.WriteLine($"Processed user {user.id}");
}
}
[HttpGet, Route("DotNetCount")]
static public async Task<int> GetDotNetCountAsync(string URL)
{
// Suspends GetDotNetCountAsync() 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;
}
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 GetDotNetCountAsync(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("Processing tasks as they complete...");
await ProcessTasksAsTheyCompleteAsync(ids);
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.