From 8272d1d3dec6ed3cb6d25ce434bb2e34b7715526 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:13:47 +0000 Subject: [PATCH 1/3] Fix HasJsonPropertyName for complex properties Fixes #37009 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../RelationalComplexPropertyExtensions.cs | 52 +++++++++++-- .../RelationalTypeBaseExtensions.cs | 19 +++-- .../RelationalModelBuilderTest.cs | 10 +++ .../Query/AdHocJsonQueryRelationalTestBase.cs | 73 +++++++++++++++++++ 4 files changed, 141 insertions(+), 13 deletions(-) diff --git a/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs index e3f6e63b074..b8cd0ca6506 100644 --- a/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs @@ -11,6 +11,9 @@ namespace Microsoft.EntityFrameworkCore; /// public static class RelationalComplexPropertyExtensions { + private static readonly bool UseOldBehavior37009 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue37009", out var enabled37009) && enabled37009; + /// /// Gets the value of JSON property name used for the given complex property of an entity mapped to a JSON column. /// @@ -23,8 +26,15 @@ 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); + { + if (UseOldBehavior37009) + { + return (string?)complexProperty.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.Value + ?? (complexProperty.DeclaringType.IsMappedToJson() ? complexProperty.Name : null); + } + + return 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,9 +42,20 @@ public static class RelationalComplexPropertyExtensions /// The complex property. /// The name to be used. public static void SetJsonPropertyName(this IMutableComplexProperty complexProperty, string? name) - => complexProperty.SetOrRemoveAnnotation( - RelationalAnnotationNames.JsonPropertyName, - Check.NullButNotEmpty(name)); + { + if (UseOldBehavior37009) + { + complexProperty.SetOrRemoveAnnotation( + RelationalAnnotationNames.JsonPropertyName, + Check.NullButNotEmpty(name)); + } + else + { + complexProperty.ComplexType.SetOrRemoveAnnotation( + RelationalAnnotationNames.JsonPropertyName, + Check.NullButNotEmpty(name)); + } + } /// /// Sets the value of JSON property name used for the given complex property of an entity mapped to a JSON column. @@ -47,10 +68,20 @@ public static void SetJsonPropertyName(this IMutableComplexProperty complexPrope this IConventionComplexProperty complexProperty, string? name, bool fromDataAnnotation = false) - => (string?)complexProperty.SetOrRemoveAnnotation( + { + if (UseOldBehavior37009) + { + return (string?)complexProperty.SetOrRemoveAnnotation( + RelationalAnnotationNames.JsonPropertyName, + Check.NullButNotEmpty(name), + fromDataAnnotation)?.Value; + } + + return (string?)complexProperty.ComplexType.SetOrRemoveAnnotation( RelationalAnnotationNames.JsonPropertyName, Check.NullButNotEmpty(name), fromDataAnnotation)?.Value; + } /// /// Gets the for the JSON property name for a given complex property. @@ -58,5 +89,12 @@ 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(); + { + if (UseOldBehavior37009) + { + return complexProperty.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource(); + } + + return complexProperty.ComplexType.FindAnnotation(RelationalAnnotationNames.JsonPropertyName)?.GetConfigurationSource(); + } } diff --git a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs index 0ce1ae6a31b..30c18bd86cf 100644 --- a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs @@ -464,12 +464,19 @@ 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 - : ((IReadOnlyComplexType)typeBase).ComplexProperty.Name); + { + var annotation = typeBase.FindAnnotation(RelationalAnnotationNames.JsonPropertyName); + if (annotation != null) + { + return (string?)annotation.Value; + } + + return typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnName) != null || !typeBase.IsMappedToJson() + ? null + : typeBase is IReadOnlyEntityType entityType + ? entityType.FindOwnership()!.GetNavigation(pointsToPrincipal: false)!.Name + : ((IReadOnlyComplexType)typeBase).ComplexProperty.Name; + } #endregion } 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 2909659f30f853199debaedc14a9b3c93143a1f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:25:58 +0000 Subject: [PATCH 2/3] Add JsonPropertyName generation to ComplexType and remove from ComplexProperty Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Design/AnnotationCodeGenerator.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index b3c1718ea06..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,11 +402,6 @@ public virtual IReadOnlyList GenerateFluentApiCalls( { var methodCallCodeFragments = new List(); - GenerateSimpleFluentApiCall( - annotations, - RelationalAnnotationNames.JsonPropertyName, nameof(RelationalComplexPropertyBuilderExtensions.HasJsonPropertyName), - methodCallCodeFragments); - methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(complexProperty, annotations, GenerateFluentApi)); return methodCallCodeFragments; From 62d158254ee681b9cad559ce71afeb596ecfa453 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:58:10 +0000 Subject: [PATCH 3/3] Restore JsonPropertyName generation in ComplexProperty method Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- src/EFCore.Relational/Design/AnnotationCodeGenerator.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index 1762c7e9f1c..05a71dd8bba 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -402,6 +402,11 @@ public virtual IReadOnlyList GenerateFluentApiCalls( { var methodCallCodeFragments = new List(); + GenerateSimpleFluentApiCall( + annotations, + RelationalAnnotationNames.JsonPropertyName, nameof(RelationalComplexPropertyBuilderExtensions.HasJsonPropertyName), + methodCallCodeFragments); + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(complexProperty, annotations, GenerateFluentApi)); return methodCallCodeFragments;