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,转转请注明出处:https://www.zhengchaoqiang.com/1642.html