.Net Core 教程 Part6 – (16)(17)(17)(18) DDD落地之领域事件

Part6 – (16) DDD聚合在.NET中的实现

工作单元的实现

1、复习:什么是UnitOfWork(工作单元)。

2、EFCore的DbContext:跟踪对象状态的改变;SaveChanges把所有的改变一次性地提交到数据库中,是一个事务。因此DbContext是天然的UoW实现

3、需要把DbContext再封装一次UoW吗?两种观点的分析。

观点1: 可能以后有切换其他ORM的可能,所以把DbContext封装起来。

观点2: 不需要封装,直接用DbContext就可以了,DbContext是天然的UoW实现

聚合与聚合根的实现

即使一个实体类型没有声明对应的DbSet类型的属性,只要EF Core遇到实体对象,EF Core仍然会像对待其他实体对象一样处理。

因此我们可以在上下文中只为聚合根实体声明DbSet类型的属性。对非聚合根实体、值对象的操作都通过根实体进行。

跨聚合只能引用根实体的Id,而不是根实体对象。

聚合与DbContext的关系

1、如果一个微服务中有多个聚合根,那么是每个聚合根的实体放到一个单独的上下文中,还是把所有实体放到同一个上下文中?各自的优缺点是什么?

2、我为什么倾向于后者?它们之间的关系仍然比它们和其他微服务中的实体关系更紧密,而且我们还会在应用服务中进行跨聚合的组合操作进行联合查询的时候可以获得更好的性能,也能更容易实现强一致性的事务

区分聚合根实体和其他实体

定义一个不包含任何成员的标识接口,比如IAggregateRoot,然后要求所有的聚合根实体类都实现这个接口。

跨表查询

1、所有跨聚合的数据查询都应该是通过领域服务的协作来完成的,而不应该是直接在数据库表之间进行join查询。会有性能损失,需要做权衡,不是死规矩。

2、对于统计、汇总等报表类的应用,则不需要遵循聚合的约束,可以通过执行原生SQL等方式进行跨表的查询。

跨聚合进行实体引用,只能引用根实体,并且只能应用根实体的Id,而不是根实体对象。便于以后进行微服务的拆分。

public DbSet<Order> Orders {get; set;}
public DbSet<Mechan> Mechans{get; set;}

Internal class OrderDetail
{
  public int Id { get; set; }
  public Order Order { get; set; }// 使用Id
  public string Name { get; set; }
  //public Merchan Merchan { get; set; }// 使用Id
  public long MerchanId { get; set; }
  public int Count{ get; set; }
}

// 不建议 Join查询 
//select m.Name, m.Price, d.Count from OrderDetails d join Merchan m on d.MerchanId = m.Id
//建议按照Id查询 方便以后拆解微服务
var details=ctx.Order.SelectMany(o=>o.Details)
foreach(var d in details)
{
  long merchanId = d.MerchanId;
}
//未拆分微服务前
var m = ctx.Mercvhans.Single(m=>m.Id=merchanId);
//拆分微服务后
var m= 微服务调用获取商品信息(merchan)
WriteLine(m.Name+","+m.Price+","+d.Count);

实现实体不要面向数据库建模

1、建模的时候不要先考虑实体在数据库中如何保存。比如实体类和数据表具有直接的对应关系,实体类中属性和数据表中的列几乎完全一致。这样设计出来的类称不上“实体类”,只能被成为数据对象(Data Object)。更不要用DB First(反向工程)。

2、应该不考虑数据库实现的情况下进行领域模型建模,然后再使用Fluent API等对实体类和数据库之间做适配。在实现的时候,可能需要对建模进行妥协性修改,但是这不应该在最开始被考虑。

Part6 – (17) 用MediatR实现领域事件

领域事件的实现选型

1、复习:什么是领域事件?进程内……

2、实现方式1:C#的事件机制。

var bl = new ProcessBusinessLogic();
bl.ProcessCompleted += bl_ProcessCompleted;
bl.StartProcess();

缺点:需要显式地注册。

3、实现方式2:进程内消息传递的开源库MediatR。事件的发布和事件的处理之间解耦。MediatR中支持“一个发布者对应一个处理者”和“一个发布者对应多个处理者”这两种模式。

MediatR用法

1、创建一个ASP.NET Core项目,NuGet安装MediatR.Extensions.Microsoft.DependencyInjection

2、Program.cs中调用AddMediatR()

3、定义一个在消息的发布者和处理者之间进行数据传递的类,这个类需要实现INotification接口。一般用record类型。

