From 6773981236a036806a790d4a355fed14b2aec484 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 4 Oct 2026 12:56:12 +0200 Subject: [PATCH 1/2] Virtual keyword in EF Core: .NET 10, ILazyLoader and explicit loading samples Retarget both projects to net10.0 and EF Core 10.0.12, bump the test packages, remove the two dead null checks after First(), fix the three nullable warnings, and add an ILazyLoader entity (no virtual members) plus an explicit loading call, each covered by a new test. --- .../VirtualKeywordInEFCore/Tests/Tests.csproj | 10 +-- .../Tests/VirtualKeywordInEFCoreTests.cs | 35 ++++++++++ .../Models/AuthorInjected.cs | 24 +++++++ .../VirtualKeywordInEFCore/Models/Book.cs | 2 +- .../Models/BookInjected.cs | 9 +++ .../VirtualKeywordInEFCore/Models/BookLazy.cs | 2 +- .../Models/DataContextInjected.cs | 25 +++++++ .../Models/DataSeeder.cs | 22 ++++++ .../VirtualKeywordInEFCore/Program.cs | 67 ++++++++++++++----- .../VirtualKeywordInEFCore.csproj | 9 +-- 10 files changed, 176 insertions(+), 29 deletions(-) create mode 100644 dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/AuthorInjected.cs create mode 100644 dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/BookInjected.cs create mode 100644 dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/DataContextInjected.cs diff --git a/dotnet-efcore/VirtualKeywordInEFCore/Tests/Tests.csproj b/dotnet-efcore/VirtualKeywordInEFCore/Tests/Tests.csproj index f76aa1b039..836fde7b6c 100644 --- a/dotnet-efcore/VirtualKeywordInEFCore/Tests/Tests.csproj +++ b/dotnet-efcore/VirtualKeywordInEFCore/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/dotnet-efcore/VirtualKeywordInEFCore/Tests/VirtualKeywordInEFCoreTests.cs b/dotnet-efcore/VirtualKeywordInEFCore/Tests/VirtualKeywordInEFCoreTests.cs index d3ceb85a0b..3f5b1ba88e 100644 --- a/dotnet-efcore/VirtualKeywordInEFCore/Tests/VirtualKeywordInEFCoreTests.cs +++ b/dotnet-efcore/VirtualKeywordInEFCore/Tests/VirtualKeywordInEFCoreTests.cs @@ -55,5 +55,40 @@ public void GivenLazyLoading_WithLazyLoadingEnabledPropertySetToFalse_WhenAuthor Assert.False(authorHollyJackson.Books.Any()); Assert.Empty(authorHollyJackson.Books); } + + [Fact] + public void GivenInjectedLazyLoader_WhenAuthorRetrievedInFreshContext_ThenBooksLazyLoadedWithoutProxy() + { + // Arrange + using (var seedContext = new DataContextInjected()) + { + DataSeeder.SeedInjected(seedContext); + } + + using var context = new DataContextInjected(); + + // Act + var author = context.AuthorsInjected.First(a => a.FullName == "Freida MCFADDEN"); + + // Assert + Assert.Equal(typeof(AuthorInjected), author.GetType()); + Assert.NotNull(author.Books); + Assert.Equal(2, author.Books.Count); + } + + [Fact] + public void GivenNoLazyLoading_WhenCollectionExplicitlyLoaded_ThenEmptyCollectionIsFilled() + { + // Arrange + using var context = new DataContextWithoutLazyLoading(); + var author = context.Authors.First(a => a.FullName == "Lucy FOLEY"); + Assert.Empty(author.Books); + + // Act + context.Entry(author).Collection(a => a.Books).Load(); + + // Assert + Assert.Equal(4, author.Books.Count); + } } } \ No newline at end of file diff --git a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/AuthorInjected.cs b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/AuthorInjected.cs new file mode 100644 index 0000000000..027978a624 --- /dev/null +++ b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/AuthorInjected.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace VirtualKeywordInEFCore.Models +{ + public class AuthorInjected + { + private ICollection? _books; + + public AuthorInjected() { } + + private AuthorInjected(ILazyLoader lazyLoader) => LazyLoader = lazyLoader; + + private ILazyLoader? LazyLoader { get; set; } + + public int AuthorInjectedId { get; set; } + public string? FullName { get; set; } + + public ICollection? Books + { + get => LazyLoader.Load(this, ref _books); + set => _books = value; + } + } +} diff --git a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/Book.cs b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/Book.cs index 7a1a4fa39d..9d98d08dd5 100644 --- a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/Book.cs +++ b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/Book.cs @@ -8,6 +8,6 @@ public class Book public int BookId { get; set; } public string? Title { get; set; } public int AuthorId { get; set; } - public Author Author { get; set; } + public Author Author { get; set; } = null!; } } diff --git a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/BookInjected.cs b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/BookInjected.cs new file mode 100644 index 0000000000..e0a057cb1d --- /dev/null +++ b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/BookInjected.cs @@ -0,0 +1,9 @@ +namespace VirtualKeywordInEFCore.Models +{ + public class BookInjected + { + public int BookInjectedId { get; set; } + public string? Title { get; set; } + public int AuthorInjectedId { get; set; } + } +} diff --git a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/BookLazy.cs b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/BookLazy.cs index 2f7b3d2734..386e754202 100644 --- a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/BookLazy.cs +++ b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/BookLazy.cs @@ -8,6 +8,6 @@ public class BookLazy public int BookId { get; set; } public string? Title { get; set; } public int AuthorId { get; set; } - public virtual AuthorLazy Author { get; set; } + public virtual AuthorLazy Author { get; set; } = null!; } } diff --git a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/DataContextInjected.cs b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/DataContextInjected.cs new file mode 100644 index 0000000000..f7a3829bfa --- /dev/null +++ b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/DataContextInjected.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore; + +namespace VirtualKeywordInEFCore.Models +{ + public class DataContextInjected : DbContext + { + protected override void OnConfiguring(DbContextOptionsBuilder options) + { + options.UseSqlite("DataSource=LibraryInjected.db"); + + base.OnConfiguring(options); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity() + .HasMany(a => a.Books) + .WithOne() + .HasForeignKey(b => b.AuthorInjectedId); + } + + public DbSet AuthorsInjected { get; set; } + public DbSet BooksInjected { get; set; } + } +} diff --git a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/DataSeeder.cs b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/DataSeeder.cs index 0c082c143b..1e382869a3 100644 --- a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/DataSeeder.cs +++ b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Models/DataSeeder.cs @@ -64,5 +64,27 @@ public static void SeedLazy(DataContextLazyLoading contextLazy) contextLazy.AddRange(booksLazy); contextLazy.SaveChanges(); } + + public static void SeedInjected(DataContextInjected contextInjected) + { + contextInjected.Database.EnsureCreated(); + if (contextInjected.AuthorsInjected.Any()) + { + return; + } + + var authorInjected = new AuthorInjected + { + FullName = "Freida MCFADDEN", + Books = new List + { + new () { Title = "The Housemaid" }, + new () { Title = "The Coworker" } + } + }; + + contextInjected.Add(authorInjected); + contextInjected.SaveChanges(); + } } } diff --git a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Program.cs b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Program.cs index 6eff34f975..31c9c6b959 100644 --- a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Program.cs +++ b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Program.cs @@ -10,6 +10,10 @@ static void Main() LoadData(); LoadDataWithLazyLoading(); + + LoadDataWithInjectedLazyLoader(); + + LoadDataWithExplicitLoading(); } public static void LoadData() @@ -21,15 +25,12 @@ public static void LoadData() Author author; author = contextWithoutLazyLoading.Authors.AsNoTracking().First(a => a.FullName == "Lucy FOLEY"); - if (author is not null) - { - Console.WriteLine($"Author Name: {author.FullName}"); - Console.WriteLine($"{author.FullName}'s Books number: {author.Books.Count}"); + Console.WriteLine($"Author Name: {author.FullName}"); + Console.WriteLine($"{author.FullName}'s Books number: {author.Books.Count}"); - foreach (var book in author.Books) - { - Console.WriteLine($"Book Title: {book.Title}"); - } + foreach (var book in author.Books) + { + Console.WriteLine($"Book Title: {book.Title}"); } } @@ -42,18 +43,48 @@ public static void LoadDataWithLazyLoading() AuthorLazy authorLazy; authorLazy = contextLazy.AuthorsLazy.AsNoTracking().First(a => a.FullName == "Holly JACKSON"); - if (authorLazy is not null) + Console.WriteLine($"Author Name: {authorLazy.FullName}"); + Console.WriteLine($"{authorLazy.FullName}'s Books number: {authorLazy.Books.Count}"); + Console.WriteLine($"{nameof(AuthorLazy)} datatype: {authorLazy.GetType()}"); + Console.WriteLine($"{nameof(BookLazy)} datatype: {authorLazy.Books.First().GetType()}"); + + foreach (var bookLazy in authorLazy.Books) + { + Console.WriteLine($"Book Title: {bookLazy.Title}"); + } + } + + public static void LoadDataWithInjectedLazyLoader() + { + Console.WriteLine("\nLazy Loading with an injected ILazyLoader:"); + + using (var seedContext = new DataContextInjected()) { - Console.WriteLine($"Author Name: {authorLazy.FullName}"); - Console.WriteLine($"{authorLazy.FullName}'s Books number: {authorLazy.Books?.Count}"); - Console.WriteLine($"{nameof(AuthorLazy)} datatype: {authorLazy.GetType()}"); - Console.WriteLine($"{nameof(BookLazy)} datatype: {authorLazy.Books?.First().GetType()}"); - - foreach (var bookLazy in authorLazy.Books) - { - Console.WriteLine($"Book Title: {bookLazy.Title}"); - } + DataSeeder.SeedInjected(seedContext); } + + using var context = new DataContextInjected(); + var author = context.AuthorsInjected.First(a => a.FullName == "Freida MCFADDEN"); + + Console.WriteLine($"{nameof(AuthorInjected)} datatype: {author.GetType()}"); + Console.WriteLine($"{author.FullName}'s Books number: {author.Books?.Count}"); + } + + public static void LoadDataWithExplicitLoading() + { + Console.WriteLine("\nExplicit Loading:"); + + using (var seedContext = new DataContextWithoutLazyLoading()) + { + DataSeeder.SeedWithoutLazy(seedContext); + } + + using var context = new DataContextWithoutLazyLoading(); + var author = context.Authors.First(a => a.FullName == "Lucy FOLEY"); + + context.Entry(author).Collection(a => a.Books).Load(); + + Console.WriteLine($"{author.FullName}'s Books number after Load(): {author.Books.Count}"); } } } diff --git a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/VirtualKeywordInEFCore.csproj b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/VirtualKeywordInEFCore.csproj index 3a3baa353b..2e8acdf706 100644 --- a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/VirtualKeywordInEFCore.csproj +++ b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/VirtualKeywordInEFCore.csproj @@ -2,15 +2,16 @@ Exe - net8.0 + net10.0 enable enable - - - + + + + From 26b5318aaba51ff051cb4c2bcde22728e2794ec5 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 4 Oct 2026 13:16:38 +0200 Subject: [PATCH 2/2] Virtual keyword in EF Core: keep the null check in LoadDataWithLazyLoading Restore the original if (authorLazy is not null) block and the ?. uses so the article's debugger screenshot matches lines 36 to 57 of Program.cs again. --- .../VirtualKeywordInEFCore/Program.cs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Program.cs b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Program.cs index 31c9c6b959..dc49d46e64 100644 --- a/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Program.cs +++ b/dotnet-efcore/VirtualKeywordInEFCore/VirtualKeywordInEFCore/Program.cs @@ -12,7 +12,6 @@ static void Main() LoadDataWithLazyLoading(); LoadDataWithInjectedLazyLoader(); - LoadDataWithExplicitLoading(); } @@ -43,14 +42,17 @@ public static void LoadDataWithLazyLoading() AuthorLazy authorLazy; authorLazy = contextLazy.AuthorsLazy.AsNoTracking().First(a => a.FullName == "Holly JACKSON"); - Console.WriteLine($"Author Name: {authorLazy.FullName}"); - Console.WriteLine($"{authorLazy.FullName}'s Books number: {authorLazy.Books.Count}"); - Console.WriteLine($"{nameof(AuthorLazy)} datatype: {authorLazy.GetType()}"); - Console.WriteLine($"{nameof(BookLazy)} datatype: {authorLazy.Books.First().GetType()}"); - - foreach (var bookLazy in authorLazy.Books) + if (authorLazy is not null) { - Console.WriteLine($"Book Title: {bookLazy.Title}"); + Console.WriteLine($"Author Name: {authorLazy.FullName}"); + Console.WriteLine($"{authorLazy.FullName}'s Books number: {authorLazy.Books?.Count}"); + Console.WriteLine($"{nameof(AuthorLazy)} datatype: {authorLazy.GetType()}"); + Console.WriteLine($"{nameof(BookLazy)} datatype: {authorLazy.Books?.First().GetType()}"); + + foreach (var bookLazy in authorLazy.Books) + { + Console.WriteLine($"Book Title: {bookLazy.Title}"); + } } }