EF Core lazy loading result in exception in entity Equals method

0

Summary: Using EF Core lazy loading, I get the message: "A second operation started on this context before a previous operation completed." But it does not seem to be the case. I can work-around it using the "id field" instead of the "lazy-loaded entity". Is there another way?

Here are the long details:

I created a very simple Aspnet.Core WEB API 2.1 with EF Core 2.1.8.

The main point is that i configured the lazy-loading by installing the Microsoft.EntityFrameworkCore.Proxies and configuring it in my DbContext class like this:

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
  {
    base.OnConfiguring(optionsBuilder);
    optionsBuilder.UseLazyLoadingProxies();
  }

In Startup, i just added:

services.AddDbContext<XDbContext>(options => options.UseSqlServer(connection_string_here));

And here is may API service:

  [HttpGet]
  [Route("test")]
  public ActionResult<IEnumerable<string>> Test()
  {
    string txt = "";
    var sprint = dbContext.SprintDbSet.Find(1);
    txt += sprint.Id + ":";
    foreach (var item in sprint.SprintBacklog) //EXCEPTION HERE
      txt += item.Id + "," + item.PlannedSprint.Id + "," + item.PlannedTask.Id + ". ";
    return Ok(txt);
  }

When i run it, the foreach try to load the collection "lazily", then it throws the following exception:

System.InvalidOperationException
  HResult=0x80131509
  Message=A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.
  Source=Microsoft.EntityFrameworkCore
  StackTrace:
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.MoveNext()
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.Load[TSource](IQueryable`1 source)
   at Microsoft.EntityFrameworkCore.Internal.EntityFinder`1.Load(INavigation navigation, InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.NavigationEntry.Load()
   at Microsoft.EntityFrameworkCore.Internal.LazyLoader.Load(Object entity, String navigationName)
   at Microsoft.EntityFrameworkCore.Proxies.Internal.LazyLoadingInterceptor.Intercept(IInvocation invocation)
   at Castle.DynamicProxy.AbstractInvocation.Proceed()
   at Castle.Proxies.SprintBacklogItemProxy.get_PlannedTask()
   at SprintBacklogItem.Equals(Object obj) in SprintBacklogItem.cs:line

As it can be seen in the stack trace, the issue occurs in the Equals of my entity.

  public override bool Equals(object obj) {
    //some lines here...
    return object.Equals(this.PlannedSprint, outro.PlannedSprint) &&
      object.Equals(this.PlannedTask, outro.PlannedTask); //EXCEPTION HERE
  }

It is interessting that it gets PlannedSprint property with no problem. But it throws an exception when it try to get PlannedTask property.

So, it may be relevant to take a look at the mappings:

  public class SprintBacklogItemMapping : IEntityTypeConfiguration<SprintBacklogItem>
  {
    public void Configure(EntityTypeBuilder<SprintBacklogItem> builder)
    {
      builder.ToTable("SprintBacklogItem");
      builder.HasKey(entity => entity.Id);
      builder.Property(entity => entity.Id).IsRequired().ValueGeneratedOnAdd();
      builder.HasOne(e => e.PlannedSprint).WithMany(s => s.SprintBacklog);
      builder.HasOne(e => e.PlannedTask).WithMany().IsRequired();
  }
}

I did the following hated work-around (using the id instead of the entity):

  public override bool Equals(object obj) {
    //some lines here...
    return object.Equals(this.PlannedSprintId, outro.PlannedSprintId) &&
      object.Equals(this.PlannedTaskId, outro.PlannedTaskId);
  }

That is pretty much interesting. It works now, since I got rid of the lazy-loading in the method. But I was expecting an exception in my service, where the lazy-loading still occurs. But it just worked.

I'm very suspicious about EF Core now, since i had to change my model (the equals method of the entity) because of the persistence tech.

My questions:

  1. Is there another solution?
  2. My original Equals did not work because I did something wrong with EF Core? Am I misunderstanding something about it?
  3. If I cannot go back to my original Equals implementation, then how do I know when I can trust on lazy-loading?

Extra notes:

  1. NHibernate works fine with that. Actually, I am migrating a system from NH to EF.
  2. In the original project, if I change the DbContext from Scoped to Singleton, it works. I spent 2 days investigating if there were two instances running concurrently. Then i did a whole new project extremelly simplified to narrow down the situation. In the new project, when a test DbContext as a singleton, another exception occurs:Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
  3. I did a lot of tests changing the lifecycle of DbContext, event abandoning DI and instantiating it inside the API method, but the result is the same.
  4. I've read a lot of posts in StackOverflow and others sources regarding the same exception, but most of them where about explicit async operations.

Full code to reproduce: https://github.com/brheringer/EFCoreIssue

entity-framework-core
lazy-loading
asked on Stack Overflow Feb 14, 2019 by heringer

0 Answers

Nobody has answered this question yet.


User contributions licensed under CC BY-SA 3.0