You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在清洁架构中实现领域模型与持久化的同步?

解决领域模型与持久化层同步的重复操作问题(遵循清洁架构)

问题背景

当前解决方案包含三层结构:

  • 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 16:45:21