【问题标题】:EF Core 3.1: Navigation property doesn't lazy load entities when calling the backing field firstEF Core 3.1:导航属性在首先调用支持字段时不会延迟加载实体
【发布时间】:2020-12-02 21:09:46
【问题描述】:

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

似乎引用的实体在访问支持字段时不会延迟加载。这是 Castle.Proxy 类的缺陷还是配置不正确?

比较 IsRegisteredForACourse 的 Student 类实现与 IsRegisteredForACourse2 的相关行为。

数据库表和关系。

学生实体

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;
        }
    }
}

数据库上下文

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

【问题讨论】:

    标签: entity-framework ef-fluent-api ef-core-3.1 navigation-properties backing-field


    【解决方案1】:

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

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

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

    【讨论】:

    • 虚拟属性是延迟加载的“信号”是有道理的。自动初始化建议的问题是,在类内部我仍然需要添加到集合中的能力,但在外部它应该是只读的。有关示例,请参见 Student 类的 AddCourse 方法。
    • 那么它不应该是只读的,只是一个虚拟的 ICollection。实体应该反映系统的数据域,因此尝试强制执行只读规则是不寻常的,除非该数据状态确实无法在该上下文范围内更改。像这样的强制执行通常是 DDD 等模式的一部分,在这种模式下您将使用有界上下文。如果您有一个负责阅读/查询的上下文,则 Courses 可以是只读的,而另一个用于编辑学生和关联课程的有界上下文将使用 ICollection 域。视图模型等可以限制操作。
    • 设计目标是拥有一个支持 DDD 的丰富领域模型。我不是 DDD 专家,但似乎有界上下文的建议并不完全准确。丰富的领域模型自然应该能够查询和执行命令。回到最初的问题。看来这就是 Castle.Proxy 的工作方式,它仅在属性上触发,而不是在属性和支持字段上触发。我想我的另一个选择是跳过延迟加载并总是急切地加载所有内容。还有其他建议吗?
    • 我将有属性或依赖项的支持字段我遵循一个简单的规则,即访问它们的唯一代码是属性。我对私有成员使用下划线表示法,因此很容易在代码中发现它们。如果我在任何领域进行查找用法,很容易确定谁可能使用过它并与其他开发人员掩饰。最终,您必须相信与您一起工作的人才能理解并采用最佳实践。我不会将支持字段用于集合。它们要么是可变的,要么不是。
    猜你喜欢
    • 1970-01-01
    • 2011-05-03
    • 1970-01-01
    • 2020-06-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-08-11
    • 1970-01-01
    相关资源
    最近更新 更多