如何在清洁架构中实现领域模型与持久化的同步?
解决领域模型与持久化层同步的重复操作问题(遵循清洁架构)
问题背景
当前解决方案包含三层结构:
Project.Storage:定义领域模型与仓储接口Project:主业务逻辑项目Project.Storage.EntityFramework:仓储接口的EF实现
现有Grant领域模型、IGrantStore接口及EF实现存在同步痛点:业务层修改领域模型实例后,必须在调用UpdateAsync时重复执行相同修改操作,无法自动同步到数据库,且不能依赖EF的变更追踪(需兼容Dapper等无追踪持久化方案)。
可行解决方案
方案1:领域模型封装变更,暴露变更集
在领域模型中内置轻量变更追踪逻辑,记录属性修改情况;仓储接口直接接收已修改的模型实例,通过变更集生成针对性的数据库更新操作,完全避免重复代码。
步骤1:修改Grant模型(Project.Storage)
public class Grant { public string GrantId { get; private set; } public string Subject { get; private set; } public string ClientId { get; private set; } public ICollection<AuthorizationDetail> AuthorizationDetails { get; private set; } = new List<AuthorizationDetail>(); public ICollection<string> Claims { get; private set; } = new HashSet<string>(); public DateTime GrantedAt { get; private set; } // 轻量变更追踪:记录已修改的属性名 private readonly HashSet<string> _changedProperties = new HashSet<string>(); public IReadOnlyCollection<string> ChangedProperties => _changedProperties.AsReadOnly(); // 封装属性修改方法,自动记录变更 public void UpdateAuthorizationDetails(ICollection<AuthorizationDetail> newDetails) { AuthorizationDetails = newDetails; _changedProperties.Add(nameof(AuthorizationDetails)); } public void UpdateClaims(ICollection<string> newClaims) { Claims = newClaims; _changedProperties.Add(nameof(Claims)); } // 其他属性的修改方法同理... // 重置变更集(仓储更新完成后调用) public void ResetChanges() => _changedProperties.Clear(); }
步骤2:重构IGrantStore接口(Project.Storage)
public interface IGrantStore { // 直接接收已修改的Grant实例 Task UpdateAsync(Grant grant, CancellationToken cancellationToken = default); }
步骤3:EF仓储实现(Project.Storage.EntityFramework)
public class GrantStore : IGrantStore { private readonly GrantsDbContext _dbContext; public GrantStore(GrantsDbContext dbContext) { _dbContext = dbContext; } public async Task UpdateAsync(Grant grant, CancellationToken cancellationToken = default) { var dbEntity = await _dbContext.Grants.FindAsync(new[] { grant.GrantId }, cancellationToken); if (dbEntity == null) return; // 根据变更集精准更新数据库实体 foreach (var prop in grant.ChangedProperties) { switch (prop) { case nameof(Grant.AuthorizationDetails): dbEntity.AuthorizationDetails = grant.AuthorizationDetails; break; case nameof(Grant.Claims): dbEntity.Claims = grant.Claims; break; // 处理其他属性变更... } } await _dbContext.SaveChangesAsync(cancellationToken); // 重置领域模型的变更标记 grant.ResetChanges(); } }
步骤4:业务层调用示例(Project)
public async Task SomeMethod(Grant grant, CancellationToken cancellationToken = default) { var newAuthorizationDetails = // 生成新的授权详情数据 var newClaims = // 生成新的Claims数据 // 仅需调用领域模型的修改方法 grant.UpdateAuthorizationDetails(newAuthorizationDetails); grant.UpdateClaims(newClaims); // 直接传入模型实例,无需重复编写修改逻辑 await _grantStore.UpdateAsync(grant, cancellationToken); }
Dapper兼容实现示例
如果后续切换到Dapper,仓储可通过变更集生成动态SQL:
public class DapperGrantStore : IGrantStore { private readonly IDbConnection _dbConnection; public DapperGrantStore(IDbConnection dbConnection) { _dbConnection = dbConnection; } public async Task UpdateAsync(Grant grant, CancellationToken cancellationToken = default) { var sqlBuilder = new StringBuilder("UPDATE Grants SET "); var parameters = new DynamicParameters(); parameters.Add("@GrantId", grant.GrantId); var firstProp = true; foreach (var prop in grant.ChangedProperties) { if (!firstProp) sqlBuilder.Append(", "); firstProp = false; switch (prop) { case nameof(Grant.AuthorizationDetails): // 假设以JSON格式存储集合 sqlBuilder.Append("AuthorizationDetails = @AuthorizationDetails"); parameters.Add("@AuthorizationDetails", JsonSerializer.Serialize(grant.AuthorizationDetails)); break; case nameof(Grant.Claims): sqlBuilder.Append("Claims = @Claims"); parameters.Add("@Claims", JsonSerializer.Serialize(grant.Claims)); break; } } sqlBuilder.Append(" WHERE GrantId = @GrantId"); await _dbConnection.ExecuteAsync(sqlBuilder.ToString(), parameters, cancellationToken: cancellationToken); grant.ResetChanges(); } }
方案2:扩展仓储接口,实现"获取-更新-持久化"一体化
保留原有接口的同时,新增一个一体化方法,业务层无需持有模型实例,仅传递修改逻辑即可完成全流程操作。
步骤1:扩展IGrantStore接口(Project.Storage)
public interface IGrantStore { Task UpdateAsync(string grantId, Action<Grant> update, CancellationToken cancellationToken = default); // 新增:获取模型→应用修改→持久化,返回更新后的模型 Task<Grant> GetAndUpdateAsync(string grantId, Action<Grant> update, CancellationToken cancellationToken = default); }
步骤2:EF仓储实现(Project.Storage.EntityFramework)
public async Task<Grant> GetAndUpdateAsync(string grantId, Action<Grant> update, CancellationToken cancellationToken = default) { var entity = await _dbContext.Grants.FindAsync(new[] { grantId }, cancellationToken); if (entity == null) return null; update(entity); await _dbContext.SaveChangesAsync(cancellationToken); // 返回更新后的领域模型供业务层使用 return entity; }
步骤3:业务层调用示例(Project)
public async Task SomeMethod(string grantId, CancellationToken cancellationToken = default) { var newAuthorizationDetails = // 生成新数据 var newClaims = // 生成新数据 // 仅编写一次修改逻辑,仓储负责全流程操作 var updatedGrant = await _grantStore.GetAndUpdateAsync(grantId, grant => { grant.UpdateAuthorizationDetails(newAuthorizationDetails); grant.UpdateClaims(newClaims); }, cancellationToken); // 后续可直接使用updatedGrant进行业务操作 }
方案3:领域事件驱动自动持久化
让领域模型在属性修改时发布领域事件,仓储订阅事件并自动执行数据库更新,完全解耦业务逻辑与持久化操作。
步骤1:定义领域事件(Project.Storage)
public record GrantUpdatedEvent(Grant Grant) : IDomainEvent; public interface IDomainEvent { }
步骤2:修改Grant模型,发布事件(Project.Storage)
public class Grant { // 原有属性与修改方法... private readonly List<IDomainEvent> _domainEvents = new List<IDomainEvent>(); public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly(); public void UpdateAuthorizationDetails(ICollection<AuthorizationDetail> newDetails) { AuthorizationDetails = newDetails; _domainEvents.Add(new GrantUpdatedEvent(this)); } public void UpdateClaims(ICollection<string> newClaims) { Claims = newClaims; _domainEvents.Add(new GrantUpdatedEvent(this)); } public void ClearDomainEvents() => _domainEvents.Clear(); }
步骤3:实现事件处理器(Project.Storage.EntityFramework)
public class GrantUpdatedEventHandler : INotificationHandler<GrantUpdatedEvent> { private readonly IGrantStore _grantStore; public GrantUpdatedEventHandler(IGrantStore grantStore) { _grantStore = grantStore; } public async Task Handle(GrantUpdatedEvent notification, CancellationToken cancellationToken) { await _grantStore.UpdateAsync(notification.Grant, cancellationToken); notification.Grant.ClearDomainEvents(); } }
步骤4:业务层调用示例(Project)
public async Task SomeMethod(Grant grant, IMediator mediator, CancellationToken cancellationToken = default) { var newAuthorizationDetails = // 生成新数据 var newClaims = // 生成新数据 grant.UpdateAuthorizationDetails(newAuthorizationDetails); grant.UpdateClaims(newClaims); // 发布事件,自动触发持久化操作 await mediator.Publish(grant.DomainEvents, cancellationToken); }
方案选择建议
- 若遵循严格领域驱动设计(DDD),优先选方案1:让领域模型封装行为与变更,完全符合清洁架构的依赖规则(领域层不依赖基础设施)。
- 若业务逻辑简单、不想改动领域模型,可选方案2:仅扩展仓储接口,代码改动最小。
- 若项目已采用事件驱动架构,选方案3:实现业务逻辑与持久化的完全解耦,无需手动调用仓储方法。
内容的提问来源于stack exchange,提问作者Szyszka947
相关产品推荐
相关产品推荐

