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

如何在DbContext的SaveChanges中注入Facade避免循环依赖(遵循Clean Architecture)

问题解决思路与实现方案

核心问题拆解

  1. 循环依赖:DataBaseContext 依赖 IUserActionLogFacade,而 UserActionsLogFacade 又依赖 IDataBaseContext,形成构造注入循环。
  2. Clean Architecture 违反:持久层(DataBaseContext)直接调用领域层的 Facade 服务,打破了"依赖倒置"和"分层隔离"原则。

解决方案:事件驱动解耦

通过领域事件机制将实体变更逻辑与日志记录逻辑解耦,既打破循环依赖,又符合 Clean Architecture 分层要求。

步骤1:定义领域事件(领域层)

在领域层封装实体变更的事件信息,作为上下文传递的载体:

// 领域事件基类
public abstract class DomainEvent
{
    public DateTime OccurredOn { get; } = DateTime.UtcNow;
}

// 实体变更事件
public class EntityChangedEvent : DomainEvent
{
    public string EntityName { get; }
    public string Action { get; }
    public string PrimaryKeyValue { get; }
    public long UserId { get; }
    public Dictionary<string, (object OldValue, object NewValue)> ChangedProperties { get; }

    public EntityChangedEvent(string entityName, string action, string primaryKeyValue, long userId, Dictionary<string, (object, object)> changedProps)
    {
        EntityName = entityName;
        Action = action;
        PrimaryKeyValue = primaryKeyValue;
        UserId = userId;
        ChangedProperties = changedProps;
    }
}

// 领域事件发布接口
public interface IDomainEventPublisher
{
    void Publish(DomainEvent domainEvent);
}

步骤2:修改DataBaseContext(持久层)

移除对 IUserActionLogFacade 的依赖,改为在 SaveChanges 中收集变更并发布领域事件:

public class DataBaseContext : DbContext, IDataBaseContext
{
    private readonly IDomainEventPublisher _domainEventPublisher;

    public DataBaseContext(DbContextOptions options, IDomainEventPublisher domainEventPublisher) 
        : base(options)
    {
        _domainEventPublisher = domainEventPublisher;
    }

    // DbSet定义保持不变...

    public override int SaveChanges()
    {
        // 排除日志实体,避免循环触发事件
        var modifiedEntries = ChangeTracker.Entries()
            .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted
                        && e.Entity.GetType() != typeof(UsersActionsLog));

        foreach (var entry in modifiedEntries)
        {
            // 处理时间字段逻辑保持不变
            var entityType = entry.Context.Model.FindEntityType(entry.Entity.GetType());
            var inserted = entityType.FindProperty("InsertDate");
            var updated = entityType.FindProperty("UpdateDate");
            var deleted = entityType.FindProperty("DeleteDate");

            switch (entry.State)
            {
                case EntityState.Added:
                    inserted?.PropertyInfo.SetValue(entry.Entity, DateTime.Now);
                    break;
                case EntityState.Modified:
                    updated?.PropertyInfo.SetValue(entry.Entity, DateTime.Now);
                    break;
                case EntityState.Deleted:
                    deleted?.PropertyInfo.SetValue(entry.Entity, DateTime.Now);
                    entry.State = EntityState.Modified;
                    break;
            }

            // 收集变更详情(仅针对修改操作)
            var changedProps = new Dictionary<string, (object OldValue, object NewValue)>();
            if (entry.State == EntityState.Modified)
            {
                foreach (var prop in entry.Properties.Where(p => p.IsModified))
                {
                    changedProps.Add(prop.Metadata.Name, (prop.OriginalValue, prop.CurrentValue));
                }
            }

            // 发布领域事件
            var action = entry.State switch
            {
                EntityState.Added => "Added",
                EntityState.Modified => "Modified",
                EntityState.Deleted => "Deleted",
                _ => string.Empty
            };

            // 获取主键值(通用逻辑,适配不同实体)
            var primaryKey = entry.Properties.First(p => p.Metadata.IsPrimaryKey()).CurrentValue?.ToString() ?? string.Empty;

            // 实际项目中从上下文获取当前用户ID,这里暂时硬编码
            var userId = 1;

            _domainEventPublisher.Publish(new EntityChangedEvent(
                entry.Entity.GetType().Name,
                action,
                primaryKey,
                userId,
                changedProps
            ));
        }

