【问题标题】:Different threads getting same DbContext from Microsoft.Extensions.DependencyInjection不同的线程从 Microsoft.Extensions.DependencyInjection 获得相同的 DbContext
【发布时间】:2021-12-19 21:38:13
【问题描述】:

我有一个 WPF 应用程序,其视图模型调用数据访问类来执行数据库工作。 DataAccess 类具有所有异步函数,VM 使用 await _dataAccess.DoWork(withItem);有时我会收到一个错误,即在同一上下文中执行两个操作。除非服务提供者在对 GetService 的不同调用中使用相同的上下文,否则这是不可能的。

在 App.xaml.cs 中

services.AddDbContext<MyItemDbContext>(options =>
{
    options.UseLoggerFactory(_loggerFactory);
    options.UseSqlServer(config.GetConnectionString("My_Items_ConnectionString"));
});
// register concrete class for IDataAccess
services.AddScoped<IDataAccess, ItemDataAccess>();

查看代码

private async void BtnReady_Click(object sender, RoutedEventArgs e)
{
    await SetStatusForSelectedItem(StatusIds.Ready);
}
private async Task SetStatusForSelectedItems(StatusIds statusId)
{
    // get selected itemDetail item from grid
    await _mainWindowVM.UpdateItemDetailState(itemDetail, stateId);
}

在 ViewModel 代码中

private readonly IDataAccess _dataAccess;
public MainWindowVM(IDataAccess dataAccess)
{
    _dataAccess = dataAccess;
}
public async Task UpdateItemDetailState(Item item, StatusIds stateId)
{
    // other necessary code
    item.StateId = stateId;
    await _dataAccess.UpdateItem(item);
}

在 DataAccess (DalBase) 代码中

private readonly IServiceProvider _serviceProvider;
public DalBase(IServiceProvider serviceProvider)
{
    _serviceProvider = serviceProvider;
}

protected MyItemDbContext Get_Item_DbContext()
{
    return _serviceProvider.GetService<MyItemDbContext>();
}
// other contexts for different purposes

在 DataAccess 中(派生)

public ItemDataAccess(IServiceProvider serviceProvider) : base(serviceProvider) { }

public async Task UpdateItem(Item item)
{
    MyItemDbContext db = Get_Item_DbContext(); // <<< === should be new instance each time, right?
    db.Items.Update(item);
    await db.SaveChangesAsync();
}

我自始至终都遵循这种模式,只是我尝试了很多尝试...catch 来减小尺寸。 DataAccess 中的每个函数都以调用Get_Item_DbContext() 开始,我曾经拥有using (var db = Get_Item_DbContext()),但我读到的一篇文章说让依赖注入决定生命周期。我找不到任何我没有在异步函数上使用 await 的地方,但我收到这样的错误......

在前一个操作完成之前在此上下文上启动了第二个操作...

有一个操作在列表中运行,它调用了这个更新状态函数,还有一个按钮,用户可以使用它来调用相同的函数。那是我得到错误的时候。

编辑我曾经有using (var db = Get_Item_DbContext),但后来我得到了另一个错误...无法访问已释放的上下文实例。

更新 我尝试将上下文注入 DalBase。单击运行第二个操作时仍然出现错误。在这种情况下,可能是因为这两个操作都通过同一个 VM 运行,该 VM 现在具有一个 _dataAccess 和一个注入的上下文。我试图通过从每个函数中获取一个新实例来避免这种情况。

所以我的问题是这些 如何找出哪些线程(及其调用堆栈)正在获得相同的上下文? 为什么两个单独的线程(来自异步调用)从 GetService 获取相同的上下文? 是否有我必须添加到 AddDbContext 以使其范围正确的选项?

谢谢 迈克

【问题讨论】:

  • 您是否可能在没有先将查询结果复制到 BindingList 的情况下对查询结果进行数据绑定?还是启用了延迟加载?
  • 如果有一个列表被返回,那么我使用Task&lt;List&lt;Item&gt;&gt; 而不是Task&lt;IEnumerable&lt;Item&gt;&gt;await db.Items.ToListAsync() 如果这就是你的意思。我不确定延迟加载。如果有记忆,默认情况下不会启用。我必须专门启用它,对吗?
  • 如果您的导航属性是虚拟的,则在 EF6 中默认启用延迟加载。 docs.microsoft.com/en-us/ef/ef6/querying/…
  • 感谢您的快速回复。我会通读一遍,看看是否可行。
  • 同样在 WPF MVVM 中,我认为您应该只看到来自 UI 线程的 DbContext 访问。所以你可以在 EF Logging 中转储 ThreadId。

