在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
需求上一篇文章中我们完成了数据存储服务的接入,从这一篇开始将正式进入业务逻辑部分的开发。 首先要定义和解决的问题是,根据 长文预警!包含大量代码 目标在本文中,我们希望达到以下几个目标:
原理和思路虽然 首先比较明确的是,我们的实体对象应该有两个: 其次,对于实体的数据库配置,有两种方式:通过 最后,对于DDD来说有一些核心概念诸如领域事件,值对象,聚合根等等,我们都会在定义领域实体的时候有所涉及,但是目前还不会过多地使用。关于这些基本概念的含义,请参考这篇文章:浅谈Java开发架构之领域驱动设计DDD落地。在我们的开发过程中,会进行一些精简,有部分内容也会随着后续的文章逐步完善。 实现基础的领域概念框架搭建所有和领域相关的概念都会进入到 基础实体定义以及可审计实体定义我这两个类都应该是抽象基类,他们的存在是为了让我们的业务实体继承使用的,并且为了允许不同的实体可以定义自己主键的类型,我们将基类定义成泛型的。
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; } } 在
namespace TodoList.Domain.Base.Interfaces; public interface IEntity<T> { public T Id { get; set; } } 除了这两个对象之外,我们还需要增加关于领域事件框架的定义。
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; } 我们还剩下
namespace TodoList.Domain.Base.Interfaces; public interface IHasDomainEvent { public List<DomainEvent> DomainEvents { get; set; } }
namespace TodoList.Domain.Base.Interfaces; // 聚合根对象仅仅作为标记来使用 public interface IAggregateRoot { }
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<object> 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); } } 关于 定义TodoLIst/TodoItem实体
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<Guid>, 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<DomainEvent> DomainEvents { get; set; } = new List<DomainEvent>(); }
namespace TodoList.Domain.Enums; public enum PriorityLevel { None = 0, Low = 1, Medium = 2, High = 3 }
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; } }
using TodoList.Domain.Base; using TodoList.Domain.Base.Interfaces; using TodoList.Domain.ValueObjects; namespace TodoList.Domain.Entities; public class TodoList : AuditableEntity, IEntity<Guid>, IHasDomainEvent, IAggregateRoot { public Guid Id { get; set; } public string? Title { get; set; } public Colour Colour { get; set; } = Colour.White; public IList<TodoItem> Items { get; private set; } = new List<TodoItem>(); public List<DomainEvent> DomainEvents { get; set; } = new List<DomainEvent>(); } 为了演示
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<Colour> 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<object> GetEqualityComponents() { yield return Code; } }
namespace TodoList.Domain.Exceptions; public class UnsupportedColourException : Exception { public UnsupportedColourException(string code) : base($"Colour \"[code]\" is unsupported.") { } } 关于领域服务的内容我们暂时不去管,继续看看如何向数据库配置实体对象。 领域实体的数据库配置这部分内容相对会熟悉一些,我们在
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TodoList.Domain.Entities; namespace TodoList.Infrastructure.Persistence.Configurations; public class TodoItemConfiguration : IEntityTypeConfiguration<TodoItem> { public void Configure(EntityTypeBuilder<TodoItem> builder) { builder.Ignore(e => e.DomainEvents); builder.Property(t => t.Title) .HasMaxLength(200) .IsRequired(); } }
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace TodoList.Infrastructure.Persistence.Configurations; public class TodoListConfiguration : IEntityTypeConfiguration<Domain.Entities.TodoList> { public void Configure(EntityTypeBuilder<Domain.Entities.TodoList> builder) { builder.Ignore(e => e.DomainEvents); builder.Property(t => t.Title) .HasMaxLength(200) .IsRequired(); builder.OwnsOne(b => b.Colour); } } 修改DbContext因为下一篇里我们将要使用 在这一步里面,我们需要完成以下几件事:
对于第一件事,很简单。向 // TodoLIst实体与命名空间名称有冲突,所以需要显示引用其他命名空间里的对象 public DbSet<Domain.Entities.TodoList> TodoLists => Set<Domain.Entities.TodoList>(); public DbSet<TodoItem> TodoItems => Set<TodoItem>(); 对于第二件事,我们需要先向
using TodoList.Domain.Base; namespace TodoList.Application.Common.Interfaces; public interface IDomainEventService { Task Publish(DomainEvent domainEvent); }
using Microsoft.Extensions.Logging; using TodoList.Application.Common.Interfaces; using TodoList.Domain.Base; namespace TodoList.Infrastructure.Services; public class DomainEventService : IDomainEventService { private readonly ILogger<DomainEventService> _logger; public DomainEventService(ILogger<DomainEventService> logger) { _logger = logger; } public async Task Publish(DomainEvent domainEvent) { // 在这里暂时什么都不做,到CQRS那一篇的时候再回来补充这里的逻辑 _logger.LogInformation("Publishing domain event. Event - {event}", domainEvent.GetType().Name); } } 在 // 省略以上...并且这一句可以不需要了 // services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<TodoListDbContext>()); // 增加依赖注入 services.AddScoped<IDomainEventService, DomainEventService>(); return services; 最终的
using System.Reflection; using Microsoft.EntityFrameworkCore; using TodoList.Application.Common.Interfaces; using TodoList.Domain.Base; using TodoList.Domain.Base.Interfaces; using TodoList.Domain.Entities; namespace TodoList.Infrastructure.Persistence; public class TodoListDbContext : DbContext { private readonly IDomainEventService _domainEventService; public TodoListDbContext( DbContextOptions<TodoListDbContext> options, IDomainEventService domainEventService) : base(options) { _domainEventService = domainEventService; } public DbSet<Domain.Entities.TodoList> TodoLists => Set<Domain.Entities.TodoList>(); public DbSet<TodoItem> TodoItems => Set<TodoItem>(); public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new()) { // 在我们重写的SaveChangesAsync方法中,去设置审计相关的字段,目前对于修改人这个字段暂时先给个定值,等到后面讲到认证鉴权的时候再回过头来看这里 foreach (var entry in ChangeTracker.Entries<AuditableEntity>()) { switch (entry.State) { case EntityState.Added: entry.Entity.CreatedBy = "Anonymous"; entry.Entity.Created = DateTime.UtcNow; break; case EntityState.Modified: entry.Entity.LastModifiedBy = "Anonymous"; entry.Entity.LastModified = DateTime.UtcNow; break; } } // 在写数据库的时候同时发送领域事件,这里要注意一定要保证写入数据库成功后再发送领域事件,否则会导致领域对象状态的不一致问题。 var events = ChangeTracker.Entries<IHasDomainEvent>() .Select(x => x.Entity.DomainEvents) .SelectMany(x => x) .Where(domainEvent => !domainEvent.IsPublished) .ToArray(); var result = await base.SaveChangesAsync(cancellationToken); await DispatchEvents(events); return result; } protected override void OnModelCreating(ModelBuilder builder) { // 应用当前Assembly中定义的所有的Configurations,就不需要一个一个去写了。 builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); base.OnModelCreating(builder); } private async Task DispatchEvents(DomainEvent[] events) { foreach (var @event in events) { @event.IsPublished = true; await _domainEventService.Publish(@event); } } } 验证生成Migrations老办法,先生成Migrations。 $ dotnet ef migrations add AddEntities -p src/TodoList.Infrastructure/TodoList.Infrastructure.csproj -s src/TodoList.Api/TodoList.Api.csproj Build started... Build succeeded. [14:06:15 INF] Entity Framework Core 6.0.1 initialized 'TodoListDbContext' using provider 'Microsoft.EntityFrameworkCore.SqlServer:6.0.1' with options: MigrationsAssembly=TodoList.Infrastructure, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null Done. To undo this action, use 'ef migrations remove' 使用种子数据更新数据库为了演示效果,在
using Microsoft.EntityFrameworkCore; using TodoList.Domain.Entities; using TodoList.Domain.Enums; using TodoList.Domain.ValueObjects; namespace TodoList.Infrastructure.Persistence; public static class TodoListDbContextSeed { public static async Task SeedSampleDataAsync(TodoListDbContext context) { if (!context.TodoLists.Any()) { var list = new Domain.Entities.TodoList { Title = "Shopping", Colour = Colour.Blue }; list.Items.Add(new TodoItem { Title = "Apples", Done = true, Priority = PriorityLevel.High}); list.Items.Add(new TodoItem { Title = "Milk", Done = true }); list.Items.Add(new TodoItem { Title = "Bread", Done = true }); list.Items.Add(new TodoItem { Title = "Toilet paper" }); list.Items.Add(new TodoItem { Title = "Pasta" }); list.Items.Add(new TodoItem { Title = "Tissues" }); list.Items.Add(new TodoItem { Title = "Tuna" }); list.Items.Add(new TodoItem { Title = "Water" }); context.TodoLists.Add(list); await context.SaveChangesAsync(); } } public static async Task UpdateSampleDataAsync(TodoListDbContext context) { var sampleTodoList = await context.TodoLists.FirstOrDefaultAsync(); if (sampleTodoList == null) { return; } sampleTodoList.Title = "Shopping - modified"; // 演示更新时审计字段的变化 context.Update(sampleTodoList); await context.SaveChangesAsync(); } } 在应用程序初始化的扩展中进行初始化和更新:
// 省略以上... try { var context = services.GetRequiredService<TodoListDbContext>(); context.Database.Migrate(); // 生成种子数据 TodoListDbContextSeed.SeedSampleDataAsync(context).Wait(); // 更新部分种子数据以便查看审计字段 TodoListDbContextSeed.UpdateSampleDataAsync(context).Wait(); } catch (Exception ex) // 省略以下... 运行
我们再去看看数据库中的数据:
总结在本文中,我们着手搭建了基本的领域驱动设计对应的 参考资料 Domain Driven DesignDDD领域驱动设计基本理论知识总结 到此这篇关于使用.NET 6开发TodoList应用之领域实体创建原理和思路的文章就介绍到这了,更多相关.NET 6 开发TodoList应用内容请搜索极客世界以前的文章或继续浏览下面的相关文章希望大家以后多多支持极客世界! |
请发表评论