diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index 92736474a3c..68a703e8559 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -14,6 +14,9 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor { private sealed partial class ShaperProcessingExpressionVisitor : ExpressionVisitor { + private static readonly bool UseOldBehavior30028 + = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue30028", out var enabled30028) && enabled30028; + // Reading database values private static readonly MethodInfo IsDbNullMethod = typeof(DbDataReader).GetRuntimeMethod(nameof(DbDataReader.IsDBNull), new[] { typeof(int) })!; @@ -34,6 +37,9 @@ private static readonly MethodInfo CollectionAccessorAddMethodInfo private static readonly MethodInfo JsonElementGetPropertyMethod = typeof(JsonElement).GetMethod(nameof(JsonElement.GetProperty), new[] { typeof(string) })!; + private static readonly MethodInfo JsonElementTryGetPropertyMethod + = typeof(JsonElement).GetMethod(nameof(JsonElement.TryGetProperty), new[] { typeof(string), typeof(JsonElement).MakeByRefType() })!; + private static readonly PropertyInfo _objectArrayIndexerPropertyInfo = typeof(object[]).GetProperty("Item")!; @@ -1112,17 +1118,43 @@ private Expression CreateJsonShapers( shaperBlockVariables.Add(innerJsonElementParameter); - // TODO: do TryGetProperty and short circuit if failed instead - var innerJsonElementAssignment = Expression.Assign( - innerJsonElementParameter, - Expression.Convert( - Expression.Call( - jsonElementShaperLambdaParameter, - JsonElementGetPropertyMethod, - Expression.Constant(ownedNavigation.TargetEntityType.GetJsonPropertyName())), - typeof(JsonElement?))); + if (UseOldBehavior30028) + { + var innerJsonElementAssignment = Expression.Assign( + innerJsonElementParameter, + Expression.Convert( + Expression.Call( + jsonElementShaperLambdaParameter, + JsonElementGetPropertyMethod, + Expression.Constant(ownedNavigation.TargetEntityType.GetJsonPropertyName())), + typeof(JsonElement?))); - shaperBlockExpressions.Add(innerJsonElementAssignment); + shaperBlockExpressions.Add(innerJsonElementAssignment); + } + else + { + // JsonElement temp; + // JsonElement? innerJsonElement = jsonElement.TryGetProperty("PropertyName", temp) + // ? (JsonElement?)temp + // : null; + var tempParameter = Expression.Variable(typeof(JsonElement)); + shaperBlockVariables.Add(tempParameter); + + var innerJsonElementAssignment = Expression.Assign( + innerJsonElementParameter, + Expression.Condition( + Expression.Call( + jsonElementShaperLambdaParameter, + JsonElementTryGetPropertyMethod, + Expression.Constant(ownedNavigation.TargetEntityType.GetJsonPropertyName()), + tempParameter), + Expression.Convert( + tempParameter, + typeof(JsonElement?)), + Expression.Constant(null, typeof(JsonElement?)))); + + shaperBlockExpressions.Add(innerJsonElementAssignment); + } var innerShaperResult = CreateJsonShapers( ownedNavigation.TargetEntityType, @@ -1288,37 +1320,86 @@ private Expression CreateJsonShapers( var jsonElementVariable = Expression.Variable( typeof(JsonElement?)); - var jsonElementValueExpression = index == 0 - ? CreateGetValueExpression( - _dataReaderParameter, - jsonColumnProjectionIndex, - nullable: true, - jsonColumnTypeMapping, - typeof(JsonElement?), - property: null) - : Expression.Condition( - Expression.MakeMemberAccess( - currentJsonElementVariable!, - _nullableJsonElementHasValuePropertyInfo), - Expression.Convert( - Expression.Call( + if (UseOldBehavior30028) + { + var jsonElementValueExpression = index == 0 + ? CreateGetValueExpression( + _dataReaderParameter, + jsonColumnProjectionIndex, + nullable: true, + jsonColumnTypeMapping, + typeof(JsonElement?), + property: null) + : Expression.Condition( + Expression.MakeMemberAccess( + currentJsonElementVariable!, + _nullableJsonElementHasValuePropertyInfo), + Expression.Convert( + Expression.Call( + Expression.MakeMemberAccess( + currentJsonElementVariable!, + _nullableJsonElementValuePropertyInfo), + JsonElementGetPropertyMethod, + Expression.Constant(additionalPath[index - 1])), + currentJsonElementVariable!.Type), + Expression.Default(currentJsonElementVariable!.Type)); + + var jsonElementAssignment = Expression.Assign( + jsonElementVariable, + jsonElementValueExpression); + + _variables.Add(jsonElementVariable); + _expressions.Add(jsonElementAssignment); + _existingJsonElementMap[(jsonColumnProjectionIndex, additionalPath[..index])] = jsonElementVariable; + + currentJsonElementVariable = jsonElementVariable; + } + else + { + Expression jsonElementValueExpression; + if (index == 0) + { + jsonElementValueExpression = CreateGetValueExpression( + _dataReaderParameter, + jsonColumnProjectionIndex, + nullable: true, + jsonColumnTypeMapping, + typeof(JsonElement?), + property: null); + } + else + { + var tempParameter = Expression.Variable(typeof(JsonElement)); + _variables.Add(tempParameter); + + var tryGetPropertyCall = Expression.Call( + Expression.MakeMemberAccess( + currentJsonElementVariable!, + _nullableJsonElementValuePropertyInfo), + JsonElementTryGetPropertyMethod, + Expression.Constant(additionalPath[index - 1]), + tempParameter); + + jsonElementValueExpression = Expression.Condition( + Expression.AndAlso( Expression.MakeMemberAccess( currentJsonElementVariable!, - _nullableJsonElementValuePropertyInfo), - JsonElementGetPropertyMethod, - Expression.Constant(additionalPath[index - 1])), - currentJsonElementVariable!.Type), - Expression.Default(currentJsonElementVariable!.Type)); + _nullableJsonElementHasValuePropertyInfo), + tryGetPropertyCall), + Expression.Convert(tempParameter, typeof(JsonElement?)), + Expression.Constant(null, typeof(JsonElement?))); + } - var jsonElementAssignment = Expression.Assign( - jsonElementVariable, - jsonElementValueExpression); + var jsonElementAssignment = Expression.Assign( + jsonElementVariable, + jsonElementValueExpression); - _variables.Add(jsonElementVariable); - _expressions.Add(jsonElementAssignment); - _existingJsonElementMap[(jsonColumnProjectionIndex, additionalPath[..index])] = jsonElementVariable; + _variables.Add(jsonElementVariable); + _expressions.Add(jsonElementAssignment); + _existingJsonElementMap[(jsonColumnProjectionIndex, additionalPath[..index])] = jsonElementVariable; - currentJsonElementVariable = jsonElementVariable; + currentJsonElementVariable = jsonElementVariable; + } } else { diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs index ae2442bd052..ffa9d52363f 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryAdHocTestBase.cs @@ -83,4 +83,129 @@ public class MyJsonEntity29219 public int NonNullableScalar { get; set; } public int? NullableScalar { get; set; } } + + protected abstract void Seed30028(MyContext30028 ctx); + + protected class MyContext30028 : DbContext + { + public MyContext30028(DbContextOptions options) + : base(options) + { + } + + public DbSet Entities { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne(x => x.Json, nb => + { + nb.ToJson(); + nb.OwnsMany(x => x.Collection, nnb => nnb.OwnsOne(x => x.Nested)); + nb.OwnsOne(x => x.OptionalReference, nnb => nnb.OwnsOne(x => x.Nested)); + nb.OwnsOne(x => x.RequiredReference, nnb => nnb.OwnsOne(x => x.Nested)); + nb.Navigation(x => x.RequiredReference).IsRequired(); + }); + }); + } + } + + public class MyEntity30028 + { + public int Id { get; set; } + public MyJsonRootEntity30028 Json { get; set; } + } + + public class MyJsonRootEntity30028 + { + public string RootName { get; set; } + public MyJsonBranchEntity30028 RequiredReference { get; set; } + public MyJsonBranchEntity30028 OptionalReference { get; set; } + public List Collection { get; set; } + } + + public class MyJsonBranchEntity30028 + { + public string BranchName { get; set; } + public MyJsonLeafEntity30028 Nested { get; set; } + } + + public class MyJsonLeafEntity30028 + { + public string LeafName { get; set; } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Accessing_missing_navigation_works(bool async) + { + var contextFactory = await InitializeAsync(seed: Seed30028); + using (var context = contextFactory.CreateContext()) + { + var result = context.Entities.OrderBy(x => x.Id).ToList(); + Assert.Equal(4, result.Count); + Assert.NotNull(result[0].Json.Collection); + Assert.NotNull(result[0].Json.OptionalReference); + Assert.NotNull(result[0].Json.RequiredReference); + + Assert.Null(result[1].Json.Collection); + Assert.NotNull(result[1].Json.OptionalReference); + Assert.NotNull(result[1].Json.RequiredReference); + + Assert.NotNull(result[2].Json.Collection); + Assert.Null(result[2].Json.OptionalReference); + Assert.NotNull(result[2].Json.RequiredReference); + + Assert.NotNull(result[3].Json.Collection); + Assert.NotNull(result[3].Json.OptionalReference); + Assert.Null(result[3].Json.RequiredReference); + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Missing_navigation_works_with_deduplication(bool async) + { + var contextFactory = await InitializeAsync(seed: Seed30028); + using (var context = contextFactory.CreateContext()) + { + var result = context.Entities.OrderBy(x => x.Id).Select(x => new + { + x, + x.Json, + x.Json.OptionalReference, + x.Json.RequiredReference, + NestedOptional = x.Json.OptionalReference.Nested, + NestedRequired = x.Json.RequiredReference.Nested, + x.Json.Collection, + }).ToList(); + + Assert.Equal(4, result.Count); + Assert.NotNull(result[0].OptionalReference); + Assert.NotNull(result[0].RequiredReference); + Assert.NotNull(result[0].NestedOptional); + Assert.NotNull(result[0].NestedRequired); + Assert.NotNull(result[0].Collection); + + Assert.NotNull(result[1].OptionalReference); + Assert.NotNull(result[1].RequiredReference); + Assert.NotNull(result[1].NestedOptional); + Assert.NotNull(result[1].NestedRequired); + Assert.Null(result[1].Collection); + + Assert.Null(result[2].OptionalReference); + Assert.NotNull(result[2].RequiredReference); + Assert.Null(result[2].NestedOptional); + Assert.NotNull(result[2].NestedRequired); + Assert.NotNull(result[2].Collection); + + Assert.NotNull(result[3].OptionalReference); + Assert.Null(result[3].RequiredReference); + Assert.NotNull(result[3].NestedOptional); + Assert.Null(result[3].NestedRequired); + Assert.NotNull(result[3].Collection); + } + } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryAdHocSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryAdHocSqlServerTest.cs index 1095b9c192a..571b281efb1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryAdHocSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryAdHocSqlServerTest.cs @@ -43,4 +43,31 @@ protected override void Seed29219(MyContext29219 ctx) ctx.Database.ExecuteSqlRaw(@"INSERT INTO [Entities] ([Id], [Reference], [Collection]) VALUES(3, N'{{ ""NonNullableScalar"" : 30 }}', N'[{{ ""NonNullableScalar"" : 10001 }}]')"); } + + protected override void Seed30028(MyContext30028 ctx) + { + // complete + ctx.Database.ExecuteSqlRaw(@"INSERT INTO [Entities] ([Id], [Json]) +VALUES( +1, +N'{{""RootName"":""e1"",""Collection"":[{{""BranchName"":""e1 c1"",""Nested"":{{""LeafName"":""e1 c1 l""}}}},{{""BranchName"":""e1 c2"",""Nested"":{{""LeafName"":""e1 c2 l""}}}}],""OptionalReference"":{{""BranchName"":""e1 or"",""Nested"":{{""LeafName"":""e1 or l""}}}},""RequiredReference"":{{""BranchName"":""e1 rr"",""Nested"":{{""LeafName"":""e1 rr l""}}}}}}')"); + + // missing collection + ctx.Database.ExecuteSqlRaw(@"INSERT INTO [Entities] ([Id], [Json]) +VALUES( +2, +N'{{""RootName"":""e2"",""OptionalReference"":{{""BranchName"":""e2 or"",""Nested"":{{""LeafName"":""e2 or l""}}}},""RequiredReference"":{{""BranchName"":""e2 rr"",""Nested"":{{""LeafName"":""e2 rr l""}}}}}}')"); + + // missing optional reference + ctx.Database.ExecuteSqlRaw(@"INSERT INTO [Entities] ([Id], [Json]) +VALUES( +3, +N'{{""RootName"":""e3"",""Collection"":[{{""BranchName"":""e3 c1"",""Nested"":{{""LeafName"":""e3 c1 l""}}}},{{""BranchName"":""e3 c2"",""Nested"":{{""LeafName"":""e3 c2 l""}}}}],""RequiredReference"":{{""BranchName"":""e3 rr"",""Nested"":{{""LeafName"":""e3 rr l""}}}}}}')"); + + // missing required reference + ctx.Database.ExecuteSqlRaw(@"INSERT INTO [Entities] ([Id], [Json]) +VALUES( +4, +N'{{""RootName"":""e4"",""Collection"":[{{""BranchName"":""e4 c1"",""Nested"":{{""LeafName"":""e4 c1 l""}}}},{{""BranchName"":""e4 c2"",""Nested"":{{""LeafName"":""e4 c2 l""}}}}],""OptionalReference"":{{""BranchName"":""e4 or"",""Nested"":{{""LeafName"":""e4 or l""}}}}}}')"); + } }