Skip to content

Commit 041a704

Browse files
committed
Optional complex types.
1 parent f404f41 commit 041a704

File tree

36 files changed

+691
-301
lines changed

36 files changed

+691
-301
lines changed

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Lines changed: 0 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,6 @@
130130
<data name="CannotChangeWhenOpen" xml:space="preserve">
131131
<value>The instance of DbConnection is currently in use. The connection can only be changed when the existing connection is not being used.</value>
132132
</data>
133-
<data name="CannotCompareComplexTypeToNull" xml:space="preserve">
134-
<value>Comparing complex types to null is not supported.</value>
135-
</data>
136-
<data name="CannotProjectNullableComplexType" xml:space="preserve">
137-
<value>You are attempting to project out complex type '{complexType}' via an optional navigation; that is currently not supported. Either project out the complex type in a non-optional context, or project the containing entity type along with the complex type.</value>
138-
</data>
139133
<data name="CannotSetAliasOnJoin" xml:space="preserve">
140134
<value>Join expressions have no aliases; set the alias on the enclosed table expression.</value>
141135
</data>
Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
45
namespace Microsoft.EntityFrameworkCore.Query.Internal;
56

6-
#pragma warning disable EF1001 // EntityMaterializerSource is pubternal
7+
#pragma warning disable EF1001 // StructuralTypeMaterializerSource is pubternal
78

