MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的消息传递与MediatR框架应用

2022-10-223.0k 阅读

C# 中的消息传递基础概念

在 C# 编程领域,消息传递是一种重要的设计模式,它允许不同组件之间进行通信和交互。从本质上讲,消息传递模式模拟了现实世界中信息传递的过程,一个组件(发送者)将特定的信息(消息)发送给另一个或多个组件(接收者)。

在 C# 中,最基本的消息传递形式可以通过委托(Delegate)和事件(Event)来实现。委托是一种类型安全的函数指针,它允许将方法作为参数传递。事件则是基于委托的一种特殊机制,用于在对象状态发生变化或特定操作完成时通知其他对象。

以下是一个简单的示例,展示如何使用委托和事件实现消息传递:

// 定义委托类型
public delegate void MessageReceivedEventHandler(string message);

public class MessageSender
{
    // 定义事件
    public event MessageReceivedEventHandler MessageReceived;

    public void SendMessage(string message)
    {
        // 检查是否有订阅者
        if (MessageReceived != null)
        {
            // 触发事件,调用所有订阅者的方法
            MessageReceived(message);
        }
    }
}

public class MessageReceiver
{
    public void ReceiveMessage(string message)
    {
        Console.WriteLine($"Received message: {message}");
    }
}

在上述代码中:

  1. 首先定义了 MessageReceivedEventHandler 委托,它指定了消息接收处理方法的签名,即接收一个 string 类型的参数。
  2. MessageSender 类包含一个 MessageReceived 事件,当调用 SendMessage 方法时,如果有订阅者,就会触发该事件。
  3. MessageReceiver 类包含 ReceiveMessage 方法,该方法符合 MessageReceivedEventHandler 委托的签名,因此可以订阅 MessageSenderMessageReceived 事件。

使用示例如下:

class Program
{
    static void Main()
    {
        var sender = new MessageSender();
        var receiver = new MessageReceiver();

        // 订阅事件
        sender.MessageReceived += receiver.ReceiveMessage;

        // 发送消息
        sender.SendMessage("Hello, this is a message!");

        // 取消订阅
        sender.MessageReceived -= receiver.ReceiveMessage;
    }
}

Main 方法中,创建了 MessageSenderMessageReceiver 的实例,通过 += 操作符将 receiver.ReceiveMessage 方法订阅到 sender.MessageReceived 事件。当调用 sender.SendMessage 时,receiver.ReceiveMessage 方法会被调用。最后,可以通过 -= 操作符取消订阅。

复杂场景下委托和事件的局限性

虽然委托和事件提供了一种简单的消息传递机制,但在大型复杂应用程序中,它们存在一些局限性:

代码耦合度高

订阅者和发布者之间存在直接的引用关系。例如,在上面的例子中,MessageSender 类需要知道 MessageReceivedEventHandler 委托的具体签名,并且订阅者必须按照这个签名来实现方法。这使得代码的可维护性和可扩展性较差。如果需要修改消息的格式或处理逻辑,可能需要同时修改发布者和所有订阅者的代码。

缺乏中间层和灵活性

委托和事件的实现相对直接,没有中间层来管理消息的分发、过滤或转换。在一些场景中,可能需要对消息进行预处理,比如验证消息的合法性、对消息进行加密等,使用委托和事件实现这些功能会比较繁琐,并且代码会变得杂乱无章。

难以实现异步处理

虽然可以通过一些技巧在委托和事件中实现异步处理,但原生的委托和事件模型并没有很好地支持异步消息传递。在现代应用开发中,异步操作越来越重要,尤其是在处理高并发和 I/O 密集型任务时,委托和事件在这方面的不足愈发明显。

MediatR 框架简介

MediatR 是一个在 .NET 生态系统中广泛使用的库,旨在解决复杂应用程序中消息传递的问题。它基于中介者设计模式,通过引入一个中介者(Mediator)来处理组件之间的通信,从而降低组件之间的耦合度。

