EF Core 3.1:第一次调用后备字段时,导航属性不会延迟加载实体

问题描述

我正在使用EF Core 3.1.7。 DbContext具有UseLazyLoadingProxies集。流利的API映射用于将实体映射到数据库我有一个具有使用后备字段的导航属性的实体。加载和保存到数据库似乎工作正常,除了在访问导航属性之前访问后备字段时出现的问题。

在访问后备字段时,似乎引用实体不会延迟加载。这是Castle.Proxy类的不足还是配置错误

针对存在问题的行为,将IsRegisteredForACourse的Student类实现与IsRegisteredForACourse2进行比较。

数据库表和关系。

Student and Courses

学生实体

using System.Collections.Generic;

namespace EFCoreMappingTests
{
    public class Student
    {
        public int Id { get; }
        public string Name { get; }

        private readonly List<Course> _courses;
        public virtual IReadOnlyList<Course> Courses => _courses.AsReadOnly();

        protected Student()
        {
            _courses = new List<Course>();
        }

        public Student(string name) : this()
        {
            Name = name;
        }

        public bool IsRegisteredForACourse()
        {
            return _courses.Count > 0;
        }

        public bool IsRegisteredForACourse2()
        {
            //Note the use of the property compare to the prevIoUs method using the backing field.
            return Courses.Count > 0;
        }

        public void AddCourse(Course course)
        {
            _courses.Add(course);
        }
    }
}

课程实体

namespace EFCoreMappingTests
{
    public class Course
    {
        public int Id { get; }
        public string Name { get; }
        public virtual Student Student { get; }

        protected Course()
        {
        }
        public Course(string name) : this()
        {
            Name = name;
        }
    }
}

DbContext

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace EFCoreMappingTests
{
    public sealed class Context : DbContext
    {
        private readonly string _connectionString;
        private readonly bool _useConsoleLogger;

        public DbSet<Student> Students { get; set; }
        public DbSet<Course> Courses { get; set; }

        public Context(string connectionString,bool useConsoleLogger)
        {
            _connectionString = connectionString;
            _useConsoleLogger = useConsoleLogger;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>
            {
                builder
                    .AddFilter((category,level) =>
                        category == DbLoggerCategory.Database.Command.Name && level == LogLevel.information)
                    .AddConsole();
            });

            optionsBuilder
                .UsesqlServer(_connectionString)
                .UseLazyLoadingProxies(); 

            if (_useConsoleLogger)
            {
                optionsBuilder
                    .UseLoggerFactory(loggerFactory)
                    .EnableSensitiveDataLogging();
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Student>(x =>
            {
                x.ToTable("Student").HasKey(k => k.Id);
                x.Property(p => p.Id).HasColumnName("Id");
                x.Property(p => p.Name).HasColumnName("Name");
                x.HasMany(p => p.Courses)
                    .WithOne(p => p.Student)
                    .OnDelete(DeleteBehavior.Cascade)
                    .Metadata.PrincipalToDependent.SetPropertyAccessMode(PropertyAccessMode.Field);
            });
            modelBuilder.Entity<Course>(x =>
            {
                x.ToTable("Course").HasKey(k => k.Id);
                x.Property(p => p.Id).HasColumnName("Id");
                x.Property(p => p.Name).HasColumnName("Name");
                x.HasOne(p => p.Student).WithMany(p => p.Courses);
                
            });
        }
    }
}

演示该问题的测试程序。

using Microsoft.Extensions.Configuration;
using System;
using System.IO;
using System.Linq;

namespace EFCoreMappingTests
{
    class Program
    {
        static void Main(string[] args)
        {
            string connectionString = GetConnectionString();

            using var context = new Context(connectionString,true);

            var student2 = context.Students.FirstOrDefault(q => q.Id == 5);

            Console.WriteLine(student2.IsRegisteredForACourse());
            Console.WriteLine(student2.IsRegisteredForACourse2()); // The method uses the property which forces the lazy loading of the entities
            Console.WriteLine(student2.IsRegisteredForACourse());
        }

        private static string GetConnectionString()
        {
            IConfigurationRoot configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .Build();

            return configuration["ConnectionString"];
        }
    }
}

控制台输出

False
True
True

解决方法

当您将EF实体中的映射属性声明为虚拟时,EF会生成一个代理,该代理能够拦截请求并评估是否需要加载数据。如果您尝试在访问该虚拟属性之前使用备用字段,则EF没有“信号”可以延迟加载该属性。

作为实体的一般规则,您应该始终使用属性,并避免使用/访问后备字段。自动初始化可以帮助:

public virtual IReadOnlyList<Course> Courses => new List<Course>().AsReadOnly();