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