MediatR 的核心概念包括:

  1. 请求(Request):表示需要处理的特定任务或查询,通常是一个类。
  2. 处理程序(Handler):负责处理请求的类,实现了特定请求类型的处理逻辑。
  3. 中介者(Mediator):作为消息传递的中心枢纽,接收请求并将其分发给相应的处理程序。

安装和配置 MediatR

在使用 MediatR 之前,需要将其安装到项目中。可以通过 NuGet 包管理器来安装 MediatR 和 MediatR.Extensions.Microsoft.DependencyInjection(如果使用 ASP.NET Core 应用程序,后者用于依赖注入集成)。

在 ASP.NET Core 项目中,配置 MediatR 通常在 Startup.cs 文件的 ConfigureServices 方法中进行:

using MediatR;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // 注册 MediatR
        services.AddMediatR(typeof(Startup));
    }

    // 其他配置方法
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // 配置应用程序管道
    }
}

在上述代码中,services.AddMediatR(typeof(Startup)) 方法会扫描指定程序集中的所有请求和处理程序,并自动注册它们。这里传入 typeof(Startup) 是为了指定扫描的起始点,通常可以传入任何在项目中的类型,只要该类型所在的程序集包含需要注册的请求和处理程序。

MediatR 中的请求与处理程序

简单请求与处理程序示例

假设我们有一个简单的需求,获取系统当前时间。我们可以定义一个请求和相应的处理程序。

定义请求类:

public class GetCurrentTimeRequest : IRequest<DateTime>
{
}

在上述代码中,GetCurrentTimeRequest 类实现了 IRequest<DateTime> 接口,IRequest 接口是 MediatR 定义的用于标识请求的接口,泛型参数 DateTime 表示处理该请求后返回的结果类型。

定义处理程序类:

public class GetCurrentTimeRequestHandler : IRequestHandler<GetCurrentTimeRequest, DateTime>
{
    public Task<DateTime> Handle(GetCurrentTimeRequest request, CancellationToken cancellationToken)
    {
        return Task.FromResult(DateTime.Now);
    }
}

GetCurrentTimeRequestHandler 类实现了 IRequestHandler<GetCurrentTimeRequest, DateTime> 接口,Handle 方法是处理请求的具体逻辑,这里简单地返回当前时间。

使用 MediatR 发送请求:

using MediatR;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        var services = new ServiceCollection();
        services.AddMediatR(typeof(Program));

        var provider = services.BuildServiceProvider();
        var mediator = provider.GetRequiredService<IMediator>();

        var currentTime = await mediator.Send(new GetCurrentTimeRequest());
        Console.WriteLine($"Current time: {currentTime}");
    }
}

Main 方法中,首先通过 ServiceCollection 注册 MediatR,然后获取 IMediator 实例。通过调用 mediator.Send 方法发送 GetCurrentTimeRequest 请求,Send 方法会找到对应的 GetCurrentTimeRequestHandler 来处理请求并返回结果。

带有参数的请求

在实际应用中,请求通常需要携带参数。例如,我们有一个需求,根据用户 ID 获取用户信息。

定义请求类:

public class GetUserByIdRequest : IRequest<User>
{
    public int UserId { get; set; }

    public GetUserByIdRequest(int userId)
    {
        UserId = userId;
    }
}

这里 GetUserByIdRequest 类包含一个 UserId 属性用于传递用户 ID,构造函数用于初始化该属性。

定义用户类:

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    // 其他用户相关属性
}

定义处理程序类:

public class GetUserByIdRequestHandler : IRequestHandler<GetUserByIdRequest, User>
{
    private readonly List<User> _users = new List<User>
    {
        new User { Id = 1, Name = "Alice" },
        new User { Id = 2, Name = "Bob" }
    };

    public Task<User> Handle(GetUserByIdRequest request, CancellationToken cancellationToken)
    {
        var user = _users.FirstOrDefault(u => u.Id == request.UserId);
        return Task.FromResult(user);
    }
}

GetUserByIdRequestHandler 类在 Handle 方法中从模拟的用户列表中查找对应的用户。

使用示例:

