实体框架核心: 在前一个操作完成之前,在此上下文上启动的第二个操作

我正在做一个 ASP.Net Core 2.0项目,使用的是实体框架核心

<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.0.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.0.0"/>

在我的一个列表方法中,我得到了这个错误:

InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.
Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()

这是我的方法:

    [HttpGet("{currentPage}/{pageSize}/")]
[HttpGet("{currentPage}/{pageSize}/{search}")]
public ListResponseVM<ClientVM> GetClients([FromRoute] int currentPage, int pageSize, string search)
{
var resp = new ListResponseVM<ClientVM>();
var items = _context.Clients
.Include(i => i.Contacts)
.Include(i => i.Addresses)
.Include("ClientObjectives.Objective")
.Include(i => i.Urls)
.Include(i => i.Users)
.Where(p => string.IsNullOrEmpty(search) || p.CompanyName.Contains(search))
.OrderBy(p => p.CompanyName)
.ToPagedList(pageSize, currentPage);


resp.NumberOfPages = items.TotalPage;


foreach (var item in items)
{
var client = _mapper.Map<ClientVM>(item);


client.Addresses = new List<AddressVM>();
foreach (var addr in item.Addresses)
{
var address = _mapper.Map<AddressVM>(addr);
address.CountryCode = addr.CountryId;
client.Addresses.Add(address);
}


client.Contacts = item.Contacts.Select(p => _mapper.Map<ContactVM>(p)).ToList();
client.Urls = item.Urls.Select(p => _mapper.Map<ClientUrlVM>(p)).ToList();
client.Objectives = item.Objectives.Select(p => _mapper.Map<ObjectiveVM>(p)).ToList();
resp.Items.Add(client);
}


return resp;
}

我有点迷茫,特别是当我在本地运行它的时候,它还能正常工作,但是当我部署到我的登台服务器(IIS 8.5)时,它会给我一个错误,而且它正常工作。在我增加了一个模型的最大长度之后,错误开始出现。我还更新了相应视图模型的最大长度。还有很多其他类似的列表方法,它们都很有效。

我运行了一个 Hangfire 作业,但这个作业不使用同一个实体。我只能想到这些。知道是什么引起的吗?

247249 次浏览

异常意味着 _context同时被两个线程使用; 要么是同一个请求中的两个线程,要么是两个请求。

你的 _context声明静态也许? 它不应该是。

还是在同一个请求中从代码中的其他地方多次调用 GetClients

您可能已经在这样做了,但是理想情况下,您将对 DbContext使用 依赖注入,这意味着您将在 Startup.cs 中使用 AddDbContext(),并且您的控制器构造函数将类似于下面这样:

private readonly MyDbContext _context; //not static


public MyController(MyDbContext context) {
_context = context;
}

如果您的代码不是这样的,请展示给我们,也许我们可以提供进一步的帮助。

我不确定您是否正在使用 IoC 和依赖注入来解析可能使用的 dbContext。如果您使用的是。NET 核心(或任何其他 IoC-Container) ,并且您正在收到此错误,请确保将您的 DbContext 注册为瞬态。做

services.AddDbContext<MyContext>(ServiceLifetime.Transient);

或者

services.AddTransient<MyContext>();

而不是

services.AddDbContext<MyContext>();

AddDbContext 将上下文作为作用域添加,这可能会在处理多个线程时引起麻烦。

在使用异步 lambda 表达式时,异步/等待操作也会导致这种行为。

将其添加为暂时的也有其缺点。您将无法通过使用上下文的多个类对某些实体进行更改,因为每个类都将获得自己的 DbContext 实例。

对此的简单解释是,DbContext实现不是线程安全的

我面临着同样的问题,但原因不是上面列出的那些。我创建了一个任务,在任务内部创建了一个范围,并要求容器获得一个服务。这个方法工作得很好,但是我在任务内部使用了第二个服务,而且我忘记了在新范围中也要求使用它。因此,第2个服务正在使用已经释放的 DbContext。

