9.2领域事件

举报
步步为营 发表于 2023/02/23 15:35:18 2023/02/23
【摘要】 领域(近似理解为实现某个功能的多个模型)事件可以切断领域模型之间的强依赖关系,事件发布后,由事件的处理者决定如何响应事件,以便于实现事件发布和事件处理的解耦。

9.2领域事件

基本使用

领域(近似理解为实现某个功能的多个模型)事件可以切断领域模型之间的强依赖关系,事件发布后,由事件的处理者决定如何响应事件,以便于实现事件发布和事件处理的解耦。

MediatR可实现进程内事件的传递,支持一对一和一对多,使用步骤如下:

  1. NuGet安装MediatR.Extensions.Microsoft.DependencyInjection

  2. 在Program.cs中调用AddMediatR方法进行注册,参数为事件处理者所在的程序集

builder.Services.AddMediatR(Assembly.Load("用MediatR实现领域事件"));

  1. 定义在发布者和处理者之间进行数据传递的类,即消息类型。该类需要实现INotification接口

public record TestEvent(string UserName) : INotification;

  1. 事件处理者要实现NotificationHandler<TNotification>接口,泛型参数代表的是要处理的消息类型
public class TestEventHandler1 : INotificationHandler<TestEvent>
{
    public Task Handle(TestEvent notification, CancellationToken cancellationToken)
    {
        Console.WriteLine($"我收到了{notification.UserName}");
        return Task.CompletedTask;
    }
}
public class TestEventHandler2 : INotificationHandler<TestEvent>
{
    public async Task Handle(TestEvent notification, CancellationToken cancellationToken)
    {
        await File.WriteAllTextAsync("d:/1.txt", $"来了{notification.UserName}");
    }
}
  1. 在需要发布事件的类中注入IMediator类型的服务,调用Publish方法来发布一对多事件,Send用来发布一对一事件。
[Route("api/[controller]/[action]")]
[ApiController]
public class TestController : ControllerBase
{
    private readonly IMediator mediator;

    public TestController(IMediator mediator)
    {
        this.mediator = mediator;
    }

    [HttpPost]
    public async Task<IActionResult> Login(LoginRequest req)
    {
        //不要写成Send
        //如果使用await那么要等所有的Handle方法执行完才能继续执行
        await mediator.Publish(new TestEvent(req.UserName));
        return Ok("ok");
    }
}

EF Core中发布领域事件

我们一般在操作EF Core的changeName、构造方法等方法中调用IMediator的publish方法来发布领域事件,但是在这些方法中立即处理发布的事件会有以下问题:

  • 可能存在重复发送领域事件的情况。如分别调用changeName,changeAge方法进行修改,由于每个changeXXX都会发布“实体类被修改”的事件,到导致出多次处理事件,其实只需最后执行一次就可以。
  • 领域事件发布的太早。为了能够发布“新增实体类”的领域事件,我们一般在实体类的构造方法中发布领域事件,但可能存在数据验证没有通过等等的原因最终没有将新增实体类保存在数据库,那就会出现了事件发布过早的错误问题。

**解决方法:**把领域事件的发布延迟到上下文修改时,即在实体类中仅仅是注册领域事件,而在上下文中的SaveChanges方法中发布事件。

实现步骤:

  1. 为了方便实体类关于领域事件的管理,定义接口
public interface IDomainEvents
{
    IEnumerable<INotification> GetDomainEvents();//获得注册的领域事件
    void AddDomainEvent(INotification eventItem);//注册领域事件
    void AddDomainEventIfAbsent(INotification eventItem);//如果领域事件不存在,则注册事件
    void ClearDomainEvents();//清除领域事件
}
  1. 为了简化实体类的编写,定义实体类的抽象类,该抽象类要实现自定义的IDomainEvents接口
public abstract class BaseEntity : IDomainEvents
{
    private List<INotification> DomainEvents = new();

    public void AddDomainEvent(INotification eventItem)
    {
        DomainEvents.Add(eventItem);
    }

    public void AddDomainEventIfAbsent(INotification eventItem)
    {
        if (!DomainEvents.Contains(eventItem))
        {
            DomainEvents.Add(eventItem);
        }
    }

    public void ClearDomainEvents()
    {
        DomainEvents.Clear();
    }

    public IEnumerable<INotification> GetDomainEvents()
    {
        return DomainEvents;
    }
}

  1. 需要在上下文中保存数据的时候发布注册的领域事件,为了简化上下文代码的编写,声明上下文抽象类
