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

如何在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原则,具体步骤如下:

  1. 创建扩展类:
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;
    }
}
  1. 修改会被用于并行流程的方法,比如_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原则?


回答

方案可行性分析

这个方案可以正常运行,但存在几个潜在隐患:

  1. 资源浪费与配置不一致:每次创建新DbContext时都重新构建EF Core服务提供者,会重复初始化内部服务(如日志、缓存组件),不仅消耗额外资源,还可能导致新实例与原DbContext的配置(如拦截器、变更跟踪设置)不一致。
  2. 构造函数扩展性差:使用Activator.CreateInstance创建DbContext,如果后续DbContext构造函数新增了DbContextOptions以外的参数,这个方法会直接抛出异常。
  3. 配置丢失风险:仅通过连接字符串复用配置,原DbContext的自定义配置(如日志级别、查询超时)无法传递到新实例,可能引发不可预期的行为。

SOLID原则合规性分析

该方案违反了部分SOLID原则:

  • 单一职责原则(SRP):CarData原本仅负责数据访问逻辑,现在额外承担了DbContext实例的创建与管理职责,职责边界模糊。
  • 依赖倒置原则(DIP):CarData直接依赖具体的DbContext创建实现,而非抽象工厂,降低了代码的可测试性——测试时无法轻松替换为模拟的DbContext实例。
  • 开闭原则(OCP):后续若需修改DbContext的创建逻辑(如切换数据库提供商、添加全局拦截器),必须修改DbContextExtensions及所有调用CreateNew的方法,不符合"对扩展开放、对修改关闭"的要求。

更优替代方案

EF Core官方针对并行数据访问场景,推荐使用**IDbContextFactory<TContext>**抽象,这是更符合SOLID原则的解决方案:

  1. 注册DbContext工厂:在Program.cs中注册工厂而非直接注册DbContext:
builder.Services.AddDbContextFactory<CarContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
    // 保留所有原DbContext的自定义配置(如拦截器、日志)
});
  1. 在数据访问类中注入工厂:
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 11:59:56