DDD领域驱动设计是什么
1 DDD是什么?
DDD是领域驱动设计,是Eric Evans于2003年提出的,离现在有17年。
2 为什么需要DDD
当软件越来越复杂,实际开发中,大量的业务逻辑堆积在一个巨型类中的例子屡见不鲜,代码的复用性和扩展性无法得到保证。为了解决这样的问题,DDD提出了清晰的分层架构和领域对象的概念,让面向对象的分析和设计进入了一个新的阶段,对企业级软件开发起到了巨大的推动作用。
2.1 POP,OOP,DDD
是如何解决问题
面向过程编程(POP),接触到需求第一步考虑把需求自顶向下分解成一个一个函数。并且在这个过程中考虑分层,模块化等具体的组织方式,从而分解软件的复杂度。当软件的复杂度不是很大,POP也能得到很好的效果。
面向对象编程(OOP),接触到需求第一步考虑把需求分解成一个一个对象,然后每个对象添加一个一个方法和属性,程序通过各种对象之间的调用以及协作,从而实现计算机软件的功能。跟很多工程方法一样,OOP的初衷就是一种处理软件复杂度的设计方法。
领域驱动设计(DDD),接触到需求第一步考虑把需求分解成一个一个问题域,然后再把每个问题域分解成一个一个对象,程序通过各种问题域之间的调用以及协作,从而实现计算机软件的功能。DDD是解决复杂中大型软件的一套行之有效方式,现已成为主流。
2.2 POP,OOP,DDD的特点
POP,无边界,软件复杂度小适用,例如“盖房子”。
OOP,以“对象”为边界,软件复杂度中适用,例如“盖小区”。
DDD,以“问题域”为边界,软件复杂度大适用,例如“盖城市”。
3 DDD的分层架构和构成要素
3.1 分层架构
整个架构分为四层,其核心就是领域层(Domain
),所有的业务逻辑应该在领域层实现,具体描述如下:
用户界面/展现层,负责向用户展现信息以及解释用户命令。
应用层,定义软件要完成的作业并指导富有表现力的领域对象解决问题。 这一层负责执行对业务具有意义的任务或与其他系统的应用层进行交互时需执行的任务。 这一层很“薄”。 它不包含业务规则或知识,仅针对下一层中领域对象之间的协作,协调任务和委派工作。 它不具有反映业务状况的状态,但它可以具有状态,用于反映用户或程序的任务的进度。
.NET 中微服务的应用层通常被编码为 ASP.NET Core Web API 项目。 该项目实现微服务的交互、远程网络访问和从 UI 或客户端应用中使用的外部 Web API。 它包括查询(如果使用 CQRS
方法)、微服务接受的命令,甚至是微服务之间的事件驱动的通信(集成事件)。 表示应用程序层的 ASP.NET Core Web API 不能包含业务规则或领域知识(尤其是用于事务或更新的域规则);这些规则和知识应由域模型类库所有。 应用层须只能协调任务,不能保有或定义任何领域状态(领域模型)。 它将业务规则的执行委托给领域模型类自身(聚合根和领域实体),最终在这些领域实体内更新数据。
领域层,负责表示业务概念、有关业务状况的信息和业务规则。 反映业务状况的状态是通过这个层进行控制和利用的,但有关状态存储的具体技术细节则由基础结构负责实施。 这一层是业务软件的核心。
领域层是表述业务的地方。 在 .NET 中实现微服务领域层时,该层被编码为类库,具有用于捕获数据和行为(方法和逻辑)的领域实体。
领域层实体不应在任何数据访问基础结构框架(如 Entity Framework
或 NHibernate
)上具有任何直接的依赖关系(如派生自基类)。 理想情况下,域实体不应派生自或实现任何在基础结构框架中定义的类型。
基础设施层,基础设施层是关于如何将最初存放在域实体中的数据(内存中)持久保存在数据库或另一个持久性存储区中。 一个例子是使用 Entity Framework Core
代码实现存储库模式类,该类使用 DBContext
将数据持久保存在关系数据库中。
3.2 构成要素
实体(Entity
),具备唯一ID,能够被持久化,具备业务逻辑,对应现实世界业务对象。
值对象(Value Object
),不具有唯一ID,由对象的属性描述,一般为内存中的临时对象,可以用来传递参数或对实体进行补充描述。
领域服务(Domain Service
),为上层建筑提供可操作的接口,负责对领域对象进行调度和封装,同时可以对外提供各种形式的服务。
聚合根(Aggregate Root
),聚合根属于实体对象,聚合根具有全局唯一ID,而实体只有在聚合内部有唯一的本地ID,值对象没有唯一ID
工厂(Factories
),主要用来创建聚合根,目前架构实践中一般采用IOC
容器来实现工厂的功能。
仓储(Repository
),封装了基础设施来提供查询和持久化聚合操作。
4 小结
通过本文介绍,我们了解DDD是为解决软件复杂性而诞生,与OOP最大的区别就是划分边界的方式不一样,所以DDD本身掌握起来并不会感觉复杂,DDD其实是研究将包含业务逻辑的ifelse
语句放在哪里的学问。
DDD领域驱动设计:CQRS
1 前置阅读
在阅读本文章之前,你可以先阅读:
- DDD领域驱动设计是什么
- DDD领域驱动设计:实体、值对象、聚合根
- DDD领域驱动设计:仓储
MediatR
一个优秀的.NET中介者框架
2 什么是CQRS
?
CQRS
,即命令和查询职责分离,是一种分离数据读取与写入的体系结构模式。 基本思想是把系统划分为两个界限:
- 查询,不改变系统的状态,且没有副作用。
- 命令,更改系统状态。
我们通过Udi Dahan的《Clarified CQRS》文章中的图来介绍一下:
2.1 查询 (Query
)
上图中,可以看到Query
不是通过DB来查询,而是通过一个专门用于查询的Cache
(或ReadDB
),ReadDB
中的表是专门针对UI优化过的,例如最新的产品列表,销量最好的产品列表等,基本属于用空间换时间。
2.2 命令 (Command
)
上图中,Command
类似于Application Service
,Command
中主要做的事情有两个:
1、通过调用领域层,把相关业务数据写入到DB中。
2、同时更新ReadDB
。
2.3 领域事件 (Domain Event
)
上图中,更新ReadDB
有两种方式,一种是直接在Command
中进行更新,还有一种监听领域事件,把相应更改的数据同步到ReadDB
中。
3 如何实现CQRS?
我们在这里使用最简单的方法:只将查询与命令分离,且执行这两种操作时使用相同的数据库。
3.1 命令 (Command
)
首先,命令类
命令是让系统执行更改系统状态的操作的请求。 命令具有命令性,且应仅处理一次。
由于命令具有命令性,所以通常采用命令语气使用谓词(如“create
”或“update
”)命名,命令可能包括聚合类型,例如 CreateTodoCommand
与事件不同,命令不是过去发生的事实,它只是一个请求,因此可以拒绝它。
命令可能源自 UI,由用户发出请求而产生,也可能来自进程管理器,由进程管理器指导聚合执行操作而产生。
命令的一个重要特征是它应该由单一接收方处理,且仅处理一次。 这是因为命令是要在应用程序中执行的单个操作或事务。 例如,同一个“创建待办事项”的处理次数不应超过一次。 这是命令和事件之间的一个重要区别。 事件可能会经过多次处理,因为许多系统或微服务可能会对该事件感兴趣。
命令通过包含数据字段或集合(其中包含执行命令所需的所有信息)的类实现。 命令是一种特殊的数据传输对象 (DTO),专门用于请求更改或事务。 命令本身完全基于处理命令所需的信息,别无其他。
下面的示例显示了简化的 CreateTodoCommand
类。
public class CreateTodoCommand : IRequest<TodoDTO>
{
public Guid Id { get; set; }
public string Name { get; set; }
}
然后,命令处理程序类
应为每个命令实现特定命令处理程序类。 这是该模式的工作原理,是应用命令对象、域对象和基础结构存储库对象的情景。
命令处理程序收到命令,并从使用的聚合获取结果。 结果应为成功执行命令,或者异常。 出现异常时,系统状态应保持不变。
命令处理程序通常执行以下步骤:
- 它接收
DTO
等命令对象。 - 它会验证命令是否有效。
- 它会实例化作为当前命令目标的聚合根实例。
- 它会在聚合根实例上执行方法,从命令获得所需数据。
- 它将聚合的新状态保持到相关数据库。
通常情况下,命令处理程序处理由聚合根(根实体)驱动的单个聚合。 如果多个聚合应受到单个命令接收的影响,可使用域事件跨多个聚合传播状态或操作。
作为命令处理程序类的示例,下面的代码演示本章开头介绍的同一个 CreateTodoCommandHandler
类。 这个示例还强调了 Handle
方法以及域模型对象/聚合的操作。
public class CreateTodoCommandHandler
: IRequestHandler<CreateTodoCommand, TodoDTO>
{
private readonly IRepository repository;
private readonly IMapper mapper;
public CreateTodoCommandHandler(IRepository repository, IMapper mapper)
{
this.repository = repository ?? throw new ArgumentNullException(nameof(repository));
this.mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
public async Task<TodoDTO> Handle(CreateTodoCommand message, CancellationToken cancellationToken)
{
var todo = Todo.Create(message.Name);
repository.Entry(todo);
await repository.SaveAsync();
var todoForDTO = mapper.Map<TodoDTO>(todo);
return todoForDTO;
}
}
最后,通过MediatR
实现命令进程管道
首先,让我们看一下示例 WebAPI
控制器,你会在其中使用MediatR
,如以下示例所示:
[Route("api/[controller]")]
[ApiController]
public class TodosController : ControllerBase
{
//...
private readonly MediatR.IMediator mediator;
public TodosController(MediatR.IMediator mediator)
{
this.mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
//...
}
在控制器方法中,将命令发送到MediatR
的代码几乎只有一行:
[HttpPost]
public async Task<ActionResult<TodoDTO>> Create(CreateTodoCommand param)
{
var ret = await mediator.Send(param);
return CreatedAtAction(nameof(Get), new { id = ret.Id }, ret);
}
3.2 查询 (Query
)
首先,定义DTO
[Table("T_Todo")]
public class TodoDTO
{
#region Public Properties
public Guid Id { get; set; }
public string Name { get; set; }
#endregion
}
然后,创建具体的查询方法
public class TodoQueries
{
private readonly TodoingQueriesContext context;
public TodoQueries(TodoingQueriesContext context)
{
this.context = context;
}
//...
public async Task<PaginatedItems<TodoDTO>> Query(int pageIndex, int pageSize)
{
var total = await context.Todos
.AsNoTracking()
.CountAsync();
var todos = await context.Todos
.AsNoTracking()
.OrderBy(o => o.Id)
.Skip(pageSize * (pageIndex - 1))
.Take(pageSize)
.ToListAsync();
return new PaginatedItems<TodoDTO>(total, todos);
}
//...
}
请注意TodoingQueriesContext
和命令处理中的Context
不是同一个,实现查询端除了用EFCore
、还可以用存储过程、视图、具体化视图或Dapper
等等。
最后,调用查询方法
[Route("api/[controller]")]
[ApiController]
public class TodosController : ControllerBase
{
private readonly TodoQueries todoQueries;
public TodosController(TodoQueries todoQueries)
{
this.todoQueries = todoQueries ?? throw new ArgumentNullException(nameof(todoQueries));
}
//...
[HttpGet]
public async Task<ActionResult<PaginatedItems<TodoDTO>>> Query(int pageIndex, int pageSize)
{
return todoQueries.Query(pageIndex, pageSize).Result;
}
//...
}
DDD领域驱动设计:实体、值对象、聚合根
1 前置阅读
在阅读本文章之前,你可以先阅读:
- DDD领域驱动设计是什么
2 实现值对象
值对象有两个主要特征:它们没有任何标识。它们是不可变的。
我们举个例子:小明是“浙江宁波”人,小红也是“浙江宁波”人,小王是“浙江杭州”人,在这个例子中,我们把地址可以独立出一个值对象出来,我们会遇到了多个对象是否相同的问题,例如小明和小红的地址应该是相等,小明和小王应该是不相等,这很好理解,我们来看一下例子;
public class Address
{
public string Province;
public string City;
}
var xm = new Address { Province = "浙江", City = "宁波" };
var xh = new Address { Province = "浙江", City = "宁波" };
var xw = new Address { Province = "浙江", City = "杭州" };
Console.WriteLine(xm.Equals(xh));
Console.WriteLine(xm.Equals(xw));
让我们来看看输出结果:
False
False
这个显然不符合我们预期,我们需要重写一下Equals,确保地址值相等的情况下对象相等。
public class Address
{
public string Province;
public string City;
public bool Equals(Address obj)
{
return this.Province.Equals(obj.Province) && this.City.Equals(obj.City);
}
}
var xm = new Address { Province = "浙江", City = "宁波" };
var xh = new Address { Province = "浙江", City = "宁波" };
var xw = new Address { Province = "浙江", City = "杭州" };
Console.WriteLine(xm.Equals(xh));
Console.WriteLine(xm.Equals(xw));
让我们来看看输出结果:
True
False
这个显然符合我们预期了,接下来我们把值对象的Equals方法封装到基类中。
public abstract class ValueObject
{
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
}
public class Address : ValueObject
{
public string Province;
public string City;
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Province;
yield return City;
}
}
3 实现实体
实体主要特征:具有唯一标识。
前面我们讲到值对象将特定值都相等的对象视为相等对象,在实体中比较容易理解,标识相等的对象视为相等对象。
public abstract class Entity
{
#region IEntity Members
public abstract Guid Id
{
get; set;
}
#endregion
public override bool Equals(object obj)
{
if (obj == null || !(obj is Entity))
return false;
if (Object.ReferenceEquals(this, obj))
return true;
if (this.GetType() != obj.GetType())
return false;
Entity item = (Entity)obj;
return item.Id == this.Id;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
}
4 实现聚合根
聚合根与实体的区别,实体只在聚合内部进行操作,聚合根是对外打交道的唯一实体。我们在这里设计时聚合根需要有增改删状态字段。
public enum AggregateState
{
Added = 1,
Updated = 2,
Deleted = 3
}
public abstract class AggregateRoot : Entity
{
#region IAggregateRoot Members
public AggregateState AggregateState { set; get; }
#endregion
}
DDD领域驱动设计:仓储
1 前置阅读
在阅读本文章之前,你可以先阅读:
- DDD领域驱动设计是什么
- DDD领域驱动设计:实体、值对象、聚合根
2 什么是仓储?
仓储封装了基础设施来提供查询和持久化聚合操作。 它们集中提供常见的数据访问功能,从而提供更好的可维护性,并将用于访问数据库的基础结构或技术与领域模型层分离。 创建数据访问层和应用程序的业务逻辑层之间的抽象层。 实现仓储可让你的应用程序对数据存储介质的更改不敏感。
3 为什么仓储?
直接访问数据:
- 重复的代码
- 难以集中化与数据相关的策略(例如缓存)
- 编程错误的可能性更高
- 无法独立于外部依赖项轻松测试业务逻辑
使用仓储优点:
- 可以通过将业务逻辑与数据或服务访问逻辑分开来提高代码的可维护性和可读性。
- 可以从许多位置访问数据源,并希望应用集中管理的,一致的访问规则和逻辑。
- 可以通过自动化进行测试的代码量,并隔离数据层以支持单元测试。
- 可以使用强类型的业务实体,以便可以在编译时而不是在运行时识别问题。
- 可以将行为与相关数据相关联。例如,您要计算字段或在实体中的数据元素之间强制执行复杂的关系或业务规则。
- 应用DDD来简化复杂的业务逻辑。
4 实现仓储?
实现基本的增删改查及事务的提交和回滚
首先,定义接口
/// <summary>
/// IRepository提供应用程序仓储模式基本操作的接口
/// </summary>
public interface IRepository
{
#region Methods
void Entry<T>(T t) where T : AggregateRoot;
void Save();
T Get<T>(Guid id, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot;
T Get<T>(Expression<Func<T, bool>> where, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot;
IQueryable<T> Query<T>(Expression<Func<T, bool>> filter=null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy=null, Func<IQueryable<T>, IQueryable<T>> includes=null) where T : AggregateRoot;
IQueryable<T> QueryByPage<T>(int pageIndex, int pageSize, Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot;
void BeginTransaction();
void Commit();
void Rollback();
#endregion
}
最后,实现以上接口
public class Repository : IDisposable, IRepository
{
#region Private Fields
private readonly DbContext context;
private IDbContextTransaction transaction;
#endregion
#region Constructors
public Repository(DbContext context)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
}
#endregion
#region IRepository<T> Members
public void Entry<T>(T t) where T : AggregateRoot
{
switch (t.AggregateState)
{
case AggregateState.Added:
context.Entry(t).State = EntityState.Added;
break;
case AggregateState.Deleted:
context.Entry(t).State = EntityState.Deleted;
break;
default:
context.Entry(t).State = EntityState.Modified;
break;
}
}
public void Save()
{
context.SaveChanges();
}
public T Get<T>(Guid id, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot
{
return Get(w => w.Id.Equals(id), includes: includes);
}
public T Get<T>(Expression<Func<T, bool>> filter, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot
{
return Query(filter, includes: includes).SingleOrDefault();
}
public IQueryable<T> Query<T>(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot
{
IQueryable<T> query = context.Set<T>();
if (filter != null)
{
query = query.Where(filter);
}
if (includes != null)
{
query = includes(query);
}
if (orderBy != null)
{
query = orderBy(query);
}
return query;
}
public IQueryable<T> QueryByPage<T>(int pageIndex, int pageSize, Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot
{
var query = Query(filter, orderBy, includes)
.Skip(pageSize * (pageIndex - 1))
.Take(pageSize);
return query;
}
public void BeginTransaction()
{
transaction = context.Database.BeginTransaction();
}
public void Rollback()
{
transaction.Rollback();
}
public void Commit()
{
transaction.Commit();
}
public void Dispose()
{
if (transaction != null)
{
transaction.Dispose();
}
context.Dispose();
}
#endregion
}
为数据库上下文和事务上下文声明类变量:
private readonly DbContext context;
private IDbContextTransaction transaction;
构造函数接受数据库上下文实例:
public Repository(DbContext context)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
}
Get
分为通过ID查询或过滤条件进行查询,返回序列中的唯一元素:
public T Get<T>(Guid id, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot
{
return Get(w => w.Id.Equals(id), includes: includes);
}
public T Get<T>(Expression<Func<T, bool>> filter, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot
{
return Query(filter, includes: includes).SingleOrDefault();
}
Query
方法使用 lambda
表达式来允许调用代码指定筛选条件,使用一列来对结果进行排序,允许调用方为预先加载导航属性列表:
// 代码 Expression<Func<T, bool>> filter 意味着调用方将基于 AggregateRoot 类型提供 lambda 表达式,并且此表达式将返回一个布尔值。
// 代码 Func<IQueryable<T>, IOrderedQueryable<T>> orderBy 也意味着调用方将提供 lambda 表达式。 但在这种情况下,表达式的输入是 AggregateRoot 类型的 IQueryable 对象。 表达式将返回 IQueryable 对象的有序版本。
// 代码 Func<IQueryable<T>, IQueryable<T>> includes 也意味着调用方将提供 lambda 表达式。 允许预先加载导航属性列表。
public IQueryable<T> Query<T>(Expression<Func<T, bool>> filter = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, Func<IQueryable<T>, IQueryable<T>> includes = null) where T : AggregateRoot
{
IQueryable<T> query = context.Set<T>();
if (filter != null)
{
query = query.Where(filter);
}
if (includes != null)
{
query = includes(query);
}
if (orderBy != null)
{
query = orderBy(query);
}
return query;
}
Entry
方法使用 AggregateRoot.AggregateState
来置 context.Entry(t).State
状态,完成增删改
public void Entry<T>(T t) where T : AggregateRoot
{
switch (t.AggregateState)
{
case AggregateState.Added:
context.Entry(t).State = EntityState.Added;
break;
case AggregateState.Deleted:
context.Entry(t).State = EntityState.Deleted;
break;
default:
context.Entry(t).State = EntityState.Modified;
break;
}
}
Save
等其他方法也类似实现。
DDD领域驱动设计:领域事件
1 前置阅读
在阅读本文章之前,你可以先阅读:
- DDD领域驱动设计是什么
- DDD领域驱动设计:实体、值对象、聚合根
- DDD领域驱动设计:仓储
MediatR
一个优秀的.NET中介者框架
2 什么是领域事件?
领域事件是在领域中发生的事,你希望同一个领域(进程)的其他部分了解它。 通知部分通常以某种方式对事件作出反应。
3 实现领域事件?
重点强调领域事件发布/订阅是使用 MediatR
同步实现的。
首先,定义待办事项已更新的领域事件
public class TodoUpdatedDomainEvent : INotification
{
public Todo Todo { get; }
public TodoUpdatedDomainEvent(Todo todo)
{
Todo = todo;
}
}
然后,引发领域事件,将域事件添加到集合,然后在提交事务之前或之后立即调度这些域事件,而不是立即调度到域事件处理程序 。
public abstract class Entity
{
//...
private List<INotification> domainEvents;
public IReadOnlyCollection<INotification> DomainEvents => domainEvents?.AsReadOnly();
public void AddDomainEvent(INotification eventItem)
{
domainEvents = domainEvents ?? new List<INotification>();
domainEvents.Add(eventItem);
}
public void RemoveDomainEvent(INotification eventItem)
{
domainEvents?.Remove(eventItem);
}
public void ClearDomainEvents()
{
domainEvents?.Clear();
}
//... 其他代码
}
要引发事件时,只需将其在聚合根实体的方法处添加到代码中的事件集合。
public class Todo : AggregateRoot
{
//...
public void Update(
string name)
{
Name = name;
AddDomainEvent(new TodoUpdatedDomainEvent(this));
}
//... 其他代码
}
请注意 AddDomainEvent
方法的唯一功能是将事件添加到列表。 尚未调度任何事件,尚未调用任何事件处理程序。你需要在稍后将事务提交到数据库时调度事件。
public class Repository : IDisposable, IRepository
{
//...
private readonly IMediator mediator;
private readonly DbContext context;
public Repository(DbContext context, IMediator mediator)
{
this.context = context ?? throw new ArgumentNullException(nameof(context));
this.mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
public void Save()
{
mediator.DispatchDomainEvents(context);
context.SaveChanges();
}
public static void DispatchDomainEvents(this IMediator mediator, DbContext ctx)
{
var domainEntities = ctx.ChangeTracker
.Entries<Entity>()
.Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());
var domainEvents = domainEntities
.SelectMany(x => x.Entity.DomainEvents)
.ToList();
domainEntities.ToList()
.ForEach(entity => entity.Entity.ClearDomainEvents());
foreach (var domainEvent in domainEvents)
mediator.Publish(domainEvent);
}
//... 其他代码
}
最后,订阅并处理领域事件
public class TodoUpdatedDomainEventHandler : INotificationHandler<TodoUpdatedDomainEvent>
{
private readonly ILoggerFactory logger;
public TodoUpdatedDomainEventHandler(ILoggerFactory logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public Task Handle(TodoUpdatedDomainEvent todoUpdatedDomainEvent, CancellationToken cancellationToken)
{
logger.CreateLogger<TodoUpdatedDomainEvent>().LogDebug("Todo with Id: {TodoId} has been successfully updated",
todoUpdatedDomainEvent.Todo.Id);
return Task.CompletedTask;
}
}