大家好,我是沙漠盡頭的狼!
avaloniaui是一個強大的跨平台.net客戶端開發框架,讓開發者能夠針對windows、linux、macos、android和ios等多個平台構建應用程式。在構建複雜的應用程式時,模塊化和組件間的通信變得尤為重要。prism框架提供了模塊化的開發方式,支持插件的熱拔插,而mediatr則是一個實現了中介者(mediator)模式的事件訂閱發布框架,非常適合用於模塊之間以及模塊與主程式之間的通信。
本文重点是介绍MediatR,它 是 .NET 中的开源简单中介者模式实现。它通过一种进程内消息传递机制(无其他外部依赖),进行请求/响应、命令、查询、通知和事件的消息传递,并通过泛型来支持消息的智能调度。开源库地址是 https://github.com/jbogard/MediatR。
本文將詳細居間如何在avalonia項目中使用mediatr和microsoft的依賴注入(ms.di)庫來實現事件驅動的通信。

0. 基礎知識準備-mediatr的基本用法
MediatR中有两种消息传递的方式:
Request/Response,用于一个单独的Handler。Notification,用于多个Handler。
Request/Response
Request/Response 有点类似于 HTTP 的 Request/Response,发出一个 Request 会得到一个 Response。
Request 消息在 MediatR 中,有两种类型:
IRequest<T>返回一个T类型的值。IRequest不返回值。
對於每個 request 類型,都有相應的 handler 接口:
IRequestHandler<T, U>实现该接口并返回Task<U>RequestHandler<T, U>继承该类并返回UIRequestHandler<T>实现该接口并返回Task<Unit>AsyncRequestHandler<T>继承该类并返回TaskRequestHandler<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>
/// <returns></returns>
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. mediatr2種傳遞方式
有了前面的基础知识准备,我们添加类库工程CodeWF.Tools.MediatR.Notifications,并添加请求定义(主工程及模块的响应处理程序需要实现):
public class TestRequest : IRequest<string>
{
public string? Args { get; set; }
}
添加通知定義:
public class TestNotification : INotification
{
public string? Args { get; set; }
}
請求和通知定義結構一樣(實現接口不同),只有一個字符串屬性。
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】中添加請求響應處理程式(因為順序關係,不會觸發,這裡添加只是演示請求為一對一響應):
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 or DDD?
这节直接复制MediatR 在 .NET 应用中的实践 - 明志唯新 (yimingzhi.net),大家应该可以学到些什么:
软件开发发展到今天,模式和理念不断在架构中刷新:从分布式到微服务,再到云原生 ……。时代对一个程序员,尤其是服务端程序员,提出的要求越来越高。DDD(领域驱动设计)在微服务架构中一再被提及,甚至有人提出这是必须项!
实施一个完美的 DDD 还是有难度的,现实中奋战在一线的 CRUD 程序员还是不少。那么在 CRUD 和 DDD 之间我们是否还有缓冲区呢?MediatR 的作者曾经也撰文讨论过这个问题,我很认同他的基本观点:设计是为应用服务的,不能为了 DDD 而 DDD。
CQRS 的全称是:"Command and Query Responsibility Segregation",直译过来就是命令与查询责任分离,可以通俗的理解为 读写分离。

微軟的官方文檔中對此做過如下陳述:
cqrs 命令和查詢責任分離數據存儲的讀取和更新操作分離的模式。在應用程式中實現 cqrs 可以最大程度地提高其性能、可伸縮性和安全性。通過遷移到 cqrs 而創建的靈活性使系統能夠隨著時間的推移更好地發展,並防止更新命令在域級別導致合併衝突。
微軟也給出了相應的隔離模型解決方案:
cqrs 使用命令來更新數據,使用查詢來讀取數據,將讀取和寫入 分離到不同的 模型中。
- 命令應基於任務,而不是以數據為中心。
- 命令可以放置在隊列中進行異步處理,而不是同步處理。
- 查詢從不修改資料庫。查詢返回的 dto 不封裝任何域知識。

cqrs 的好處包括:
- 獨立縮放: cqrs 允許讀取和寫入工作負載獨立縮放,這可能會減少鎖爭用。
- 優化的數據架構: 讀取端可使用針對查詢優化的架構,寫入端可使用針對更新優化的架構。
- 安全性: 更輕鬆地確保僅正確的域實體對數據執行寫入操作。
- 關注點分離: 分離讀取和寫入端可使模型更易維護且更靈活。大多數複雜的業務邏輯被分到寫模型。讀模型會變得相對簡單。
- 查詢更簡單: 通過將具體化視圖存儲在讀取資料庫中,應用程式可在查詢時避免複雜聯接。
有了 MediatR 我们可以在应用中轻松实现 CQRS:
IRequest<>的消息名称以Command为结尾的是命令,其对应的 Handler 执行写任务IRequest<>的消息名称以Query为结尾的是查询,其对应的 Handler 执行读数据
結束語
MediatR 是一个简单的中介者实现,可以极大降低我们的应用复杂度,也能够使得我们一路从 CRUD 到 CQRS 到 DDD 进行逐级演进。毕竟我们是生活在现实中的人,不能罔顾商业现实,纯粹一味追求技术。

商業技術的演進,應該是一路持續的改革而不是來一場革命。疫情總有反覆,但是我們得活著,相對輕鬆的活著!
參考
文中示例寫了主要代碼,但可能缺失部分細節,源碼連結如下,歡迎留言交流。
本文源码:GitHub