使用.NET?6開發(fā)TodoList應用之領域?qū)嶓w創(chuàng)建原理和思路
需求
上一篇文章中我們完成了數(shù)據(jù)存儲服務的接入,從這一篇開始將正式進入業(yè)務邏輯部分的開發(fā)。
首先要定義和解決的問題是,根據(jù)TodoList
項目的需求,我們應該設計怎樣的數(shù)據(jù)實體,如何去進行操作?
長文預警!包含大量代碼
目標
在本文中,我們希望達到以下幾個目標:
- 定義領域?qū)嶓w;
- 通過數(shù)據(jù)庫操作領域?qū)嶓w;
原理和思路
雖然TodoList
是一個很簡單的應用,業(yè)務邏輯并不復雜,至少在這個系列文章中我并不想使其過度復雜。但是我還是打算借此簡單地涉及領域驅(qū)動開發(fā)(DDD)的基礎概念。
首先比較明確的是,我們的實體對象應該有兩個:TodoList
和TodoItem
,并且一個TodoList
是由多個TodoItem
的列表構成,除此以外在實際的開發(fā)中,我們可能還需要追蹤實體的變更情況,比如需要知道創(chuàng)建時間/修改時間/創(chuàng)建者/修改者,這種需求一般作為審計要求出現(xiàn),而對實體的審計又是一個比較通用的需求。所以我們會將實體分成兩部分:和業(yè)務需求直接相關的屬性,以及和實體審計需求相關的屬性。
其次,對于實體的數(shù)據(jù)庫配置,有兩種方式:通過Attribute
或者通過IEntityTypeConfiguration<T>
以代碼的方式進行。我推薦使用第二種方式,將所有的具體配置集中到Infrastructure
層去管理,避免后續(xù)修改字段屬性而去頻繁修改位于Domain
層的實體對象定義,我們希望實體定義本身是穩(wěn)定的。
最后,對于DDD來說有一些核心概念諸如領域事件,值對象,聚合根等等,我們都會在定義領域?qū)嶓w的時候有所涉及,但是目前還不會過多地使用。關于這些基本概念的含義,請參考這篇文章:淺談Java開發(fā)架構之領域驅(qū)動設計DDD落地。在我們的開發(fā)過程中,會進行一些精簡,有部分內(nèi)容也會隨著后續(xù)的文章逐步完善。
實現(xiàn)
基礎的領域概念框架搭建
所有和領域相關的概念都會進入到Domain
這個項目中,我們首先在Domain
項目里新建文件夾Base
用于存放所有的基礎定義,下面將一個一個去實現(xiàn)。(另一種方式是把這些最基礎的定義單獨提出去新建一個SharedDefinition
類庫并讓Domain
引用這個項目。)
基礎實體定義以及可審計實體定義
我這兩個類都應該是抽象基類,他們的存在是為了讓我們的業(yè)務實體繼承使用的,并且為了允許不同的實體可以定義自己主鍵的類型,我們將基類定義成泛型的。
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<T> { 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
, ValueObject
和Domain Service
以及Domain Exception
,其他的相關概念暫時就不涉及了。
IHasDomainEvent.cs
namespace TodoList.Domain.Base.Interfaces; public interface IHasDomainEvent { public List<DomainEvent> DomainEvents { get; set; } }
ValueObject
的實現(xiàn)有幾乎固定的寫法,請參考: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<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); } }
關于Domain Exception
的定義,是根據(jù)業(yè)務內(nèi)容來確定的,暫時不在Base
里實現(xiàn),而是放到各個聚合根的層級里。
定義TodoLIst/TodoItem實體
TodoList
對象從領域建模上來說屬于聚合根,并且下一節(jié)將要實現(xiàn)的TodoItem
是構成該聚合根的一部分,業(yè)務意思上不能單獨存在。有一種實現(xiàn)方式是按照聚合根的關聯(lián)性進行代碼組織:即在Domain
項目里新建文件夾AggregateRoots/TodoList
來保存和這個聚合根相關的所有業(yè)務定義:即:Events
/Exceptions
和Enums
三個文件夾用于存放對應的內(nèi)容。但是這樣可能會導致項目的目錄層級太多,實際上在這里,我更傾向于在Domain
項目的根目錄下創(chuàng)建Entities
/Events
/Enums
/Exceptions
/ValueObjects
文件夾來扁平化領域模型,對于世紀的開發(fā)查找起來也并不麻煩。所以才去后一種方式,然后在Entities
中創(chuàng)建TodoItem
和TodoList
實體:
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<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>(); }
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<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>(); }
為了演示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<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; } }
UnsupportedColourException.cs
namespace TodoList.Domain.Exceptions; public class UnsupportedColourException : Exception { public UnsupportedColourException(string code) : base($"Colour \"[code]\" is unsupported.") { } }
關于領域服務的內(nèi)容我們暫時不去管,繼續(xù)看看如何向數(shù)據(jù)庫配置實體對象。
領域?qū)嶓w的數(shù)據(jù)庫配置
這部分內(nèi)容相對會熟悉一些,我們在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<TodoItem> { public void Configure(EntityTypeBuilder<TodoItem> 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<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
因為下一篇里我們將要使用Repository
模式,所以我們可以不需要讓TodoListDbContext
繼續(xù)繼承IApplicationDbContext
了。關于直接在Application
里使用Context比較簡單,就不繼續(xù)演示了。
在這一步里面,我們需要完成以下幾件事:
- 添加數(shù)據(jù)表;
- 重寫SaveChangesAsync方法,自動補充審計相關字段值,并且在此發(fā)送領域事件;
對于第一件事,很簡單。向TodoListDbContext.cs
類定義中加入:
// TodoLIst實體與命名空間名稱有沖突,所以需要顯示引用其他命名空間里的對象 public DbSet<Domain.Entities.TodoList> TodoLists => Set<Domain.Entities.TodoList>(); public DbSet<TodoItem> TodoItems => Set<TodoItem>();
對于第二件事,我們需要先向Application/Common/Interfaces
中添加一個接口用于管理領域事件的分發(fā),但是在講到CQRS
之前,我們暫時以Dummy
的方式實現(xiàn)這個接口,因為將要使用第三方框架實現(xiàn)具體邏輯,所以我們把實現(xiàn)類放到Infrastrcucture/Services
目錄下,并在TodoListDbContext
中注入使用。
IDomainEventService.cs
using TodoList.Domain.Base; namespace TodoList.Application.Common.Interfaces; public interface IDomainEventService { Task Publish(DomainEvent domainEvent); }
DomainEventService.cs
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); } }
在DependencyInjection
中注入:
// 省略以上...并且這一句可以不需要了 // services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<TodoListDbContext>()); // 增加依賴注入 services.AddScoped<IDomainEventService, DomainEventService>(); return services;
最終的TodoListDbContext
實現(xiàn)如下:
TodoListDbContext.cs
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; } } // 在寫數(shù)據(jù)庫的時候同時發(fā)送領域事件,這里要注意一定要保證寫入數(shù)據(jù)庫成功后再發(fā)送領域事件,否則會導致領域?qū)ο鬆顟B(tài)的不一致問題。 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'
使用種子數(shù)據(jù)更新數(shù)據(jù)庫
為了演示效果,在Infrastructure/Persistence/
下創(chuàng)建TodoListDbContextSeed.cs
文件并初始化種子數(shù)據(jù):
TodoListDbContextSeed.cs
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(); } }
在應用程序初始化的擴展中進行初始化和更新:
ApplicationStartupExtensions.cs
// 省略以上... try { var context = services.GetRequiredService<TodoListDbContext>(); context.Database.Migrate(); // 生成種子數(shù)據(jù) TodoListDbContextSeed.SeedSampleDataAsync(context).Wait(); // 更新部分種子數(shù)據(jù)以便查看審計字段 TodoListDbContextSeed.UpdateSampleDataAsync(context).Wait(); } catch (Exception ex) // 省略以下...
運行Api
項目,得到下面的輸出,中間我省略了一些SQL語句的輸出:
$ dotnet run --project src/TodoList.Api
Building...
# ...省略
[14:06:24 INF] Applying migration '20211222060615_AddEntities'.
# ...省略
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20211222060615_AddEntities', N'6.0.1');
# ...省略,注意下面的三個domain event,因為我們在造種子數(shù)據(jù)的時候有設置三個TodoItem標記為已完成,將會觸發(fā)event。
[14:06:25 INF] Publishing domain event. Event - TodoItemCompletedEvent
[14:06:25 INF] Publishing domain event. Event - TodoItemCompletedEvent
[14:06:25 INF] Publishing domain event. Event - TodoItemCompletedEvent
# ...省略
[14:06:25 INF] Now listening on: https://localhost:7039
[14:06:25 INF] Now listening on: http://localhost:5050
[14:06:25 INF] Application started. Press Ctrl+C to shut down.
[14:06:25 INF] Hosting environment: Development
# ...省略
我們再去看看數(shù)據(jù)庫中的數(shù)據(jù):
TodoLists
數(shù)據(jù)表:
TodoItems
數(shù)據(jù)表
__EFMigrationsHistory
遷移表:
總結
在本文中,我們著手搭建了基本的領域驅(qū)動設計對應的Domain
層實現(xiàn),包括兩個領域?qū)嶓w對象及其關聯(lián)的其他知識。最后通過種子數(shù)據(jù)的方式進行數(shù)據(jù)庫數(shù)據(jù)操作的驗證,下一篇我們將繼續(xù)實現(xiàn)一個通用的Repository
模式。
參考資料
Domain Driven DesignDDD領域驅(qū)動設計基本理論知識總結
到此這篇關于使用.NET?6開發(fā)TodoList應用之領域?qū)嶓w創(chuàng)建原理和思路的文章就介紹到這了,更多相關.NET?6?開發(fā)TodoList應用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- .NET 6開發(fā)TodoList應用之實現(xiàn)查詢分頁
- .NET 6開發(fā)TodoList應用之實現(xiàn)ActionFilter
- .NET 6開發(fā)TodoList應用之實現(xiàn)接口請求驗證
- .NET?6開發(fā)TodoList應用之實現(xiàn)DELETE請求與HTTP請求冪等性
- .NET 6開發(fā)TodoList應用之實現(xiàn)PUT請求
- .NET 6開發(fā)TodoList應用之實現(xiàn)全局異常處理
- .NET 6開發(fā)TodoList應用之使用AutoMapper實現(xiàn)GET請求
- .NET?6開發(fā)TodoList應用之實現(xiàn)Repository模式
- .NET?6開發(fā)TodoList應用之使用MediatR實現(xiàn)POST請求
- .NET 6開發(fā)TodoList應用引入數(shù)據(jù)存儲
- .NET?6開發(fā)TodoList應用引入第三方日志庫
- .NET 6開發(fā)TodoList應用實現(xiàn)結構搭建
- .NET?6開發(fā)TodoList應用實現(xiàn)系列背景
- 使用.NET?6開發(fā)TodoList應用之引入數(shù)據(jù)存儲的思路詳解
- .NET?6開發(fā)TodoList應用之請求日志組件HttpLogging介紹
相關文章
利用VS2019創(chuàng)建Web項目并發(fā)送到IIS及IIS與ASP.NET配置教程
這篇文章主要介紹了利用VS2019創(chuàng)建Web項目,并發(fā)送到IIS,以及IIS與ASP.NET配置,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03ASP.NET MVC3 SEO優(yōu)化:利用Routing特性提高站點權重
這篇文章主要介紹了ASP.NET MVC3 SEO優(yōu)化:利用Routing特性消除多個路徑指向同一個Action,從而提高站點權重,需要的朋友可以參考下。2016-06-06ASP .NET調(diào)用javascript中Response.Write和ClientScript.RegisterSta
最近在用ASP .NET的code behind 調(diào)用javascript中發(fā)現(xiàn)Response.Write不能拿到form的值,而ClientScript.RegisterStartupScript可以。2010-12-12asp.net iis 無法顯示網(wǎng)頁的解決方法分析
使用過IIS的朋友都可能遇到過這樣的情況:即使您按照教科書的步驟做好各步設置以后,仍會出現(xiàn)“無法顯示網(wǎng)頁”的現(xiàn)象。2010-06-06VisualStudio2019中為.NET Core WinForm App啟用窗體設計器
這篇文章主要介紹了VisualStudio2019中為.NET Core WinForm App啟用窗體設計器,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-04-04web.config中配置數(shù)據(jù)庫連接的方式
Web.config文件是一個XML文本文件,它用來儲存 ASP.NET Web 應用程序的配置信息(如最常用的設置ASP.NET Web 應用程序的身份驗證方式),它可以出現(xiàn)在應用程序的每一個目錄中。本文主要介紹web.config中配置數(shù)據(jù)庫連接的兩種方式,一起來看。2015-10-10.Net Core3.0 WEB API中使用FluentValidation驗證(批量注入)
這篇文章主要介紹了.Net Core3.0 WEB API中使用FluentValidation驗證(批量注入),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-12-12