diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index d5efd7f6a81..5b1ba37c024 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Internal; @@ -837,6 +838,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(JsonElement))?.StoreType; + if (containerColumnType != null) + { + annotations[RelationalAnnotationNames.ContainerColumnType] = new Annotation( + RelationalAnnotationNames.ContainerColumnType, + containerColumnType); + } + } + GenerateTableMapping(entityTypeBuilderName, entityType, stringBuilder, annotations); GenerateSplitTableMapping(entityTypeBuilderName, entityType, stringBuilder); diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index ce667239197..4d82613e507 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -4362,7 +4362,9 @@ public virtual void Owned_types_mapped_to_json_are_stored_in_snapshot() b1.ToTable("EntityWithOneProperty", "DefaultSchema"); - b1.ToJson("EntityWithTwoProperties"); + b1 + .ToJson("EntityWithTwoProperties") + .HasColumnType("nvarchar(max)"); b1.WithOwner("EntityWithOneProperty") .HasForeignKey("EntityWithOnePropertyId"); @@ -4437,6 +4439,7 @@ public virtual void Owned_types_mapped_to_json_are_stored_in_snapshot() Assert.Equal(nameof(EntityWithOneProperty), ownedType1.GetTableName()); Assert.Equal("EntityWithTwoProperties", ownedType1.GetContainerColumnName()); + Assert.Equal("nvarchar(max)", ownedType1.GetContainerColumnType()); var ownership2 = ownedType1.FindNavigation(nameof(EntityWithStringKey)).ForeignKey; Assert.Equal("EntityWithTwoPropertiesEntityWithOnePropertyId", ownership2.Properties[0].Name); @@ -4473,6 +4476,83 @@ public virtual void Owned_types_mapped_to_json_are_stored_in_snapshot() Assert.Equal("Name", ownedProperties3[3].Name); }); + [ConditionalFact] + public virtual void Owned_types_mapped_to_json_with_explicit_column_type_are_stored_in_snapshot() + => Test( + builder => + { + builder.Entity(b => + { + b.HasKey(x => x.Id).HasName("PK_Custom"); + + b.OwnsOne( + x => x.EntityWithTwoProperties, bb => + { + bb.ToJson().HasColumnType("json"); + bb.Ignore(x => x.Id); + bb.Property(x => x.AlternateId).HasJsonPropertyName("NotKey"); + bb.WithOwner(e => e.EntityWithOneProperty); + }); + }); + }, + AddBoilerPlate( + GetHeading() + + """ + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.HasKey("Id") + .HasName("PK_Custom"); + + b.ToTable("EntityWithOneProperty", "DefaultSchema"); + }); + + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", b => + { + b.OwnsOne("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithTwoProperties", "EntityWithTwoProperties", b1 => + { + b1.Property("EntityWithOnePropertyId") + .HasColumnType("int"); + + b1.Property("AlternateId") + .HasColumnType("int") + .HasAnnotation("Relational:JsonPropertyName", "NotKey"); + + b1.HasKey("EntityWithOnePropertyId"); + + b1.ToTable("EntityWithOneProperty", "DefaultSchema"); + + b1 + .ToJson("EntityWithTwoProperties") + .HasColumnType("json"); + + b1.WithOwner("EntityWithOneProperty") + .HasForeignKey("EntityWithOnePropertyId"); + + b1.Navigation("EntityWithOneProperty"); + }); + + b.Navigation("EntityWithTwoProperties"); + }); +""", usingSystem: false), + o => + { + var entityWithOneProperty = o.FindEntityType(typeof(EntityWithOneProperty)); + Assert.Equal("PK_Custom", entityWithOneProperty.GetKeys().Single().GetName()); + + var ownership1 = entityWithOneProperty.FindNavigation(nameof(EntityWithOneProperty.EntityWithTwoProperties)) + .ForeignKey; + var ownedType1 = ownership1.DeclaringEntityType; + Assert.Equal(nameof(EntityWithOneProperty), ownedType1.GetTableName()); + Assert.Equal("EntityWithTwoProperties", ownedType1.GetContainerColumnName()); + Assert.Equal("json", ownedType1.GetContainerColumnType()); + }); + private class Order { public int Id { get; set; } diff --git a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs index 0f8dddae5a4..4b180344c3c 100644 --- a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs +++ b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs @@ -1900,4 +1900,57 @@ public void Rebuild_index_with_different_datacompression_value() Assert.Equal(DataCompressionType.Page, annotationValue); }); + + [ConditionalFact] + public void Alter_column_from_nvarchar_max_to_json_for_owned_type() + => Execute( + _ => { }, + source => source.Entity( + "Blog", + x => + { + x.Property("Id"); + x.HasKey("Id"); + x.OwnsOne( + "Details", "Details", d => + { + d.Property("Author"); + d.Property("Viewers"); + d.ToJson(); + }); + }), + target => target.Entity( + "Blog", + x => + { + x.Property("Id"); + x.HasKey("Id"); + x.OwnsOne( + "Details", "Details", d => + { + d.Property("Author"); + d.Property("Viewers"); + d.ToJson().HasColumnType("json"); + }); + }), + upOps => + { + Assert.Equal(1, upOps.Count); + + var operation = Assert.IsType(upOps[0]); + Assert.Equal("Blog", operation.Table); + Assert.Equal("Details", operation.Name); + Assert.Equal("json", operation.ColumnType); + Assert.Equal("nvarchar(max)", operation.OldColumn.ColumnType); + }, + downOps => + { + Assert.Equal(1, downOps.Count); + + var operation = Assert.IsType(downOps[0]); + Assert.Equal("Blog", operation.Table); + Assert.Equal("Details", operation.Name); + Assert.Equal("nvarchar(max)", operation.ColumnType); + Assert.Equal("json", operation.OldColumn.ColumnType); + }); }