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();