From e9aa875fe127f723820288c9bf12ff691b85b633 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:52:43 +0000 Subject: [PATCH 1/6] Initial plan From cf6d4c5ec66b3d3548ab8f54cb8abd1c6256c030 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:23:06 +0000 Subject: [PATCH 2/6] Make GetContainerColumnType() return the default JSON mapping type in finalized models Set the default ContainerColumnType annotation during model finalization in RelationalMapToJsonConvention for JSON-mapped entity types and complex types that don't have it explicitly set. Also remove the now-dead workaround code in CSharpSnapshotGenerator that manually resolved the default JSON type. Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Design/CSharpSnapshotGenerator.cs | 28 ---------- .../RelationalMapToJsonConvention.cs | 35 ++++++++++++ .../Metadata/RelationalModelTest.cs | 54 +++++++++++++++++++ 3 files changed, 89 insertions(+), 28 deletions(-) diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 927366e1203..40a533fe39b 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -703,20 +703,6 @@ protected virtual void GenerateComplexPropertyAnnotations( .FilterIgnoredAnnotations(property.ComplexType.GetAnnotations()) .ToDictionary(a => a.Name, a => a); - // Add ContainerColumnType annotation if complex type is mapped to JSON but the type annotation is missing - if (typeAnnotations.ContainsKey(RelationalAnnotationNames.ContainerColumnName) - && !typeAnnotations.ContainsKey(RelationalAnnotationNames.ContainerColumnType)) - { - var containerColumnType = property.ComplexType.GetContainerColumnType() - ?? Dependencies.RelationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))?.StoreType; - if (containerColumnType != null) - { - typeAnnotations[RelationalAnnotationNames.ContainerColumnType] = new Annotation( - RelationalAnnotationNames.ContainerColumnType, - containerColumnType); - } - } - GenerateAnnotations( propertyBuilderName, property, stringBuilder, propertyAnnotations, inChainedCall: false, hasAnnotationMethodInfo: HasPropertyAnnotationMethodInfo); @@ -917,20 +903,6 @@ protected virtual void GenerateEntityTypeAnnotations( .FilterIgnoredAnnotations(entityType.GetAnnotations()) .ToDictionary(a => a.Name, a => a); - // Add ContainerColumnType annotation if entity is mapped to JSON but the type annotation is missing - if (annotations.ContainsKey(RelationalAnnotationNames.ContainerColumnName) - && !annotations.ContainsKey(RelationalAnnotationNames.ContainerColumnType)) - { - var containerColumnType = entityType.GetContainerColumnType() - ?? Dependencies.RelationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))?.StoreType; - if (containerColumnType != null) - { - annotations[RelationalAnnotationNames.ContainerColumnType] = new Annotation( - RelationalAnnotationNames.ContainerColumnType, - containerColumnType); - } - } - GenerateTableMapping(entityTypeBuilderName, entityType, stringBuilder, annotations); GenerateSplitTableMapping(entityTypeBuilderName, entityType, stringBuilder); diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs index 41ccc57d379..63efa07912d 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs @@ -52,8 +52,18 @@ public virtual void ProcessModelFinalizing( IConventionModelBuilder modelBuilder, IConventionContext context) { + var defaultJsonStoreType = + ((RelationalTypeMapping?)Dependencies.TypeMappingSource.FindMapping(typeof(JsonTypePlaceholder)))?.StoreType; + foreach (var jsonEntityType in modelBuilder.Metadata.GetEntityTypes().Where(e => e.IsMappedToJson())) { + if (defaultJsonStoreType != null + && jsonEntityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnName)?.Value is string + && jsonEntityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnType) == null) + { + jsonEntityType.SetContainerColumnType(defaultJsonStoreType); + } + foreach (var enumProperty in jsonEntityType .GetDeclaredProperties() .Where(p => p.ClrType.UnwrapNullableType().IsEnum)) @@ -67,5 +77,30 @@ public virtual void ProcessModelFinalizing( } } } + + if (defaultJsonStoreType != null) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + SetDefaultContainerColumnTypeForComplexTypes(entityType, defaultJsonStoreType); + } + } + } + + private static void SetDefaultContainerColumnTypeForComplexTypes( + IConventionTypeBase typeBase, + string defaultJsonStoreType) + { + foreach (var complexProperty in typeBase.GetComplexProperties()) + { + var complexType = complexProperty.ComplexType; + if (complexType.FindAnnotation(RelationalAnnotationNames.ContainerColumnName)?.Value is string + && complexType.FindAnnotation(RelationalAnnotationNames.ContainerColumnType) == null) + { + complexType.SetContainerColumnType(defaultJsonStoreType); + } + + SetDefaultContainerColumnTypeForComplexTypes(complexType, defaultJsonStoreType); + } } } diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 21c3f3d9cbc..9d6238784dd 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -3053,6 +3053,60 @@ public void Complex_collection_container_column_type_is_used_in_relational_model Assert.Equal("some_json_mapping", column.StoreType); } + [ConditionalFact] + public void Complex_property_gets_default_container_column_type_when_not_set_explicitly() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(eb => + { + eb.ComplexProperty( + e => e.ComplexProperty, cb => + { + cb.ToJson("complex_data"); + }); + }); + + var model = Finalize(modelBuilder); + + var entityType = model.Model.FindEntityType(typeof(EntityWithComplexProperty)); + var complexProperty = entityType.GetComplexProperties().Single(); + var complexType = complexProperty.ComplexType; + + Assert.Equal("some_json_mapping", complexType.GetContainerColumnType()); + + var table = entityType.GetTableMappings().Single().Table; + var column = table.Columns.Single(c => c.Name == "complex_data"); + Assert.Equal("some_json_mapping", column.StoreType); + } + + [ConditionalFact] + public void Complex_collection_gets_default_container_column_type_when_not_set_explicitly() + { + var modelBuilder = CreateConventionModelBuilder(); + + modelBuilder.Entity(eb => + { + eb.ComplexCollection( + e => e.ComplexCollection, cb => + { + cb.ToJson("collection_data"); + }); + }); + + var model = Finalize(modelBuilder); + + var entityType = model.Model.FindEntityType(typeof(EntityWithComplexCollection)); + var complexProperty = entityType.GetComplexProperties().Single(); + var complexType = complexProperty.ComplexType; + + Assert.Equal("some_json_mapping", complexType.GetContainerColumnType()); + + var table = entityType.GetTableMappings().Single().Table; + var column = table.Columns.Single(c => c.Name == "collection_data"); + Assert.Equal("some_json_mapping", column.StoreType); + } + [ConditionalFact] public void Can_use_relational_model_with_functions() { From f6b6f0a893da919d53dbf214ee3954f1f69b527b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:12:57 +0000 Subject: [PATCH 3/6] Move default JSON type resolution from convention to GetContainerColumnType() Instead of setting the ContainerColumnType annotation in the convention, resolve the default from the type mapping source directly in the GetContainerColumnType() method when the model is finalized (read-only). Reverts the convention and snapshot generator changes. Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Design/CSharpSnapshotGenerator.cs | 28 +++++++++++++++ .../RelationalTypeBaseExtensions.cs | 29 ++++++++++++--- .../RelationalMapToJsonConvention.cs | 35 ------------------- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 40a533fe39b..927366e1203 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -703,6 +703,20 @@ protected virtual void GenerateComplexPropertyAnnotations( .FilterIgnoredAnnotations(property.ComplexType.GetAnnotations()) .ToDictionary(a => a.Name, a => a); + // Add ContainerColumnType annotation if complex type is mapped to JSON but the type annotation is missing + if (typeAnnotations.ContainsKey(RelationalAnnotationNames.ContainerColumnName) + && !typeAnnotations.ContainsKey(RelationalAnnotationNames.ContainerColumnType)) + { + var containerColumnType = property.ComplexType.GetContainerColumnType() + ?? Dependencies.RelationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))?.StoreType; + if (containerColumnType != null) + { + typeAnnotations[RelationalAnnotationNames.ContainerColumnType] = new Annotation( + RelationalAnnotationNames.ContainerColumnType, + containerColumnType); + } + } + GenerateAnnotations( propertyBuilderName, property, stringBuilder, propertyAnnotations, inChainedCall: false, hasAnnotationMethodInfo: HasPropertyAnnotationMethodInfo); @@ -903,6 +917,20 @@ protected virtual void GenerateEntityTypeAnnotations( .FilterIgnoredAnnotations(entityType.GetAnnotations()) .ToDictionary(a => a.Name, a => a); + // Add ContainerColumnType annotation if entity is mapped to JSON but the type annotation is missing + if (annotations.ContainsKey(RelationalAnnotationNames.ContainerColumnName) + && !annotations.ContainsKey(RelationalAnnotationNames.ContainerColumnType)) + { + var containerColumnType = entityType.GetContainerColumnType() + ?? Dependencies.RelationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))?.StoreType; + if (containerColumnType != null) + { + annotations[RelationalAnnotationNames.ContainerColumnType] = new Annotation( + RelationalAnnotationNames.ContainerColumnType, + containerColumnType); + } + } + GenerateTableMapping(entityTypeBuilderName, entityType, stringBuilder, annotations); GenerateSplitTableMapping(entityTypeBuilderName, entityType, stringBuilder); diff --git a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs index b5ee748ab77..d3aabcaab76 100644 --- a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs @@ -416,11 +416,30 @@ public static void SetContainerColumnName(this IMutableTypeBase typeBase, string /// The type. /// The database column type. public static string? GetContainerColumnType(this IReadOnlyTypeBase typeBase) - => typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnType)?.Value is string columnName - ? columnName - : typeBase is IReadOnlyEntityType entityType - ? entityType.FindOwnership()?.PrincipalEntityType.GetContainerColumnType() - : ((IReadOnlyComplexType)typeBase).ComplexProperty.DeclaringType.GetContainerColumnType(); + { + if (typeBase.FindAnnotation(RelationalAnnotationNames.ContainerColumnType)?.Value is string columnType) + { + return columnType; + } + + var parentType = typeBase is IReadOnlyEntityType entityType + ? entityType.FindOwnership()?.PrincipalEntityType.GetContainerColumnType() + : ((IReadOnlyComplexType)typeBase).ComplexProperty.DeclaringType.GetContainerColumnType(); + + if (parentType != null) + { + return parentType; + } + + if (typeBase.IsMappedToJson() + && typeBase.Model is IModel model) + { + return ((IRelationalTypeMappingSource)model.GetModelDependencies().TypeMappingSource) + .FindMapping(typeof(JsonTypePlaceholder))?.StoreType; + } + + return null; + } /// /// Sets the type of the container column to which the type is mapped. diff --git a/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs b/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs index 63efa07912d..41ccc57d379 100644 --- a/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/RelationalMapToJsonConvention.cs @@ -52,18 +52,8 @@ public virtual void ProcessModelFinalizing( IConventionModelBuilder modelBuilder, IConventionContext context) { - var defaultJsonStoreType = - ((RelationalTypeMapping?)Dependencies.TypeMappingSource.FindMapping(typeof(JsonTypePlaceholder)))?.StoreType; - foreach (var jsonEntityType in modelBuilder.Metadata.GetEntityTypes().Where(e => e.IsMappedToJson())) { - if (defaultJsonStoreType != null - && jsonEntityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnName)?.Value is string - && jsonEntityType.FindAnnotation(RelationalAnnotationNames.ContainerColumnType) == null) - { - jsonEntityType.SetContainerColumnType(defaultJsonStoreType); - } - foreach (var enumProperty in jsonEntityType .GetDeclaredProperties() .Where(p => p.ClrType.UnwrapNullableType().IsEnum)) @@ -77,30 +67,5 @@ public virtual void ProcessModelFinalizing( } } } - - if (defaultJsonStoreType != null) - { - foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) - { - SetDefaultContainerColumnTypeForComplexTypes(entityType, defaultJsonStoreType); - } - } - } - - private static void SetDefaultContainerColumnTypeForComplexTypes( - IConventionTypeBase typeBase, - string defaultJsonStoreType) - { - foreach (var complexProperty in typeBase.GetComplexProperties()) - { - var complexType = complexProperty.ComplexType; - if (complexType.FindAnnotation(RelationalAnnotationNames.ContainerColumnName)?.Value is string - && complexType.FindAnnotation(RelationalAnnotationNames.ContainerColumnType) == null) - { - complexType.SetContainerColumnType(defaultJsonStoreType); - } - - SetDefaultContainerColumnTypeForComplexTypes(complexType, defaultJsonStoreType); - } } } From 272f90716d5b46acc86b001989808618b2963893 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 03:17:43 +0000 Subject: [PATCH 4/6] Remove redundant fallback in CSharpSnapshotGenerator now that GetContainerColumnType resolves defaults Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Migrations/Design/CSharpSnapshotGenerator.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 927366e1203..07c895f736e 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -707,8 +707,7 @@ protected virtual void GenerateComplexPropertyAnnotations( if (typeAnnotations.ContainsKey(RelationalAnnotationNames.ContainerColumnName) && !typeAnnotations.ContainsKey(RelationalAnnotationNames.ContainerColumnType)) { - var containerColumnType = property.ComplexType.GetContainerColumnType() - ?? Dependencies.RelationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))?.StoreType; + var containerColumnType = property.ComplexType.GetContainerColumnType(); if (containerColumnType != null) { typeAnnotations[RelationalAnnotationNames.ContainerColumnType] = new Annotation( @@ -921,8 +920,7 @@ protected virtual void GenerateEntityTypeAnnotations( if (annotations.ContainsKey(RelationalAnnotationNames.ContainerColumnName) && !annotations.ContainsKey(RelationalAnnotationNames.ContainerColumnType)) { - var containerColumnType = entityType.GetContainerColumnType() - ?? Dependencies.RelationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))?.StoreType; + var containerColumnType = entityType.GetContainerColumnType(); if (containerColumnType != null) { annotations[RelationalAnnotationNames.ContainerColumnType] = new Annotation( From cab6eeb8434209e14cab67cb3e85bc67bb0552fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 05:08:03 +0000 Subject: [PATCH 5/6] Cast model to Model and check IsReadOnly instead of checking for IModel Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Extensions/RelationalTypeBaseExtensions.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs index d3aabcaab76..2b3b5a49632 100644 --- a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; // ReSharper disable once CheckNamespace #pragma warning disable IDE0130 // Namespace does not match folder structure @@ -432,9 +433,11 @@ public static void SetContainerColumnName(this IMutableTypeBase typeBase, string } if (typeBase.IsMappedToJson() - && typeBase.Model is IModel model) +#pragma warning disable EF1001 // Internal EF Core API usage. + && (Model)typeBase.Model is { IsReadOnly: true } model) +#pragma warning restore EF1001 // Internal EF Core API usage. { - return ((IRelationalTypeMappingSource)model.GetModelDependencies().TypeMappingSource) + return ((IRelationalTypeMappingSource)((IModel)model).GetModelDependencies().TypeMappingSource) .FindMapping(typeof(JsonTypePlaceholder))?.StoreType; } From 520417f728b2b4e909ba01b6b378e74963d265f0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 05:24:30 +0000 Subject: [PATCH 6/6] Use || condition for non-Model types and simplify IsReadOnly check Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Extensions/RelationalTypeBaseExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs index 2b3b5a49632..89b67725b3a 100644 --- a/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalTypeBaseExtensions.cs @@ -434,10 +434,10 @@ public static void SetContainerColumnName(this IMutableTypeBase typeBase, string if (typeBase.IsMappedToJson() #pragma warning disable EF1001 // Internal EF Core API usage. - && (Model)typeBase.Model is { IsReadOnly: true } model) + && (typeBase.Model is not Model model || model.IsReadOnly)) #pragma warning restore EF1001 // Internal EF Core API usage. { - return ((IRelationalTypeMappingSource)((IModel)model).GetModelDependencies().TypeMappingSource) + return ((IRelationalTypeMappingSource)((IModel)typeBase.Model).GetModelDependencies().TypeMappingSource) .FindMapping(typeof(JsonTypePlaceholder))?.StoreType; }