标签: c# entity-framework mvvm dependency-injection async-await


【解决方案1】:

我找到了另一个答案。我们可以使用具有相同选项的 AddDbContextFactory 而不是使用 AddDbContext:

services.AddDbContextFactory<ItemContext>(options =>
{
    options.UseLoggerFactory(_loggerFactory);
    options.UseSqlServer(config.GetConnectionString("ItemConnString"));
});

我使用构造函数注入来获取工厂,从而避免了反模式隐藏依赖:

public DalBase(IDbContextFactory<ItemContext> itemContextFactory, ...)

然后在将上下文分配给调用者的函数中:

return _itemContextFactory.CreateDbContext();

这会为每个调用函数创建一个新实例。

它似乎适用于服务范围与 DbContext 生命周期不一致的 Blazor 应用程序,如 using-a-dbcontext-factory-eg-for-blazor 中所述。我的 WPF 视图模型似乎就是这种情况。

【讨论】:

    【解决方案2】:

    作为 WPF 应用程序需要对管理 EF DbContexts 有一些不同的看法。许多关于依赖注入和生命周期范围的例子都是指 Web 应用程序,这些应用程序通常定义了一个明确的生命周期范围,即 Web 请求。在这些情况下,您可以将 DbContext 直接注册到具有“每个请求”生命周期范围的容器中,并且一切正常。使用 WPF,在呈现视图和响应事件时,没有明确的范围。因此,您必须明确定义 DbContext 的生命周期范围。通常这是通过工作单元模式完成的,您的依赖注入器可以提供 UoW 范围工厂。

    使用using(var context = new AppDbContext()) 方法本质上没有任何问题,但是您必须遵守一个非常重要的细节:在 DbContext 范围内读取的实体实例必须保持在该 DbContext 范围内,否则将被分离并从那时起按原样处理。这意味着,只有在 DbContext 处于作用域内时,才能在后台获取相关数据的延迟加载等功能才有效。

    当你有这样的代码时:

    using(var context = new AppDbContext())
    {
        var order = context.Orders.Single(x => x.OrderId == orderId);
        orderForm.Model = order;
    }
    

    或类似的东西,我们定义一个上下文范围,获取一个实体,然后共享该引用到任何东西,我们允许该实体引用离开产生它的 DbContext 的范围。该实体现在是处于孤立状态的分离实体。我们可以访问它的属性,但是当我们尝试访问任何没有预先加载或在加载之前由 DbContext 填充的导航属性时,我们将得到一个错误。实体仍然认为它已附加到 DbContext 但已释放 DbContext。我们可以显式分离它:

    using(var context = new AppDbContext())
    {
        var order = context.Orders.Single(x => x.OrderId == orderId);
        context.Entity(order).State = EntityState.Detached;
        orderForm.Model = order;
    }
    

    或在不跟踪的情况下加载它:

    using(var context = new AppDbContext())
    {
        var order = context.Orders.AsNoTracking().Single(x => x.OrderId == orderId);
        orderForm.Model = order;
    }
    

    这将有效地做同样的事情,返回一个分离的实体。但是现在,如果您尝试访问未加载的导航属性,您将遇到#nulls。这可能是有问题的,因为 #null 是否意味着订单实际上没有引用,或者只是没有预取?

    您更改为让服务定位器按需提供 DbContext 避免了“超出范围”问题,但现在问题变成了每次调用 Get_Item_DbContext() 都将返回单个 DbContext 实例,并且您必须如果您尝试异步或通过工作线程运行操作,则会遇到跨线程异常。要使用服务定位器解决此问题,您需要显式管理 DbContext 的范围,并知道何时应提供共享的 DbContext 实例与新的 DbContext 实例。这仍然会导致在这些范围之间传递实体的问题。 (即在工作线程上加载并返回到具有不同 DbContext 实例的另一个线程。)

    应尽可能避免使用分离的实体。它们不仅会导致错误和潜在的无效数据状态,而且还存在用过时值覆盖数据的风险。延迟加载是系统在需要时按需加载数据的便捷拐杖,但围绕它进行设计意味着您的应用程序将遇到很多 SELECT n+1 性能问题。在设计系统时,无论是 Web 还是 WPF Windows 应用程序,我的建议是为所有离开 DbContext 范围的数据利用 POCO 视图模型,并仅在绝对必要时保持这些 DbContext 实例打开。

    using(var context = new AppDbContext())
    {
        var orderViewModel = context.Orders
            .Where(x => x.OrderId == orderId)
            .Select(x => new OrderViewModel 
            {
                // Populate view model with just the fields about the order and related data that the view needs.
            }).Single();
        orderForm.Model = orderViewModel;
    }
    

    或使用 Automapper:

    //Note: Configuration can be centralized elsewhere, and can include any/all mappings for the entire root aggregate (Order and it's associated entities)
    var config = new MapperConfiguration(cfg => cfg.CreateMap<Order, OrderViewModel>());
    
    using(var context = new AppDbContext())
    {
        var orderViewModel = context.Orders
            .Where(x => x.OrderId == orderId)
            .ProjectTo<OrderViewModel>(config)
            .Single();
        orderForm.Model = orderViewModel;
    }
    

    虽然断开连接的实体可以预先加载所需的导航属性并将其传递给视图,但向下投影到视图模型可以产生更高效的查询,在新关系出现时防止系统出现意外错误或性能问题等。附加,并有助于避免在传递实体引用时混淆方法可能在不同完成级别中附加或分离实体的位置。 (您可以轻松区分视图模型和关联实体)

    【讨论】:

    • 感谢@Steve Py 的回复。您完美地捕捉到了意图,我正在尝试获取分离的实体。我认为通过从服务提供者获取 DbContext 并在退出函数时使用 .Include()ToList() 使 DbContext 超出范围会发生这种情况。我会阅读ProjectTo 看看是否有帮助。我认为用new ItemDbContext() 重构可能会奏效。
    • 好的。我重构了基础以提供新实例,而不是指望服务提供商在每次调用时提供新实例。我将寻找方法让服务提供商在每次使用的基础上进行,但现在一切正常。非常感谢。
    • 是的,使用.Include() 将确保加载相关实体。但是,随着项目随着时间的推移而成熟并且模式适应,如果代码开始假设可以使用新的关系,您可能会遇到问题。如果Include 使用是选择性的,那么您的情况是所有接受实体的代码都必须检查,或者假设什么可用,什么不可用,或者您走上实体总是Include 一切只是为了安全/完全的。这会导致获取和传输的数据远远超出所需......
    • 投影视图模型/dtos 有助于避免域更改的影响。 DTO 仅在需要时进行更改,并且对基础域实体的添加不会危及现有代码,因为这些新关系仅在需要更新 DTO 时才会发挥作用。我推荐显式映射配置,而不是依赖于映射时的约定,因为这为您提供了对可能的重大更改的构建时验证,而不是运行时验证。 (即,如果您正在考虑删除/移动/重命名属性,则在您的实体上查找用法会返回命中。)
    • 感谢@Steve Py 的提示。有很多东西要学:)
    【解决方案3】:

    我建议停止在类中注入 ServiceProvider 并获得这样的服务。我称之为反模式(服务定位器)。 而是直接在类中注入您的依赖项。

    所以改为:

    在 DataAccess (DalBase) 代码中

    private readonly MyItemDbContext _myItemDbContext;
    public DalBase(MyItemDbContext myItemDbContext)
    {
        _myItemDbContext = myItemDbContext;
    }
    
    // Don't expose the myItemDbContext from this class. Just use it to what you need
    
    // other contexts for different purposes
    

    在 DataAccess 中(派生)

    private readonly MyItemDbContext _myItemDbContext;
    public ItemDataAccess(MyItemDbContext myItemDbContext)
    {
        _myItemDbContext = myItemDbContext;
    }
    
    public async Task UpdateItem(Item item)
    {
        _myItemDbContext.Items.Update(item);
        await _myItemDbContext.SaveChangesAsync();
    }
    

    如果您不将 MyItemDbContext 的使用暴露给其他类,我认为您的问题将会消失。

    【讨论】:

    • 感谢您的快速回答。我尝试将上下文注入 DalBase,但它不起作用(我用注释更新了问题)。如果在类中使用 IServiceProvider 是一种反模式,那么如何在需要时创建服务?
    猜你喜欢
    • 1970-01-01
    • 2011-11-24
    • 2021-05-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多