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

时间:2018-02-13 13:26:19

标签: c# entity-framework asp.net-web-api

我正在使用Entity Framework Core

开发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作业,但是这个作业不使用同一个实体。这就是我能想到的所有相关内容。有什么可能导致这个问题的想法?

25 个答案:

答案 0 :(得分:31)

我不确定您是否正在使用IoC和依赖注入来解析您可能使用的DbContext。如果您这样做并且您正在使用来自.NET Core(或任何其他IoC-Container)的本机IoC,并且您收到此错误,请确保将DbContext注册为Transient。做

services.AddTransient<MyContext>();

OR

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

而不是

services.AddDbContext<MyContext>();

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

使用异步lambda表达式时,async / await operations也会导致此行为。

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

答案 1 :(得分:20)

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

你的_context可能是静态的吗?它不应该是。

或者您是否在代码中其他位置的同一请求中多次调用GetClients

您可能已经这样做了,但理想情况下,您将DbContext用于private readonly MyDbContext _context; //not static public MyController(MyDbContext context) { _context = context; } ,这意味着您将在Startup.cs中使用dependency injection,并且你的控制器构造函数看起来像这样:

{{1}}

如果您的代码不是这样,请告诉我们,也许我们可以提供更多帮助。

答案 2 :(得分:11)

  • 使用Startup.cs文件中的这一行代码解决我的问题。
    添加临时服务意味着每次请求服务时,在工作时都会创建一个新实例 与 依赖注入

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

答案 3 :(得分:10)

我遇到了同样的问题,结果证明父母服务是单身汉。因此,上下文也自动成为singelton。即使在DI中被声明为“ Per Life Time Scoped”。

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

  1. 切勿将范围和瞬态服务注入Singleton服务。 ( 这可以有效地将临时服务或范围服务转换为 单身人士。 )

  2. 切勿将瞬态服务注入作用域服务(这会转换 过渡服务进入范围。 )

答案 4 :(得分:8)

在某些情况下,此错误是由这种情况引起的:您调用 async 方法,但在调用该方法之前未使用 await 。我的问题通过在该方法之前添加等待来解决。但是答案可能与提到的问题无关,但可以帮助解决类似的错误。

答案 5 :(得分: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();

答案 6 :(得分:2)

Entity Framework Core不支持在同一DbContext实例上运行的多个并行操作。这包括async查询的并行执行以及来自多个线程的任何显式并发使用。因此,始终await async会立即调用,或者对并行执行的操作使用单独的DbContext实例。

答案 7 :(得分:2)

我有同样的错误。发生这种情况是因为我调用了一个构造为public async void ...而不是public async Task ...的方法。

答案 8 :(得分:1)

如果您创建了迁移 (Add-Migration) 但忘记实际更新数据库 (Update-Database),也可能会出现此错误。

答案 9 :(得分:1)

我的情况有所不同: 我试图为30个用户(属于特定角色)播种数据库,所以我正在运行以下代码:

for (var i = 1; i <= 30; i++)
{
    CreateUserWithRole("Analyst", $"analyst{i}", UserManager);
}

这是一个同步功能。 在其中,我有3个呼叫:

UserManager.FindByNameAsync(username).Result
UserManager.CreateAsync(user, pass).Result
UserManager.AddToRoleAsync(user, roleName).Result

当我将.Result替换为.GetAwaiter().GetResult()时,此错误消失了。

答案 10 :(得分:1)

首先,支持(至少)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);
    }
}

和DI积累。 (同样,这是针对控制台应用程序(命令行界面)的。。。它的行为与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”中,所以这些注入的对象需要是Transient的才能完成链。

关键是......不仅仅是需要(My)DbContext .Transient ...而是更大的DI图块。

调试提示:

此行:

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

在此处放置调试器断点。如果您的MySpecialObjectThatSpawnsThreads正在创建N个线程(例如,说10个线程)……而该行仅被命中一次……那是您的问题。您的DbContext正在穿越线程。

奖金:

我建议您在下面的url / article(既是老书又是老书)中阅读有关Web应用程序和控制台应用程序之间差异的内容

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

如果链接发生更改,这是文章的标题。

使用实体框架6正确地管理DBCONTEXT:深入了解 指南Mehdi El Gueddari

我通过WorkFlowCore https://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);
                }

答案 11 :(得分:0)

异步方法将向您发出警告消息,提示代码块将在没有等待的情况下同步运行(如果不存在)。删除等待的Task.Yield()后,我收到此错误。我没有使用Task.Run(async()=> await ...)模式。如前所述,同步线程现在同时被异步线程和同步线程引用了dbContext,这导致单元测试中“在上一个操作完成之前,在此上下文上启动了第二个操作”

答案 12 :(得分:0)

另一种可能的情况:如果您直接使用连接,请不要忘记关闭。我需要执行任意SQL查询,并读取结果。这是一个快速解决方案,我不想定义数据类,也不想设置“普通” SQL连接。因此,我简单地将EFC的数据库连接重用为FragmentContainerView。在执行var connection = Context.Database.GetDbConnection() as SqlConnection之前,请确保先致电connection.Close()

答案 13 :(得分:0)

在我的情况下,我使用的锁不允许您使用await,并且当您不等待异步时也不会创建编译器警告。

问题:

lock (someLockObject) {
    // do stuff
    context.SaveChangesAsync();
}

// some other code somewhere else doing await context.SaveChangesAsync() shortly after the lock gets the concurrency error

