首页 > 其他分享 >MediatR:EF Core中发布领域事件

MediatR:EF Core中发布领域事件

时间:2022-10-14 16:48:45浏览次数:55  
标签:Core 实体类 MediatR EF 领域 事件 context public user

领域事件大部分发生在领域模型的业务逻辑方法上或者领域服务上,我们可以在一个领域事件发生的时候立即调用IMediatorPublish方法来发布领域事件。

我们一般在聚合根的实体类对象的ChangeName、构造方法等方法中发布领域事件,因为无论是应用服务还是领域服务,最终都要调用聚合根中的方法来操作聚合,我们这样做可以确保领域事件不会被漏掉。但是在实体类的业务方法中立即进行领域事件的发布可能会有如下的问题:

  • 可能存在重复发送领域事件的情况。比如,在“修改用户信息”这个应用服务操作中,我们分别调用实体类的ChangeName、ChangeAge、ChangeEmail方法修改用户的姓名、年龄和邮箱。因为每个ChangeXXX方法中都会发布“实体类被修改”的领域事件,所以领域事件的处理者就会被多次调用,这是没有必要的,其实只要发布一次“实体类被修改”的领域事件即可。
  • 领域事件发布太早。为了确保新增加的实体类能够发布“新增实体类”的领域事件,我们需要在实体类的构造方法中发布领域事件,但是有可能因为数据验证没有通过等原因,我们最终没有把这个新增的实体类保存到数据库中,这样在构造方法中过早地发布领域事件就可能导致“误报”的问题。

参考微软开源的eShopOnContainers项目中的做法,把领域事件的发布延迟到上下文保存修改时也就是实体类中只注册要发布的领域事件,然后在上下文的SaveChanges方法被调用时,我们再发布领域事件。

第1步:
领域事件是由聚合根进行管理的,因此我们定义了供聚合根进行事件注册的接口IDomainEvents。

public interface IDomainEvents
{
    //获取注册的领域事件
    IEnumerable<INotification> GetDomainEvents();
    //注册领域事件
    void AddDomainEvent(INotification notification);
    //如果领域事件不存在,则注册事件
    void AddDomainEventIfAbsent(INotification notification);
    //清除注册的领域事件
    void ClearDomainEvents();
}

为了简化实体类的代码编写,我们编写实现了IDomainEvents接口的抽象实体类BaseEntity。

public abstract class BaseEntity : IDomainEvents
{
    public List<INotification> DomainEvents = new();
    public void AddDomainEvent(INotification notification)
    {
        DomainEvents.Add(notification);
    }
    public void AddDomainEventIfAbsent(INotification notification)
    {
        if (!DomainEvents.Contains(notification))
        {
            DomainEvents.Add(notification);
        }
    }
    public void ClearDomainEvents()
    {
        DomainEvents.Clear();
    }
    public IEnumerable<INotification> GetDomainEvents()
    {
        return DomainEvents;
    }
}

第2步:
我们需要在上下文中保存数据的时候发布注册的领域事件。在DDD中,每个聚合都对应一个上下文,因此项目中的上下文类非常多。为了简化上下文代码的编写,我们编写BaseDbContext类,将在SaveChanges中发布领域事件的代码封装到这个类中。

public abstract class BaseDbContext : DbContext
{
    private IMediator _mediator;
    public BaseDbContext(DbContextOptions options, IMediator mediator) 
        : base(options)
    {
        _mediator = mediator;
    }
    public override int SaveChanges(bool acceptAllChangesOnSucess)
    {
        //在项目中强制要求不能使用同步方法,因此对SaveChanges的调用抛出异常。
        throw new NotImplementedException("未调用SaveChanges");
    }
    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());
        //在调用父类的SaveChangesAsync方法保存修改之前,
        //我们把所有实体类中注册的领域事件发布出去
        foreach (var domainEvent in domainEvents)
        {
            await _mediator.Publish(domainEvent);
        }
        return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
    }
}

至此,我们完成了EF Core中简化领域事件发布的几个接口和抽象类的开发。

第3步:
接下来,我们编写用来测试的实体类和上下文。首先我们编写代表用户的实体类User。

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() { }

    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 ChangeNickName(string? value)
    {
        this.NickName = value;
        AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
    }
    public void ChangeAge(int value)
    {
        this.Age = value;
        AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
    }
    public void ChangeEmail(string value)
    {
        this.Email = value;
        AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
    }
    public void SoftDelete()
    {
        this.IsDeleted = true;
        AddDomainEvent(new UserSoftDeletedEvent(Id));
    }
}

public record UserAddedEvent(User Item) : INotification;
public record UserUpdatedEvent(Guid Id) : INotification;
public record UserSoftDeletedEvent(Guid Id) : INotification;

我们在有参构造方法中,AddDomainEvent发布了UserAddedEvent领域事件,这样当我们创建新的实体类并且保存修改的时候,这个领域事件就会被发布。但是如果EF Core从数据库中加载已有数据的时候,也执行有参构造方法,就会导致在加载数据的时候也发布UserAddedEvent领域事件,这就发生逻辑错误了,因此我们提供了一个无参构造方法供EF Core从数据库中加载数据时使用。

因为我们可能连续调用ChangeNickName、ChangeAge等方法,所以我们通过AddDomainEventIfAbsent注册领域事件,从而避免消息的重复发布。