Task task = Task.Run(() =>
{
using (var scope = serviceScopeFactory.CreateScope())
{
var otherOfferService = scope.ServiceProvider.GetService<IOfferService>();
// everything was ok here. then I did:
productService.DoSomething(); // (from the main scope) and this failed because the db context associated to that service was already disposed.
...
}
}

我应该这么做的:

var otherProductService = scope.ServiceProvider.GetService<IProductService>();
otherProductService.DoSomething();

在某些情况下,在没有使用 await关键字调用 异步方法时会发生此错误,只需在方法调用之前添加 await即可解决此错误。然而,答案可能与上述问题无关,但它可以帮助解决类似的错误。

我也收到了同样的信息。但这对我来说毫无意义。我的问题是我错误地使用了“ NotMap”属性。 在某些情况下,它可能只意味着 Linq 语法或模型类的错误。错误消息似乎具有误导性。这条消息的原意是,在同一个请求中,不能对同一个 dbcontext 多次调用  。

[NotMapped]
public int PostId { get; set; }
public virtual Post Post { get; set; }

您可以查看这个链接的详细信息, Https://www.softwareblogs.com/posts/details/5/error-a-second-operation-started-on-this-context-before-a-previous-operation-completed

我也出现了同样的错误,这是因为我调用了一个构造为 public async void ...而不是 public async Task ...的方法。

我认为这个答案仍然可以帮助一些人,并节省很多次。我通过将 IQueryable更改为 List(或者更改为 array、 Collection...)来解决类似的问题。

例如:

var list = _context.table1.Where(...);

var list = _context.table1.Where(...).ToList(); //or ToArray()...

我有一个后台服务,它为表中的每个条目执行一个操作。问题是,如果我在 DbContext 的同一个实例上迭代并修改一些数据,就会发生这个错误。

正如本线程中提到的,一种解决方案是将 DbContext 的生存期更改为瞬态,方法是像下面这样定义它

services.AddDbContext<DbContext>(ServiceLifetime.Transient);

但是因为我在多个不同的服务中进行更改并使用 SaveChanges()方法一次提交它们,所以这个解决方案在我的情况下不起作用。

因为我的代码在服务中运行,所以我做了类似于

using (var scope = Services.CreateScope())
{
var entities = scope.ServiceProvider.GetRequiredService<IReadService>().GetEntities();
var writeService = scope.ServiceProvider.GetRequiredService<IWriteService>();
foreach (Entity entity in entities)
{
writeService.DoSomething(entity);
}
}

能够使用服务,如果它是一个简单的请求。为了解决这个问题,我将单个作用域分为两个,一个用于查询,另一个用于写操作,如下所示:

using (var readScope = Services.CreateScope())
using (var writeScope = Services.CreateScope())
{
var entities = readScope.ServiceProvider.GetRequiredService<IReadService>().GetEntities();
var writeService = writeScope.ServiceProvider.GetRequiredService<IWriteService>();
foreach (Entity entity in entities)
{
writeService.DoSomething(entity);
}
}

与此类似,实际上使用了两个不同的 DbContext 实例。

另一个可能的解决方案是确保在开始迭代之前读取操作已经终止。在我的例子中,这不是很实用,因为可能有很多结果都需要加载到内存中,以便执行我一开始就试图避免使用 Queryable 的操作。

我设法通过将一个 IQueryable传递到一个方法中来获得这个错误,然后该方法将这个 IQueryable‘ list’作为另一个查询的一部分使用到相同的上下文中。

public void FirstMethod()
{
// This is returning an IQueryable
var stockItems = _dbContext.StockItems
.Where(st => st.IsSomething);


SecondMethod(stockItems);
}


public void SecondMethod(IEnumerable<Stock> stockItems)
{
var grnTrans = _dbContext.InvoiceLines
.Where(il => stockItems.Contains(il.StockItem))
.ToList();
}

为了阻止这种情况的发生,我使用了 靠近这里并在传递第二个方法之前具体化了这个列表,方法是将对 SecondMethod的调用改为 SecondMethod(stockItems.ToList()

首先,支持(至少) Alsami 的回答,这让我走上了正确的道路。

但是对于那些做 IoC 的人来说,这里有一个更深入的探索。

我的错误(和其他人一样)

发生了一个或多个错误 先前的操作完成之前的上下文。这通常是造成 通过使用相同的 DbContext 实例的不同线程 有关如何避免 DbContext 的线程问题的信息,请参见 Https://go.microsoft.com/fwlink/?linkid=2097913

我的代码设置。“只是基本的”..。

public class MyCoolDbContext: DbContext{
public DbSet <MySpecialObject> MySpecialObjects {        get;        set;    }
}

还有

public interface IMySpecialObjectDomainData{}

和(注意正在注入 MyCoolDbContext)

public class MySpecialObjectEntityFrameworkDomainDataLayer: IMySpecialObjectDomainData{
public MySpecialObjectEntityFrameworkDomainDataLayer(MyCoolDbContext context) {
/* HERE IS WHERE TO SET THE BREAK POINT, HOW MANY TIMES IS THIS RUNNING??? */
this.entityDbContext = context ?? throw new ArgumentNullException("MyCoolDbContext is null", (Exception)null);
}
}

还有

public interface IMySpecialObjectManager{}

还有

public class MySpecialObjectManager: IMySpecialObjectManager
{
public const string ErrorMessageIMySpecialObjectDomainDataIsNull = "IMySpecialObjectDomainData is null";
private readonly IMySpecialObjectDomainData mySpecialObjectDomainData;


public MySpecialObjectManager(IMySpecialObjectDomainData mySpecialObjectDomainData) {
this.mySpecialObjectDomainData = mySpecialObjectDomainData ?? throw new ArgumentNullException(ErrorMessageIMySpecialObjectDomainDataIsNull, (Exception)null);
}
}

最后,从控制台应用程序(命令行接口应用程序)调用我的多线程类

    public interface IMySpecialObjectThatSpawnsThreads{}

还有

public class MySpecialObjectThatSpawnsThreads: IMySpecialObjectThatSpawnsThreads
{
public const string ErrorMessageIMySpecialObjectManagerIsNull = "IMySpecialObjectManager is null";


private readonly IMySpecialObjectManager mySpecialObjectManager;


public MySpecialObjectThatSpawnsThreads(IMySpecialObjectManager mySpecialObjectManager) {
this.mySpecialObjectManager = mySpecialObjectManager ?? throw new ArgumentNullException(ErrorMessageIMySpecialObjectManagerIsNull, (Exception)null);
}
}

还有督察的积累。(同样,这是一个控制台应用(命令行界面) ... ... 它的行为与 web 应用程序略有不同)

private static IServiceProvider BuildDi(IConfiguration configuration) {
/* this is being called early inside my command line application ("console application") */


string defaultConnectionStringValue = string.Empty; /* get this value from configuration */


////setup our DI
IServiceCollection servColl = new ServiceCollection()
////.AddLogging(loggingBuilder => loggingBuilder.AddConsole())


/* THE BELOW TWO ARE THE ONES THAT TRIPPED ME UP.  */
.AddTransient<IMySpecialObjectDomainData, MySpecialObjectEntityFrameworkDomainDataLayer>()
.AddTransient<IMySpecialObjectManager, MySpecialObjectManager>()


/* so the "ServiceLifetime.Transient" below................is what you will find most commonly on the internet search results */
# if (MY_ORACLE)
.AddDbContext<ProvisioningDbContext>(options => options.UseOracle(defaultConnectionStringValue), ServiceLifetime.Transient);
# endif


# if (MY_SQL_SERVER)
.AddDbContext<ProvisioningDbContext>(options => options.UseSqlServer(defaultConnectionStringValue), ServiceLifetime.Transient);
# endif


servColl.AddSingleton <IMySpecialObjectThatSpawnsThreads,        MySpecialObjectThatSpawnsThreads>();


ServiceProvider servProv = servColl.BuildServiceProvider();


return servProv;
}

令我惊讶的是(变成)暂时的

        .AddTransient<IMySpecialObjectDomainData, MySpecialObjectEntityFrameworkDomainDataLayer>()
.AddTransient<IMySpecialObjectManager, MySpecialObjectManager>()

注意,我认为因为 IMySpecialObjectManager 被注入到“ MySpecialObjectThatSpawnsThreads”中,这些被注入的对象需要是瞬态的才能完成链。

重点是... ... 需要的不仅仅是(我的) DbContext。瞬态... ... 而是 DI 图的更大块。

调试技巧:

这句话:

this.entityDbContext = context ?? throw new ArgumentNullException("MyCoolDbContext is null", (Exception)null);

将调试器断点放在此处。如果 MySpecialObjectThatSpawnsThreads 生成 N 个线程(例如10个线程) ... ... 而这一行只被命中一次... ... 那就是你的问题了。您的 DbContext 正在跨线程。

额外收获:

我建议你阅读下面的网址/文章(老的但是好的) ,了解网络应用程序和控制台应用程序的区别

Https://mehdi.me/ambient-dbcontext-in-ef6/

以下是文章的标题,以防链接发生变化。

使用实体框架6以正确的方式管理 DBContext: 一个深入的 引导 Mehdi El Gueddari

我用 WorkFlowCorehttps://github.com/danielgerlag/workflow-core解决了这个问题

  <ItemGroup>
<PackageReference Include="WorkflowCore" Version="3.1.5" />
</ItemGroup>

下面的示例代码可以帮助未来的互联网搜索者

 namespace MyCompany.Proofs.WorkFlowCoreProof.BusinessLayer.Workflows.MySpecialObjectInterview.Workflows
{
using System;
using MyCompany.Proofs.WorkFlowCoreProof.BusinessLayer.Workflows.MySpecialObjectInterview.Constants;
using MyCompany.Proofs.WorkFlowCoreProof.BusinessLayer.Workflows.MySpecialObjectInterview.Glue;
using MyCompany.Proofs.WorkFlowCoreProof.BusinessLayer.Workflows.WorkflowSteps;


using WorkflowCore.Interface;
using WorkflowCore.Models;


public class MySpecialObjectInterviewDefaultWorkflow : IWorkflow<MySpecialObjectInterviewPassThroughData>
{
public const string WorkFlowId = "MySpecialObjectInterviewWorkflowId";


public const int WorkFlowVersion = 1;


public string Id => WorkFlowId;


public int Version => WorkFlowVersion;


public void Build(IWorkflowBuilder<MySpecialObjectInterviewPassThroughData> builder)
{
builder
.StartWith(context =>
{
Console.WriteLine("Starting workflow...");
return ExecutionResult.Next();
})


/* bunch of other Steps here that were using IMySpecialObjectManager.. here is where my DbContext was getting cross-threaded */




.Then(lastContext =>
{
Console.WriteLine();


bool wroteConcreteMsg = false;
if (null != lastContext && null != lastContext.Workflow && null != lastContext.Workflow.Data)
{
MySpecialObjectInterviewPassThroughData castItem = lastContext.Workflow.Data as MySpecialObjectInterviewPassThroughData;
if (null != castItem)
{
Console.WriteLine("MySpecialObjectInterviewDefaultWorkflow complete :)  {0}   -> {1}", castItem.PropertyOne, castItem.PropertyTwo);
wroteConcreteMsg = true;
}
}


if (!wroteConcreteMsg)
{
Console.WriteLine("MySpecialObjectInterviewDefaultWorkflow complete (.Data did not cast)");
}


return ExecutionResult.Next();
}))


.OnError(WorkflowCore.Models.WorkflowErrorHandling.Retry, TimeSpan.FromSeconds(60));


}
}
}

还有

ICollection<string> workFlowGeneratedIds = new List<string>();
for (int i = 0; i < 10; i++)
{
MySpecialObjectInterviewPassThroughData currentMySpecialObjectInterviewPassThroughData = new MySpecialObjectInterviewPassThroughData();
currentMySpecialObjectInterviewPassThroughData.MySpecialObjectInterviewPassThroughDataSurrogateKey = i;


////  private readonly IWorkflowHost workflowHost;
string wfid = await this.workflowHost.StartWorkflow(MySpecialObjectInterviewDefaultWorkflow.WorkFlowId, MySpecialObjectInterviewDefaultWorkflow.WorkFlowVersion, currentMySpecialObjectInterviewPassThroughData);
workFlowGeneratedIds.Add(wfid);
}
  • 使用 Startup.cs 文件中的这行代码解决我的问题。
    添加临时服务意味着每次请求服务时,都会在工作时创建一个新实例 和 依赖注入

           services.AddDbContext<Context>(options =>
    options.UseSqlServer(_configuration.GetConnectionString("ContextConn")),
    ServiceLifetime.Transient);
    

我知道两年前就有人问过这个问题,但是我只是遇到了这个问题,我所使用的修复方法真的很有帮助。

如果你用相同的上下文做两个查询-你可能需要删除 AsNoTracking。如果您使用 AsNoTracking,您将为每次读取创建一个新的数据读取器。两个数据读取器不能读取相同的数据。

我也有同样的问题,结果是 所以上下文也自动变成了 singelton。在 DI 中被声明为 Per Life Time Scoped。

将不同生命周期的服务注入另一个

  1. 永远不要将作用域和暂态服务注入到单例服务中 这将有效地将瞬态服务或作用域服务转换为 单身。)

  2. 永远不要将瞬态服务注入到作用域服务中(这会转换 进入作用域的临时服务。)

