皆さん、こんにちは!私は砂漠の果ての狼(沙漠尽头的狼)です!
AvaloniaUIは、Windows、Linux、macOS、Android、iOSなど複数のプラットフォーム向けにアプリケーションを構築できる、強力なクロスプラットフォームな.NETクライアント開発フレームワークです。複雑なアプリケーションを構築する際、モジュール化とコンポーネント間の通信が非常に重要になります。Prismフレームワークはモジュール開発方式を提供し、プラグインのホットプラグ/アンプラグをサポートします。一方、MediatRはMediatorパターンを実装したイベントサブスクリプション・パブリッシュフレームワークであり、モジュール間およびモジュールとメインプログラム間の通信に非常に適しています。
この記事では、MediatRに焦点を当てます。これは.NET向けのシンプルなMediatorパターンのオープンソース実装です。プロセス内メッセージングメカニズム(外部依存なし)を通じて、リクエスト/レスポンス、コマンド、クエリ、通知、イベントのメッセージングを行い、ジェネリックを使用してメッセージのスマートなディスパッチをサポートします。オープンソースライブラリのアドレスは https://github.com/jbogard/MediatR です。
この記事では、AvaloniaプロジェクトでMediatRとMicrosoftの依存性注入(MS.DI)ライブラリを使用して、イベント駆動型通信を実装する方法を詳しく紹介します。

0. 基礎知識の準備 - MediatRの基本的な使い方
MediatRには、2つのメッセージング方法があります。
Request/Response:単一のHandlerに使用します。Notification:複数のHandlerに使用します。
Request/Response
Request/Responseは、HTTPのRequest/Responseに似ており、Requestを送信するとResponseを受け取ります。
RequestメッセージはMediatRでは2つのタイプがあります。
IRequest<T>:T型の値を返します。IRequest:値を返しません。
各リクエストタイプには、対応するhandlerインターフェースが存在します。
IRequestHandler<T, U>:このインターフェースを実装し、Task<U>を返します。RequestHandler<T, U>:このクラスを継承し、Uを返します。IRequestHandler<T>:このインターフェースを実装し、Task<Unit>を返します。AsyncRequestHandler<T>:このクラスを継承し、Taskを返します。RequestHandler<T>:このクラスを継承し、値を返しません。
Notification
Notificationは通知です。呼び出し側が一度送信し、複数の処理側が処理に参加できます。

