From 923980938117f7a81fed6e3e52d1bbd861c21365 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 23 Oct 2025 13:47:35 +0200 Subject: [PATCH] Fix HasJsonPropertyName for complex JSON properties Fixes #37009 --- .../Query/JsonQueryExpression.cs | 5 +- ...sitor.ShaperProcessingExpressionVisitor.cs | 7 +- .../Query/SqlExpressions/SelectExpression.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- .../Query/AdHocJsonQueryRelationalTestBase.cs | 73 +++++++++++++++++++ 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs index b0168a5c1f1..286c31e13f1 100644 --- a/src/EFCore.Relational/Query/JsonQueryExpression.cs +++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs @@ -204,12 +204,11 @@ public virtual JsonQueryExpression BindStructuralProperty(IPropertyBase structur Check.DebugAssert(KeyPropertyMap is null); - var targetComplexType = complexProperty.ComplexType; var newPath = Path.ToList(); - newPath.Add(new PathSegment(targetComplexType.GetJsonPropertyName()!)); + newPath.Add(new PathSegment(complexProperty.GetJsonPropertyName()!)); return new JsonQueryExpression( - targetComplexType, + complexProperty.ComplexType, JsonColumn, keyPropertyMap: null, newPath, diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index e180d6f5856..5bd69b1add5 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs @@ -1628,10 +1628,10 @@ private Expression CreateJsonShapers( nestedStructuralProperty is not IComplexProperty { ComplexType: var complexType } || complexType.IsMappedToJson(), "Non-JSON complex type within JSON complex type"); - var (relatedStructuralType, inverseNavigation, isStructuralPropertyNullable) = nestedStructuralProperty switch + var (relatedStructuralType, navigationJsonPropertyName, inverseNavigation, isStructuralPropertyNullable) = nestedStructuralProperty switch { - INavigation n => ((ITypeBase)n.TargetEntityType, n.Inverse, !n.ForeignKey.IsRequiredDependent), - IComplexProperty cp => (cp.ComplexType, null, cp.IsNullable), + INavigation n => ((ITypeBase)n.TargetEntityType, n.TargetEntityType.GetJsonPropertyName()!, n.Inverse, !n.ForeignKey.IsRequiredDependent), + IComplexProperty cp => (cp.ComplexType, cp.GetJsonPropertyName()!, null, cp.IsNullable), _ => throw new UnreachableException() }; @@ -1645,7 +1645,6 @@ private Expression CreateJsonShapers( containerEntityExpression: null, nestedStructuralProperty); - var navigationJsonPropertyName = relatedStructuralType.GetJsonPropertyName()!; innerShapersMap[navigationJsonPropertyName] = innerShaper; if (nestedStructuralProperty.IsCollection) diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 2d57cfb6298..faffd086887 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -2895,7 +2895,7 @@ public static Expression GenerateComplexPropertyShaperExpression( // Otherwise, if the source type isn't mapped to JSON, we're just binding to an actual JSON column in a relational table, and not within it. var containerColumnExpression = complexProperty.DeclaringType.IsMappedToJson() ? new ColumnExpression( - complexType.GetJsonPropertyName() + complexProperty.GetJsonPropertyName() ?? throw new UnreachableException($"No JSON property name for complex property {complexProperty.Name}"), tableAlias, complexProperty.ClrType.UnwrapNullableType(), diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs index fee45469cba..64584da8329 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQueryableMethodTranslatingExpressionVisitor.cs @@ -291,7 +291,7 @@ IEntityType entityType .Select(n => n.TargetEntityType.GetJsonPropertyName() ?? throw new UnreachableException()), IComplexType complexType - => complexType.GetComplexProperties().Select(p => p.ComplexType.GetJsonPropertyName() ?? throw new UnreachableException()), + => complexType.GetComplexProperties().Select(p => p.GetJsonPropertyName() ?? throw new UnreachableException()), _ => throw new UnreachableException() }; diff --git a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs index a0bb693f889..31f6879552d 100644 --- a/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Sqlite.Core/Query/Internal/SqliteQueryableMethodTranslatingExpressionVisitor.cs @@ -422,7 +422,7 @@ [new PathSegment(jsonNavigationName)], foreach (var complexProperty in structuralType.GetComplexProperties()) { - var jsonNavigationName = complexProperty.ComplexType.GetJsonPropertyName(); + var jsonNavigationName = complexProperty.GetJsonPropertyName(); Check.DebugAssert(jsonNavigationName is not null, "Invalid complex property found on JSON-mapped structural type"); var projectionMember = new ProjectionMember().Append(new FakeMemberInfo(jsonNavigationName)); diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs index 288aa8f373f..f99990292a3 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs @@ -618,6 +618,79 @@ public class JsonEntity #endregion + #region HasJsonPropertyName + + [ConditionalFact] + public virtual async Task HasJsonPropertyName() + { + var contextFactory = await InitializeAsync( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: m => m.Entity().ComplexProperty(e => e.Json, b => + { + b.ToJson(); + + b.Property(j => j.String).HasJsonPropertyName("string"); + + b.ComplexProperty(j => j.Nested, b => + { + b.HasJsonPropertyName("nested"); + b.Property(x => x.Int).HasJsonPropertyName("int"); + }); + + b.ComplexCollection(a => a.NestedCollection, b => + { + b.HasJsonPropertyName("nested_collection"); + b.Property(x => x.Int).HasJsonPropertyName("int"); + }); + }), + seed: context => + { + context.Set().Add(new Context37009.Entity + { + Json = new Context37009.JsonComplexType + { + String = "foo", + Nested = new Context37009.JsonNestedType { Int = 1 }, + NestedCollection = [new Context37009.JsonNestedType { Int = 2 }] + } + }); + + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateContext(); + + Assert.Equal(1, await context.Set().CountAsync(e => e.Json.String == "foo")); + Assert.Equal(1, await context.Set().CountAsync(e => e.Json.Nested.Int == 1)); + Assert.Equal(1, await context.Set().CountAsync(e => e.Json.NestedCollection.Any(x => x.Int == 2))); + } + + protected class Context37009(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + public JsonComplexType Json { get; set; } + } + + public class JsonComplexType + { + public string String { get; set; } + + public JsonNestedType Nested { get; set; } + public List NestedCollection { get; set; } + } + + public class JsonNestedType + { + public int Int { get; set; } + } + } + + #endregion HasJsonPropertyName + protected TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory;