ネットワーク データ要求、データベース アクセス、またはファイル システムの読み取り/書き込みをサポートする I/O バインド シナリオをコードで実装する場合は、非同期プログラミングが最適な方法です。 また、高価な計算などの CPU バインド シナリオの非同期コードを記述することもできます。
C# には言語レベルの非同期プログラミング モデルがあり、コールバックを処理したり、非同期をサポートするライブラリに準拠したりする必要なく、非同期コードを簡単に記述できます。 このモデルは、 タスク ベースの非同期パターン (TAP) と呼ばれるものに従います。
非同期プログラミング モデルを調べる
TaskオブジェクトとTask<T> オブジェクトは、非同期プログラミングの中核を表します。 これらのオブジェクトは、 async キーワードと await キーワードをサポートすることで非同期操作をモデル化するために使用されます。 ほとんどの場合、モデルは I/O バインドと CPU バインドの両方のシナリオで非常に単純です。
async メソッド内:
-
I/O バインド コードは、
Taskメソッド内のTask<T>またはasyncオブジェクトによって表される操作を開始します。 - CPU バインド コードは 、 Task.Run メソッドを使用してバックグラウンド スレッドで操作を開始します。
どちらの場合も、アクティブな Task は、完了していない可能性がある非同期操作を表します。
await キーワードはマジックが行われる場所であり、
await式を含むメソッドの呼び出し元に制御を渡し、最終的には UI の応答性を高めるか、サービスをエラスティックにすることができます。
式とasync式を使用する以外に非同期コードにアプローチするawaitが、この記事では言語レベルのコンストラクトについて説明します。
注
この記事で紹介する例の一部では、 System.Net.Http.HttpClient クラスを使用して Web サービスからデータをダウンロードします。 コード例では、 s_httpClient オブジェクトはクラス Program 型の静的フィールドです。
private static readonly HttpClient s_httpClient = new();
詳細については、この記事の最後にある 完全なコード例 を参照してください。
基になる概念を確認する
C# コードで非同期プログラミングを実装すると、コンパイラによってプログラムがステート マシンに変換されます。 このコンストラクトは、コードが await 式に達したときに実行を生成したり、バックグラウンド ジョブが完了したときに実行を再開するなど、コード内のさまざまな操作と状態を追跡します。
コンピューター サイエンス理論では、非同期プログラミングは非同期の Promise モデルの実装です。
非同期プログラミング モデルでは、いくつかの重要な概念を理解する必要があります。
- I/O バインド コードと CPU バインド コードの両方に非同期コードを使用できますが、実装は異なります。
- 非同期コードでは、バックグラウンドで実行されている作業をモデル化するために、
Task<T>オブジェクトとTaskオブジェクトをコンストラクトとして使用します。 -
asyncキーワードは、メソッドを非同期メソッドとして宣言します。これにより、メソッド本体でawaitキーワードを使用できます。 -
awaitキーワードを適用すると、コードは呼び出し元メソッドを中断し、タスクが完了するまで制御を呼び出し元に戻します。 -
await式は、非同期メソッドでのみ使用できます。
I/O バインドの例: Web サービスからデータをダウンロードする
この例では、ユーザーがボタンを選択すると、アプリは Web サービスからデータをダウンロードします。 ダウンロード プロセス中にアプリの UI スレッドをブロックしたくない。 次のコードは、このタスクを実行します。
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);
};
コードでは、Task オブジェクトとの対話に煩わされることなく意図すること (データを非同期的にダウンロードする) が表されています。
CPUに依存する例:ゲーム計算を実行する
次の例では、モバイル ゲームがボタン イベントに応答して画面上の複数のエージェントに損害を与えます。 破損の計算を実行すると、コストがかかる場合があります。 UI スレッドで計算を実行すると、計算中に表示と UI の相互作用の問題が発生する可能性があります。
タスクを処理する最善の方法は、バックグラウンド スレッドを開始して、 Task.Run メソッドで作業を完了することです。 この操作は、 await 式を使用して生成されます。 タスクが完了すると、操作が再開されます。 この方法では、バックグラウンドで作業が完了している間に UI をスムーズに実行できます。
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);
};
このコードは、イベント Clicked ボタンの意図を明確に表しています。 バックグラウンド スレッドを手動で管理する必要はありません。また、非ブロッキングの方法でタスクを完了します。
CPU バインドと I/O バインドのシナリオを認識する
前の例では、I/O バインドおよび CPU バインド作業に対して async 修飾子と await 式を使用する方法を示します。 各シナリオの例では、操作がバインドされている場所に基づいてコードがどのように異なるかを示します。 実装を準備するには、操作が I/O バインドまたは CPU バインドのタイミングを識別する方法を理解する必要があります。 実装の選択は、コードのパフォーマンスに大きく影響し、コンストラクトが誤って使用される可能性があります。
コードを記述する前に、主に 2 つの質問に対処する必要があります。
| 質問 | シナリオ | 実装 |
|---|---|---|
| コードは、データベースからのデータなど、結果またはアクションを待機する必要がありますか? | I/O バウンド |
async メソッドawait、修飾子とTask.Run式を使用します。 タスク並列ライブラリは使用しないでください。 |
| コードは高価な計算を実行する必要がありますか? | CPU 制約 |
async修飾子とawait式を使用しますが、Task.Run メソッドを使用して別のスレッドで作業を開始します。 この方法では、CPU の応答性に関する問題に対処します。 処理がコンカレンシーと並列処理に適している場合は、タスク並列ライブラリを使うことも考慮します。 |
常にコードの実行を測定します。 マルチスレッド時のコンテキスト切り替えのオーバーヘッドに比べて、CPU に依存する作業が十分にコストがかからないことに気づくかもしれません。 すべての選択肢にトレードオフがあります。 状況に合った適切なトレードオフを選択します。
その他の例を調べる
このセクションの例では、C# で非同期コードを記述する方法をいくつか示します。 これらは、発生する可能性のあるいくつかのシナリオに対応しています。
ネットワークからデータを抽出する
次のコードは、特定の URL から HTML をダウンロードし、文字列 ".NET" が HTML で発生した回数をカウントします。 このコードでは、ASP.NET を使用して、タスクを実行してカウントを返す Web API コントローラー メソッドを定義します。
注
運用コードで HTML の解析の実行を計画している場合は、正規表現を使用しないでください。 代わりに解析ライブラリを使用します。
[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;
}
ユニバーサル Windows アプリの同様のコードを記述し、ボタンを押した後にカウント タスクを実行できます。
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;
}
複数タスクの完了を待機する
シナリオによっては、コードで複数のデータを同時に取得する必要があります。
Task API には、複数のバックグラウンド ジョブで非ブロッキング待機を実行する非同期コードを記述できるメソッドが用意されています。
- Task.WhenAll メソッド
- Task.WhenAny メソッド
次の例は、一連の User オブジェクトの userId オブジェクト データを取得する方法を示しています。
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);
}
LINQ を使用すると、このコードをより簡潔に記述できます。
private static async Task<User[]> GetUsersByLINQAsync(IEnumerable<int> userIds)
{
var getUserTasks = userIds.Select(id => GetUserAsync(id)).ToArray();
return await Task.WhenAll(getUserTasks);
}
LINQ を使用して記述するコードは少なくなりますが、LINQ と非同期コードを混在させる場合は注意が必要です。 LINQ は遅延 (または遅延) 実行を使用します。つまり、即時評価を行わないと、シーケンスが列挙されるまで非同期呼び出しは行われません。
前の例は、 Enumerable.ToArray メソッドを使用して LINQ クエリをすぐに評価し、タスクを配列に格納するため、正しく安全です。 この方法により、 id => GetUserAsync(id) 呼び出しがすぐに実行され、 foreach ループアプローチと同様にすべてのタスクが同時に開始されます。 LINQ を使用してタスクを作成するときは、常に Enumerable.ToArray または Enumerable.ToList を使用して、タスクの即時実行と同時実行を確保します。
ToList()でTask.WhenAnyを使用して、タスクの完了時に処理する例を次に示します。
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}");
}
}
この例では、ToList()Remove()操作をサポートするリストを作成し、完了したタスクを動的に削除できるようにします。 このパターンは、すべてのタスクが完了するのを待つのではなく、結果が使用可能になったらすぐに処理する場合に特に便利です。
LINQ を使用して記述するコードは少なくなりますが、LINQ と非同期コードを混在させる場合は注意が必要です。 LINQ では、遅延実行が使用されます。
foreachまたは .ToList() メソッドの呼び出しを使用して生成されたシーケンスを強制的に反復処理しない限り、非同期呼び出しは、.ToArray() ループ内で行われるようにすぐには行われません。
シナリオに基づいて、 Enumerable.ToArray と Enumerable.ToList のいずれかを選択できます。
-
ToArray()など、すべてのタスクをまとめて処理する予定の場合は、Task.WhenAllを使用します。 配列は、コレクション サイズが固定されているシナリオでは効率的です。 -
ToList()は、タスクを動的に管理する必要がある場合、Task.WhenAnyのように完了したタスクをコレクションから削除するときに使用します。
非同期プログラミングに関する考慮事項を確認する
非同期プログラミングでは、予期しない動作を防ぐことができるいくつかの詳細に留意する必要があります。
async() メソッド本体内で await を使用する
async修飾子を使用する場合は、メソッド本体に 1 つ以上のawait式を含める必要があります。 コンパイラで await 式が見つからない場合、メソッドは生成に失敗します。 コンパイラは警告を生成しますが、コードは引き続きコンパイルされ、コンパイラはメソッドを実行します。 非同期メソッド用に C# コンパイラによって生成されたステート マシンでは何も実行されないため、プロセス全体が非常に非効率的です。
非同期メソッド名に "Async" サフィックスを追加する
.NET スタイルの規則では、すべての非同期メソッド名に "Async" サフィックスを追加します。 この方法は、同期メソッドと非同期メソッドをより簡単に区別するのに役立ちます。 コードによって明示的に呼び出されない特定のメソッド (イベント ハンドラーや Web コントローラー メソッドなど) は、必ずしもこのシナリオでは適用されません。 これらの項目はコードによって明示的に呼び出されないため、明示的な名前付けの使用はそれほど重要ではありません。
イベント ハンドラーからのみ 'async void' を返します
イベント ハンドラーは void 戻り値の型を宣言する必要があり、他のメソッドと同様に Task オブジェクトと Task<T> オブジェクトを使用または返すことはできません。 非同期イベント ハンドラーを記述するときは、ハンドラーのasync戻りメソッドでvoid修飾子を使用する必要があります。
async void戻りメソッドの他の実装は TAP モデルに従っず、課題を提示する可能性があります。
-
async voidメソッドでスローされた例外を、そのメソッドの外部でキャッチすることはできません -
async voidメソッドのテストが困難である -
async voidメソッドは、呼び出し元が非同期であると想定していない場合に悪影響を及ぼす可能性があります
LINQ の非同期ラムダには注意が必要です
LINQ 式で非同期ラムダを実装するときは注意が必要です。 LINQ のラムダ式では遅延実行が使用されます。つまり、予期しないタイミングでコードが実行される可能性があります。 このシナリオにブロック タスクを導入すると、コードが正しく記述されていない場合、デッドロックが発生する可能性があります。 さらに、非同期コードを入れ子にすると、コードの実行について推論するのが困難になる場合もあります。 Async と LINQ は強力ですが、これらの手法はできるだけ慎重かつ明確に組み合わせて使用する必要があります。
非ブロッキング方式でタスクを待機する
プログラムでタスクの結果が必要な場合は、 await 式を非ブロッキング方式で実装するコードを記述します。
Task項目が完了するまで同期的に待機する手段として現在のスレッドをブロックすると、デッドロックが発生し、コンテキスト スレッドがブロックされる可能性があります。 このプログラミング手法では、より複雑なエラー処理が必要になる場合があります。 次の表は、タスクからのアクセスが非ブロッキングな方法でどのように結果を得るかについてのガイダンスを示しています。
| タスクのシナリオ | 現在のコード | 'await' に置き換えます |
|---|---|---|
| バックグラウンド タスクの結果を取得する |
Task.Wait または Task.Result |
await |
| タスクが完了したら続行する | Task.WaitAny |
await Task.WhenAny |
| すべてのタスクが完了したら続行する | Task.WaitAll |
await Task.WhenAll |
| しばらくしてから続行する | Thread.Sleep |
await Task.Delay |
ValueTask 型の使用を検討する
非同期メソッドが Task オブジェクトを返すと、特定のパスでパフォーマンスのボトルネックが発生する可能性があります。
Taskは参照型であるため、Task オブジェクトはヒープから割り当てられます。
async修飾子を使用して宣言されたメソッドがキャッシュされた結果を返すか、同期的に完了した場合、追加の割り当てによって、コードのパフォーマンスクリティカルセクションで大幅な時間コストが発生する可能性があります。 このシナリオは、厳密なループで割り当てが発生すると、コストがかかる場合があります。 詳しくは、「一般化された async の戻り値の型」をご覧ください。
ConfigureAwait(false) を設定するタイミングを理解する
開発者は、多くの場合、 Task.ConfigureAwait(Boolean) ブール値を使用するタイミングについて問い合わせています。 この API を使用すると、 Task インスタンスは、任意の await 式を実装するステート マシンのコンテキストを構成できます。 ブール値が正しく設定されていない場合、パフォーマンスが低下したり、デッドロックが発生したりする可能性があります。 詳細については、「 ConfigureAwait FAQ」を参照してください。
ステートフルでないコードを記述する
グローバル オブジェクトの状態または特定のメソッドの実行に依存するコードを記述しないでください。 代わりに、メソッドの戻り値のみに依存するようにします。 ステートフルではないコードを記述する利点は多数あります。
- コードを理解しやすい
- 簡単にコードをテストする
- 非同期コードと同期コードを簡単に組み合わせることができます
- コード内の競合状態を回避できる
- 戻り値に依存する非同期コードを簡単に調整
- (ボーナス)コード内の依存関係の挿入に対して適切に機能する
推奨される目標は、完全またはほぼ完全な参照の透過性をコードで実現することです。 このアプローチにより、予測可能、テスト可能、保守可能なコードベースが得られます。
非同期操作への同期アクセス
シナリオでは、呼び出し履歴全体で await キーワードを使用できない場合に、非同期操作をブロックすることが必要になる場合があります。 この状況は、従来のコードベースで発生するか、非同期メソッドを変更できない同期 API に統合する場合に発生します。
Warnung
非同期操作での同期ブロックはデッドロックにつながる可能性があり、可能な限り回避する必要があります。 推奨される解決策は、呼び出し履歴全体で async/await を使用することです。
Taskで同期的にブロックする必要がある場合は、使用可能なアプローチを次に示します。ほとんどの方法から最も望ましいものまでを示します。
GetAwaiter() を使用します。GetResult()
GetAwaiter().GetResult() パターンは、通常、同期的にブロックする必要がある場合に推奨されるアプローチです。
// When you cannot use await
Task<string> task = GetDataAsync();
string result = task.GetAwaiter().GetResult();
この方法:
- 元の例外を
AggregateExceptionでラップせずに保持します。 - タスクが完了するまで、現在のスレッドをブロックします。
- 注意深く使用しないと、デッドロックのリスクが引き続き発生します。
複雑なシナリオで Task.Run を使用する
非同期作業を分離する必要がある複雑なシナリオの場合:
// Offload to thread pool to avoid context deadlocks
string result = Task.Run(async () => await GetDataAsync()).GetAwaiter().GetResult();
このパターン:
- スレッド プール スレッドで非同期メソッドを実行します。
- 一部のデッドロック シナリオを回避するのに役立ちます。
- スレッド プールに作業をスケジュールすることでオーバーヘッドを増やします。
Wait() と Result を使用する
Wait()とResultを呼び出すことで、ブロッキング アプローチを使用できます。 ただし、 AggregateExceptionで例外をラップするため、この方法はお勧めしません。
Task<string> task = GetDataAsync();
task.Wait();
string result = task.Result;
Wait()とResultに関する問題:
- 例外は
AggregateExceptionでラップされるため、エラー処理がより複雑になります。 - デッドロック リスクが高くなります。
- コード内の意図が明確ではありません。
その他の考慮事項
- デッドロック防止: UI アプリケーションや同期コンテキストを使用する場合は特に注意してください。
- パフォーマンスへの影響: スレッドをブロックするとスケーラビリティが低下します。
- 例外処理: 例外の動作がパターンによって異なるため、エラー シナリオを慎重にテストします。
非同期メソッドの同期ラッパーの課題と考慮事項の詳細なガイダンスについては、「非同期メソッドの 同期ラッパーを公開する必要がある」を参照してください。
完全な例を確認する
次のコードは、 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.
関連リンク
.NET