1. 準備
まず、Avaloniaプロジェクトに必要なNuGetパッケージがインストールされていることを確認します。依存性注入コンテナとしてPrism.DryIoc.Avalonia、イベントの公開と購読を処理するためにMediatRが必要です。また、MediatRをDryIocコンテナに統合するために、DryIoc.Microsoft.DependencyInjectionパッケージが必要です(ここでは、ユーザー「寒」からの技術的な回答に感謝します)。
プロジェクトの.csprojファイルまたはNuGetパッケージマネージャーに以下の参照を追加します。
<PackageReference Include="Prism.DryIoc.Avalonia" Version="8.1.97.11072" />
<PackageReference Include="MediatR" Version="12.2.0" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="8.0.0-preview-01" />
2. コンテナの設定とサービスの登録
Avaloniaプロジェクトでは、DryIocコンテナを設定してMicrosoftのDI拡張機能を使用し、MediatRサービスを登録する必要があります。これは通常、メインの起動クラス(App.axaml.csなど)で行われます。
以下は、コンテナの設定とサービスの登録のサンプルコードです。
namespace CodeWF.Tools.Desktop;
public class App : PrismApplication
{
// モジュールインジェクションなど、テーマに関係ないコードは省略。興味のある方は記事末のソースコードを参照してください。
/// <summary>
/// 1. DryIoc.Microsoft.DependencyInjectionの低バージョン(5.1.0以下)ではこのメソッドは不要です。
/// 2. 高バージョンでは必須。そうしないと例外がスローされます:System.MissingMethodException:“Method not found: 'DryIoc.Rules DryIoc.Rules.WithoutFastExpressionCompiler()'.”
/// Issues参照:https://github.com/dadhi/DryIoc/issues/529
/// </summary>
protected override Rules CreateContainerRules()
{
return Rules.Default.WithConcreteTypeDynamicRegistrations(reuse: Reuse.Transient)
.With(Made.Of(FactoryMethod.ConstructorWithResolvableArguments))
.WithFuncAndLazyWithoutRegistration()
.WithTrackingDisposableTransients()
//.WithoutFastExpressionCompiler()
.WithFactorySelector(Rules.SelectLastRegisteredFactory());
}
protected override IContainerExtension CreateContainerExtension()
{
IContainer container = new Container(CreateContainerRules());
container.WithDependencyInjectionAdapter();
return new DryIocContainerExtension(container);
}
protected override void RegisterRequiredTypes(IContainerRegistry containerRegistry)
{
base.RegisterRequiredTypes(containerRegistry);
IServiceCollection services = ConfigureServices();
IContainer container = ((IContainerExtension<IContainer>)containerRegistry).Instance;
container.Populate(services);
}
private static ServiceCollection ConfigureServices()
{
var services = new ServiceCollection();
// MediatRの注入
var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
// モジュールの注入を追加。モジュールタイプを明示的に呼び出すまで、モジュールアセンブリは現在のアプリケーションドメイン`AppDomain.CurrentDomain`に読み込まれません。
var assembly = typeof(SlugifyStringModule).GetAssembly();
assemblies.Add(assembly);
services.AddMediatR(configure =>
{
configure.RegisterServicesFromAssemblies(assemblies.ToArray());
});
return services;
}
}
上記のコードでは、CreateContainerRules、CreateContainerExtension、RegisterRequiredTypesメソッドをオーバーライドしてDryIocコンテナを設定し、MediatRサービスと関連ハンドラーを登録しています。
MediatRサービスを登録する際、現在読み込まれているアセンブリリストからハンドラーを検索して登録していることに注意してください。モジュールが必要に応じて読み込まれる場合は、ハンドラーの登録前に該当モジュールが読み込まれていることを確認してください。
また、ハンドラーを登録するためにモジュールアセンブリを手動でリストに追加する方法も示しています。これは、どのモジュールやハンドラーを登録するかを明示的に制御したい場合に便利です。ただし、ほとんどの場合、より自動化された方法(特定のディレクトリのスキャンや規約の使用など)でモジュールやハンドラーを読み込み・登録したいでしょう。これはプロジェクトの具体的な要件や構造によります。
さらに、コード中のコメントや説明は、各手順や設定の追加情報を提供しています。実際のプロジェクトでは、プロジェクトの実際の状況や要件に応じて調整や最適化が必要になる場合があります。例えば、循環依存の処理、スコープの設定、インターセプターやデコレーターといった高度な機能の使用などです。これらについては、DryIocとMediatRのドキュメントで詳しい説明とサンプルが提供されています。
3. MediatRの2つの転送方法
基礎知識の準備を踏まえ、クラスライブラリプロジェクトCodeWF.Tools.MediatR.Notificationsを追加し、リクエスト定義(メインプロジェクトおよびモジュールのレスポンスハンドラーで実装する必要があるもの)を追加します。
public class TestRequest : IRequest<string>
{
public string? Args { get; set; }
}
通知定義を追加します。
public class TestNotification : INotification
{
public string? Args { get; set; }
}
リクエストと通知の定義は同じ構造(実装するインターフェースが異なる)で、文字列プロパティが1つだけあります。
4. ハンドラーの追加
サンプルプロジェクトの構造は以下の通りです。このオープンソースプロジェクト(記事末のリンク)は、管理人のAvaloniaUIデスクトップツールプロジェクトに書かれています。この記事では、下図の3つのプロジェクトのみに注目します。

