I'm using Entity Framework Core 2.1.2 with lazy loading enabled and am performing a query using AsNoTracking. I'm using Include to bring in my navigation property (a collection).
If all my entities have at least one child in their collection then it all works fine.
However, if any of my entities have no children then I get an error:
System.InvalidOperationException: Error generated for warning 'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning: An attempt was made to lazy-load navigation property 'Children' on detached entity of type 'ParentProxy'. Lazy-loading is not supported for detached entities or entities that are loaded with 'AsNoTracking()'.'
Here's a reproduction of the problem (it can be run from a console app after using NuGet to bring in Microsoft.EntityFrameworkCore 2.1.2, Microsoft.EntityFrameworkCore.Proxies 2.1.2, Microsoft.EntityFrameworkCore.InMemory 2.1.2):
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
namespace LazyLoadingIssue
{
public class Parent
{
public int Id { get; set; }
public string ParentName { get; set; }
public virtual ICollection<Child> Children { get; set; }
}
public class Child
{
public int Id { get; set; }
public int ParentId { get; set; }
public virtual Parent Parent { get; set; }
public string ChildName { get; set; }
}
public class Program
{
public static void Main(string[] args)
{
SetupDatabase(setupToFail: true);
PerformTest();
Console.WriteLine("Press any key to finish");
Console.ReadLine();
}
private static void PerformTest()
{
using (var db = new MyContext())
{
try
{
IQueryable<Parent> parents = db.Rounds.Include(r => r.Children).AsNoTracking();
foreach (Parent parent in parents)
{
Console.WriteLine($"Parent (Id={parent.Id}) '{parent.ParentName}'");
foreach (Child child in parent.Children)
{
Console.WriteLine($" - Child (Id={child.Id}, ParentId={child.ParentId}) '{child.ChildName}'");
}
}
Console.WriteLine("** WORKED **");
}
catch (Exception ex)
{
Console.WriteLine("** FAILED **");
Console.WriteLine(ex);
}
}
}
private static void SetupDatabase(bool setupToFail)
{
using (var db = new MyContext())
{
db.Database.EnsureDeleted();
db.Database.EnsureCreated();
var parent1 = new Parent
{
ParentName = "First sample parent (has children)",
Children = new List<Child>
{
new Child {ChildName = "child-1"},
new Child {ChildName = "child-2"},
new Child {ChildName = "child-3"}
}
};
var parent2 = new Parent
{
ParentName = $"Second sample parent ({(setupToFail ? "with no children" : "has children")})",
Children = new List<Child>()
};
if (!setupToFail)
parent2.Children.Add(new Child {ChildName = "child-4"});
db.AddRange(parent1, parent2);
db.SaveChanges();
}
}
}
public class MyContext : DbContext
{
public DbSet<Parent> Rounds { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
// .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=_ModelApp;Trusted_Connection=True;Connect Timeout=5;ConnectRetryCount=0")
.UseInMemoryDatabase(databaseName: "_modelApp")
.UseLazyLoadingProxies()
;
}
}
}
Am I doing something wrong? Or is this a bug in EF Core? (I've posted an issue there too.)
For posterity, here's the response from the EF Core team:
This is because lazy-loading isn't supported for NoTracking queries (#10042) but we tried to not make it throw if it looked like lazy-loading wasn't needed. In retrospect it might have been better to always throw. Note that the warning can be configured to not throw using ConfigureWarnings in the DbContextOptionsBuilder.
Just in case it's of use for someone, what I ended up doing was creating a second "ReadOnlyRepository" configured to not use lazy loading and to always return untracked sets. I use this repository for queries where I'm never going to persist changes to any of the entities, where the resultset can be large and when it needs to perform well.
public class ReadOnlyRepository : MainDbContextBase, IReadOnlyRepository
{
public ReadOnlyRepository(IConfigurationSettings configurationSettings)
: base(configurationSettings, false)
{
}
public IQueryable<T> Retrieve<T>() where T : class, IAmAnAggregateRoot
{
return GetDbSet<T>().AsNoTracking();
}
}
public class MainDbContextBase : DbContext
{
private readonly IConfigurationSettings configurationSettings;
private readonly bool useLazyLoading;
protected MainDbContextBase(IConfigurationSettings configurationSettings, bool useLazyLoading)
{
this.configurationSettings = configurationSettings;
this.useLazyLoading = useLazyLoading;
}
protected DbSet<T> GetDbSet<T>() where T : class
{
return Set<T>();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder
.UseLazyLoadingProxies(useLazyLoading)
.UseSqlServer(configurationSettings.ConnectionString);
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
}
}
}