如果您的方法返回一些内容,您可以通过将 到作业结束和 如果它不返回任何东西,就返回 .Wait()

实体框架核心不支持在同一 DbContext实例上运行多个并行操作。这既包括 async查询的并行执行,也包括来自多个线程的任何显式并发使用。因此,始终 await async立即调用,或对并行执行的操作使用单独的 DbContext实例。

您可以使用 SemaphoreSlim 来阻止将尝试执行 EF 调用的下一个线程。

static SemaphoreSlim semSlim = new SemaphoreSlim(1, 1);


await semSlim.WaitAsync();
try
{
// something like this here...
// EmployeeService.GetList(); or...
var result = await _ctx.Employees.ToListAsync();
}
finally
{
semSlim.Release();
}

如果创建迁移(添加-迁移) 却忘了更新数据库(更新-数据库) ,也可能出现此错误。

有一些异步函数,但是在它之前不调用 wait。查找并添加调用该函数之前的等待时间将对您有所帮助。

为此错误添加另一种可能的解决方案,以防它对某人有帮助。

在我的案例中,问题是在查询中使用导航属性,如下所示:

var selectedOrder = dbContext.Orders.Where(x => x.Id == id).Single();
var relatedOrders = dbContext.Orders.Where(x => x.User.Id == selectedOrder.User.Id).ToList();

