diff --git a/src/EFCore/Query/EntityMaterializerSourceParameters.cs b/src/EFCore/Query/EntityMaterializerSourceParameters.cs index 95750d7da96..07178eed624 100644 --- a/src/EFCore/Query/EntityMaterializerSourceParameters.cs +++ b/src/EFCore/Query/EntityMaterializerSourceParameters.cs @@ -8,7 +8,15 @@ namespace Microsoft.EntityFrameworkCore.Query; /// /// The entity or complex type being materialized. /// The name of the instance being materialized. -/// CLR type of the result. +/// +/// CLR type of the result. +/// Note that this differs from on +/// when a nullable value type is being projected out. +/// +/// +/// Whether the type being materialized is nullable. +/// A complex type may be non-nullable even if its CLR type is nullable (i.e. a class), based on model configuration. +/// /// /// The query tracking behavior, or if this materialization is not from a query. /// @@ -16,6 +24,7 @@ public readonly record struct StructuralTypeMaterializerSourceParameters( ITypeBase StructuralType, string InstanceName, Type ClrType, + bool IsNullable, QueryTrackingBehavior? QueryTrackingBehavior); /// diff --git a/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs b/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs index 3154069780a..d0b55bfe469 100644 --- a/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs +++ b/src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs @@ -31,6 +31,9 @@ public static readonly MethodInfo PopulateListMethod = typeof(StructuralTypeMaterializerSource).GetMethod( nameof(PopulateList), BindingFlags.Public | BindingFlags.Static)!; + private static readonly bool UseOldBehavior37162 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37162", out var enabled37162) && enabled37162; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -103,7 +106,7 @@ public Expression CreateMaterializeExpression( || structuralType is not IEntityType ? properties.Count == 0 && blockExpressions.Count == 0 ? constructorExpression - : CreateMaterializeExpression(blockExpressions, instanceVariable, constructorExpression, getValueBufferExpression, properties, bindingInfo) + : CreateMaterializeExpression(blockExpressions, instanceVariable, constructorExpression, getValueBufferExpression, properties, bindingInfo, parameters.IsNullable) : CreateInterceptionMaterializeExpression( structuralType, properties, @@ -112,11 +115,12 @@ public Expression CreateMaterializeExpression( constructorExpression, getValueBufferExpression, instanceVariable, - blockExpressions); + blockExpressions, + parameters.IsNullable); return structuralType is IComplexType complexType && ReadComplexTypeDirectly(complexType) - && parameters.ClrType.IsNullableType() + && (UseOldBehavior37162 ? parameters.ClrType.IsNullableType() : parameters.IsNullable) ? HandleNullableComplexTypeMaterialization( complexType, parameters.ClrType, @@ -180,7 +184,8 @@ protected virtual void AddInitializeExpression( ParameterBindingInfo bindingInfo, Expression instanceVariable, MethodCallExpression getValueBufferExpression, - List blockExpressions) + List blockExpressions, + bool nullable) { if (property is IComplexProperty cp && !ReadComplexTypeDirectly(cp.ComplexType)) { @@ -202,7 +207,7 @@ IServiceProperty serviceProperty IComplexProperty complexProperty => CreateMaterializeExpression( - new StructuralTypeMaterializerSourceParameters(complexProperty.ComplexType, "complexType", complexProperty.ClrType, QueryTrackingBehavior: null), + new StructuralTypeMaterializerSourceParameters(complexProperty.ComplexType, "complexType", complexProperty.ClrType, nullable || complexProperty.IsNullable, QueryTrackingBehavior: null), bindingInfo.MaterializationContextExpression), _ => throw new UnreachableException() @@ -258,11 +263,12 @@ private void AddInitializeExpressions( ParameterBindingInfo bindingInfo, Expression instanceVariable, MethodCallExpression getValueBufferExpression, - List blockExpressions) + List blockExpressions, + bool nullable) { foreach (var property in properties) { - AddInitializeExpression(property, bindingInfo, instanceVariable, getValueBufferExpression, blockExpressions); + AddInitializeExpression(property, bindingInfo, instanceVariable, getValueBufferExpression, blockExpressions, nullable); } } @@ -350,11 +356,12 @@ private Expression CreateMaterializeExpression( Expression constructorExpression, MethodCallExpression getValueBufferExpression, HashSet properties, - ParameterBindingInfo bindingInfo) + ParameterBindingInfo bindingInfo, + bool nullable) { blockExpressions.Add(Assign(instanceVariable, constructorExpression)); - AddInitializeExpressions(properties, bindingInfo, instanceVariable, getValueBufferExpression, blockExpressions); + AddInitializeExpressions(properties, bindingInfo, instanceVariable, getValueBufferExpression, blockExpressions, nullable); if (bindingInfo.StructuralType is IEntityType) { @@ -374,7 +381,8 @@ private Expression CreateInterceptionMaterializeExpression( Expression constructorExpression, MethodCallExpression getValueBufferExpression, ParameterExpression instanceVariable, - List blockExpressions) + List blockExpressions, + bool nullable) { // Something like: // Dictionary)> accessorFactory = CreateAccessors() @@ -456,7 +464,7 @@ private Expression CreateInterceptionMaterializeExpression( instanceVariable, Default(typeof(InterceptionResult))), IsSuppressedProperty)), - CreateInitializeExpression())); + CreateInitializeExpression(nullable))); blockExpressions.Add( Assign( instanceVariable, @@ -527,11 +535,11 @@ Expression CreateAccessorReadExpression() return Block([dictionaryVariable], snapshotBlockExpressions); } - BlockExpression CreateInitializeExpression() + BlockExpression CreateInitializeExpression(bool nullable) { var initializeBlockExpressions = new List(); - AddInitializeExpressions(properties, bindingInfo, instanceVariable, getValueBufferExpression, initializeBlockExpressions); + AddInitializeExpressions(properties, bindingInfo, instanceVariable, getValueBufferExpression, initializeBlockExpressions, nullable); if (bindingInfo.StructuralType is IEntityType) { @@ -553,9 +561,12 @@ public virtual Func GetMaterializer(IEntityType var materializationContextParameter = Parameter(typeof(MaterializationContext), "materializationContext"); + // GetMaterializer only gets called from the model/change tracking, and not from the materializer; + // all such usages assume non-nullable materialization (i.e. no checking of scalar properties is needed + // for complex types) return Lambda>( ((IStructuralTypeMaterializerSource)this).CreateMaterializeExpression( - new StructuralTypeMaterializerSourceParameters(entityType, "instance", entityType.ClrType, null), materializationContextParameter), + new StructuralTypeMaterializerSourceParameters(entityType, "instance", entityType.ClrType, IsNullable: false, null), materializationContextParameter), materializationContextParameter) .Compile(); } @@ -570,9 +581,12 @@ public virtual Func GetMaterializer(IComplexType { var materializationContextParameter = Parameter(typeof(MaterializationContext), "materializationContext"); + // GetMaterializer only gets called from the model/change tracking, and not from the materializer; + // all such usages assume non-nullable materialization (i.e. no checking of scalar properties is needed + // for complex types) return Lambda>( ((IStructuralTypeMaterializerSource)this).CreateMaterializeExpression( - new StructuralTypeMaterializerSourceParameters(complexType, "instance", complexType.ClrType, null), materializationContextParameter), + new StructuralTypeMaterializerSourceParameters(complexType, "instance", complexType.ClrType, IsNullable: false, null), materializationContextParameter), materializationContextParameter) .Compile(); } @@ -622,11 +636,16 @@ public virtual Func GetEmptyMaterializer(IComple public virtual Func GetEmptyMaterializer( ITypeBase entityType, InstantiationBinding binding, List serviceProperties) { + // GetEmptyMaterializer only gets called from the model/change tracking, and not from the materializer; + // all such usages assume non-nullable materialization (i.e. no checking of scalar properties is needed + // for complex types) + var nullable = false; + binding = ModifyBindings(entityType, binding); var materializationContextExpression = Parameter(typeof(MaterializationContext), "mc"); var bindingInfo = new ParameterBindingInfo( - new StructuralTypeMaterializerSourceParameters(entityType, "instance", entityType.ClrType, null), materializationContextExpression); + new StructuralTypeMaterializerSourceParameters(entityType, "instance", entityType.ClrType, nullable, null), materializationContextExpression); var blockExpressions = new List(); var instanceVariable = Variable(binding.RuntimeType, "instance"); @@ -648,7 +667,7 @@ public virtual Func GetEmptyMaterializer( ? properties.Count == 0 && blockExpressions.Count == 0 ? constructorExpression : CreateMaterializeExpression( - blockExpressions, instanceVariable, constructorExpression, getValueBufferExpression, properties, bindingInfo) + blockExpressions, instanceVariable, constructorExpression, getValueBufferExpression, properties, bindingInfo, nullable) : CreateInterceptionMaterializeExpression( entityType, [], @@ -657,7 +676,8 @@ public virtual Func GetEmptyMaterializer( constructorExpression, getValueBufferExpression, instanceVariable, - blockExpressions), + blockExpressions, + nullable), materializationContextExpression) .Compile(); } diff --git a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs index 5ac7452b439..7e071275b9e 100644 --- a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitor.cs @@ -642,7 +642,7 @@ private Expression MaterializeEntity( valueBufferExpression, shaper.MaterializationCondition.Body); - var expressionContext = (returnType, materializationContextVariable, concreteEntityTypeVariable, shadowValuesVariable); + var expressionContext = (returnType, shaper.IsNullable, materializationContextVariable, concreteEntityTypeVariable, shadowValuesVariable); expressions.Add(Assign(concreteEntityTypeVariable, materializationConditionBody)); var (primaryKey, concreteStructuralTypes) = structuralType is IEntityType entityType @@ -708,11 +708,13 @@ private Expression MaterializeEntity( private BlockExpression CreateFullMaterializeExpression( ITypeBase concreteStructuralType, (Type ReturnType, + bool IsNullable, ParameterExpression MaterializationContextVariable, ParameterExpression ConcreteEntityTypeVariable, ParameterExpression ShadowValuesVariable) materializeExpressionContext) { var (returnType, + nullable, materializationContextVariable, _, shadowValuesVariable) = materializeExpressionContext; @@ -722,7 +724,7 @@ private BlockExpression CreateFullMaterializeExpression( var materializer = materializerSource .CreateMaterializeExpression( new StructuralTypeMaterializerSourceParameters( - concreteStructuralType, "instance", returnType, queryTrackingBehavior), materializationContextVariable); + concreteStructuralType, "instance", returnType, nullable, queryTrackingBehavior), materializationContextVariable); if (_queryStateManager && concreteStructuralType is IRuntimeEntityType { ShadowPropertyCount: > 0 } runtimeEntityType) diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index 534effb606e..bd927bd2906 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -220,6 +220,54 @@ public class ComplexThing #endregion 36837 + #region 37162 + + [ConditionalFact] + public virtual async Task Non_optional_complex_type_with_all_nullable_properties() + { + var contextFactory = await InitializeAsync( + seed: context => + { + context.Add( + new Context37162.EntityType + { + NonOptionalComplexType = new Context37162.ComplexTypeWithAllNulls + { + // All properties are null + } + }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateContext(); + + var entity = await context.Set().SingleAsync(); + + Assert.NotNull(entity.NonOptionalComplexType); + Assert.Null(entity.NonOptionalComplexType.NullableString); + Assert.Null(entity.NonOptionalComplexType.NullableDateTime); + } + + private class Context37162(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().ComplexProperty(b => b.NonOptionalComplexType); + + public class EntityType + { + public int Id { get; set; } + public ComplexTypeWithAllNulls NonOptionalComplexType { get; set; } = null!; + } + + public class ComplexTypeWithAllNulls + { + public string? NullableString { get; set; } + public DateTime? NullableDateTime { get; set; } + } + } + + #endregion 37162 + protected override string StoreName => "AdHocComplexTypeQueryTest"; } diff --git a/test/EFCore.Tests/Query/EntityMaterializerSourceTest.cs b/test/EFCore.Tests/Query/EntityMaterializerSourceTest.cs index 1b50819ea15..a05b3e4ba17 100644 --- a/test/EFCore.Tests/Query/EntityMaterializerSourceTest.cs +++ b/test/EFCore.Tests/Query/EntityMaterializerSourceTest.cs @@ -26,7 +26,7 @@ public void Throws_for_abstract_types() CoreStrings.CannotMaterializeAbstractType(nameof(SomeAbstractEntity)), Assert.Throws( () => source.CreateMaterializeExpression( - new StructuralTypeMaterializerSourceParameters((IEntityType)entityType, "", entityType.ClrType, null), null!)) + new StructuralTypeMaterializerSourceParameters((IEntityType)entityType, "", entityType.ClrType, IsNullable: false, null), null!)) .Message); } @@ -547,7 +547,7 @@ public virtual Func GetMaterializer( IReadOnlyEntityType entityType) => Expression.Lambda>( source.CreateMaterializeExpression( - new StructuralTypeMaterializerSourceParameters((IEntityType)entityType, "instance", entityType.ClrType, null), + new StructuralTypeMaterializerSourceParameters((IEntityType)entityType, "instance", entityType.ClrType, IsNullable: false, null), _contextParameter), _contextParameter) .Compile();