第4步:
接下来,我们编写事件处理类来对这些领域事件进行处理。首先我们编写响应UserAddedEvent领域事件,然后向用户发送注册邮件的NewUserSendEmailHandler类。

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.User;
        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.FindAsync(notification.Id);
        logger.LogInformation($"通知用户{user.Email}的信息被修改");
    }
}

因为UserUpdatedEvent中只包含被修改用户的标识符,所以我们通过FindAsync获取被修改用户的详细信息。因为FindAsync会首先从上下文的缓存中获取对象,而修改操作之前被修改的对象已经存在于缓存中了,所以用FindAsync不仅能够获取还没有提交到数据库的对象,而且由于FindAsync操作不会再到数据库中查询,因此程序的性能更高。

最后:
我们编写一个控制器类UsersController来执行用户新增、用户修改等操作。

[Route("api/[controller]/[action]")]
[ApiController]
public class UsersController : ControllerBase
{
    private UserDbContext context;
    public UsersController(UserDbContext context)
    {
        this.context = context;
    }

    [HttpPost]
    public async Task<IActionResult> Add(AddUserRequest req)
    {
        var user = new User(req.UserName, req.Email);
        context.Users.Add(user);
        await context.SaveChangesAsync();
        return Ok();
    }

    [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();
    }

    [HttpDelete]
    [Route("id")]
    public async Task<IActionResult> Delete(Guid id)
    {
        User? user = context.Users.Find(id);
        if (user == null)
        {
            return NotFound($"id={id}的User不存在");
        }
        user.SoftDelete();
        await context.SaveChangesAsync();
        return Ok();
    }
}

运行后,可以看到,UserAddedEvent和UserUpdatedEvent两个领域事件的事件处理者的代码都执行了。在UsersController中,我们调用了多个ChangeXXX方法,这些方法都通过AddDomainEventIfAbsent方法向聚合根中注册领域事件,只有一个领域事件注册成功了,因此在修改用户的时候,UserUpdatedEvent事件只被发布了一次。

控制器中的方法Update是一个典型的应用服务。我们在ChangeAge等领域方法中只修改数据,并不会立即把修改保存到数据库中,因为只有应用服务才是最终面对用户请求的地方,只有应用服务才知道什么时候把对数据的修改保存到数据库中。我们调用context.SaveChangesAsync标志工作单元的结束,由于我们在此方法中把发布领域事件的代码放到了调用父类上下文的SaveChangesAsync方法之前,而领域事件的处理者的代码也是同步运行的,因此领域事件的处理者的代码也会在把上下文中模型的修改保存到数据库之前执行,这样所有的代码都在同一个数据库事务中执行,就构成了一个强一致性的事务。

本文学习参考自:ASP.NET Core技术内幕与项目实战

标签:Core,实体类,MediatR,EF,领域,事件,context,public,user
From: https://www.cnblogs.com/nullcodeworld/p/16792021.html

相关文章

  • asp.net core 使用转外部对象为Controller
    应用程序部件 MSDN说明:应用程序部件是对应用资源的抽象化,借助应用程序部件,ASP.NETCore可以发现控制器、视图组件、标记帮助程序、RazorPages、Razor编译源等。核......
  • CefSharp 谷歌浏览器控件——弹窗与右键
    虽然能用CefSharp将web应用嵌入到窗体程序,但遇到<a>标签(_blank)会弹出一个窗口,而且在任意位置可以弹出右键菜单,需要解决这个问题才像一个真正的客户端软件。不弹子窗体......
  • .Net Core Mvc (CRUD)Demo
    目录目录顺手联系了个DEMO控制器Index页面Add页面修改页面最后效果顺手联系了个DEMO控制器usingSystem;usingSystem.Collections.Generic;usingSystem.Dynamic;u......
  • Codeforces Round #825 (Div. 2)(补题中)
    战绩:A.MakeAEqualtoB 实际上就只有两种操作可能,一种是遇到了不同的位置直接换,一种是换出0和1一样的个数后重新排列顺序,两种操作比较最小值输出。intmain(){......
  • 异步编程利器:CompletableFuture详解
    前言最近刚好使用CompeletableFuture优化了项目中的代码,所以跟大家一起学习CompletableFuture。公众号:捡田螺的小男孩一个例子回顾Future因为CompletableFuture实现了​​......
  • 【EF Core 6.0 】实体映射table-- 实体特性(Data Annonation 标注)&Fluent Api
    usingSystem.Data.Entity.ModelConfiguration.Configuration;namespaceCodeFirst_FluentAPI_Tutorials{publicclassSchoolContext:DbContext{......
  • 【EF Core 6.0 】开篇
    概览EFCore6.0是建立在ADO.NET框架之上的,它下面仍旧使用了ADO.NET方法和类来执行数据操作。DbContext负责将sqlite翻译成sqlite,跟踪数据状态。EFCore 6.0底层是Mis......
  • 22.10.14 codeforce D
    题目D.Coprime给定n个正整数数组a1,a2,…,an(1≤ai≤1000)。求i+j的最大值,使ai和aj为互素,†或−1(如果不存在i,j)。例如,考虑数组[1,3,5,2,4,7,7]。i+j可以得到的最大值......
  • Codeforces Round #827 (Div. 4) A - G
    A.Sumvoidsolve(){inta[3]={};cin>>a[0]>>a[1]>>a[2];sort(a,a+3);if(a[2]==a[0]+a[1])cout<<"YES\n";elsecout<<"NO......
  • CSharp: null object pattern in donet core 3
     ///<summary>///空对象模式nullobjectpattern///geovin,GeovinDueidt///</summary>interfaceIVehicle{voidTrave......