模块化单体架构中多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,但这种方式会增加代码复杂度:
- 修改
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; }
- 在Handler中通过键值获取对应的UnitOfWork:
public SignUpCustomerCommandHandler(ICustomerRepository customerRepository, IKeyedServiceProvider keyedServiceProvider, /* 其他依赖 */) { _customerRepository = customerRepository; _unitOfWork = keyedServiceProvider.GetRequiredKeyedService<IUnitOfWork>("Customers"); // ... }
但这种方式不如专属接口直观,长期维护成本更高,所以还是优先推荐专属接口方案。
备注:内容来源于stack exchange,提问作者Mateusz Babski