AvaloniaUIメインプロジェクト(CodeWF.Tools.Desktop)にリクエストレスポンスハンドラーを追加します。
public class TestHandler : IRequestHandler<TestRequest, string>
{
public async Task<string> Handle(TestRequest request, CancellationToken cancellationToken)
{
return await Task.FromResult($"メインプロジェクト処理ハンドラー:Args = {request.Args}, Now = {DateTime.Now}");
}
}
通知レスポンスハンドラーを追加します。
public class TestNotificationHandler(INotificationService notificationService) : INotificationHandler<TestNotification>
{
public Task Handle(TestNotification notification, CancellationToken cancellationToken)
{
notificationService.Show("Notification",
$"メインプロジェクトNotification処理ハンドラー:Args = {notification.Args}, Now = {DateTime.Now}");
return Task.CompletedTask;
}
}
モジュール【CodeWF.Tools.Modules.SlugifyString】にリクエストレスポンスハンドラーを追加します(順序の関係でトリガーされません。ここではリクエストが1対1レスポンスであることを示すための追加です)。
public class TestHandler : IRequestHandler<TestRequest, string>
{
public async Task<string> Handle(TestRequest request, CancellationToken cancellationToken)
{
return await Task.FromResult($"モジュール【SlugifyString】Request処理ハンドラー:Args = {request.Args}, Now = {DateTime.Now}");
}
}
通知レスポンスハンドラーを追加します(メインプロジェクトの通知レスポンスハンドラーと同様にトリガーされます)。
public class TestNotificationHandler(INotificationService notificationService) : INotificationHandler<TestNotification>
{
public Task Handle(TestNotification notification, CancellationToken cancellationToken)
{
notificationService.Show("Notification",
$"モジュール【SlugifyString】Notification処理ハンドラー:Args = {notification.Args}, Now = {DateTime.Now}");
return Task.CompletedTask;
}
}
いくつかのレスポンスハンドラーのクラス定義は似ています。リクエストを受信すると書式設定された文字列を返し、通知を受信すると現在の位置を示すポップアップを表示してデモンストレーション効果を発揮します。
5. リクエストと通知のデモ
トリガー操作はモジュール【CodeWF.Tools.Modules.SlugifyString】に記述します。モジュールのViewModelクラスで依存性注入を介して、リクエストおよび通知の送信者インスタンスISenderおよびIPublisherを取得します。
using Unit = System.Reactive.Unit;
namespace CodeWF.Tools.Modules.SlugifyString.ViewModels;
public class SlugifyViewModel : ViewModelBase
{
// エイリアス変換関連のロジックコードは省略。ソースコードは記事末を参照。
private readonly INotificationService _notificationService;
private readonly IClipboardService? _clipboardService;
private readonly ITranslationService? _translationService;
public SlugifyViewModel(INotificationService notificationService, IClipboardService clipboardService,
ITranslationService translationService, ISender sender, IPublisher publisher) : base(sender, publisher)
{
_notificationService = notificationService;
_clipboardService = clipboardService;
_translationService = translationService;
KindChanged = ReactiveCommand.Create<TranslationKind>(OnKindChanged);
}
public async Task ExecuteMediatRRequestAsync()
{
var result = Sender.Send(new TestRequest() { Args = To });
_notificationService.Show("MediatR", $"レスポンスを受信:{result.Result}");
}
public async Task ExecuteMediatRNotificationAsync()
{
await Publisher.Publish(new TestNotification() { Args = To });
}
}
「テストMediatR-Request」ボタンをクリックすると、ISender.Sendを呼び出してリクエストを送信し、レスポンスを取得します。「テストMediatR-Notification」ボタンをクリックすると、IPublisher.Publishを呼び出して通知を送信します。
リクエストの効果:

上記のリクエスト効果を確認してください。メインプロジェクトとモジュールプロジェクトの両方にレスポンスが登録されていますが、メインプロジェクトのみがトリガーされています。
通知の効果:

