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

模块化单体架构中多DbContext场景下UnitOfWork模式的数据库保存异常问题

模块化单体架构中多DbContext场景下UnitOfWork模式的数据库保存异常问题

看起来你遇到的问题核心在于通用IUnitOfWork的服务注册被覆盖了,咱们一步步拆解问题和解决方案:

问题根源分析

你的AddUnitOfWork<T>扩展方法里,每次注册都会执行services.AddScoped<IUnitOfWork, T>()。在ASP.NET Core的DI容器规则里,当多次为同一个接口(这里是IUnitOfWork)注册不同实现时,最后一次注册的实现会完全覆盖之前的所有。

所以你先注册CustomersUnitOfWork,再注册BasketsUnitOfWork后,所有注入IUnitOfWork的地方都会拿到BasketsUnitOfWork实例,对应的DbContext自然是BasketsDbContext——这就解释了为什么创建Customer时日志显示用的是BasketsDbContext,数据根本没往CustomersDbContext里写入。

解决方案:为每个模块定义专属UnitOfWork接口

最清晰且符合SOLID原则的方式是给每个模块的UnitOfWork创建专属接口,避免通用接口的覆盖问题,具体步骤如下:

1. 定义模块专属的UnitOfWork接口

在Shared.Abstractions.UnitOfWork中,为每个模块添加继承自IUnitOfWork的专属接口:

// 为Customers模块定义专属UoW接口
public interface ICustomersUnitOfWork : IUnitOfWork
{
}

// 为Baskets模块定义专属UoW接口
public interface IBasketsUnitOfWork : IUnitOfWork
{
}

2. 修改模块UnitOfWork的实现

让CustomersUnitOfWork实现专属接口:

internal class CustomersUnitOfWork : SqlServerUnitOfWork<CustomersDbContext>, ICustomersUnitOfWork
{
    public CustomersUnitOfWork(CustomersDbContext dbContext,
        IDomainEventDispatcher domainEventDispatcher)
        : base(dbContext, domainEventDispatcher)
    {
    }
}

同理,BasketsUnitOfWork也要实现IBasketsUnitOfWork。

3. 调整AddUnitOfWork扩展方法

修改扩展方法,支持注册专属接口,避免覆盖通用IUnitOfWork:

public static IServiceCollection AddUnitOfWork<TInterface, TImplementation>(this IServiceCollection services) 
    where TInterface : class, IUnitOfWork
    where TImplementation : class, TInterface
{
    services.AddScoped<TInterface, TImplementation>();
    services.AddScoped<TImplementation>();
    using var serviceProvider = services.BuildServiceProvider();
    serviceProvider.GetRequiredService<UnitOfWorkTypeRegistry>().Register<TImplementation>();
    return services;
}

4. 更新模块的服务注册

在Customers模块的注册代码中,使用专属接口注册UnitOfWork:

public static IServiceCollection AddCustomersInfrastructure(this IServiceCollection services, IConfiguration configuration)
{
    services.AddDbContext<CustomersDbContext>(options =>
    {
        options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"));
    });
    services.AddScoped<ICustomerRepository, CustomerRepository>();
    // 注册专属的ICustomersUnitOfWork
    services.AddUnitOfWork<ICustomersUnitOfWork, CustomersUnitOfWork>();
    return services;
}

Baskets模块同理,注册IBasketsUnitOfWork。

5. 在Handler中注入专属UnitOfWork

修改SignUpCustomerCommandHandler,注入ICustomersUnitOfWork而非通用IUnitOfWork:

public class SignUpCustomerCommandHandler
{
    private readonly ICustomerRepository _customerRepository;
    private readonly ICustomersUnitOfWork _unitOfWork;
    private readonly IHashingService _hashingService;
    private readonly ITokenManager _tokenManager;

    public SignUpCustomerCommandHandler(ICustomerRepository customerRepository,
        ICustomersUnitOfWork unitOfWork,
        IHashingService hashingService,
        ITokenManager tokenManager)
    {
        _customerRepository = customerRepository;
        _unitOfWork = unitOfWork;
        _hashingService = hashingService;
        _tokenManager = tokenManager;
    }

    public async Task<AuthenticationResult> Handle(SignUpCustomerCommand command, CancellationToken cancellationToken)
    {
        // 原有业务逻辑保持不变...
        await _customerRepository.Add(customer);
        // 现在调用的是Customers模块专属的UnitOfWork
        await _unitOfWork.CommitAndDispatchDomainEventsAsync(customer);
        // 原有业务逻辑保持不变...
    }
}

验证效果

修改完成后,再次执行SignUp接口,日志里应该会显示dbcontext: CustomersDbContext,此时Customer数据就能正常写入对应的数据库上下文了。

备选方案:使用命名服务(不推荐,可读性较差)

如果不想创建大量专属接口,也可以用DI的命名服务来区分不同的UnitOfWork,但这种方式会增加代码复杂度:

  1. 修改AddUnitOfWork方法,用命名方式注册:
public static IServiceCollection AddUnitOfWork<T>(this IServiceCollection services, string name) where T : class, IUnitOfWork
{
    services.AddScoped<T>();
    services.AddScopedKeyed<IUnitOfWork, T>(name);
    using var serviceProvider = services.BuildServiceProvider();
    serviceProvider.GetRequiredService<UnitOfWorkTypeRegistry>().Register<T>();
    return services;
}
  1. 在Handler中通过键值获取对应的UnitOfWork:
public SignUpCustomerCommandHandler(ICustomerRepository customerRepository,
    IKeyedServiceProvider keyedServiceProvider,
    /* 其他依赖 */)
{
    _customerRepository = customerRepository;
    _unitOfWork = keyedServiceProvider.GetRequiredKeyedService<IUnitOfWork>("Customers");
    // ...
}

但这种方式不如专属接口直观,长期维护成本更高,所以还是优先推荐专属接口方案。

备注:内容来源于stack exchange,提问作者Mateusz Babski

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.22 07:52:58