您现在的位置是:网站首页> 编程资料编程资料

使用.NET 6开发TodoList应用之领域实体创建原理和思路_实用技巧_

2023-05-24 275人已围观

简介 使用.NET 6开发TodoList应用之领域实体创建原理和思路_实用技巧_

需求

上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发。

首先要定义和解决的问题是,根据TodoList项目的需求,我们应该设计怎样的数据实体,如何去进行操作?

长文预警!包含大量代码

目标

在本文中,我们希望达到以下几个目标:

  • 定义领域实体;
  • 通过数据库操作领域实体;

原理和思路

虽然TodoList是一个很简单的应用,业务逻辑并不复杂,至少在这个系列文章中我并不想使其过度复杂。但是我还是打算借此简单地涉及领域驱动开发(DDD)的基础概念。

首先比较明确的是,我们的实体对象应该有两个:TodoListTodoItem,并且一个TodoList是由多个TodoItem的列表构成,除此以外在实际的开发中,我们可能还需要追踪实体的变更情况,比如需要知道创建时间/修改时间/创建者/修改者,这种需求一般作为审计要求出现,而对实体的审计又是一个比较通用的需求。所以我们会将实体分成两部分:和业务需求直接相关的属性,以及和实体审计需求相关的属性。

其次,对于实体的数据库配置,有两种方式:通过Attribute或者通过IEntityTypeConfiguration以代码的方式进行。我推荐使用第二种方式,将所有的具体配置集中到Infrastructure层去管理,避免后续修改字段属性而去频繁修改位于Domain层的实体对象定义,我们希望实体定义本身是稳定的。

最后,对于DDD来说有一些核心概念诸如领域事件,值对象,聚合根等等,我们都会在定义领域实体的时候有所涉及,但是目前还不会过多地使用。关于这些基本概念的含义,请参考这篇文章:浅谈Java开发架构之领域驱动设计DDD落地。在我们的开发过程中,会进行一些精简,有部分内容也会随着后续的文章逐步完善。

实现

基础的领域概念框架搭建

所有和领域相关的概念都会进入到Domain这个项目中,我们首先在Domain项目里新建文件夹Base用于存放所有的基础定义,下面将一个一个去实现。(另一种方式是把这些最基础的定义单独提出去新建一个SharedDefinition类库并让Domain引用这个项目。)

基础实体定义以及可审计实体定义

我这两个类都应该是抽象基类,他们的存在是为了让我们的业务实体继承使用的,并且为了允许不同的实体可以定义自己主键的类型,我们将基类定义成泛型的。

AuditableEntity.cs

 namespace TodoList.Domain.Base; public abstract class AuditableEntity { public DateTime Created { get; set; } public string? CreatedBy { get; set; } public DateTime? LastModified { get; set; } public string? LastModifiedBy { get; set; } } 

Base里增加Interface文件夹来保存接口定义。

IEntity.cs

 namespace TodoList.Domain.Base.Interfaces; public interface IEntity { public T Id { get; set; } } 

除了这两个对象之外,我们还需要增加关于领域事件框架的定义。

DomainEvent.cs

 namespace TodoList.Domain.Base; public abstract class DomainEvent { protected DomainEvent() { DateOccurred = DateTimeOffset.UtcNow; } public bool IsPublished { get; set; } public DateTimeOffset DateOccurred { get; protected set; } = DateTime.UtcNow; } 

我们还剩下Aggregate Root, ValueObjectDomain Service以及Domain Exception,其他的相关概念暂时就不涉及了。

IHasDomainEvent.cs

 namespace TodoList.Domain.Base.Interfaces; public interface IHasDomainEvent { public List DomainEvents { get; set; } } 

ValueObject的实现有几乎固定的写法,请参考:https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/implement-value-objects

IAggregateRoot.cs

 namespace TodoList.Domain.Base.Interfaces; // 聚合根对象仅仅作为标记来使用 public interface IAggregateRoot { } 

ValueObject.cs

 namespace TodoList.Domain.Base; public abstract class ValueObject { protected static bool EqualOperator(ValueObject left, ValueObject right) { if (left is null ^ right is null) { return false; } return left?.Equals(right!) != false; } protected static bool NotEqualOperator(ValueObject left, ValueObject right) { return !(EqualOperator(left, right)); } protected abstract IEnumerable GetEqualityComponents(); public override bool Equals(object? obj) { if (obj == null || obj.GetType() != GetType()) { return false; } var other = (ValueObject)obj; return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); } public override int GetHashCode() { return GetEqualityComponents() .Select(x => x != null ? x.GetHashCode() : 0) .Aggregate((x, y) => x ^ y); } } 

关于Domain Exception的定义,是根据业务内容来确定的,暂时不在Base里实现,而是放到各个聚合根的层级里。