89
/// <summary>
910
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -15,39 +16,9 @@ public class RelationalStructuralTypeMaterializerSource(StructuralTypeMaterializ
1516
: StructuralTypeMaterializerSource(dependencies)
1617
{
1718
/// <summary>
18-
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
19-
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
20-
/// any release. You should only use it directly in your code with extreme caution and knowing that
21-
/// doing so can result in application failures when updating to a new Entity Framework Core release.
22-
/// </summary>
23-
protected override void AddInitializeExpression(
24-
IPropertyBase property,
25-
ParameterBindingInfo bindingInfo,
26-
Expression instanceVariable,
27-
MethodCallExpression valueBufferExpression,
28-
List<Expression> blockExpressions)
29-
{
30-
// JSON complex properties are not handled in the initial materialization expression, since they're not
31-
// simply e.g. DbDataReader.GetFieldValue<>() calls. So they're handled afterwards in the shaper, and need
32-
// to be skipped here.
33-
if (property is IComplexProperty { ComplexType: var complexType } && complexType.IsMappedToJson())
34-
{
35-
return;
36-
}
37-
38-
base.AddInitializeExpression(property, bindingInfo, instanceVariable, valueBufferExpression, blockExpressions);
39-
}
40-
41-
/// <summary>
42-
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
43-
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
44-
/// any release. You should only use it directly in your code with extreme caution and knowing that
45-
/// doing so can result in application failures when updating to a new Entity Framework Core release.
19+
/// JSON complex properties are not handled in the initial materialization expression,
20+
/// since they're not simply e.g. DbDataReader.GetFieldValue calls.
21+
/// So they're handled afterwards in the shaper, and need to be skipped.
4622
/// </summary>
47-
[EntityFrameworkInternal]
48-
public static readonly MethodInfo MaterializeJsonComplexTypeMethod
49-
= typeof(RelationalStructuralTypeMaterializerSource).GetTypeInfo().GetDeclaredMethod(nameof(MaterializeJsonComplexType))!;
50-
51-
private static T MaterializeJsonComplexType<T>(in ValueBuffer valueBuffer, IComplexProperty complexProperty)
52-
=> throw new UnreachableException();
23+
protected override bool ReadComplexTypeDirectly(IComplexType complexType) => !complexType.IsMappedToJson();
5324
}

src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.StructuralEquality.cs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -311,15 +311,6 @@ bool TryRewriteComplexTypeEquality(bool collection, [NotNullWhen(true)] out SqlE
311311

312312
Check.DebugAssert(complexType != null, "We checked that at least one side is a complex type before calling this function");
313313

314-
// Comparison to null needs to be handled in a special way for table splitting, but for JSON mapping is handled via
315-
// the regular JSON flow below.
316-
if ((IsNullSqlConstantExpression(left) || IsNullSqlConstantExpression(right)) && !complexType.IsMappedToJson())
317-
{
318-
// TODO: when we support optional complex types with table splitting - or projecting required complex types via optional
319-
// navigations - we'll be able to translate this, #31376
320-
throw new InvalidOperationException(RelationalStrings.CannotCompareComplexTypeToNull);
321-
}
322-
323314
// If a complex type is the result of a subquery, then comparing its columns would mean duplicating the subquery, which would
324315
// be potentially very inefficient.
325316
// TODO: Enable this by extracting the subquery out to a common table expressions (WITH), #31237

src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -492,12 +492,6 @@ public void ApplyProjection()
492492

493493
void AddStructuralTypeProjection(StructuralTypeProjectionExpression projection)
494494
{
495-
if (_projection.Count == 0
496-
&& projection is { StructuralType: IComplexType complexType, IsNullable: true })
497-
{
498-
throw new InvalidOperationException(RelationalStrings.CannotProjectNullableComplexType(complexType.DisplayName()));
499-
}
500-
501495
ProcessTypeProjection(projection);
502496

503497
void ProcessTypeProjection(StructuralTypeProjectionExpression projection)
@@ -1367,11 +1361,6 @@ Expression CopyProjectionToOuter(SelectExpression innerSelectExpression, Express
13671361

13681362
ConstantExpression AddStructuralTypeProjection(StructuralTypeProjectionExpression projection)
13691363
{
1370-
if (projection is { StructuralType: IComplexType complexType, IsNullable: true })
1371-
{
1372-
throw new InvalidOperationException(RelationalStrings.CannotProjectNullableComplexType(complexType.DisplayName()));
1373-
}
1374-
13751364
// JSON entity that had some query operations applied on it - it has been converted to a query root via OPENJSON/json_each
13761365
// so it requires different materialization path than regular entity
13771366
// e.g. we need to also add all the child navigations, JSON entity builds all the includes as part of it's own materializer

src/EFCore/Query/EntityMaterializerSourceParameters.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ namespace Microsoft.EntityFrameworkCore.Query;
88
/// </summary>
99
/// <param name="StructuralType">The entity or complex type being materialized.</param>
1010
/// <param name="InstanceName">The name of the instance being materialized.</param>
11+
/// <param name="AllowNullable">Whether nullable result is allowed.</param>
1112
/// <param name="QueryTrackingBehavior">
1213
/// The query tracking behavior, or <see langword="null" /> if this materialization is not from a query.
1314
/// </param>
1415
public readonly record struct StructuralTypeMaterializerSourceParameters(
1516
ITypeBase StructuralType,
1617
string InstanceName,
18+
bool? AllowNullable,
1719
QueryTrackingBehavior? QueryTrackingBehavior);
1820

1921
/// <summary>

src/EFCore/Query/Internal/StructuralTypeMaterializerSource.cs

Lines changed: 107 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Reflection.Metadata.Ecma335;
45
using Microsoft.EntityFrameworkCore.Diagnostics.Internal;
56
using Microsoft.EntityFrameworkCore.Internal;
7+
using Microsoft.EntityFrameworkCore.Metadata;
8+
using Microsoft.EntityFrameworkCore.Metadata.Internal;
69
using static System.Linq.Expressions.Expression;
710

811
namespace Microsoft.EntityFrameworkCore.Query.Internal;
@@ -95,28 +98,46 @@ public Expression CreateMaterializeExpression(
9598
properties.Remove(consumedProperty);
9699
}
97100

98-
var constructorExpression = constructorBinding.CreateConstructorExpression(bindingInfo);
101+
var materializationExpression = HandleMaterializationInterception();
99102

100-
if (_materializationInterceptor == null
101-
// TODO: This currently applies the materialization interceptor only on the root structural type - any contained complex types
102-
// don't get intercepted. #35883
103-
|| structuralType is not IEntityType)
103+
return
104+
structuralType is IComplexType complexType && ReadComplexTypeDirectly(complexType)
105+
&& (IsNullable(complexType) || parameters.AllowNullable == true)
106+
? HandleNullableComplexTypeMaterialization(
107+
complexType,
108+
complexType.ClrType,
109+
materializationExpression,
110+
bindingInfo)
111+
: materializationExpression;
112+
113+
Expression HandleMaterializationInterception()
104114
{
105-
return properties.Count == 0 && blockExpressions.Count == 0
106-
? constructorExpression
107-
: CreateMaterializeExpression(blockExpressions, instanceVariable, constructorExpression, properties, bindingInfo);
115+
var constructorExpression = constructorBinding.CreateConstructorExpression(bindingInfo);
116+
117+
return _materializationInterceptor == null
118+
// TODO: This currently applies the materialization interceptor only on the root structural type - any contained complex types
119+
// don't get intercepted. #35883
120+
|| structuralType is not IEntityType
121+
? properties.Count == 0 && blockExpressions.Count == 0
122+
? constructorExpression
123+
: CreateMaterializeExpression(blockExpressions, instanceVariable, constructorExpression, properties, bindingInfo)
124+
: CreateInterceptionMaterializeExpression(
125+
structuralType,
126+
properties,
127+
_materializationInterceptor,
128+
bindingInfo,
129+
constructorExpression,
130+
instanceVariable,
131+
blockExpressions);
108132
}
109-
110-
return CreateInterceptionMaterializeExpression(
111-
structuralType,
112-
properties,
113-
_materializationInterceptor,
114-
bindingInfo,
115-
constructorExpression,
116-
instanceVariable,
117-
blockExpressions);
118133
}
119134

135+
/// <summary>
136+
/// Should complex type be read directly using e.g. DbDataReader.GetFieldValue
137+
/// or is it going to be handled separately (i.e. relational JSON).
138+
/// </summary>
139+
protected virtual bool ReadComplexTypeDirectly(IComplexType complexType) => true;
140+
120141
/// <summary>
121142
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
122143
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -130,11 +151,17 @@ protected virtual void AddInitializeExpression(
130151
MethodCallExpression valueBufferExpression,
131152
List<Expression> blockExpressions)
132153
{
154+
if (property is IComplexProperty cp && !ReadComplexTypeDirectly(cp.ComplexType))
155+
{
156+
return;
157+
}
158+
133159
var memberInfo = property.GetMemberInfo(forMaterialization: true, forSet: true);
134160

135161
var valueExpression = property switch
136162
{
137-
IProperty p => valueBufferExpression.CreateValueBufferReadValueExpression(memberInfo.GetMemberType(), p.GetIndex(), p),
163+
IProperty p
164+
=> valueBufferExpression.CreateValueBufferReadValueExpression(memberInfo.GetMemberType(), p.GetIndex(), p),
138165

139166
IServiceProperty serviceProperty
140167
=> serviceProperty.ParameterBinding.BindToParameter(bindingInfo),
@@ -143,9 +170,7 @@ IServiceProperty serviceProperty
143170
=> Default(complexProperty.ClrType), // Initialize collections to null, they'll be populated separately
144171

145172
IComplexProperty complexProperty
146-
=> CreateMaterializeExpression(
147-
new StructuralTypeMaterializerSourceParameters(complexProperty.ComplexType, "complexType", QueryTrackingBehavior: null),
148-
bindingInfo.MaterializationContextExpression),
173+
=> CreateComplexTypeMaterializeExpression(complexProperty, bindingInfo),
149174

150175
_ => throw new UnreachableException()
151176
};
@@ -193,6 +218,21 @@ static Expression CreateMemberAssignment(Expression parameter, MemberInfo member
193218
value)
194219
: MakeMemberAccess(parameter, memberInfo).Assign(value);
195220
}
221+
222+
Expression CreateComplexTypeMaterializeExpression(IComplexProperty complexProperty, ParameterBindingInfo bindingInfo)
223+
{
224+
var materializeExpression = CreateMaterializeExpression(
225+
new StructuralTypeMaterializerSourceParameters(complexProperty.ComplexType, "complexType", null, QueryTrackingBehavior: null),
226+
bindingInfo.MaterializationContextExpression);
227+
228+
return IsNullable(complexProperty)
229+
? HandleNullableComplexTypeMaterialization(
230+
complexProperty.ComplexType,
231+
complexProperty.ClrType,
232+
materializeExpression,
233+
bindingInfo)
234+
: materializeExpression;
235+
}
196236
}
197237

198238
private void AddInitializeExpressions(
@@ -493,15 +533,14 @@ BlockExpression CreateInitializeExpression()
493533
/// any release. You should only use it directly in your code with extreme caution and knowing that
494534
/// doing so can result in application failures when updating to a new Entity Framework Core release.
495535
/// </summary>
496-
public virtual Func<MaterializationContext, object> GetMaterializer(
497-
IEntityType entityType)
536+
public virtual Func<MaterializationContext, object> GetMaterializer(IEntityType entityType)
498537
{
499538
var materializationContextParameter
500539
= Parameter(typeof(MaterializationContext), "materializationContext");
501540

502541
return Lambda<Func<MaterializationContext, object>>(
503542
((IStructuralTypeMaterializerSource)this).CreateMaterializeExpression(
504-
new StructuralTypeMaterializerSourceParameters(entityType, "instance", null), materializationContextParameter),
543+
new StructuralTypeMaterializerSourceParameters(entityType, "instance", null, null), materializationContextParameter),
505544
materializationContextParameter)
506545
.Compile();
507546
}
@@ -518,7 +557,7 @@ public virtual Func<MaterializationContext, object> GetMaterializer(IComplexType
518557

519558
return Lambda<Func<MaterializationContext, object>>(
520559
((IStructuralTypeMaterializerSource)this).CreateMaterializeExpression(
521-
new StructuralTypeMaterializerSourceParameters(complexType, "instance", null), materializationContextParameter),
560+
new StructuralTypeMaterializerSourceParameters(complexType, "instance", null, null), materializationContextParameter),
522561
materializationContextParameter)
523562
.Compile();
524563
}
@@ -572,7 +611,7 @@ public virtual Func<MaterializationContext, object> GetEmptyMaterializer(
572611

573612
var materializationContextExpression = Parameter(typeof(MaterializationContext), "mc");
574613
var bindingInfo = new ParameterBindingInfo(
575-
new StructuralTypeMaterializerSourceParameters(entityType, "instance", null), materializationContextExpression);
614+
new StructuralTypeMaterializerSourceParameters(entityType, "instance", null, null), materializationContextExpression);
576615

577616
var blockExpressions = new List<Expression>();
578617
var instanceVariable = Variable(binding.RuntimeType, "instance");
@@ -644,4 +683,46 @@ private static void CreateServiceInstances(
644683
}
645684
}
646685
}
686+
687+
private Expression HandleNullableComplexTypeMaterialization(IComplexType complexType, Type clrType, Expression materializeExpression, ParameterBindingInfo bindingInfo)
688+
{
689+
var valueBufferExpression = Call(
690+
bindingInfo.MaterializationContextExpression,
691+
MaterializationContext.GetValueBufferMethod);
692+
693+
// Get all scalar properties of the complex type (including nested ones).
694+
var allScalarProperties = complexType.GetFlattenedProperties()
695+
.Where(p => !p.IsShadowProperty()).ToList();
696+
697+
if (allScalarProperties is [])
698+
{
699+
// If no scalar properties, just create the instance.
700+
return CreateMaterializeExpression(
701+
new StructuralTypeMaterializerSourceParameters(complexType, "complexType", null, QueryTrackingBehavior: null),
702+
bindingInfo.MaterializationContextExpression);
703+
}
704+
705+
// Create null checks for all scalar properties using direct ValueBuffer access.
706+
var nullChecks = allScalarProperties.Select(p =>
707+
Equal(
708+
valueBufferExpression.CreateValueBufferReadValueExpression(typeof(object), p.GetIndex(), p),
709+
Constant(null, typeof(object))));
710+
711+
// Combine all null checks with AndAlso.
712+
var allNullCondition = nullChecks.Aggregate(AndAlso);
713+
714+
// If all properties are null, return null; otherwise materialize the complex type.
715+
return Condition(
716+
allNullCondition,
717+
Default(clrType),
718+
materializeExpression);
719+
}
720+
721+
private static bool IsNullable(IComplexType complexType)
722+
=> IsNullable(complexType.ComplexProperty);
723+
724+
private static bool IsNullable(IComplexProperty complexProperty)
725+
=> complexProperty.IsNullable
726+
|| (complexProperty.DeclaringType is IComplexType complexType
727+
&& IsNullable(complexType.ComplexProperty));
647728
}

0 commit comments

Comments
 (0)