如何在Entity Framework Core中实现多线程且不违反SOLID原则?
问题描述
我有一个基于ASP.NET Core Web API的应用程序,使用Entity Framework Core 7连接数据库。我理解依赖注入的概念,也知道通过AddDbContext可以在请求生命周期内向任意对象注入特定的DbContext。
这种方式运行良好,但当我需要并行执行数据库请求时就会出现问题。以下是负责从数据库获取数据的示例类:
public class CarData : ICarData { private readonly CarContext _car; private readonly IMapper _mapper; public CarData(CarContext car, IMapper mapper) { _car = car; _mapper = mapper; } public bool CarExists(int carId) { return _car.Cars.Any(b => b.CarId == carId); } public async Task<CarDetailsOutput> GetCarDetails(int carId) { return (await GetCarsDetails(new List<int> { carId })).FirstOrDefault(); } public async Task<List<CarDetailsOutput>> GetCarsDetails(List<int> carIds) { var cars = await _car.Cars .Where(i => carIds.Contains(i.CarId)) .ToListAsync(); var output = _mapper.Map<IEnumerable<CarDetailsOutput>>(cars).ToList(); return output; } }
如果有服务需要车辆数据,可以注入ICarData并复用。但如果要并行调用GetCarsDetails和其他同样使用CarContext的方法,代码会执行失败。例如,无法正常运行以下代码:
public async Task<ItemDetailsOutput> DoSomeLogic(List<int> carIds) { var task1 = _carData.GetCarsDetails(carIds); var task2 = _shopData.GetSomeInformation(); var task3 = _saleData.GetSomeInformation(); var task4 = _offerData.GetSomeInformation(); await Task.WhenAll(task1, task2, task3, task4); // 后续逻辑处理 }
只要其中任意两个任务使用了CarContext,代码就会出错。
我想到了一个解决方案,但不确定是否违反SOLID原则,具体步骤如下:
- 创建扩展类:
public static class DbContextExtensions { public static TDbContext CreateNew<TDbContext>(this TDbContext db) where TDbContext : DbContext { var connectionString = db.Database.GetDbConnection().ConnectionString; var options = CreateBuilderOptions<TDbContext>(connectionString); return (TDbContext)Activator.CreateInstance(typeof(TDbContext), options); } private static DbContextOptions<TEntity> CreateBuilderOptions<TEntity>(string connectionString) where TEntity : DbContext { var serviceProvider = new ServiceCollection() .AddEntityFrameworkSqlServer() .BuildServiceProvider(); var builder = new DbContextOptionsBuilder<TEntity>(); builder.UseSharedOptions(connectionString) .UseInternalServiceProvider(serviceProvider); return builder.Options; } }
- 修改会被用于并行流程的方法,比如
_carData.GetCarsDetails:
public async Task<List<CarDetailsOutput>> GetCarsDetails(List<int> carIds) { await using var newCarContext = _car.CreateNew(); var cars = await newCarContext.Cars .Where(i => carIds.Contains(i.CarId)) .ToListAsync(); var output = _mapper.Map<IEnumerable<CarDetailsOutput>>(cars).ToList(); return output; }
通过这种方式,我无需考虑在哪里注入工厂、在哪里注入DbContext,只需注入所需的DbContext即可。但当我知道方法会被用于并行流程时,会基于最初注入的DbContext创建一个新实例。
我不会在并行流程中使用事务。
请问这个方案是否可行?是否违反了SOLID原则?
回答
方案可行性分析
这个方案可以正常运行,但存在几个潜在隐患:
- 资源浪费与配置不一致:每次创建新DbContext时都重新构建EF Core服务提供者,会重复初始化内部服务(如日志、缓存组件),不仅消耗额外资源,还可能导致新实例与原DbContext的配置(如拦截器、变更跟踪设置)不一致。
- 构造函数扩展性差:使用
Activator.CreateInstance创建DbContext,如果后续DbContext构造函数新增了DbContextOptions以外的参数,这个方法会直接抛出异常。 - 配置丢失风险:仅通过连接字符串复用配置,原DbContext的自定义配置(如日志级别、查询超时)无法传递到新实例,可能引发不可预期的行为。
SOLID原则合规性分析
该方案违反了部分SOLID原则:
- 单一职责原则(SRP):
CarData原本仅负责数据访问逻辑,现在额外承担了DbContext实例的创建与管理职责,职责边界模糊。 - 依赖倒置原则(DIP):
CarData直接依赖具体的DbContext创建实现,而非抽象工厂,降低了代码的可测试性——测试时无法轻松替换为模拟的DbContext实例。 - 开闭原则(OCP):后续若需修改DbContext的创建逻辑(如切换数据库提供商、添加全局拦截器),必须修改
DbContextExtensions及所有调用CreateNew的方法,不符合"对扩展开放、对修改关闭"的要求。
更优替代方案
EF Core官方针对并行数据访问场景,推荐使用**IDbContextFactory<TContext>**抽象,这是更符合SOLID原则的解决方案:
- 注册DbContext工厂:在
Program.cs中注册工厂而非直接注册DbContext:
builder.Services.AddDbContextFactory<CarContext>(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); // 保留所有原DbContext的自定义配置(如拦截器、日志) });
- 在数据访问类中注入工厂:
public class CarData : ICarData { private readonly IDbContextFactory<CarContext> _contextFactory; private readonly IMapper _mapper; public CarData(IDbContextFactory<CarContext> contextFactory, IMapper mapper) { _contextFactory = contextFactory; _mapper = mapper; } public async Task<List<CarDetailsOutput>> GetCarsDetails(List<int> carIds) { await using var carContext = await _contextFactory.CreateDbContextAsync(); var cars = await carContext.Cars .Where(i => carIds.Contains(i.CarId)) .ToListAsync(); return _mapper.Map<IEnumerable<CarDetailsOutput>>(cars).ToList(); } }
这种方式的优势:
- 严格遵循SOLID原则,
CarData仅负责数据访问,DbContext的创建由抽象工厂处理,符合依赖倒置。 - 自动复用EF Core的服务提供者与所有配置,不会出现资源浪费或配置不一致问题。
- 可测试性更强,测试时可通过模拟
IDbContextFactory轻松替换真实DbContext。 - 扩展性更好,后续修改DbContext配置只需调整注册代码,无需改动数据访问类。
内容的提问来源于stack exchange,提问作者Ish Thomas
相关产品推荐
相关产品推荐