class Program
{
    static async Task Main()
    {
        var services = new ServiceCollection();
        services.AddMediatR(typeof(Program));

        var provider = services.BuildServiceProvider();
        var mediator = provider.GetRequiredService<IMediator>();

        var user = await mediator.Send(new GetUserByIdRequest(1));
        if (user != null)
        {
            Console.WriteLine($"User name: {user.Name}");
        }
    }
}

在这个例子中,通过 mediator.Send 方法发送 GetUserByIdRequest 请求时传入用户 ID,处理程序根据该 ID 返回相应的用户信息。

MediatR 的通知(Notification)机制

除了请求 - 响应模式,MediatR 还提供了通知机制。通知是一种广播式的消息传递方式,一个通知可以有多个处理程序,当通知发布时,所有注册的处理程序都会被调用。

定义通知

public class UserRegisteredNotification : INotification
{
    public string UserName { get; set; }

    public UserRegisteredNotification(string userName)
    {
        UserName = userName;
    }
}

UserRegisteredNotification 类实现了 INotification 接口,表示这是一个通知。它包含一个 UserName 属性,用于传递用户注册的相关信息。

定义通知处理程序

public class SendWelcomeEmailNotificationHandler : INotificationHandler<UserRegisteredNotification>
{
    public Task Handle(UserRegisteredNotification notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Sending welcome email to {notification.UserName}...");
        // 实际发送邮件的逻辑
        return Task.CompletedTask;
    }
}

public class LogUserRegistrationNotificationHandler : INotificationHandler<UserRegisteredNotification>
{
    public Task Handle(UserRegisteredNotification notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"Logging user registration: {notification.UserName}");
        // 实际的日志记录逻辑
        return Task.CompletedTask;
    }
}

SendWelcomeEmailNotificationHandlerLogUserRegistrationNotificationHandler 类分别实现了 INotificationHandler<UserRegisteredNotification> 接口,用于处理 UserRegisteredNotification 通知。它们分别实现了发送欢迎邮件和记录用户注册日志的逻辑。

发布通知

class Program
{
    static async Task Main()
    {
        var services = new ServiceCollection();
        services.AddMediatR(typeof(Program));

        var provider = services.BuildServiceProvider();
        var mediator = provider.GetRequiredService<IMediator>();

        await mediator.Publish(new UserRegisteredNotification("John"));
    }
}

Main 方法中,通过 mediator.Publish 方法发布 UserRegisteredNotification 通知,此时所有注册的通知处理程序(SendWelcomeEmailNotificationHandlerLogUserRegistrationNotificationHandler)都会被调用。

MediatR 中的管道(Pipeline)

MediatR 的管道功能允许在请求处理前后添加额外的逻辑,例如日志记录、性能监测、异常处理等。这是通过中间件(Middleware)来实现的。

创建管道行为(Pipeline Behavior)

假设我们要创建一个用于记录请求处理时间的管道行为。

定义管道行为类:

public class RequestPerformanceLoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly ILogger<RequestPerformanceLoggingBehavior<TRequest, TResponse>> _logger;

    public RequestPerformanceLoggingBehavior(ILogger<RequestPerformanceLoggingBehavior<TRequest, TResponse>> logger)
    {
        _logger = logger;
    }

    public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
    {
        var watch = Stopwatch.StartNew();

        var response = await next();

        watch.Stop();

        _logger.LogInformation($"Request {typeof(TRequest).Name} processed in {watch.ElapsedMilliseconds} ms");

        return response;
    }
}

RequestPerformanceLoggingBehavior 类实现了 IPipelineBehavior<TRequest, TResponse> 接口,其中 TRequest 是请求类型,TResponse 是响应类型。在 Handle 方法中,通过 Stopwatch 记录请求处理的时间,并使用日志记录器记录处理时间。

注册管道行为

Startup.cs 文件的 ConfigureServices 方法中注册管道行为:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMediatR(typeof(Startup));

    services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceLoggingBehavior<,>));
}

通过 services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestPerformanceLoggingBehavior<,>)) 方法将 RequestPerformanceLoggingBehavior 注册为管道行为。这样,当任何请求通过 MediatR 处理时,都会经过这个管道行为,记录请求的处理时间。

