Skip to content
Go back

.NET领域事件解耦实战:手把手教你构建自定义事件分发器

Published:  at  12:00 AM

.NET领域事件解耦实战:手把手教你构建自定义事件分发器

对于追求高内聚、低耦合架构的C#/.NET开发者而言,领域事件无疑是解耦业务逻辑、提升系统可维护性的关键利器。本文将结合实际案例,深入剖析如何在.NET中实现一个轻量级的领域事件分发器,无需引入第三方库,帮助你迈出DDD与Clean Architecture落地的关键一步。

目标读者:有一定C#/.NET开发经验的软件工程师、架构师,以及关注DDD、微服务或Clean Architecture实践的中高级开发者。


引言:为什么要用领域事件?

在企业级应用开发中,业务需求的演进往往会让“核心服务”变得越来越臃肿。以用户注册为例,刚开始也许只需保存用户信息,但不久后就会接连添加“发送欢迎邮件”“埋点统计”“发放新人礼包”等功能。每加一个新功能,核心服务的代码就多耦合一个依赖,最终变得难以维护。

领域事件的好处:


正文

1️⃣ 传统做法的隐患

来看一段典型的“紧耦合”代码:

public class UserService
{
    public async Task RegisterUser(string email, string password)
    {
        var user = new User(email, password);
        await _userRepository.SaveAsync(user);

        // 直接调用邮件服务
        await _emailService.SendWelcomeEmail(user.Email);

        // 直接调用分析服务
        await _analyticsService.TrackUserRegistration(user.Id);

        // 新需求不断加入,方法越来越臃肿...
    }
}

这种写法让UserService既要关注业务核心,又要知晓所有扩展需求,一旦业务扩展,维护成本剧增。

2️⃣ 用领域事件解耦业务逻辑

借助领域事件,我们可以让UserService只负责用户注册,其他逻辑通过“发布事件”来通知感兴趣的模块:

public class UserService
{
    public async Task RegisterUser(string email, string password)
    {
        var user = new User(email, password);
        await _userRepository.SaveAsync(user);

        // 发布领域事件,其他模块订阅处理
        await _domainEventsDispatcher.DispatchAsync(
            [new UserRegisteredDomainEvent(user.Id, user.Email)]
        );
    }
}

优势一目了然:

3️⃣ 设计领域事件基础抽象

实现解耦的关键,是合理设计事件与处理器接口:

// 领域事件标记接口
public interface IDomainEvent { }

// 泛型领域事件处理器接口
public interface IDomainEventHandler<in T> where T : IDomainEvent
{
    Task Handle(T domainEvent, CancellationToken cancellationToken = default);
}

4️⃣ 实现具体的事件处理器

比如,我们需要两个不同的处理器响应“用户注册”这一事件:

// 发送欢迎邮件
internal sealed class SendWelcomeEmailHandler(IEmailService emailService)
    : IDomainEventHandler<UserRegisteredDomainEvent>
{
    public async Task Handle(UserRegisteredDomainEvent domainEvent, CancellationToken cancellationToken = default)
    {
        var welcomeEmail = new WelcomeEmail(domainEvent.Email, domainEvent.UserId);
        await emailService.SendAsync(welcomeEmail, cancellationToken);
    }
}

// 埋点分析处理器
internal sealed class TrackUserRegistrationHandler(IAnalyticsService analyticsService)
    : IDomainEventHandler<UserRegisteredDomainEvent>
{
    public async Task Handle(UserRegisteredDomainEvent domainEvent, CancellationToken cancellationToken = default)
    {
        await analyticsService.TrackEvent(
            "user_registered",
            new { user_id = domainEvent.UserId, registration_date = domainEvent.RegisteredAt },
            cancellationToken);
    }
}

5️⃣ 自动注册事件处理器

手动注册每个处理器太繁琐?可以用Scrutor自动扫描注册

services.Scan(scan => scan.FromAssembliesOf(typeof(DependencyInjection))
    .AddClasses(classes => classes.AssignableTo(typeof(IDomainEventHandler<>)), publicOnly: false)
    .AsImplementedInterfaces()
    .WithScopedLifetime());

6️⃣ 构建自定义事件分发器

核心调度器代码如下:

public interface IDomainEventsDispatcher
{
    Task DispatchAsync(IEnumerable<IDomainEvent> domainEvents, CancellationToken cancellationToken = default);
}

internal sealed class DomainEventsDispatcher(IServiceProvider serviceProvider)
    : IDomainEventsDispatcher
{
    // ... 省略字典缓存和Wrapper实现 ...

    public async Task DispatchAsync(IEnumerable<IDomainEvent> domainEvents, CancellationToken cancellationToken = default)
    {
        foreach (IDomainEvent domainEvent in domainEvents)
        {
            using IServiceScope scope = serviceProvider.CreateScope();
            Type handlerType = typeof(IDomainEventHandler<>).MakeGenericType(domainEvent.GetType());
            IEnumerable<object?> handlers = scope.ServiceProvider.GetServices(handlerType);

            foreach (object? handler in handlers)
            {
                // 利用泛型wrapper避免反射损耗,强类型调用
                var handlerWrapper = HandlerWrapper.Create(handler, domainEvent.GetType());
                await handlerWrapper.Handle(domainEvent, cancellationToken);
            }
        }
    }
}

7️⃣ 实际调用场景

在Controller中使用自定义分发器,只需:

public class UserController(
    IUserService userService,
    IDomainEventsDispatcher domainEventsDispatcher) : ControllerBase
{
    [HttpPost("register")]
    public async Task<IActionResult> Register([FromBody] RegisterUserRequest request)
    {
        var user = await userService.CreateUserAsync(request.Email, request.Password);
        var userRegisteredEvent = new UserRegisteredDomainEvent(user.Id, user.Email);

        await domainEventsDispatcher.DispatchAsync([userRegisteredEvent]);

        return Ok(new { UserId = user.Id, Message = "User registered successfully" });
    }
}

✅ 保证注册流程专注于主线,所有副作用都交由事件分发器处理。


结论与最佳实践建议

⚠️ 注意边界与权衡

领域事件不是银弹,但它是清晰分离关注点和提升系统可演化性的有力工具。


总结&互动

本文带你从零实现了一个.NET自定义领域事件分发器,并结合实际场景解析了其设计理念、使用方式及适用边界。希望你能把这个模式灵活地应用到自己的DDD或Clean Architecture项目中。

🤔 你在实际开发中遇到过哪些“业务膨胀”或“难以扩展”的痛点?
💬 欢迎在评论区留言分享你的经验,或提出关于领域事件实现与架构演进的问题!
🚀 如果觉得本文有启发,不妨分享给你的同事或团队,一起打造更优雅的.NET系统!


更多精彩内容:



Previous Post
ASP.NET Core 9.0 静态文件处理新特性全解:MapStaticAssets 与 UseStaticFiles 对比
Next Post
利用Windows AI和OCR功能:从图片到可编辑表格的高效实践