不同的线程从 Microsoft.Extensions.DependencyInjection 获取相同的 DbContext
Different threads getting same DbContext from Microsoft.Extensions.DependencyInjection
我有一个 WPF 应用程序,其视图模型调用数据访问 class 来完成数据库工作。 DataAccess class 具有所有异步功能,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();
}
我自始至终都遵循这个模式,除了我用了很多 try...catch 来减小尺寸。 DataAccess 中的每个函数都以调用 Get_Item_DbContext()
开始,我曾经有 using (var db = Get_Item_DbContext())
但我读过的一篇文章说让依赖注入决定生命周期。我找不到任何地方没有在异步函数上使用等待,但我收到这样的错误...
在上一个操作完成之前在此上下文中启动了第二个操作...
有一个操作正在处理列表,它调用此更新状态函数,并且有一个按钮可供用户用来调用相同的函数。那是我收到错误的时候。
编辑 我曾经有 using (var db = Get_Item_DbContext)
但后来我得到了一个不同的错误...无法访问已处置的上下文实例。
UPDATE 我尝试将上下文注入 DalBase。当我点击 运行 第二次操作时仍然出现错误。在这种情况下,可能是因为两个操作 运行 通过同一个 VM,现在有一个 _dataAccess 和一个被注入的上下文。我试图通过从每个函数获取一个新实例来避免这种情况。
所以我的问题是这些
我如何找出哪些线程(及其调用堆栈)正在获得相同的上下文?
为什么两个单独的线程(来自异步调用)从 GetService 获得相同的上下文?
有没有我必须添加到 AddDbContext 以使其范围正确的选项?
谢谢
麦克
我建议停止在 类 中注入 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 的用法,我认为您的问题就会消失 类。
作为 WPF 应用程序需要对管理 EF DbContext 有一些不同的看法。许多讨论依赖注入和生命周期范围的例子都指的是 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;
}
这将有效地做同样的事情,returning 一个分离的实体。但是现在,如果您尝试访问未加载的导航 属性,您将遇到 #nulls。这可能会有问题,因为 #null 是否意味着该订单实际上没有引用,或者只是没有预取?
让服务定位器按需提供 DbContext 的更改避免了“超出范围”问题,但现在的问题是每次对 Get_Item_DbContext()
的调用都会 return单个 DbContext 实例,如果您尝试异步或通过工作线程 运行 操作,您一定会遇到跨线程异常。要使用服务定位器解决此问题,您需要显式管理 DbContext 的范围,并知道何时应提供共享 DbContext 实例与新的 DbContext 实例。这仍然会导致在这些范围之间传递实体的问题。 (即加载到工作线程上并 returned 到另一个具有不同 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;
}
虽然断开连接的实体可能需要急切加载并传递给视图的导航属性,但向下投影到视图模型可以产生更高效的查询,防止系统在未来出现意外错误或性能问题等。关系被附加,并有助于避免在围绕方法可能在不同完成级别附加或分离实体的位置传递实体引用时造成混淆。 (您可以轻松区分视图模型和关联实体)
我找到了另一个答案。我们可以使用具有相同选项的 AddDbContextFactory 而不是使用 AddDbContext:
services.AddDbContextFactory<ItemContext>(options =>
{
options.UseLoggerFactory(_loggerFactory);
options.UseSqlServer(config.GetConnectionString("ItemConnString"));
});
我使用构造函数注入来获取工厂,从而避免了隐藏依赖项的反模式:
public DalBase(IDbContextFactory<ItemContext> itemContextFactory, ...)
然后在向调用者分发上下文的函数中:
return _itemContextFactory.CreateDbContext();
这会为每个调用函数创建一个新实例。
它似乎适用于服务范围与 using-a-dbcontext-factory-eg-for-blazor 中所述的 DbContext 生命周期不一致的 Blazor 应用程序。这似乎是我的 WPF 视图模型的情况。
我有一个 WPF 应用程序,其视图模型调用数据访问 class 来完成数据库工作。 DataAccess class 具有所有异步功能,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();
}
我自始至终都遵循这个模式,除了我用了很多 try...catch 来减小尺寸。 DataAccess 中的每个函数都以调用 Get_Item_DbContext()
开始,我曾经有 using (var db = Get_Item_DbContext())
但我读过的一篇文章说让依赖注入决定生命周期。我找不到任何地方没有在异步函数上使用等待,但我收到这样的错误...
在上一个操作完成之前在此上下文中启动了第二个操作...
有一个操作正在处理列表,它调用此更新状态函数,并且有一个按钮可供用户用来调用相同的函数。那是我收到错误的时候。
编辑 我曾经有 using (var db = Get_Item_DbContext)
但后来我得到了一个不同的错误...无法访问已处置的上下文实例。
UPDATE 我尝试将上下文注入 DalBase。当我点击 运行 第二次操作时仍然出现错误。在这种情况下,可能是因为两个操作 运行 通过同一个 VM,现在有一个 _dataAccess 和一个被注入的上下文。我试图通过从每个函数获取一个新实例来避免这种情况。
所以我的问题是这些 我如何找出哪些线程(及其调用堆栈)正在获得相同的上下文? 为什么两个单独的线程(来自异步调用)从 GetService 获得相同的上下文? 有没有我必须添加到 AddDbContext 以使其范围正确的选项?
谢谢 麦克
我建议停止在 类 中注入 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 的用法,我认为您的问题就会消失 类。
作为 WPF 应用程序需要对管理 EF DbContext 有一些不同的看法。许多讨论依赖注入和生命周期范围的例子都指的是 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;
}
这将有效地做同样的事情,returning 一个分离的实体。但是现在,如果您尝试访问未加载的导航 属性,您将遇到 #nulls。这可能会有问题,因为 #null 是否意味着该订单实际上没有引用,或者只是没有预取?
让服务定位器按需提供 DbContext 的更改避免了“超出范围”问题,但现在的问题是每次对 Get_Item_DbContext()
的调用都会 return单个 DbContext 实例,如果您尝试异步或通过工作线程 运行 操作,您一定会遇到跨线程异常。要使用服务定位器解决此问题,您需要显式管理 DbContext 的范围,并知道何时应提供共享 DbContext 实例与新的 DbContext 实例。这仍然会导致在这些范围之间传递实体的问题。 (即加载到工作线程上并 returned 到另一个具有不同 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;
}
虽然断开连接的实体可能需要急切加载并传递给视图的导航属性,但向下投影到视图模型可以产生更高效的查询,防止系统在未来出现意外错误或性能问题等。关系被附加,并有助于避免在围绕方法可能在不同完成级别附加或分离实体的位置传递实体引用时造成混淆。 (您可以轻松区分视图模型和关联实体)
我找到了另一个答案。我们可以使用具有相同选项的 AddDbContextFactory 而不是使用 AddDbContext:
services.AddDbContextFactory<ItemContext>(options =>
{
options.UseLoggerFactory(_loggerFactory);
options.UseSqlServer(config.GetConnectionString("ItemConnString"));
});
我使用构造函数注入来获取工厂,从而避免了隐藏依赖项的反模式:
public DalBase(IDbContextFactory<ItemContext> itemContextFactory, ...)
然后在向调用者分发上下文的函数中:
return _itemContextFactory.CreateDbContext();
这会为每个调用函数创建一个新实例。
它似乎适用于服务范围与 using-a-dbcontext-factory-eg-for-blazor 中所述的 DbContext 生命周期不一致的 Blazor 应用程序。这似乎是我的 WPF 视图模型的情况。