4、消息的处理者要实现NotificationHandler<TNotification>接口,其中的泛型参数TNotification代表此消息处理者要处理的消息类型。

5、在需要发布消息的的类中注入IMediator类型的服务,然后我们调用Publish方法来发布消息。Send()方法是用来发布一对一消息的,而Publish()方法是用来发布一对多消息的。

using MediatR;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMediatR(Assembly.Load("用MediatR实现领域事件"));//指定程序集
// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
using MediatR;

namespace 领域事件1
{
    public record TestEvent(string UserName) : INotification;
}
//发布消息
using MediatR;
using Microsoft.AspNetCore.Mvc;

namespace 领域事件1.Controllers
{
    [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 mediator.Publish(new TestEvent(req.UserName));
            return Ok("ok");
        }
    }
}
//接受消息
using MediatR;

namespace 领域事件1
{
    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}");
        }
    }
}

Part6 – (18) EF Core中发布领域事件的时机

领域事件的时机1

1、在聚合根的实体对象的ChangeName()、构造方法等方法中立即发布领域事件,因为无论是应用服务还是领域服务,最终要调用聚合根中的方法来操作聚合,我们这样做可以确保领域事件不会被漏掉。

2、缺点:

1)存在重复发送领域事件的情况;

2)领域事件发布的太早:在实体类的构造方法中发布领域事件,但是有可能因为数据验证没通过等原因,我们最终没有把这个新增的实体保存到数据库中,我们这样在构造方法中过早地发布领域事件就会导致“误报”

领域事件的时机2

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

2、供聚合根进行事件注册的接口IDomainEvents

Zack.DomainCommons/Models/IDomainEvents.cs
public interface IDomainEvents
{
	IEnumerable<INotification> GetDomainEvents();
	void AddDomainEvent(INotification eventItem);
	void AddDomainEventIfAbsent(INotification eventItem);
	void ClearDomainEvents();
}

3、简化IDomainEvents 实现的父类BaseEntity.cs

using MediatR;
using System.Collections.Generic;

namespace Zack.DomainCommons.Models
{
    public interface IDomainEvents
    {
        IEnumerable<INotification> GetDomainEvents();
        void AddDomainEvent(INotification eventItem);
        /// <summary>
        /// 如果已经存在这个元素,则跳过,否则增加。以避免对于同样的事件触发多次(比如在一个事务中修改领域模型的多个对象)
        /// </summary>
        /// <param name="eventItem"></param>
        void AddDomainEventIfAbsent(INotification eventItem);
        public void ClearDomainEvents();
    }
}
using MediatR;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace Zack.DomainCommons.Models
{
    public record BaseEntity : IEntity, IDomainEvents
    {

        [NotMapped]
        private List<INotification> domainEvents = new();

        public Guid Id { get; protected set; } = Guid.NewGuid();

        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;
        }
    }
}
using MediatR;

namespace 领域事件发布的时机1
{
    public record UserAddedEvent(User Item):INotification;
    public record UserSoftDeletedEvent(Guid Id):INotification;
    public record UserUpdatedEvent(Guid Id):INotification;
}
namespace 领域事件发布的时机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));//先注册上
        }
        public void SoftDelete()
        {
            this.IsDeleted = true;
            AddDomainEvent(new UserSoftDeletedEvent(Id));//先注册上
        }
    }
}
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace 领域事件发布的时机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)
        {
            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());
            foreach (var domainEvent in domainEvents)
            {
                await mediator.Publish(domainEvent);
            }base.SaveChangesAsync
            //把消息的发布放到base.SaveChangesAsync之前,可以保证领域时间响应代码中的事务操作和base.SaveChangesAsync中的代码在同一个事务中,实现强一致性事务
            return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
        }
    }
}
using MediatR;
using Microsoft.EntityFrameworkCore;

namespace 领域事件发布的时机1
{
    public class UserDbContext: BaseDbContext
    {
        public DbSet<User> Users { get; private set; }

        public UserDbContext(DbContextOptions<UserDbContext> options, 
            IMediator mediator): base(options, mediator)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
        }
    }
}
using MediatR;

namespace 领域事件发布的时机1.Events
{
    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;
        }
    }
}
using MediatR;

namespace 领域事件发布的时机1.Events
{
    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}的信息被修改");
        }
    }
}

本文版权归Chaoqiang's Blog站点所有,发布者:chaoqiang,转转请注明出处:https://www.zhengchaoqiang.com/1642.html

(2)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-04 14:37
下一篇 2022-04-04 20:58

相关推荐

发表回复

您的电子邮箱地址不会被公开。

近期个人博客正在迁移中,原博客请移步此处,抱歉!