Hành vi Trung bình 6 phút đọc

Mediator Pattern trong C#: MediatR Notification và Hub

Mediator pattern trong C# / .NET 10: nhiều peer publish vào hub trung tâm thay vì gọi nhau trực tiếp, với MediatR notification là cài đặt chuẩn de facto.

Mục lục
  1. Mediator pattern giải quyết bài toán gì trong C#?
  2. MediatR cài Mediator notification thế nào?
  3. Mediator khác Observer thế nào?
  4. Khi nào Mediator pattern bắn trật?
  5. MediatR xử exception trong notification thế nào?
  6. Một ví dụ thật trong .NET 10 trông thế nào?
  7. Đọc tiếp gì trong series?

Khi order được trả tiền, bốn việc phải xảy ra: inventory phải reserve stock, warehouse phải được báo ship, customer phải nhận email xác nhận, và analytics module phải ghi doanh thu. Phiên bản đầu wire bốn cái đó vào checkout service: nó phụ thuộc IInventoryService, IShippingService, INotificationService, và IAnalytics. Sáu tháng sau, finance hỏi thêm call thứ năm (post journal entry), rồi fraud hỏi thứ sáu (re-score customer). Giờ checkout service có tám inject và team sở hữu nó đánh nhau merge conflict mỗi release.

Mediator pattern là câu trả lời. Để checkout publish một notification — OrderPaid — vào hub. Hub route nó tới các handler quan tâm. Mỗi use case mới là một handler mới trong module riêng; checkout service không đổi. Trong .NET hiện đại đây là cái IMediator.Publish từ MediatR làm, và cùng pattern có thể tự cuộn khi không muốn dependency.

Mediator pattern giải quyết bài toán gì trong C#?

Pattern xứng chỗ khi nhiều peer object phải phối hợp, nhưng không nên giữ reference trực tiếp nhau. Ba hình dạng cụ thể:

  1. Domain event. "Order paid" phải trigger inventory, shipping, email, analytics. Publisher không nên biết list subscriber.
  2. Phản ứng cross-feature. Một feature mới muốn react khi event có sẵn xảy ra, không sửa code feature có sẵn.
  3. In-process bus. Một monolith với nhiều module muốn lợi ích loose coupling kiểu message bus mà không vận hành broker thật.

Cái không phải bài toán Mediator: "Tôi muốn thêm hành vi quanh một call" — đó là Decorator. "Tôi muốn dispatch một Command tới một handler" — đó là Command (và MediatR làm cả hai cùng lúc).

MediatR cài Mediator notification thế nào?

Một notification record, một hoặc nhiều handler, và một _mediator.Publish ở source:

using MediatR;

public sealed record OrderPaid(string OrderId, string CustomerId, decimal Total)
    : INotification;

public sealed class ReserveInventory : INotificationHandler<OrderPaid>
{
    private readonly IInventoryService _inv;
    public ReserveInventory(IInventoryService inv) => _inv = inv;
    public Task Handle(OrderPaid n, CancellationToken ct)
        => _inv.ReserveForAsync(n.OrderId, ct);
}

public sealed class SendConfirmationEmail : INotificationHandler<OrderPaid>
{
    private readonly INotificationService _emails;
    public SendConfirmationEmail(INotificationService emails) => _emails = emails;
    public Task Handle(OrderPaid n, CancellationToken ct)
        => _emails.SendOrderPaidAsync(n.CustomerId, n.OrderId, ct);
}

// Composition root
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<Program>());

// Publisher
public sealed class CheckoutService
{
    private readonly IMediator _mediator;
    public CheckoutService(IMediator m) => _mediator = m;

    public async Task PaidAsync(Order order, CancellationToken ct)
    {
        // ... persist order, mark paid ...
        await _mediator.Publish(new OrderPaid(order.Id, order.CustomerId, order.Total), ct);
    }
}

Thêm phản ứng thứ sáu là một class handler mới cộng đăng ký DI mà assembly scan của MediatR tự lấy. CheckoutService tiếp tục chỉ phụ thuộc IMediator.

Bức tranh cấu trúc:

flowchart LR
    Pub[CheckoutService<br>publish OrderPaid]
    Pub --> Hub[hub IMediator]
    Hub --> H1[ReserveInventory]
    Hub --> H2[SendConfirmationEmail]
    Hub --> H3[BookShipping]
    Hub --> H4[RecordAnalytics]

Hub biết về handler; publisher chỉ biết hub. Handler mới slot vào hub; publisher không bị động.

Mediator khác Observer thế nào?

Khía cạnh Mediator Observer
Class hub Bắt buộc (component trung tâm) Không bắt buộc
Hướng Nhiều publisher tới nhiều subscriber qua hub Một publisher tới nhiều subscriber thẳng
Phát hiện subscriber Hub dùng DI / type matching Subscriber attach qua event += hoặc Subscribe
.NET điển hình IMediator.Publish của MediatR C# event, IObservable<T>, Channel<T>

Tách rõ: Observer là list subscriber riêng của publisher; Mediator kéo list đó ra hub mà mọi người dùng. Khi publisher và subscriber gắn chặt, ưu tiên Observer (event). Khi publisher không nên biết subscriber có tồn tại, ưu tiên Mediator.

