Chuyển đến nội dung chính

Mediator trong .NET: Từ MediatR đến martinothamar/Mediator

Mediator Pattern là gì?

Mediator là một mẫu thiết kế hành vi (behavioral design pattern) giúp giảm sự phụ thuộc trực tiếp và phức tạp giữa các thành phần trong một hệ thống.

Hãy hình dung một hệ thống phức tạp mà không có Mediator như một mạng lưới các đối tượng giao tiếp trực tiếp với nhau. Khi số lượng đối tượng tăng lên, số lượng kết nối và sự phụ thuộc sẽ tăng theo cấp số nhân, tạo ra một kiến trúc rối rắm, khó quản lý, thường được gọi là "spaghetti code".

Mediator pattern giải quyết vấn đề này bằng cách giới thiệu một đối tượng trung gian (Mediator). Thay vì giao tiếp trực tiếp, tất cả các thành phần sẽ gửi yêu cầu hoặc thông báo đến Mediator. Mediator sau đó sẽ điều phối và chuyển tiếp các yêu cầu này đến (các) thành phần xử lý (handler) tương ứng. Điều này giúp các thành phần trở nên độc lập (decoupled) với nhau.

Mối Liên Hệ Giữa Mediator, CQS và CQRS

Để hiểu sâu hơn về sức mạnh và ứng dụng của Mediator pattern, chúng ta cần tìm hiểu về một nguyên tắc nền tảng trong thiết kế phần mềm: Command Query Separation (CQS).

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

Mình sẽ tập trung vào những phần khác biệt so với dự án .NET
Bạn cần cài đặt thêm package DotNetCompilerPlatform để hỗ trợ Roslyn:
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

.NET Core: Có sẵn IServiceCollection + AddMediator() (do Source Generator tạo ra).

.NET Framework: Không có AddMediator, bạn phải:
  • Dùng Autofac (hoặc Unity, Ninject…) để quản lý DI.
  • Tự viết SimpleMediator hoặc custom factory để resolve handler.
Cấu hình Autofac
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

.NET Core (5+): Mediator tận dụng Mediator.SourceGenerator để tự động generate code đăng ký handler, AddMediator() sẽ hoạt động.

.NET Framework: Không hỗ trợ Source Generator. Mình phải tự viết SimpleMediator (gọi Resolve(handlerType) từ Autofac), tự đăng ký các handler thủ công hoặc quét assembly (như trên).
Implement SimpleMediator
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.

Tham khảo

CQRS là gì?

Nhận xét

Bài đăng phổ biến từ blog này

[ASP.NET MVC] Authentication và Authorize

Một trong những vấn đề bảo mật cơ bản nhất là đảm bảo những người dùng hợp lệ truy cập vào hệ thống. ASP.NET đưa ra 2 khái niệm: Authentication và Authorize Authentication xác nhận bạn là ai. Ví dụ: Bạn có thể đăng nhập vào hệ thống bằng username và password hoặc bằng ssh. Authorization xác nhận những gì bạn có thể làm. Ví dụ: Bạn được phép truy cập vào website, đăng thông tin lên diễn đàn nhưng bạn không được phép truy cập vào trang mod và admin.

ASP.NET MVC: Cơ bản về Validation

Validation (chứng thực) là một tính năng quan trọng trong ASP.NET MVC và được phát triển trong một thời gian dài. Validation vắng mặt trong phiên bản đầu tiên của asp.net mvc và thật khó để tích hợp 1 framework validation của một bên thứ 3 vì không có khả năng mở rộng. ASP.NET MVC2 đã hỗ trợ framework validation do Microsoft phát triển, tên là Data Annotations. Và trong phiên bản 3, framework validation đã hỗ trợ tốt hơn việc xác thực phía máy khách, và đây là một xu hướng của việc phát triển ứng dụng web ngày nay.

Tổng hợp một số kiến thức lập trình về Amibroker

Giới thiệu về Amibroker Amibroker theo developer Tomasz Janeczko được xây dựng dựa trên ngôn ngữ C. Vì vậy bộ code Amibroker Formula Language sử dụng có syntax khá tương đồng với C, ví dụ như câu lệnh #include để import hay cách gói các object, hàm trong các block {} và kết thúc câu lệnh bằng dấu “;”. AFL trong Amibroker là ngôn ngữ xử lý mảng (an array processing language). Nó hoạt động dựa trên các mảng (các dòng/vector) số liệu, khá giống với cách hoạt động của spreadsheet trên excel.