Skip to content

Commit cd381e7

Browse files
committed
Fixes to complex values types
Part of dotnet#31376 and dotnet#36296 Continues dotnet#36557
1 parent edcb205 commit cd381e7

18 files changed

+390
-92
lines changed

src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -320,8 +320,13 @@ protected override Expression VisitExtension(Expression extensionExpression)
320320

321321
_projectionMapping[_projectionMembers.Peek()] = jsonQueryExpression;
322322

323+
#pragma warning disable EF1001
323324
return shaper.Update(
324-
new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), typeof(ValueBuffer)));
325+
new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), typeof(ValueBuffer)))
326+
// This is to handle have correct type for the shaper expression. It is later fixed in MatchTypes.
327+
// This mirrors for structural types what we do for scalars.
328+
.MakeClrTypeNullable();
329+
#pragma warning restore EF1001
325330
}
326331

327332
if (shaper.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression)
@@ -342,13 +347,23 @@ protected override Expression VisitExtension(Expression extensionExpression)
342347
{
343348
var projectionBinding = AddClientProjection(jsonQuery, typeof(ValueBuffer));
344349

345-
return shaper.Update(projectionBinding);
350+
#pragma warning disable EF1001
351+
return shaper.Update(projectionBinding)
352+
// This is to handle have correct type for the shaper expression. It is later fixed in MatchTypes.
353+
// This mirrors for structural types what we do for scalars.
354+
.MakeClrTypeNullable();
355+
#pragma warning restore EF1001
346356
}
347357

348358
_projectionMapping[_projectionMembers.Peek()] = jsonQuery;
349359

360+
#pragma warning disable EF1001
350361
return shaper.Update(
351-
new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), typeof(ValueBuffer)));
362+
new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), typeof(ValueBuffer)))
363+
// This is to handle have correct type for the shaper expression. It is later fixed in MatchTypes.
364+
// This mirrors for structural types what we do for scalars.
365+
.MakeClrTypeNullable();
366+
#pragma warning restore EF1001
352367
}
353368

354369
projection = (StructuralTypeProjectionExpression)projection2;
@@ -366,14 +381,19 @@ protected override Expression VisitExtension(Expression extensionExpression)
366381
_projectionBindingCache[projection] = entityProjectionBinding;
367382
}
368383

369-
return shaper.Update(entityProjectionBinding);
384+
#pragma warning disable EF1001
385+
return shaper.Update(entityProjectionBinding)
386+
// This is to handle have correct type for the shaper expression. It is later fixed in MatchTypes.
387+
// This mirrors for structural types what we do for scalars.
388+
.MakeClrTypeNullable();
389+
#pragma warning restore EF1001
370390
}
371391

372392
_projectionMapping[_projectionMembers.Peek()] = projection;
373393

394+
#pragma warning disable EF1001
374395
return shaper
375396
.Update(new ProjectionBindingExpression(_selectExpression, _projectionMembers.Peek(), typeof(ValueBuffer)))
376-
#pragma warning disable EF1001
377397
// This is to handle have correct type for the shaper expression. It is later fixed in MatchTypes.
378398
// This mirrors for structural types what we do for scalars.
379399
.MakeClrTypeNullable();

src/EFCore.Relational/Query/Internal/Translators/NullableMemberTranslator.cs

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/EFCore.Relational/Query/RelationalMemberTranslatorProvider.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
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 Microsoft.EntityFrameworkCore.Query.Internal;
54
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
65

76
namespace Microsoft.EntityFrameworkCore.Query;
@@ -21,7 +20,6 @@ public RelationalMemberTranslatorProvider(RelationalMemberTranslatorProviderDepe
2120
Dependencies = dependencies;
2221

2322
_plugins.AddRange(dependencies.Plugins.SelectMany(p => p.Translators));
24-
_translators.AddRange([new NullableMemberTranslator(dependencies.SqlExpressionFactory)]);
2523
}
2624

2725
/// <summary>

src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
628628

629629
var shaperResult = CreateJsonShapers(
630630
shaper.StructuralType,
631+
shaper.Type,
631632
shaper.IsNullable,
632633
jsonReaderDataVariable,
633634
keyValuesParameter,
@@ -674,8 +675,10 @@ protected override Expression VisitExtension(Expression extensionExpression)
674675
childProjectionInfo.Navigation.TargetEntityType,
675676
childProjectionInfo.Navigation.IsCollection);
676677