Khi nào Mediator pattern bắn trật?

Ba bẫy:

MediatR xử exception trong notification thế nào?

Default, MediatR dùng ForeachAwaitPublisher: await handler theo thứ tự đăng ký; nếu một cái throw, phần còn lại không chạy. Đổi policy:

builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssemblyContaining<Program>();
    cfg.NotificationPublisherType = typeof(TaskWhenAllPublisher);
});

TaskWhenAllPublisher chạy handler song song và aggregate exception. Cho fire-and-forget, viết INotificationPublisher riêng. Luôn chọn policy tường minh — default hiếm khi là cái production muốn.

Một ví dụ thật trong .NET 10 trông thế nào?

Kết hợp notification MediatR, pipeline log kiểu Decorator từ Command, và behavior Chain of Responsibility:

public sealed record OrderPaid(string OrderId, string CustomerId, decimal Total)
    : INotification;

public sealed class ReserveInventory : INotificationHandler<OrderPaid>
{
    private readonly IInventoryService _inv;
    public ReserveInventory(IInventoryService inv) => _inv = inv;
    public Task Handle(OrderPaid n, CancellationToken ct)
        => _inv.ReserveForAsync(n.OrderId, ct);
}

public sealed class SendConfirmationEmail : INotificationHandler<OrderPaid>
{
    private readonly INotificationService _emails;
    public SendConfirmationEmail(INotificationService emails) => _emails = emails;
    public Task Handle(OrderPaid n, CancellationToken ct)
        => _emails.SendOrderPaidAsync(n.CustomerId, n.OrderId, ct);
}

public sealed class RecordAnalytics : INotificationHandler<OrderPaid>
{
    private readonly IAnalytics _analytics;
    public RecordAnalytics(IAnalytics a) => _analytics = a;
    public Task Handle(OrderPaid n, CancellationToken ct)
        => _analytics.RecordRevenueAsync(n.Total, ct);
}

// Composition root
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssemblyContaining<Program>();
    cfg.NotificationPublisherType = typeof(TaskWhenAllPublisher);
});

// Publisher
public sealed class CheckoutService
{
    private readonly IMediator _mediator;
    public CheckoutService(IMediator m) => _mediator = m;

    public async Task<CheckoutResult> CheckoutAsync(CheckoutRequest req, CancellationToken ct)
    {
        // ... domain work, rồi ...
        await _mediator.Publish(new OrderPaid(req.CartId, req.CustomerId, req.Total), ct);
        return new CheckoutResult(req.CartId, req.Total, "buyer@x.com");
    }
}

// Test
[Fact]
public async Task ReserveInventory_handles_OrderPaid()
{
    var inv = new Mock<IInventoryService>();
    var sut = new ReserveInventory(inv.Object);
    await sut.Handle(new OrderPaid("o1", "c1", 9.99m), default);
    inv.Verify(i => i.ReserveForAsync("o1", It.IsAny<CancellationToken>()));
}

Publisher không import inventory, email, hay analytics. Mỗi handler là class nhỏ với một dependency. Thêm phản ứng thứ năm (journal entry tài chính) là một file mới — và zero edit checkout service.

Đọc tiếp gì trong series?

Một ghi chú thực dụng: Mediator mua decoupling với chi phí khả năng phát hiện. Một dev junior hỏi "chuyện gì xảy ra khi publish OrderPaid?" không trả lời được bằng cách đọc publisher; họ cần biết các assembly handler sống. Bù bằng quy ước đặt tên (*Handler.cs), architecture-decision record, và test coverage tốt cho mỗi handler.

Câu hỏi thường gặp

Mediator khác Observer ở điểm gì?
Observer là một publisher tới nhiều subscriber — publisher biết có subscriber (hoặc ít nhất sở hữu event). Mediator đảo: nhiều peer publish vào hub, hub route tới handler. Observer trực tiếp, point-to-many; Mediator gián tiếp, many-to-many qua component trung tâm.
MediatR có thật là Mediator pattern không?
Đúng — MediatR kết hợp Mediator (hub IMediator cộng INotificationHandler<T> subscriber) với Command (IRequest<T> cộng IRequestHandler<T,U>). Tên library là dấu hiệu. Đa số team .NET dùng MediatR đang dùng cả hai pattern; nhận ra cái nào là cái gì làm library dễ đọc hơn.
Khi nào Mediator thêm quá nhiều gián tiếp?
Khi bạn có hai peer và một notification. Một method call thẳng ngắn hơn, dễ debug hơn, dễ test hơn. Mediator xứng đáng khi có vài peer, vài notification, và phía publish không biết hết subscriber. Nếu bạn đặt được tên mọi handler trước, method call thường đơn giản hơn.
Xử lý lỗi khi notification có nhiều handler thế nào?
Quyết policy tường minh. Default MediatR là all-or-nothing — một handler throw, lời gọi publish throw và handler còn lại có thể không chạy. Implement INotificationPublisher để chuyển sang parallel-no-wait hoặc fire-and-forget nếu handler độc lập. Tài liệu hoá policy gần type notification để người đọc sau biết hành vi nào họ ký.