\n
Student Entity
\nusing System.Collections.Generic;\n\nnamespace EFCoreMappingTests\n{\n public class Student\n {\n public int Id { get; }\n public string Name { get; }\n\n private readonly List<Course> _courses;\n public virtual IReadOnlyList<Course> Courses => _courses.AsReadOnly();\n\n protected Student()\n {\n _courses = new List<Course>();\n }\n\n public Student(string name) : this()\n {\n Name = name;\n }\n\n public bool IsRegisteredForACourse()\n {\n return _courses.Count > 0;\n }\n\n public bool IsRegisteredForACourse2()\n {\n //Note the use of the property compare to the previous method using the backing field.\n return Courses.Count > 0;\n }\n\n public void AddCourse(Course course)\n {\n _courses.Add(course);\n }\n }\n}\n
\nCourse Entity
\nnamespace EFCoreMappingTests\n{\n public class Course\n {\n public int Id { get; }\n public string Name { get; }\n public virtual Student Student { get; }\n\n protected Course()\n {\n }\n public Course(string name) : this()\n {\n Name = name;\n }\n }\n}\n
\nDbContext
\nusing Microsoft.EntityFrameworkCore;\nusing Microsoft.Extensions.Logging;\n\nnamespace EFCoreMappingTests\n{\n public sealed class Context : DbContext\n {\n private readonly string _connectionString;\n private readonly bool _useConsoleLogger;\n\n public DbSet<Student> Students { get; set; }\n public DbSet<Course> Courses { get; set; }\n\n public Context(string connectionString, bool useConsoleLogger)\n {\n _connectionString = connectionString;\n _useConsoleLogger = useConsoleLogger;\n }\n\n protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)\n {\n ILoggerFactory loggerFactory = LoggerFactory.Create(builder =>\n {\n builder\n .AddFilter((category, level) =>\n category == DbLoggerCategory.Database.Command.Name && level == LogLevel.Information)\n .AddConsole();\n });\n\n optionsBuilder\n .UseSqlServer(_connectionString)\n .UseLazyLoadingProxies(); \n\n if (_useConsoleLogger)\n {\n optionsBuilder\n .UseLoggerFactory(loggerFactory)\n .EnableSensitiveDataLogging();\n }\n }\n\n protected override void OnModelCreating(ModelBuilder modelBuilder)\n {\n modelBuilder.Entity<Student>(x =>\n {\n x.ToTable("Student").HasKey(k => k.Id);\n x.Property(p => p.Id).HasColumnName("Id");\n x.Property(p => p.Name).HasColumnName("Name");\n x.HasMany(p => p.Courses)\n .WithOne(p => p.Student)\n .OnDelete(DeleteBehavior.Cascade)\n .Metadata.PrincipalToDependent.SetPropertyAccessMode(PropertyAccessMode.Field);\n });\n modelBuilder.Entity<Course>(x =>\n {\n x.ToTable("Course").HasKey(k => k.Id);\n x.Property(p => p.Id).HasColumnName("Id");\n x.Property(p => p.Name).HasColumnName("Name");\n x.HasOne(p => p.Student).WithMany(p => p.Courses);\n \n });\n }\n }\n}\n
\nTest program which demos the issue.
\nusing Microsoft.Extensions.Configuration;\nusing System;\nusing System.IO;\nusing System.Linq;\n\nnamespace EFCoreMappingTests\n{\n class Program\n {\n static void Main(string[] args)\n {\n string connectionString = GetConnectionString();\n\n using var context = new Context(connectionString, true);\n\n var student2 = context.Students.FirstOrDefault(q => q.Id == 5);\n\n Console.WriteLine(student2.IsRegisteredForACourse());\n Console.WriteLine(student2.IsRegisteredForACourse2()); // The method uses the property which forces the lazy loading of the entities\n Console.WriteLine(student2.IsRegisteredForACourse());\n }\n\n private static string GetConnectionString()\n {\n IConfigurationRoot configuration = new ConfigurationBuilder()\n .SetBasePath(Directory.GetCurrentDirectory())\n .AddJsonFile("appsettings.json")\n .Build();\n\n return configuration["ConnectionString"];\n }\n }\n}\n
\nConsole Output
\nFalse\nTrue\nTrue\n
\n","author":{"@type":"Person","name":"J Swanson"},"upvoteCount":1,"answerCount":1,"acceptedAnswer":null}}Reputation: 71
I am using EF Core 3.1.7. The DbContext has the UseLazyLoadingProxies set. Fluent API mappings are being used to map entities to the database. I have an entity with a navigation property that uses a backing field. Loads and saves to the database seem to work fine except for an issue when accessing the backing field before I access the navigation property.
It seems that referenced entities don't lazy load when accessing the backing field. Is this a deficiency of the Castle.Proxy class or an incorrect configuration?
Compare the Student class implementation of IsRegisteredForACourse to the IsRegisteredForACourse2 for the behavior in question.
Database tables and relationships.
Student Entity
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);
}
}
}
Course Entity
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);
});
}
}
}
Test program which demos the issue.
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"];
}
}
}
Console Output
False
True
True
Upvotes: 1
Views: 2987
Reputation: 34978
When you declare a mapped property in an EF entity as virtual, EF generates a proxy which is capable of intercepting requests and assessing whether the data needs to be loaded. If you attempt to use a backing field before that virtual property is accessed, EF has no "signal" to lazy load the property.
As a general rule with entities you should always use the properties and avoid using/accessing backing fields. Auto-initialization can help:
public virtual IReadOnlyList<Course> Courses => new List<Course>().AsReadOnly();
Upvotes: 0