问题是使用 selectedOrder。查询中的 User.Id。如果 User nav 属性尚未加载,EF 将尝试在尝试执行查询期间延迟加载该属性,它认为这是在尝试启动第二个操作。解决方案是为 selectedOrder 创建一个单独的变量。Id,以确保在启动查询之前加载了查询所需的信息:

var selectedOrder = dbContext.Orders.Where(x => x.Id == id).Single();
var userId = selectedOrder.User.Id;
var relatedOrders = dbContext.Orders.Where(x => x.User.Id == userId).ToList();

在某些情况下,如果您以前的操作是一些批量操作(添加或编辑1000行) ,SaveChanges ()可能需要一些时间

如果不能/不想将 DbContext 更改为瞬态,可以等待事务结束。

在调用 DbContext 实例的 DbContext (或 UnitOfWork)中实现此代码。

public bool IsBusy()
{
var transaction = _context.Database.CurrentTransaction;
return transaction != null;
}

等待,直到交易结束在必要的点

 while (_yourDbContextOrUnitOfWork.IsBusy())
{
Thread.Sleep(100);
}


await SomeAsyncCallToYourDb();

将下面的代码放入您的.csproject 文件中并更正所有错误

  <PropertyGroup>
<WarningsAsErrors>CS4014</WarningsAsErrors>
</PropertyGroup>

这段代码强制您对异步方法使用 wait