From 42e6f9c32425bde59e50f78d3ab213cd93c0da36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 18:57:18 +0000 Subject: [PATCH 1/4] Initial plan From 56662a365ff71196c4dfcf29376ceed94cf42725 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 19:23:26 +0000 Subject: [PATCH 2/4] Fix HasJsonPropertyName for complex JSON properties - Updated RelationalComplexPropertyExtensions to delegate Get/SetJsonPropertyName to ComplexType - Updated RelationalTypeBaseExtensions.GetJsonPropertyName to return null for root JSON types (those with ContainerColumnName) - Updated query pipeline files to call GetJsonPropertyName on complex property instead of complex type - Added assertion in ComplexCollection_can_have_nested_complex_properties_mapped_to_json test - Added HasJsonPropertyName test from PR #37018 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../RelationalComplexPropertyExtensions.cs | 9 +-- .../RelationalTypeBaseExtensions.cs | 4 +- .../Query/JsonQueryExpression.cs | 5 +- ...sitor.ShaperProcessingExpressionVisitor.cs | 7 +- .../Query/SqlExpressions/SelectExpression.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- .../RelationalModelBuilderTest.cs | 10 +++ .../Query/AdHocJsonQueryRelationalTestBase.cs | 73 +++++++++++++++++++ 9 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs index e3f6e63b074..5dca01445ea 100644 --- a/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs @@ -23,8 +23,7 @@ public static class RelationalComplexPropertyExtensions /// is returned for complex properties of entities that are not mapped to a JSON column. /// public static string? GetJsonPropertyName(this IReadOnlyComplexProperty complexProperty) - => (string?)complexProperty.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.Value - ?? (complexProperty.DeclaringType.IsMappedToJson() ? complexProperty.Name : null); + => complexProperty.ComplexType.GetJsonPropertyName(); /// /// Sets the value of JSON property name used for the given complex property of an entity mapped to a JSON column. @@ -32,7 +31,7 @@ public static class RelationalComplexPropertyExtensions /// The complex property. /// The name to be used. public static void SetJsonPropertyName(this IMutableComplexProperty complexProperty, string? name) - => complexProperty.SetOrRemoveAnnotation( + => ((IMutableTypeBase)complexProperty.ComplexType).SetOrRemoveAnnotation( RelationalAnnotationNames.JsonPropertyName, Check.NullButNotEmpty(name)); @@ -47,7 +46,7 @@ public static void SetJsonPropertyName(this IMutableComplexProperty complexPrope this IConventionComplexProperty complexProperty, string? name, bool fromDataAnnotation = false) - => (string?)complexProperty.SetOrRemoveAnnotation( + => (string?)((IConventionTypeBase)complexProperty.ComplexType).SetOrRemoveAnnotation( RelationalAnnotationNames.JsonPropertyName, Check.NullButNotEmpty(name), fromDataAnnotation)?.Value; @@ -58,5 +57,5 @@ public static void SetJsonPropertyName(this IMutableComplexProperty complexPrope /// The complex property. /// The for the JSON property name for a given complex property. public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionComplexProperty complexProperty) - => complexProperty.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource(); + => ((IConventionTypeBase)complexProperty.ComplexType).FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource(); } diff --git a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs index 0ce1ae6a31b..eb77a1bd9a1 100644 --- a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs @@ -469,7 +469,9 @@ public static void SetContainerColumnType(this IMutableTypeBase typeBase, string ? null : typeBase is IReadOnlyEntityType entityType ? entityType.FindOwnership()!.GetNavigation(pointsToPrincipal: false)!.Name - : ((IReadOnlyComplexType)typeBase).ComplexProperty.Name); + : typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnName) != null + ? null + : ((IReadOnlyComplexType)typeBase).ComplexProperty.Name); #endregion } 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/ModelBuilding/RelationalModelBuilderTest.cs b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs index 269a34c025e..fca2fceb55a 100644 --- a/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs +++ b/test/EFCore.Relational.Specification.Tests/ModelBuilding/RelationalModelBuilderTest.cs @@ -914,6 +914,16 @@ public virtual void ComplexCollection_can_have_nested_complex_properties_mapped_ var nestedCol3 = complexCollectionProperty1.ComplexType.FindComplexProperty("Collection1")!; Assert.Equal("CustomNestedCollection3", nestedCol3.GetJsonPropertyName()); + + // Verify that no complex properties have the JsonPropertyName annotation directly + foreach (var complexProperty in entityType.GetComplexProperties()) + { + Assert.Null(complexProperty.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)); + foreach (var nestedComplexProperty in complexProperty.ComplexType.GetComplexProperties()) + { + Assert.Null(nestedComplexProperty.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)); + } + } } [ConditionalFact] 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; From 5af3a9c29020b938ccb1ee13bc328197c39254f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 22:22:23 +0000 Subject: [PATCH 3/4] Address PR feedback - Reverted query pipeline changes (SelectExpression, JsonQueryExpression, ShaperProcessingExpressionVisitor, SqliteQueryable, SqlServerQueryable) - Removed casts in RelationalComplexPropertyExtensions by calling methods directly on ComplexType - Moved ContainerColumnName check earlier in GetJsonPropertyName and returned null if annotation value is null - Moved SetJsonPropertyName to IMutableTypeBase and IConventionTypeBase - Made EntityType and ComplexProperty SetJsonPropertyName methods delegate to TypeBase methods - Updated AnnotationCodeGenerator to read JsonPropertyName from complex type instead of complex property Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Design/AnnotationCodeGenerator.cs | 12 ++-- .../RelationalComplexPropertyExtensions.cs | 11 +--- .../RelationalEntityTypeExtensions.cs | 11 +--- .../RelationalTypeBaseExtensions.cs | 60 ++++++++++++++++--- .../Query/JsonQueryExpression.cs | 5 +- ...sitor.ShaperProcessingExpressionVisitor.cs | 7 ++- .../Query/SqlExpressions/SelectExpression.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- 9 files changed, 76 insertions(+), 36 deletions(-) diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index b3c1718ea06..faa25fcf42b 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -397,10 +397,14 @@ public virtual IReadOnlyList GenerateFluentApiCalls( { var methodCallCodeFragments = new List(); - GenerateSimpleFluentApiCall( - annotations, - RelationalAnnotationNames.JsonPropertyName, nameof(RelationalComplexPropertyBuilderExtensions.HasJsonPropertyName), - methodCallCodeFragments); + // JsonPropertyName is now stored on the complex type, not the complex property + if (complexProperty.ComplexType.FindAnnotation(RelationalAnnotationNames.JsonPropertyName) is { Value: string jsonPropertyName }) + { + methodCallCodeFragments.Add( + new MethodCallCodeFragment( + nameof(RelationalComplexPropertyBuilderExtensions.HasJsonPropertyName), + jsonPropertyName)); + } methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(complexProperty, annotations, GenerateFluentApi)); diff --git a/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs index 5dca01445ea..4c17d16ab1b 100644 --- a/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs @@ -31,9 +31,7 @@ public static class RelationalComplexPropertyExtensions /// The complex property. /// The name to be used. public static void SetJsonPropertyName(this IMutableComplexProperty complexProperty, string? name) - => ((IMutableTypeBase)complexProperty.ComplexType).SetOrRemoveAnnotation( - RelationalAnnotationNames.JsonPropertyName, - Check.NullButNotEmpty(name)); + => complexProperty.ComplexType.SetJsonPropertyName(name); /// /// Sets the value of JSON property name used for the given complex property of an entity mapped to a JSON column. @@ -46,10 +44,7 @@ public static void SetJsonPropertyName(this IMutableComplexProperty complexPrope this IConventionComplexProperty complexProperty, string? name, bool fromDataAnnotation = false) - => (string?)((IConventionTypeBase)complexProperty.ComplexType).SetOrRemoveAnnotation( - RelationalAnnotationNames.JsonPropertyName, - Check.NullButNotEmpty(name), - fromDataAnnotation)?.Value; + => complexProperty.ComplexType.SetJsonPropertyName(name, fromDataAnnotation); /// /// Gets the for the JSON property name for a given complex property. @@ -57,5 +52,5 @@ public static void SetJsonPropertyName(this IMutableComplexProperty complexPrope /// The complex property. /// The for the JSON property name for a given complex property. public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionComplexProperty complexProperty) - => ((IConventionTypeBase)complexProperty.ComplexType).FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource(); + => complexProperty.ComplexType.GetJsonPropertyNameConfigurationSource(); } diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index 0f0833b02bc..0758b3b08e3 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -1620,10 +1620,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT this IConventionEntityType entityType, string? name, bool fromDataAnnotation = false) - => (string?)entityType.SetOrRemoveAnnotation( - RelationalAnnotationNames.JsonPropertyName, - Check.NullButNotEmpty(name), - fromDataAnnotation)?.Value; + => entityType.SetJsonPropertyName(name, fromDataAnnotation); /// /// Gets the value of JSON property name used for the given entity mapped to a JSON column. @@ -1652,9 +1649,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT /// The entity type. /// The name to be used. public static void SetJsonPropertyName(this IMutableEntityType entityType, string? name) - => entityType.SetOrRemoveAnnotation( - RelationalAnnotationNames.JsonPropertyName, - Check.NullButNotEmpty(name)); + => entityType.SetJsonPropertyName(name); /// /// Gets the for the JSON property name for a given entity type. @@ -1662,7 +1657,7 @@ public static void SetJsonPropertyName(this IMutableEntityType entityType, strin /// The entity type. /// The for the JSON property name for a given entity type. public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionEntityType entityType) - => entityType.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource(); + => entityType.GetJsonPropertyNameConfigurationSource(); #endregion } diff --git a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs index eb77a1bd9a1..847ca51a101 100644 --- a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs @@ -464,14 +464,58 @@ public static void SetContainerColumnType(this IMutableTypeBase typeBase, string /// is returned for entities that are not mapped to a JSON column. /// public static string? GetJsonPropertyName(this IReadOnlyTypeBase typeBase) - => (string?)typeBase.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.Value - ?? (!typeBase.IsMappedToJson() - ? null - : typeBase is IReadOnlyEntityType entityType - ? entityType.FindOwnership()!.GetNavigation(pointsToPrincipal: false)!.Name - : typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnName) != null - ? null - : ((IReadOnlyComplexType)typeBase).ComplexProperty.Name); + { + var annotation = typeBase.FindAnnotation(RelationalAnnotationNames.JsonPropertyName); + if (annotation?.Value is string jsonPropertyName) + { + return jsonPropertyName; + } + + if (typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnName) != null) + { + return null; + } + + return !typeBase.IsMappedToJson() + ? null + : typeBase is IReadOnlyEntityType entityType + ? entityType.FindOwnership()!.GetNavigation(pointsToPrincipal: false)!.Name + : ((IReadOnlyComplexType)typeBase).ComplexProperty.Name; + } + + /// + /// Sets the value of JSON property name used for the given type mapped to a JSON column. + /// + /// The type. + /// The name to be used. + public static void SetJsonPropertyName(this IMutableTypeBase typeBase, string? name) + => typeBase.SetOrRemoveAnnotation( + RelationalAnnotationNames.JsonPropertyName, + Check.NullButNotEmpty(name)); + + /// + /// Sets the value of JSON property name used for the given type mapped to a JSON column. + /// + /// The type. + /// The name to be used. + /// Indicates whether the configuration was specified using a data annotation. + /// The configured value. + public static string? SetJsonPropertyName( + this IConventionTypeBase typeBase, + string? name, + bool fromDataAnnotation = false) + => (string?)typeBase.SetOrRemoveAnnotation( + RelationalAnnotationNames.JsonPropertyName, + Check.NullButNotEmpty(name), + fromDataAnnotation)?.Value; + + /// + /// Gets the for the JSON property name for a given type. + /// + /// The type. + /// The for the JSON property name for a given type. + public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionTypeBase typeBase) + => typeBase.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource(); #endregion } diff --git a/src/EFCore.Relational/Query/JsonQueryExpression.cs b/src/EFCore.Relational/Query/JsonQueryExpression.cs index 286c31e13f1..b0168a5c1f1 100644 --- a/src/EFCore.Relational/Query/JsonQueryExpression.cs +++ b/src/EFCore.Relational/Query/JsonQueryExpression.cs @@ -204,11 +204,12 @@ public virtual JsonQueryExpression BindStructuralProperty(IPropertyBase structur Check.DebugAssert(KeyPropertyMap is null); + var targetComplexType = complexProperty.ComplexType; var newPath = Path.ToList(); - newPath.Add(new PathSegment(complexProperty.GetJsonPropertyName()!)); + newPath.Add(new PathSegment(targetComplexType.GetJsonPropertyName()!)); return new JsonQueryExpression( - complexProperty.ComplexType, + targetComplexType, JsonColumn, keyPropertyMap: null, newPath, diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.ShaperProcessingExpressionVisitor.cs index 5bd69b1add5..e180d6f5856 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, navigationJsonPropertyName, inverseNavigation, isStructuralPropertyNullable) = nestedStructuralProperty switch + var (relatedStructuralType, inverseNavigation, isStructuralPropertyNullable) = nestedStructuralProperty switch { - INavigation n => ((ITypeBase)n.TargetEntityType, n.TargetEntityType.GetJsonPropertyName()!, n.Inverse, !n.ForeignKey.IsRequiredDependent), - IComplexProperty cp => (cp.ComplexType, cp.GetJsonPropertyName()!, null, cp.IsNullable), + INavigation n => ((ITypeBase)n.TargetEntityType, n.Inverse, !n.ForeignKey.IsRequiredDependent), + IComplexProperty cp => (cp.ComplexType, null, cp.IsNullable), _ => throw new UnreachableException() }; @@ -1645,6 +1645,7 @@ 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 faffd086887..2d57cfb6298 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( - complexProperty.GetJsonPropertyName() + complexType.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 64584da8329..fee45469cba 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.GetJsonPropertyName() ?? throw new UnreachableException()), + => complexType.GetComplexProperties().Select(p => p.ComplexType.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 31f6879552d..a0bb693f889 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.GetJsonPropertyName(); + var jsonNavigationName = complexProperty.ComplexType.GetJsonPropertyName(); Check.DebugAssert(jsonNavigationName is not null, "Invalid complex property found on JSON-mapped structural type"); var projectionMember = new ProjectionMember().Append(new FakeMemberInfo(jsonNavigationName)); From d447ce408e2fa43c138cec5680540bdf470a274e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:04:48 +0000 Subject: [PATCH 4/4] Address additional PR feedback - Moved JsonPropertyName logic from GenerateFluentApiCalls(IComplexProperty) to GenerateFluentApiCalls(IComplexType) - Fixed infinite recursion by upcasting to IConventionTypeBase/IMutableTypeBase in EntityType methods - Simplified null check in GetJsonPropertyName to check annotation != null - Combined ContainerColumnName check with IsMappedToJson check using OR operator Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Design/AnnotationCodeGenerator.cs | 14 +++++--------- .../Extensions/RelationalEntityTypeExtensions.cs | 6 +++--- .../Extensions/RelationalTypeBaseExtensions.cs | 11 +++-------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index faa25fcf42b..1762c7e9f1c 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -301,6 +301,11 @@ public virtual IReadOnlyList GenerateFluentApiCalls( annotations.Remove(RelationalAnnotationNames.ContainerColumnType); } + GenerateSimpleFluentApiCall( + annotations, + RelationalAnnotationNames.JsonPropertyName, nameof(RelationalComplexPropertyBuilderExtensions.HasJsonPropertyName), + methodCallCodeFragments); + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(complexType, annotations, GenerateFluentApi)); return methodCallCodeFragments; @@ -397,15 +402,6 @@ public virtual IReadOnlyList GenerateFluentApiCalls( { var methodCallCodeFragments = new List(); - // JsonPropertyName is now stored on the complex type, not the complex property - if (complexProperty.ComplexType.FindAnnotation(RelationalAnnotationNames.JsonPropertyName) is { Value: string jsonPropertyName }) - { - methodCallCodeFragments.Add( - new MethodCallCodeFragment( - nameof(RelationalComplexPropertyBuilderExtensions.HasJsonPropertyName), - jsonPropertyName)); - } - methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(complexProperty, annotations, GenerateFluentApi)); return methodCallCodeFragments; diff --git a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs index 0758b3b08e3..d32e1d4deb5 100644 --- a/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs @@ -1620,7 +1620,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT this IConventionEntityType entityType, string? name, bool fromDataAnnotation = false) - => entityType.SetJsonPropertyName(name, fromDataAnnotation); + => ((IConventionTypeBase)entityType).SetJsonPropertyName(name, fromDataAnnotation); /// /// Gets the value of JSON property name used for the given entity mapped to a JSON column. @@ -1649,7 +1649,7 @@ public static void SetContainerColumnTypeMapping(this IMutableEntityType entityT /// The entity type. /// The name to be used. public static void SetJsonPropertyName(this IMutableEntityType entityType, string? name) - => entityType.SetJsonPropertyName(name); + => ((IMutableTypeBase)entityType).SetJsonPropertyName(name); /// /// Gets the for the JSON property name for a given entity type. @@ -1657,7 +1657,7 @@ public static void SetJsonPropertyName(this IMutableEntityType entityType, strin /// The entity type. /// The for the JSON property name for a given entity type. public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionEntityType entityType) - => entityType.GetJsonPropertyNameConfigurationSource(); + => ((IConventionTypeBase)entityType).GetJsonPropertyNameConfigurationSource(); #endregion } diff --git a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs index 847ca51a101..b5ee748ab77 100644 --- a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs @@ -466,17 +466,12 @@ public static void SetContainerColumnType(this IMutableTypeBase typeBase, string public static string? GetJsonPropertyName(this IReadOnlyTypeBase typeBase) { var annotation = typeBase.FindAnnotation(RelationalAnnotationNames.JsonPropertyName); - if (annotation?.Value is string jsonPropertyName) + if (annotation != null) { - return jsonPropertyName; + return (string?)annotation.Value; } - if (typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnName) != null) - { - return null; - } - - return !typeBase.IsMappedToJson() + return typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnName) != null || !typeBase.IsMappedToJson() ? null : typeBase is IReadOnlyEntityType entityType ? entityType.FindOwnership()!.GetNavigation(pointsToPrincipal: false)!.Name