From ee58e3c1800a060ad774939ea606d65b7f1861c5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:57:49 +0000 Subject: [PATCH 1/5] Initial plan From 39ae0a5369a938762b82fd5bba970b3775e24f4a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:16:58 +0000 Subject: [PATCH 2/5] Add test and fix for complex type LEFT JOIN null materialization issue Co-authored-by: roji <1862641+roji@users.noreply.github.com> --- .../StructuralTypeMaterializerSource.cs | 1 + .../Query/AdHocComplexTypeQueryTestBase.cs | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs b/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs index d0b55bfe469..73b68a70005 100644 --- a/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs +++ b/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs @@ -121,6 +121,7 @@ public Expression CreateMaterializeExpression( return structuralType is IComplexType complexType && ReadComplexTypeDirectly(complexType) && (UseOldBehavior37162 ? parameters.ClrType.IsNullableType() : parameters.IsNullable) + && complexType.ComplexProperty.IsNullable // Only apply null-checking wrapper if the complex property itself is nullable ? HandleNullableComplexTypeMaterialization( complexType, parameters.ClrType, diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index e336dd34498..c7f9386ec02 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -282,6 +282,74 @@ public class OptionalComplexProperty #endregion Issue37337 + #region NonOptionalComplexTypeViaLeftJoin + + [ConditionalFact] + public virtual async Task Non_optional_complex_type_with_all_nullable_properties_via_left_join() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Projects.Add(new ContextNonOptionalComplexTypeViaLeftJoin.Project + { + Properties = new List + { + new ContextNonOptionalComplexTypeViaLeftJoin.ProjectLifetime + { + Lifetime = new ContextNonOptionalComplexTypeViaLeftJoin.Lifetime() + } + } + }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateContext(); + + var project = await context.Projects.Include(p => p.Properties).SingleAsync(); + var projectLifetime = (ContextNonOptionalComplexTypeViaLeftJoin.ProjectLifetime)project.Properties.Single(); + + Assert.NotNull(projectLifetime.Lifetime); + Assert.Null(projectLifetime.Lifetime.Start); + Assert.Null(projectLifetime.Lifetime.End); + } + + private class ContextNonOptionalComplexTypeViaLeftJoin(DbContextOptions options) : DbContext(options) + { + public DbSet Projects { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasOne(p => p.Project).WithMany(p => p.Properties).HasForeignKey(p => p.ProjectId); + modelBuilder.Entity().HasBaseType().ComplexProperty(p => p.Lifetime).IsRequired(true); + } + + public class Project + { + public int Id { get; set; } + public List Properties { get; set; } = null!; + } + + public class ProjectProperty + { + public int Id { get; set; } + public int ProjectId { get; set; } + public Project Project { get; set; } = null!; + } + + public class ProjectLifetime : ProjectProperty + { + public Lifetime Lifetime { get; set; } = null!; + } + + public class Lifetime + { + public DateTime? Start { get; init; } + public DateTime? End { get; init; } + } + } + + #endregion NonOptionalComplexTypeViaLeftJoin + protected override string StoreName => "AdHocComplexTypeQueryTest"; } From 2b855c158fc48fbc74e891e7050d5c347ed9b7db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:24:53 +0000 Subject: [PATCH 3/5] Update fix to properly handle required complex types Co-authored-by: roji <1862641+roji@users.noreply.github.com> --- .../Query/Internal/StructuralTypeMaterializerSource.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs b/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs index 73b68a70005..2ecb217415c 100644 --- a/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs +++ b/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs @@ -121,7 +121,6 @@ public Expression CreateMaterializeExpression( return structuralType is IComplexType complexType && ReadComplexTypeDirectly(complexType) && (UseOldBehavior37162 ? parameters.ClrType.IsNullableType() : parameters.IsNullable) - && complexType.ComplexProperty.IsNullable // Only apply null-checking wrapper if the complex property itself is nullable ? HandleNullableComplexTypeMaterialization( complexType, parameters.ClrType, @@ -132,6 +131,7 @@ public Expression CreateMaterializeExpression( // Creates a conditional expression that handles materialization of nullable complex types. // For nullable complex types, the method checks if all scalar properties are null // and returns default if they are, otherwise materializes the complex type instance. + // For required complex types, always materializes the instance even if all properties are null. // If there's a required (non-nullable) property, only that property is checked for efficiency. Expression HandleNullableComplexTypeMaterialization( IComplexType complexType, @@ -139,6 +139,14 @@ Expression HandleNullableComplexTypeMaterialization( Expression materializeExpression, MethodCallExpression getValueBufferExpression) { + // If the complex property is required, don't apply null-checking wrapper + if (!complexType.ComplexProperty.IsNullable) + { + return clrType.IsNullableType() + ? Convert(materializeExpression, clrType) + : materializeExpression; + } + // Get all scalar properties of the complex type (including nested ones). var allScalarProperties = complexType.GetFlattenedProperties().ToList(); From c5d55baa3d63871f7b838c9e68054fa0e0f5c62a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:26:02 +0000 Subject: [PATCH 4/5] Address code review feedback: improve fluent API readability Co-authored-by: roji <1862641+roji@users.noreply.github.com> --- .../Query/AdHocComplexTypeQueryTestBase.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index c7f9386ec02..b2ab876d43b 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -319,8 +319,15 @@ private class ContextNonOptionalComplexTypeViaLeftJoin(DbContextOptions options) protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.Entity().HasOne(p => p.Project).WithMany(p => p.Properties).HasForeignKey(p => p.ProjectId); - modelBuilder.Entity().HasBaseType().ComplexProperty(p => p.Lifetime).IsRequired(true); + modelBuilder.Entity() + .HasOne(p => p.Project) + .WithMany(p => p.Properties) + .HasForeignKey(p => p.ProjectId); + + modelBuilder.Entity() + .HasBaseType() + .ComplexProperty(p => p.Lifetime) + .IsRequired(true); } public class Project From 4b0263f512f40b5b6caadf054a7fd98ea698c23a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:57:49 +0000 Subject: [PATCH 5/5] Initial plan