web-dev-qa-db-fra.com

Obtention d'une "tentative de chargement paresseux de la propriété de navigation sur une entité détachée" malgré une recherche ardente

J'utilise Entity Framework Core 2.1.2 avec le chargement paresseux activé et j'effectue une requête en utilisant AsNoTracking. J'utilise Include pour apporter ma propriété de navigation (une collection).

Si toutes mes entités ont au moins un enfant dans leur collection, alors tout fonctionne bien.

Cependant, si l'une de mes entités n'a pas d'enfants, j'obtiens une erreur:

System.InvalidOperationException: erreur générée pour l'avertissement "Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning: une tentative de chargement paresseux de la propriété de navigation" Children "a été effectuée sur l'entité détachée de type" ParentProxy ". Le chargement différé n'est pas pris en charge pour les entités détachées ou les entités chargées avec 'AsNoTracking ()'. '

Voici une reproduction du problème (il peut être exécuté à partir d'une application console après avoir utilisé NuGet pour intégrer 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()
                ;
        }
    }

}

Est-ce que je fais quelque chose de mal? Ou est-ce un bug dans EF Core? (J'ai posté n problème là-bas aussi.)

7
Jonathan Moffatt

Pour la postérité, voici la réponse de l'équipe EF Core :

En effet, le chargement différé n'est pas pris en charge pour les requêtes NoTracking (# 10042), mais nous avons essayé de ne pas le lancer s'il semblait que le chargement différé n'était pas nécessaire. Rétrospectivement, il aurait peut-être été préférable de toujours lancer. Notez que l'avertissement peut être configuré pour ne pas lancer à l'aide de ConfigureWarnings dans DbContextOptionsBuilder.

Juste au cas où cela serait utile pour quelqu'un, ce que j'ai fini par faire était de créer un deuxième "ReadOnlyRepository" configuré pour ne pas utiliser le chargement paresseux et pour toujours renvoyer les ensembles non suivis. J'utilise ce référentiel pour les requêtes où je ne persisterai jamais les modifications apportées à l'une des entités, où l'ensemble de résultats peut être volumineux et quand il doit bien fonctionner.

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)
    {
        ...
    }
}

}

3
Jonathan Moffatt

Il n'y a pas de bug. Étant donné que vous ne suivez pas les modifications à l'aide de .AsNoTracking, le chargement paresseux ne fonctionnera jamais. Vous pouvez utiliser .Include ("ChildEntity") dans votre requête ou renoncer à utiliser .AsNoTracking.