diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 04a6940a970..ca9d2c99de9 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -691,6 +691,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); @@ -891,6 +905,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/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index dade6edb6ff..0e4b8cd2fbb 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -4347,7 +4347,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"); @@ -4417,6 +4419,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); @@ -4453,6 +4456,81 @@ 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"); + + b1.Property("AlternateId") + .HasJsonPropertyName("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; } @@ -6142,6 +6220,125 @@ public readonly struct Coordinates(decimal latitude, decimal longitude) [ConditionalFact] public virtual void Complex_types_mapped_to_json_are_stored_in_snapshot() + => Test( + builder => + { + builder.Entity(b => + { + b.HasKey(x => x.Id).HasName("PK_Custom"); + + b.ComplexProperty( + x => x.EntityWithTwoProperties, bb => + { + bb.ToJson("TwoProps"); + bb.Property(x => x.AlternateId).HasJsonPropertyName("NotKey"); + bb.ComplexProperty( + x => x.EntityWithStringKey, bbb => + { + bbb.ComplexCollection(x => x.Properties, bbbb => bbbb.HasJsonPropertyName("JsonProps")); + }); + bb.ComplexProperty( + x => x.Coordinates, bbb => + { + bbb.Property(c => c.Latitude).HasJsonPropertyName("Lat"); + bbb.Property(c => c.Longitude).HasJsonPropertyName("Lon"); + }); + }); + }); + }, + AddBoilerPlate( + GetHeading() + + """ + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.ComplexProperty(typeof(Dictionary), "EntityWithTwoProperties", "Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty.EntityWithTwoProperties#EntityWithTwoProperties", b1 => + { + b1.Property("AlternateId") + .HasJsonPropertyName("NotKey"); + + b1.Property("Id"); + + b1.ComplexProperty(typeof(Dictionary), "Coordinates", "Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty.EntityWithTwoProperties#EntityWithTwoProperties.Coordinates#Coordinates", b2 => + { + b2.IsRequired(); + + b2.Property("Latitude") + .HasJsonPropertyName("Lat"); + + b2.Property("Longitude") + .HasJsonPropertyName("Lon"); + }); + + b1.ComplexProperty(typeof(Dictionary), "EntityWithStringKey", "Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty.EntityWithTwoProperties#EntityWithTwoProperties.EntityWithStringKey#EntityWithStringKey", b2 => + { + b2.Property("Id"); + + b2.ComplexCollection(typeof(List>), "Properties", "Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty.EntityWithTwoProperties#EntityWithTwoProperties.EntityWithStringKey#EntityWithStringKey.Properties#EntityWithStringProperty", b3 => + { + b3.Property("Id"); + + b3.Property("Name"); + + b3.HasJsonPropertyName("JsonProps"); + }); + }); + + b1 + .ToJson("TwoProps") + .HasColumnType("nvarchar(max)"); + }); + + b.HasKey("Id") + .HasName("PK_Custom"); + + b.ToTable("EntityWithOneProperty", "DefaultSchema"); + }); +""", usingSystem: false, usingCollections: true), + o => + { + var entityWithOneProperty = o.FindEntityType(typeof(EntityWithOneProperty)); + Assert.Equal("PK_Custom", entityWithOneProperty.GetKeys().Single().GetName()); + + var complexProperty1 = entityWithOneProperty.FindComplexProperty(nameof(EntityWithOneProperty.EntityWithTwoProperties)); + Assert.False(complexProperty1.IsCollection); + Assert.True(complexProperty1.IsNullable); + var complexType1 = complexProperty1.ComplexType; + Assert.Equal("TwoProps", complexType1.GetContainerColumnName()); + Assert.Equal("nvarchar(max)", complexType1.GetContainerColumnType()); + + var alternateIdProperty = complexType1.FindProperty(nameof(EntityWithTwoProperties.AlternateId)); + Assert.Equal("NotKey", alternateIdProperty.GetJsonPropertyName()); + + var coordinatesComplexProperty = complexType1.FindComplexProperty(nameof(EntityWithTwoProperties.Coordinates)); + Assert.False(coordinatesComplexProperty.IsCollection); + Assert.False(coordinatesComplexProperty.IsNullable); + var coordinatesComplexType = coordinatesComplexProperty.ComplexType; + var latitudeProperty = coordinatesComplexType.FindProperty(nameof(Coordinates.Latitude)); + Assert.Equal("Lat", latitudeProperty.GetJsonPropertyName()); + var longitudeProperty = coordinatesComplexType.FindProperty(nameof(Coordinates.Longitude)); + Assert.Equal("Lon", longitudeProperty.GetJsonPropertyName()); + + var entityWithStringKeyComplexProperty = + complexType1.FindComplexProperty(nameof(EntityWithTwoProperties.EntityWithStringKey)); + Assert.False(entityWithStringKeyComplexProperty.IsCollection); + Assert.True(entityWithStringKeyComplexProperty.IsNullable); + var entityWithStringKeyComplexType = entityWithStringKeyComplexProperty.ComplexType; + + var propertiesComplexCollection = + entityWithStringKeyComplexType.FindComplexProperty(nameof(EntityWithStringKey.Properties)); + Assert.True(propertiesComplexCollection.IsCollection); + Assert.Equal("JsonProps", propertiesComplexCollection.GetJsonPropertyName()); + Assert.Equal(typeof(List>), propertiesComplexCollection.ClrType); + }); + + [ConditionalFact] + public virtual void Complex_types_mapped_to_json_with_explicit_column_type_are_stored_in_snapshot() => Test( builder => { diff --git a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs index 3253a8eeee6..7ae46b64416 100644 --- a/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs +++ b/test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs @@ -1894,4 +1894,110 @@ 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); + }); + + [ConditionalFact] + public void Alter_column_from_nvarchar_max_to_json_for_complex_type() + => Execute( + _ => { }, + source => source.Entity( + "Blog", + x => + { + x.Property("Id"); + x.HasKey("Id"); + x.ComplexProperty( + typeof(Dictionary), "Details", d => + { + d.Property("Author"); + d.Property("Viewers"); + d.ToJson(); + }); + }), + target => target.Entity( + "Blog", + x => + { + x.Property("Id"); + x.HasKey("Id"); + x.ComplexProperty( + typeof(Dictionary), "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); + }); }