解决方法: 通过使用.Wait()使其阻塞来等待锁内的异步

lock (someLockObject) {
    // do stuff
    context.SaveChangesAsync().Wait();
}

答案 14 :(得分:0)

我知道这个问题已经在两年前问过了,但是我刚刚遇到了这个问题,我使用的修复程序确实起到了帮助。

如果您要使用相同的上下文进行两个查询-您可能需要删除AsNoTracking。如果您确实使用AsNoTracking,则将为每次读取创建一个新的数据读取器。两个数据读取器无法读取相同的数据。

答案 15 :(得分:0)

就我而言,我在Blazor中使用了模板组件。

 <BTable ID="Table1" TotalRows="MyList.Count()">

问题是在组件标头中调用方法(Count)。 为了解决此问题,我将其更改为:

int total = MyList.Count();

及更高版本:

<BTable ID="Table1" TotalRows="total">

答案 16 :(得分:0)

我设法通过将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()

来使用approach here并具体化了该列表,然后再通过第二种方法

答案 17 :(得分:0)

当我尝试在下面的代码的async方法中使用FirstOrDefaultAsync()时,我遇到了同样的问题。当我修复FirstOrDefault()时-问题解决了!

_context.Issues.Add(issue);
        await _context.SaveChangesAsync();

        int userId = _context.Users
            .Where(u => u.UserName == Options.UserName)
            .FirstOrDefaultAsync()
            .Id;
...

答案 18 :(得分:0)

我有一个后台服务,该服务为表中的每个条目执行操作。问题是,如果我遍历并修改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来避免这种情况。

答案 19 :(得分:0)

我认为这个答案仍然可以帮助一些人并节省很多时间。我通过将IQueryable更改为List(或更改为数组,集合...)解决了类似的问题。

例如:

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

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

答案 20 :(得分:0)

我收到了同样的消息。但这对我来说毫无意义。我的问题是我错误地使用了“ NotMapped”属性。 在某些情况下,它可能仅意味着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

答案 21 :(得分:-1)

我有一个类似的问题。 我通过从使用foreach循环更改为使用for循环来解决我的问题。

答案 22 :(得分:-1)

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

import React, { Component } from "react";
import propTypes from "prop-types";

import Button from "elements/Button";
import { InputNumber, InputDate } from "elements/Form";

export default class BookingForm extends Component {
  constructor(props) {`enter code here`
    super(props);
    this.state = {
      data: {
        duration: 1,
        date: {
          startDate: new Date(),
          endDate: new Date(),
          key: "selection",
        },
      },
    };
  }

  updateData = (e) => {
    this.setState({
      ...this.state,
      data: {
        ...this.state.data,
        [e.target.name]: e.target.value,
      },
    });
  };

  componentDidUpdate(prevProps, prevState) {
    const { data } = this.state;

    if (prevState.data.date !== data.date) {
      const startDate = new Date(data.date.startDate);
      const endDate = new Date(data.date.endDate);
      const countDuration = new Date(endDate - startDate).getDate();
      this.setState({
        data: {
          ...this.state.data,
          duration: countDuration,
        },
      });
    }
    if (prevState.data.duration !== data.duration) {
      const starDate = new Date(data.date.startDate);
      const endDate = new Date(
        starDate.setDate(starDate.getDate() + +data.duration - 1)
      );
      this.setState({
        ...this.state,
        data: {
          ...this.state.data,
          date: {
            ...this.state.data.date,
            endDate: endDate,
          },
        },
      });
    }
  }

  render() {
    const { data } = this.state;
    const { itemDetails, startBooking } = this.props;
    return (
      <div className="card bordered" style={{ padding: "60px 80px" }}>
        <h4 className="mb-3">Start Booking</h4>
        <h5 className="h2 text-teal mb-4">
          ${itemDetails.price}{" "}
          <span className="text-gray-500 font-weight-light">
            per {itemDetails.unit}
          </span>
        </h5>

        <label htmlFor="duration">How long you will stay?</label>
        <InputNumber
          max={30}
          suffix={" night"}
          isSuffixPlural
          onChange={this.updateData}
          name="duration"
          value={data.duration}
        />

        <label htmlFor="date">Pick a date</label>
        <InputDate onChange={this.updateData} name="date" value={data.date} />
        <h6
          className="text-gray-500 font-weight-light"
          style={{ marginBottom: 40 }}
        >
          You will pay{" "}
          <span className="text-gray-900">
            ${itemDetails.price * data.duration} USD
          </span>{" "}
          <span className="text-gray-900">
            {data.duration} {itemDetails.unit}
          </span>
        </h6>

        <Button
          className="btn"
          hasShadow
          isPrimary
          isBlock
          onClik={startBooking}
        >
          Continue to Book
        </Button>
      </div>
    );
  }
}

BookingForm.propTypes = {
  itemDetails: propTypes.object,
  startBooking: propTypes.func,
};

答案 23 :(得分:-2)

如果您的方法返回了某些内容,则可以通过以下方法解决此错误: .Result至工作结束, .Wait()(如果没有返回任何内容)。

答案 24 :(得分:-3)

我只是设法让它再次运作。它没有多大意义,但它起作用了:

  1. 从StartUp中移除Hangfire(我在那里创建工作)
  2. 删除了hangfire数据库
  3. 重新启动服务器
  4. 我稍后会进一步调查,但是我用hangfire调用的方法会收到一个DBContext,这是可能的原因。