MediatR 在分层架构中的应用

在分层架构(如三层架构:表示层、业务逻辑层、数据访问层)中,MediatR 可以很好地应用于业务逻辑层,进一步解耦不同模块之间的依赖。

例如,在一个电商应用中,业务逻辑层可能包含订单处理、库存管理等模块。通过 MediatR,不同模块之间的通信可以通过请求和通知来实现,而不需要直接引用其他模块的代码。

假设我们有一个订单创建的场景,当订单创建成功后,需要通知库存模块减少相应商品的库存。

在业务逻辑层定义订单创建请求和处理程序:

public class CreateOrderRequest : IRequest<int>
{
    // 订单相关属性
    public List<OrderItem> OrderItems { get; set; }
}

public class CreateOrderRequestHandler : IRequestHandler<CreateOrderRequest, int>
{
    private readonly IOrderRepository _orderRepository;

    public CreateOrderRequestHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public async Task<int> Handle(CreateOrderRequest request, CancellationToken cancellationToken)
    {
        // 订单创建逻辑
        var orderId = await _orderRepository.CreateOrder(request.OrderItems);

        // 发布订单创建成功通知
        // 假设已经在 MediatR 中注册了通知处理程序
        await Mediator.Publish(new OrderCreatedNotification(orderId));

        return orderId;
    }
}

在库存模块定义库存减少通知和处理程序:

public class OrderCreatedNotification : INotification
{
    public int OrderId { get; set; }

    public OrderCreatedNotification(int orderId)
    {
        OrderId = orderId;
    }
}

public class ReduceInventoryNotificationHandler : INotificationHandler<OrderCreatedNotification>
{
    private readonly IInventoryRepository _inventoryRepository;

    public ReduceInventoryNotificationHandler(IInventoryRepository inventoryRepository)
    {
        _inventoryRepository = inventoryRepository;
    }

    public async Task Handle(OrderCreatedNotification notification, CancellationToken cancellationToken)
    {
        // 根据订单 ID 获取订单详情,减少库存
        var order = await _orderRepository.GetOrderById(notification.OrderId);
        foreach (var item in order.OrderItems)
        {
            await _inventoryRepository.ReduceInventory(item.ProductId, item.Quantity);
        }
    }
}

通过这种方式,订单模块和库存模块之间通过 MediatR 进行松耦合的通信,提高了系统的可维护性和扩展性。如果需要修改库存减少的逻辑,只需要修改 ReduceInventoryNotificationHandler 类,而不会影响到订单创建的逻辑。

MediatR 的优点与应用场景总结

优点

  1. 降低耦合度:通过中介者模式,使得组件之间不需要直接引用,减少了代码的耦合度,提高了代码的可维护性和可扩展性。
  2. 灵活的消息传递:支持请求 - 响应模式和通知机制,能够满足不同类型的消息传递需求,无论是一对一的请求处理还是一对多的广播通知。
  3. 易于测试:由于请求和处理程序是分离的,并且可以通过依赖注入进行替换,使得单元测试更加容易编写。可以单独测试处理程序的逻辑,而不需要依赖整个应用程序的上下文。
  4. 管道功能强大:通过管道行为可以轻松添加横切关注点,如日志记录、性能监测、认证授权等,而不需要在每个请求处理程序中重复编写这些代码。

应用场景

  1. 企业级应用开发:在大型企业级应用中,存在多个模块之间的复杂交互,MediatR 可以有效地解耦这些模块,提高系统的整体架构质量。
  2. 微服务架构:在微服务架构中,服务之间的通信可以通过 MediatR 进行封装,使得服务之间的依赖更加清晰,易于管理和维护。
  3. 事件驱动架构:MediatR 的通知机制非常适合构建事件驱动的应用程序,当某个事件发生时,可以及时通知相关的处理程序进行响应。

通过深入理解和应用 MediatR 框架,开发人员能够在 C# 项目中构建更加灵活、可维护和可扩展的消息传递系统,从而提升整个应用程序的质量和性能。无论是小型项目还是大型企业级应用,MediatR 都能在消息传递方面提供强大的支持。