定义TodoLIst/TodoItem实体

TodoList对象从领域建模上来说属于聚合根,并且下一节将要实现的TodoItem是构成该聚合根的一部分,业务意思上不能单独存在。有一种实现方式是按照聚合根的关联性进行代码组织:即在Domain项目里新建文件夹AggregateRoots/TodoList来保存和这个聚合根相关的所有业务定义:即:Events/ExceptionsEnums三个文件夹用于存放对应的内容。但是这样可能会导致项目的目录层级太多,实际上在这里,我更倾向于在Domain项目的根目录下创建Entities/Events/Enums/Exceptions/ValueObjects文件夹来扁平化领域模型,对于世纪的开发查找起来也并不麻烦。所以才去后一种方式,然后在Entities中创建TodoItemTodoList实体:

TodoItem.cs

 using TodoList.Domain.Base; using TodoList.Domain.Base.Interfaces; using TodoList.Domain.Enums; using TodoList.Domain.Events; namespace TodoList.Domain.Entities; public class TodoItem : AuditableEntity, IEntity, IHasDomainEvent { public Guid Id { get; set; } public string? Title { get; set; } public PriorityLevel Priority { get; set; } private bool _done; public bool Done { get => _done; set { if (value && _done == false) { DomainEvents.Add(new TodoItemCompletedEvent(this)); } _done = value; } } public TodoList List { get; set; } = null!; public List DomainEvents { get; set; } = new List(); } 

PriorityLevel.cs

 namespace TodoList.Domain.Enums; public enum PriorityLevel { None = 0, Low = 1, Medium = 2, High = 3 } 

TodoItemCompletedEvent.cs

 using TodoList.Domain.Base; using TodoList.Domain.Entities; namespace TodoList.Domain.Events; public class TodoItemCompletedEvent : DomainEvent { public TodoItemCompletedEvent(TodoItem item) => Item = item; public TodoItem Item { get; } } 

TodoList.cs

 using TodoList.Domain.Base; using TodoList.Domain.Base.Interfaces; using TodoList.Domain.ValueObjects; namespace TodoList.Domain.Entities; public class TodoList : AuditableEntity, IEntity, IHasDomainEvent, IAggregateRoot { public Guid Id { get; set; } public string? Title { get; set; } public Colour Colour { get; set; } = Colour.White; public IList Items { get; private set; } = new List(); public List DomainEvents { get; set; } = new List(); } 

为了演示ValueObject,添加了一个Colour对象,同时添加了一个领域异常对象UnsupportedColourException

Colour.cs

 using TodoList.Domain.Base; namespace TodoList.Domain.ValueObjects; public class Colour : ValueObject { static Colour() { } private Colour() { } private Colour(string code) => Code = code; public static Colour From(string code) { var colour = new Colour { Code = code }; if (!SupportedColours.Contains(colour)) { throw new UnsupportedColourException(code); } return colour; } public static Colour White => new("#FFFFFF"); public static Colour Red => new("#FF5733"); public static Colour Orange => new("#FFC300"); public static Colour Yellow => new("#FFFF66"); public static Colour Green => new("#CCFF99 "); public static Colour Blue => new("#6666FF"); public static Colour Purple => new("#9966CC"); public static Colour Grey => new("#999999"); public string Code { get; private set; } = "#000000"; public static implicit operator string(Colour colour) => colour.ToString(); public static explicit operator Colour(string code) => From(code); public override string ToString() => Code; protected static IEnumerable SupportedColours { get { yield return White; yield return Red; yield return Orange; yield return Yellow; yield return Green; yield return Blue; yield return Purple; yield return Grey; } } protected override IEnumerable GetEqualityComponents() { yield return Code; } } 

UnsupportedColourException.cs

 namespace TodoList.Domain.Exceptions; public class UnsupportedColourException : Exception { public UnsupportedColourException(string code) : base($"Colour \"[code]\" is unsupported.") { } } 

关于领域服务的内容我们暂时不去管,继续看看如何向数据库配置实体对象。

领域实体的数据库配置

这部分内容相对会熟悉一些,我们在Infrastructure/Persistence中新建文件夹Configurations用于存放实体配置:

TodoItemConfiguration.cs

 using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TodoList.Domain.Entities; namespace TodoList.Infrastructure.Persistence.Configurations; public class TodoItemConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.Ignore(e => e.DomainEvents); builder.Property(t => t.Title) .HasMaxLength(200) .IsRequired(); } } 

TodoListConfiguration.cs

 using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace TodoList.Infrastructure.Persistence.Configurations; public class TodoListConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.Ignore(e => e.DomainEvents); builder.Property(t => t.Title) .HasMaxLength(200) .IsRequired(); builder.OwnsOne(b => b.Colour); } } 

修改DbContext

因为下一