Mediator Pattern là gì?
Mối Liên Hệ Giữa Mediator, CQS và CQRS
CQRS được lấy cảm hứng từ mô hình Command Query Separation (CQS) do Bertrand Meyer đề xuất trong cuốn sách "Object Oriented Software Construction". Triết lý chính của CQS là
A method should either change state of an object, or return a result, but not both. In other words, asking the question should not change the answer. More formally, methods should return a value only if they are referentially transparent and hence possess no side effects.” (Wikipedia)
Dựa trên nguyên tắc này, CQS phân chia các phương thức thành hai nhóm:
- Command: Thay đổi trạng thái của đối tượng hoặc toàn bộ hệ thống (còn gọi là modifiers hoặc mutators).
- Query: Trả về kết quả và không thay đổi trạng thái của đối tượng.
MediatR – thư viện nổi tiếng
Trong .NET, thư viện MediatR do Jimmy Bogard phát triển là cái tên phổ biến nhất khi nhắc đến Mediator pattern. MediatR cho phép bạn định nghĩa:
- Request / Command / Query
- Handler (nơi xử lý logic).
Và bạn chỉ cần gọi mediator.Send(request) thay vì new trực tiếp service nào đó.
Tuy nhiên, kể từ phiên bản 13, MediatR đã chuyển sang mô hình thương mại (commercial license).
- Điều này có nghĩa là bạn không còn thoải mái sử dụng MediatR miễn phí trong mọi dự án nữa.
- Các dự án thương mại có thể phải trả phí bản quyền.
- Đối với cộng đồng .NET, đây là một thay đổi lớn, bởi MediatR vốn được xem là một OSS (open-source staple).
martinothamar/Mediator – sự thay thế nhẹ hơn (và miễn phí)
Đây là lý do mà ngày càng nhiều dev .NET tìm đến martinothamar/Mediator
- Vẫn giữ đúng triết lý Mediator pattern.
- MIT License → dùng thoải mái cho dự án cá nhân và thương mại.
- Hiệu năng tốt hơn nhờ Source Generator.
Bắt đầu với martinothamar/Mediator
Cài đặt package:
dotnet add package Mediator.SourceGenerator
dotnet add package Mediator.Abstractions
Hoặc thêm trực tiếp vào .csproj:
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Mediator.Abstractions" Version="3.0.*" />
Thêm Mediator vào DI container
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// Explicitly specify the namespace to resolve ambiguity
MediatorDependencyInjectionExtensions.AddMediator(builder.Services);
var app = builder.Build();
Tạo request và Handler
using Mediator;
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
// Request
public sealed record Ping(Guid Id) : IRequest<Pong>;
// Response
public sealed record Pong(Guid Id);
// Handler
public sealed class PingHandler : IRequestHandler<Ping, Pong>
{
public ValueTask<Pong> Handle(Ping request, CancellationToken cancellationToken)
{
return new ValueTask<Pong>(new Pong(request.Id));
}
}
Nếu bạn dùng Console Application
var mediator = serviceProvider.GetRequiredService<IMediator>();
var ping = new Ping(Guid.NewGuid());
var pong = await mediator.Send(ping);
Debug.Assert(ping.Id == pong.Id);
Console.WriteLine($"Ping Id: {ping.Id}, Pong Id: {pong.Id}");
Trường hợp bạn dùng ASP.NET Core MVC
using Mediator;
using MediatorSample.Application.Features.Requests;
using Microsoft.AspNetCore.Mvc;
namespace MediatorSample.Controllers;
public class PingController : Controller
{
private readonly IMediator _mediator;
public PingController(IMediator mediator)
{
_mediator = mediator;
}
// GET: /Ping
public IActionResult Index()
{
var model = new PingViewModel { InputId = Guid.NewGuid() }; // Pre-fill for convenience
return View(model);
}
// POST: /Ping
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Index(PingViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var pingRequest = new Ping(model.InputId);
model.PongResult = await _mediator.Send(pingRequest);
return View(model); // Stay on the same view to display the result
}
}
// View Model for the Ping page
public class PingViewModel
{
public Guid InputId { get; set; }
public Pong? PongResult { get; set; }
}
View:
@model MediatorSample.Controllers.PingViewModel
@{
ViewData["Title"] = "Ping Test";
}
<h1>Ping Test (MVC)</h1>
<div class="row">
<div class="col-md-4">
<form asp-action="Index" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="InputId" class="control-label"></label>
<input asp-for="InputId" class="form-control" />
<span asp-validation-for="InputId" class="text-danger"></span>
</div>
<div class="form-group mt-3">
<input type="submit" value="Send Ping" class="btn btn-primary" />
</div>
</form>
</div>
</div>
@if (Model.PongResult != null)
{
<h2 class="mt-4">Pong Result</h2>
<div class="alert alert-success">
<p><strong>Response ID:</strong> @Model.PongResult.Id</p>
</div>
}
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Ví dụ cho dự án .NET Framework
Install-Package Microsoft.CodeDom.Providers.DotNetCompilerPlatform
Roslyn là gì?
Roslyn (hay .NET Compiler Platform) là bộ biên dịch mới cho C# và VB.NET, được Microsoft viết lại hoàn toàn bằng C#. Nó không chỉ dịch code thành IL, mà còn mở API cho phép phân tích, refactor và biên dịch động.Dependency Injection
- Dùng Autofac (hoặc Unity, Ninject…) để quản lý DI.
- Tự viết SimpleMediator hoặc custom factory để resolve handler.
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterControllers(typeof(MvcApplication).Assembly);
containerBuilder.RegisterType<SimpleMediator>()
.As<IMediator>()
.As<ISender>()
.As<IPublisher>()
.SingleInstance();
containerBuilder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.AsClosedTypesOf(typeof(IRequestHandler<,>)) // IRequestHandler<TRequest,TResponse>
.InstancePerDependency();
containerBuilder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.AsClosedTypesOf(typeof(INotificationHandler<>)) // INotificationHandler<TNotification>
.InstancePerDependency();
//application library registrations
containerBuilder.RegisterAssemblyTypes(typeof(PingHandler).Assembly)
.AsClosedTypesOf(typeof(IRequestHandler<,>))
.InstancePerDependency();
var container = containerBuilder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}
}Source Generator
public class SimpleMediator : IMediator, ISender, IPublisher
{
private readonly IComponentContext _context;
public SimpleMediator(IComponentContext context)
{
_context = context;
}
public async ValueTask<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default)
{
var handlerType = typeof(IRequestHandler<,>).MakeGenericType(request.GetType(), typeof(TResponse));
dynamic handler = _context.Resolve(handlerType);
return await handler.Handle((dynamic)request, cancellationToken);
}
public async ValueTask Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default) where TNotification : INotification
{
var handlerType = typeof(INotificationHandler<>).MakeGenericType(notification.GetType());
var handlers = (System.Collections.IEnumerable)_context.Resolve(typeof(IEnumerable<>).MakeGenericType(handlerType));
foreach (dynamic handler in handlers)
{
await handler.Handle((dynamic)notification, cancellationToken);
}
}
public ValueTask<TResponse> Send<TResponse>(ICommand<TResponse> command, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public ValueTask<TResponse> Send<TResponse>(IQuery<TResponse> query, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public ValueTask<object> Send(object message, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(IStreamQuery<TResponse> query, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(IStreamRequest<TResponse> request, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(IStreamCommand<TResponse> command, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public IAsyncEnumerable<object> CreateStream(object message, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
public ValueTask Publish(object notification, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}
Kết luận
Với dự án .NET hiện đại, Mediator là lựa chọn “fresh & fast” thay thế MediatR.
Nhận xét
Đăng nhận xét