メインプロジェクトとモジュールプロジェクトの両方に通知レスポンスが登録されているため、両方のハンドラーがポップアップを表示します。
6. まとめ
なぜMediatRを使用し、Prismのイベントアグリゲーターを使用しないのか?
管理人の開発ツールにはオンラインバージョン(https://blazor.dotnet9.com)とクロスプラットフォームデスクトップバージョン(AvaloniaUI)があります。両方のバージョンでMediatRを使用することで、ほとんどのイベントコードを再利用できます。
CQRS または DDD?
このセクションは MediatR 在 .NET 应用中的实践 - 明志唯新 (yimingzhi.net) から直接コピーしています。何かを学べるはずです。
ソフトウェア開発が今日まで発展する中で、パターンと理念はアーキテクチャの中で絶えず刷新されてきました。分散システムからマイクロサービス、そしてクラウドネイティブへ…。時代はプログラマ、特にサーバーサイドプログラマに対してますます高い要求を突きつけます。DDD(ドメイン駆動設計)は、マイクロサービスアーキテクチャの中で繰り返し言及され、必須であるとさえ主張する人もいます!
完璧なDDDを実装するのは依然として難しいことであり、現実世界では最前線で働くCRUDプログラマーは少なくありません。では、CRUDとDDDの間にバッファゾーンは存在するのでしょうか?MediatRの作者もかつてこの問題について議論した記事を書いており、私は彼の基本的な見解に強く同意します。設計はアプリケーションに奉仕するものであり、DDDのためにDDDを行うべきではない、と。
CQRSの正式名称は「Command and Query Responsibility Segregation」で、直訳するとコマンドとクエリの責務分離であり、通俗的には「読み書き分離」と理解できます。

Microsoftの公式ドキュメントでは、これについて以下のように述べています。
CQRS コマンドとクエリの責務分離は、データストアの読み取り操作と更新操作を分離するパターンです。アプリケーションでCQRSを実装することで、パフォーマンス、スケーラビリティ、セキュリティを最大限に向上させることができます。CQRSへの移行によって生み出される柔軟性により、システムは時間の経過とともにより良く進化し、更新コマンドがドメインレベルでマージ競合を引き起こすのを防ぐことができます。
Microsoftは対応する分離モデルソリューションも提示しています。
CQRSは、データの更新にコマンドを、データの読み取りにクエリを使用し、読み取りと書き込みを異なるモデルに分離します。
- コマンドはタスクベースであるべきであり、データ中心であってはなりません。
- コマンドは同期処理ではなく、非同期処理のためにキューに入れることができます。
- クエリがデータベースを変更することは決してありません。クエリが返すDTOはドメイン知識をカプセル化しません。

CQRSの利点は次のとおりです。
- 独立したスケーリング: CQRSにより、読み取りと書き込みのワークロードを独立してスケーリングでき、ロック競合を減らす可能性があります。
- 最適化されたデータアーキテクチャ: 読み取り側ではクエリに最適化されたスキーマを、書き込み側では更新に最適化されたスキーマを使用できます。
- セキュリティ: 正しいドメインエンティティのみがデータへの書き込み操作を実行することをより簡単に保証できます。
- 懸念事項の分離: 読み取り側と書き込み側を分離することで、モデルがより保守しやすく、柔軟になります。ほとんどの複雑なビジネスロジックは書き込みモデルに割り当てられます。読み取りモデルは比較的単純になります。
- よりシンプルなクエリ: 具体化されたビューを読み取りデータベースに格納することで、アプリケーションはクエリ時に複雑な結合を回避できます。
MediatRを使用することで、アプリケーションで簡単にCQRSを実現できます。
IRequest<>のメッセージ名がCommandで終わるものはコマンドであり、対応するHandlerは書き込みタスクを実行します。IRequest<>のメッセージ名がQueryで終わるものはクエリであり、対応するHandlerはデータの読み取りを実行します。
結びの言葉
MediatRはシンプルなMediator実装であり、アプリケーションの複雑さを大幅に軽減でき、CRUDからCQRS、DDDへと段階的に進化することを可能にします。私たちは現実世界に生きる人間であり、商業的な現実を無視して純粋に技術だけを追い求めるわけにはいきません。

ビジネス技術の進化は、改革ではなく継続的な改革であるべきです。パンデミックは繰り返し発生しますが、私たちは生き残らなければなりません。比較的楽に生き残るために!
参考
記事内のサンプルは主要なコードのみを示しており、詳細が欠けている可能性があります。ソースコードは以下のリンクから入手できます。コメントをお待ちしています。
本記事のソースコード:GitHub