        return base.SaveChanges();
    }
}

步骤3:实现事件处理器(应用层)

在应用层实现事件处理器,负责接收实体变更事件并调用日志服务记录操作:

// 事件处理器通用接口
public interface IDomainEventHandler<TEvent> where TEvent : DomainEvent
{
    Task Handle(TEvent domainEvent);
}

// 实体变更事件处理器
public class EntityChangedEventHandler : IDomainEventHandler<EntityChangedEvent>
{
    private readonly IPostUserActionLogService _logService;

    public EntityChangedEventHandler(IPostUserActionLogService logService)
    {
        _logService = logService;
    }

    public Task Handle(EntityChangedEvent domainEvent)
    {
        // 遍历变更属性,生成多条日志(如果需要)
        foreach (var prop in domainEvent.ChangedProperties)
        {
            var logDto = new RequestPostUserActionLogServiceDto
            {
                Entity = domainEvent.EntityName,
                Action = domainEvent.Action,
                PrimaryKeyValue = domainEvent.PrimaryKeyValue,
                UserId = domainEvent.UserId,
                Successful = true,
                PropertyName = prop.Key,
                OldValue = prop.Value.OldValue?.ToString() ?? string.Empty,
                NewValue = prop.Value.NewValue?.ToString() ?? string.Empty
            };

            _logService.Execute(logDto);
        }

        // 新增/删除操作直接生成单条日志
        if (domainEvent.Action is "Added" or "Deleted")
        {
            var logDto = new RequestPostUserActionLogServiceDto
            {
                Entity = domainEvent.EntityName,
                Action = domainEvent.Action,
                PrimaryKeyValue = domainEvent.PrimaryKeyValue,
                UserId = domainEvent.UserId,
                Successful = true,
                PropertyName = string.Empty,
                OldValue = string.Empty,
                NewValue = string.Empty
            };

            _logService.Execute(logDto);
        }

        return Task.CompletedTask;
    }
}

步骤4:实现事件发布器(基础设施层)

实现领域事件发布器,通过依赖注入容器查找并执行对应处理器:

public class DomainEventPublisher : IDomainEventPublisher
{
    private readonly IServiceProvider _serviceProvider;

    public DomainEventPublisher(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public void Publish(DomainEvent domainEvent)
    {
        var handlerType = typeof(IDomainEventHandler<>).MakeGenericType(domainEvent.GetType());
        dynamic handler = _serviceProvider.GetService(handlerType);
        handler?.Handle((dynamic)domainEvent).Wait();
    }
}

步骤5:调整依赖注入配置

在启动类中注册所有相关服务,同时简化原Facade避免循环:

// Program.cs 或 Startup.cs
builder.Services.AddScoped<IDataBaseContext, DataBaseContext>();
builder.Services.AddScoped<IPostUserActionLogService, PostUserActionLogService>();
builder.Services.AddScoped<IDomainEventPublisher, DomainEventPublisher>();
builder.Services.AddScoped<IDomainEventHandler<EntityChangedEvent>, EntityChangedEventHandler>();

// 简化Facade,避免依赖Context
public class UserActionsLogFacade : IUserActionLogFacade
{
    private readonly IPostUserActionLogService _postService;

    public UserActionsLogFacade(IPostUserActionLogService postService)
    {
        _postService = postService;
    }

    public IPostUserActionLogService PostUserActionLogService => _postService;
}

builder.Services.AddScoped<IUserActionLogFacade, UserActionsLogFacade>();

方案优势

  1. 打破循环依赖:通过事件中间层,彻底切断 DataBaseContext 与 Facade/LogService 的直接依赖。
  2. 符合Clean Architecture:
    • 领域层定义事件和抽象接口,处于核心位置。
    • 应用层实现事件处理逻辑,协调领域服务与基础设施。
    • 持久层仅负责数据持久化和事件发布,不直接调用领域服务。
  3. 扩展性强:后续新增其他变更处理逻辑(如通知、审计),只需新增事件处理器即可,无需修改 SaveChanges 代码。

内容的提问来源于stack exchange,提问作者Ali.M Eghbaldar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 16:35:54