678+
var targetEntityType = childProjectionInfo.Navigation.TargetEntityType;
677679
var shaperResult = CreateJsonShapers(
678-
childProjectionInfo.Navigation.TargetEntityType,
680+
targetEntityType,
681+
targetEntityType.ClrType,
679682
nullable: true,
680683
jsonReaderDataVariable,
681684
keyValuesParameter,
@@ -784,6 +787,7 @@ when GetProjectionIndex(projectionBindingExpression) is JsonProjectionInfo jsonP
784787

785788
var shaperResult = CreateJsonShapers(
786789
relatedStructuralType,
790+
relationship.ClrType,
787791
nullable: true,
788792
jsonReaderDataVariable,
789793
keyValuesParameter,
@@ -1151,8 +1155,10 @@ when GetProjectionIndex(projectionBindingExpression) is JsonProjectionInfo jsonP
11511155
includeExpression.Navigation.TargetEntityType,
11521156
includeExpression.Navigation.IsCollection);
11531157

1158+
var targetEntityType = includeExpression.Navigation.TargetEntityType;
11541159
var shaperResult = CreateJsonShapers(
1155-
includeExpression.Navigation.TargetEntityType,
1160+
targetEntityType,
1161+
targetEntityType.ClrType,
11561162
nullable: true,
11571163
jsonReaderDataVariable,
11581164
keyValuesParameter,
@@ -1563,6 +1569,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
15631569

15641570
private Expression CreateJsonShapers(
15651571
ITypeBase structuralType,
1572+
Type clrType,
15661573
bool nullable,
15671574
ParameterExpression jsonReaderDataParameter,
15681575
Expression? keyValuesParameter,
@@ -1628,6 +1635,7 @@ private Expression CreateJsonShapers(
16281635

16291636
var innerShaper = CreateJsonShapers(
16301637
relatedStructuralType,
1638+
nestedRelationship.ClrType,
16311639
nullable || isRelationshipNullable,
16321640
jsonReaderDataShaperLambdaParameter,
16331641
keyValuesShaperLambdaParameter,
@@ -1847,15 +1855,13 @@ private Expression CreateJsonShapers(
18471855
return materializeJsonEntityCollectionMethodCall;
18481856
}
18491857

1850-
18511858
// Return the materializer for this JSON object, including null checks which would return null.
18521859
MethodInfo method;
18531860

1854-
if (relationship is not null && Nullable.GetUnderlyingType(relationship.ClrType) is { } underlyingType)
1861+
if (Nullable.GetUnderlyingType(clrType) is { } underlyingType)
18551862
{
1856-
// The association property into which we're assigning has a nullable value type, so generate
1857-
// a materializer that returns that nullable value type (note that the shaperLambda that
1858-
// we pass itself always returns a non-nullable value (the null checks are outside of it.))
1863+
// We need to project out a nullable value type. Note that the shaperLambda that we pass itself always returns a
1864+
// non-nullable value (the null checks are outside of it.))
18591865
Check.DebugAssert(nullable, "On non-nullable relationship but the relationship's ClrType is Nullable<T>");
18601866
Check.DebugAssert(underlyingType == structuralType.ClrType);
18611867

@@ -2538,6 +2544,7 @@ internal void ProcessTopLevelComplexJsonProperties(
25382544

25392545
var shaperResult = CreateJsonShapers(
25402546
complexType,
2547+
complexProperty.ClrType,
25412548
nullable: shaper.IsNullable || complexProperty.IsNullable,
25422549
jsonReaderDataVariable,
25432550
keyValuesParameter: null, // For owned entities only

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ SqlExpression Process(Expression expression)
369369
=> new JsonScalarExpression(
370370
jsonQuery.JsonColumn,
371371
jsonQuery.Path,
372-
jsonQuery.Type,
372+
jsonQuery.Type.UnwrapNullableType(),
373373
jsonQuery.JsonColumn.TypeMapping,
374374
jsonQuery.IsNullable),
375375

@@ -378,7 +378,7 @@ SqlExpression Process(Expression expression)
378378
=> new JsonScalarExpression(
379379
jsonQuery.JsonColumn,
380380
jsonQuery.Path,
381-
jsonQuery.Type,
381+
jsonQuery.Type.UnwrapNullableType(),
382382
jsonQuery.JsonColumn.TypeMapping,
383383
jsonQuery.IsNullable),
384384

src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -635,18 +635,44 @@ protected override Expression VisitMember(MemberExpression memberExpression)
635635
Expression.Condition(
636636
cond.Test,
637637
Expression.MakeMemberAccess(cond.IfTrue, memberExpression.Member),
638-
Expression.MakeMemberAccess(cond.IfFalse, memberExpression.Member)
639-
));
638+
Expression.MakeMemberAccess(cond.IfFalse, memberExpression.Member)));
640639
}
641640

642-
var innerExpression = Visit(memberExpression.Expression);
641+
var inner = Visit(memberExpression.Expression);
643642

644-
return TryBindMember(innerExpression, MemberIdentity.Create(memberExpression.Member), out var expression)
645-
? expression
646-
: (TranslationFailed(memberExpression.Expression, innerExpression, out var sqlInnerExpression)
643+
var member = memberExpression.Member;
644+
645+
// Try binding the member to a property on the structural type
646+
if (TryBindMember(inner, MemberIdentity.Create(member), out var expression))
647+
{
648+
return expression;
649+
}
650+
651+
// We handle translations for Nullable<> members here.
652+
// These can't be handled in regular IMemberTranslators, since those only support scalars (SqlExpressions);
653+
// but we also need to handle nullable value complex types.
654+
if (member.DeclaringType?.IsGenericType == true
655+
&& member.DeclaringType.GetGenericTypeDefinition() == typeof(Nullable<>)
656+
&& inner is not null)
657+
{
658+
switch (member.Name)
659+
{
660+
case nameof(Nullable<>.Value):
661+
return inner;
662+
case nameof(Nullable<>.HasValue) when inner is SqlExpression sqlInner:
663+
return _sqlExpressionFactory.IsNotNull(sqlInner);
664+
case nameof(Nullable<>.HasValue)
665+
when inner is StructuralTypeReferenceExpression
666+
&& TryRewriteStructuralTypeEquality(
667+
ExpressionType.NotEqual, inner, new SqlConstantExpression(null, memberExpression.Expression!.Type, null), equalsMethod: false, out var result):
668+
return result;
669+
}
670+
}
671+
672+
return (TranslationFailed(memberExpression.Expression, inner, out var sqlInnerExpression)
647673
? QueryCompilationContext.NotTranslatedExpression
648674
: Dependencies.MemberTranslatorProvider.Translate(
649-
sqlInnerExpression, memberExpression.Member, memberExpression.Type, _queryCompilationContext.Logger))
675+
sqlInnerExpression, member, memberExpression.Type, _queryCompilationContext.Logger))
650676
?? QueryCompilationContext.NotTranslatedExpression;
651677
}
652678

@@ -1692,7 +1718,7 @@ private static bool CanEvaluate(Expression expression)
16921718
private static bool IsNullSqlConstantExpression(Expression expression)
16931719
=> expression is SqlConstantExpression { Value: null };
16941720

1695-
[DebuggerStepThrough]
1721+
// [DebuggerStepThrough]
16961722
private static bool TranslationFailed(Expression? original, Expression? translation, out SqlExpression? castTranslation)
16971723
{
16981724
if (original != null

src/EFCore.Relational/Query/RelationalStructuralTypeShaperExpression.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,13 +187,13 @@ public override StructuralTypeShaperExpression Update(Expression valueBufferExpr
187187
: this;
188188

189189
/// <inheritdoc />
190-
public override StructuralTypeShaperExpression MakeClrTypeNullable()
190+
public override RelationalStructuralTypeShaperExpression MakeClrTypeNullable()
191191
=> Type != Type.MakeNullable()
192192
? new RelationalStructuralTypeShaperExpression(StructuralType, ValueBufferExpression, IsNullable, MaterializationCondition, Type.MakeNullable())
193193
: this;
194194

195195
/// <inheritdoc />
196-
public override StructuralTypeShaperExpression MakeClrTypeNonNullable()
196+
public override RelationalStructuralTypeShaperExpression MakeClrTypeNonNullable()
197197
=> Type != Type.UnwrapNullableType()
198198
? new RelationalStructuralTypeShaperExpression(StructuralType, ValueBufferExpression, IsNullable, MaterializationCondition, Type.UnwrapNullableType())
199199
: this;

test/EFCore.Relational.Specification.Tests/Query/Relationships/ComplexTableSplitting/ComplexTableSplittingProjectionRelationalTestBase.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +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 Xunit.Sdk;
4+
using Microsoft.EntityFrameworkCore.Query.Relationships.ComplexProperties;
55

66
namespace Microsoft.EntityFrameworkCore.Query.Relationships.ComplexTableSplitting;
77

8-
public abstract class ComplexTableSplittingProjectionRelationalTestBase<TFixture>
9-
: RelationshipsProjectionTestBase<TFixture>
8+
public abstract class ComplexTableSplittingProjectionRelationalTestBase<TFixture> : ComplexPropertiesProjectionTestBase<TFixture>
109
where TFixture : ComplexTableSplittingRelationalFixtureBase, new()
1110
{
1211
public ComplexTableSplittingProjectionRelationalTestBase(TFixture fixture, ITestOutputHelper testOutputHelper)

test/EFCore.Specification.Tests/Query/Relationships/ComplexProperties/ComplexPropertiesMiscellaneousTestBase.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,23 @@ namespace Microsoft.EntityFrameworkCore.Query.Relationships.ComplexProperties;
55

66
public abstract class ComplexPropertiesMiscellaneousTestBase<TFixture>(TFixture fixture)
77
: RelationshipsMiscellaneousTestBase<TFixture>(fixture)
8-
where TFixture : ComplexPropertiesFixtureBase, new();
8+
where TFixture : ComplexPropertiesFixtureBase, new()
9+
{
10+
#region Value types
11+
12+
[ConditionalFact]
13+
public virtual Task Where_property_on_non_nullable_value_type()
14+
=> AssertQuery(ss => ss.Set<ValueRootEntity>().Where(e => e.RequiredRelated.Int == 8));
15+
16+
[ConditionalFact]
17+
public virtual Task Where_property_on_nullable_value_type_Value()
18+
=> AssertQuery(
19+
ss => ss.Set<ValueRootEntity>().Where(e => e.OptionalRelated!.Value.Int == 8),
20+
ss => ss.Set<ValueRootEntity>().Where(e => e.OptionalRelated.HasValue && e.OptionalRelated!.Value.Int == 8));
21+
22+
[ConditionalFact]
23+
public virtual Task Where_HasValue_on_nullable_value_type()
24+
=> AssertQuery(ss => ss.Set<ValueRootEntity>().Where(e => e.OptionalRelated.HasValue));
25+
26+
#endregion Value types
27+
}

test/EFCore.Specification.Tests/Query/Relationships/ComplexProperties/ComplexPropertiesProjectionTestBase.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,32 @@ public virtual Task Select_root_with_value_types(QueryTrackingBehavior queryTrac
1616
ss => ss.Set<ValueRootEntity>(),
1717
queryTrackingBehavior: queryTrackingBehavior);
1818

19+
20+
[ConditionalTheory]
21+
[MemberData(nameof(TrackingData))]
22+
public virtual Task Select_non_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior)
23+
=> AssertQuery(
24+
ss => ss.Set<ValueRootEntity>().OrderBy(e => e.Id).Select(x => x.RequiredRelated),
25+
assertOrder: true,
26+
queryTrackingBehavior: queryTrackingBehavior);
27+
28+
29+
[ConditionalTheory]
30+
[MemberData(nameof(TrackingData))]
31+
public virtual Task Select_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior)
32+
=> AssertQuery(
33+
ss => ss.Set<ValueRootEntity>().OrderBy(e => e.Id).Select(x => x.OptionalRelated),
34+
assertOrder: true,
35+
queryTrackingBehavior: queryTrackingBehavior);
36+
37+
[ConditionalTheory]
38+
[MemberData(nameof(TrackingData))]
39+
public virtual Task Select_nullable_value_type_with_Value(QueryTrackingBehavior queryTrackingBehavior)
40+
=> AssertQuery(
41+
ss => ss.Set<ValueRootEntity>().OrderBy(e => e.Id).Select(x => x.OptionalRelated!.Value),
42+
ss => ss.Set<ValueRootEntity>().OrderBy(e => e.Id).Select(x => x.OptionalRelated == null ? default : x.OptionalRelated!.Value),
43+
assertOrder: true,
44+
queryTrackingBehavior: queryTrackingBehavior);
45+
1946
#endregion Value types
2047
}

0 commit comments

Comments
 (0)