public abstract class BaseDbContext : DbContext
{
    private IMediator mediator;//依赖注入

    public BaseDbContext(DbContextOptions options, IMediator mediator) : base(options)
    {
        this.mediator = mediator;
    }
	//强制不能使用同步方法
    public override int SaveChanges(bool acceptAllChangesOnSuccess)
    {
        throw new NotImplementedException("Don not call SaveChanges, please call SaveChangesAsync instead.");
    }
	//重写父类的方法
    public async override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
    {	//ChangeTracker是上下文用来对实体类变化进行追踪的对象
        //Entries<IDomainEvents>获得所有实现了IDomainEvents接口的实体类
        //只要包含了领域事件的所有实体
        var domainEntities = this.ChangeTracker.Entries<IDomainEvents>()
                        .Where(x => x.Entity.GetDomainEvents().Any());
        //获得所有实体的所有领域事件
        var domainEvents = domainEntities
            .SelectMany(x => x.Entity.GetDomainEvents()).ToList();
        //清除所有实体类中的所有领域事件,因为执行完了就要在集合中清除
        domainEntities.ToList()
            .ForEach(entity => entity.Entity.ClearDomainEvents());
        //发布所有的领域事件,要放到代用父类的SaveChangeAsync之前
        //因为这样事件的处理代码会在上下文模型修改保存之前执行
        foreach (var domainEvent in domainEvents)
        {
            await mediator.Publish(domainEvent);
        }
        //调用父类方法
        return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }
}
  1. 编写传递领域事件的类
public record UserUpdatedEvent(Guid Id):INotification;
public record UserAddedEvent(User Item):INotification;
  1. 编写实体类
public class User: BaseEntity
{
    public Guid Id { get; init; }
    public string UserName { get; init; }
    public string Email { get; private set; }
    public string? NickName { get; private set; }
    public int? Age { get; private set; }
    public bool IsDeleted { get; private set; }
    private User()
    {
        //提供无参构造方法。避免EF Core加载数据的时候调用有参的构造方法触发领域事件
    }
    public User(string userName,string email)
    {
        this.Id = Guid.NewGuid();
        this.UserName = userName;
        this.Email = email;
        this.IsDeleted = false;
        //构造方法中注册增加用户事件,放到集合中
        AddDomainEvent(new UserAddedEvent(this));
    }
    public void ChangeEmail(string value)
    {
        this.Email = value;
        //避免重复注册
        AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
    }
    public void ChangeNickName(string? value)
    {
        this.NickName = value;
        AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
    }
    public void ChangeAge(int value)
    {
        this.Age = value;
        AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
    }
}
  1. 实体上下文类继承自上面的BaseDbContext
  2. 编写事件处理类
public class NewUserSendEmailHandler : INotificationHandler<UserAddedEvent>
{
    private readonly ILogger<NewUserSendEmailHandler> logger;

    public NewUserSendEmailHandler(ILogger<NewUserSendEmailHandler> logger)
    {
        this.logger = logger;
    }

    public Task Handle(UserAddedEvent notification, CancellationToken cancellationToken)
    {
        var user = notification.Item;
        logger.LogInformation($"向{user.Email}发送欢迎邮件");
        return Task.CompletedTask;
    }
}

public class ModifyUserLogHandler : INotificationHandler<UserUpdatedEvent>
{
    private readonly UserDbContext context;
    private readonly ILogger<ModifyUserLogHandler> logger;

    public ModifyUserLogHandler(UserDbContext context, ILogger<ModifyUserLogHandler> logger)
    {
        this.context = context;
        this.logger = logger;
    }

    public async Task Handle(UserUpdatedEvent notification, CancellationToken cancellationToken)
    {
        //var user = await context.Users.SingleAsync(u=>u.Id== notification.Id);
        var user = await context.Users.FindAsync(notification.Id);
        logger.LogInformation($"通知用户{user.Email}的信息被修改");
    }
}

  1. 在控制器中使用user的增删改查
[HttpPut]
[Route("{id}")]
public async Task<IActionResult> Update(Guid id,UpdateUserRequest req)
{
    User? user = context.Users.Find(id);
    if (user==null)
    {
        return NotFound($"id={id}的User不存在");
    }
    user.ChangeAge(req.Age);
    user.ChangeEmail(req.Email);
    user.ChangeNickName(req.NickName);
    await context.SaveChangesAsync();
    return Ok();
}
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。