Skip to content

Commit 9544f91

Browse files
[release/10.0] Fix snapshot generation to capture column type for JSON columns (#37284)
Fixes #37275 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
1 parent a879f33 commit 9544f91

File tree

3 files changed

+332
-1
lines changed

3 files changed

+332
-1
lines changed

src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,20 @@ protected virtual void GenerateComplexPropertyAnnotations(
690690
.FilterIgnoredAnnotations(property.ComplexType.GetAnnotations())
691691
.ToDictionary(a => a.Name, a => a);
692692

693+
// Add ContainerColumnType annotation if complex type is mapped to JSON but the type annotation is missing
694+
if (typeAnnotations.ContainsKey(RelationalAnnotationNames.ContainerColumnName)
695+
&& !typeAnnotations.ContainsKey(RelationalAnnotationNames.ContainerColumnType))
696+
{
697+
var containerColumnType = property.ComplexType.GetContainerColumnType()
698+
?? Dependencies.RelationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))?.StoreType;
699+
if (containerColumnType != null)
700+
{
701+
typeAnnotations[RelationalAnnotationNames.ContainerColumnType] = new Annotation(
702+
RelationalAnnotationNames.ContainerColumnType,
703+
containerColumnType);
704+
}
705+
}
706+
693707
GenerateAnnotations(
694708
propertyBuilderName, property, stringBuilder, propertyAnnotations,
695709
inChainedCall: false, hasAnnotationMethodInfo: HasPropertyAnnotationMethodInfo);
@@ -890,6 +904,20 @@ protected virtual void GenerateEntityTypeAnnotations(
890904
.FilterIgnoredAnnotations(entityType.GetAnnotations())
891905
.ToDictionary(a => a.Name, a => a);
892906

907+
// Add ContainerColumnType annotation if entity is mapped to JSON but the type annotation is missing
908+
if (annotations.ContainsKey(RelationalAnnotationNames.ContainerColumnName)
909+
&& !annotations.ContainsKey(RelationalAnnotationNames.ContainerColumnType))
910+
{
911+
var containerColumnType = entityType.GetContainerColumnType()
912+
?? Dependencies.RelationalTypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))?.StoreType;
913+
if (containerColumnType != null)
914+
{
915+
annotations[RelationalAnnotationNames.ContainerColumnType] = new Annotation(
916+
RelationalAnnotationNames.ContainerColumnType,
917+
containerColumnType);
918+
}
919+
}
920+
893921
GenerateTableMapping(entityTypeBuilderName, entityType, stringBuilder, annotations);
894922
GenerateSplitTableMapping(entityTypeBuilderName, entityType, stringBuilder);
895923

test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4402,7 +4402,9 @@ public virtual void Owned_types_mapped_to_json_are_stored_in_snapshot()
44024402
44034403
b1.ToTable("EntityWithOneProperty", "DefaultSchema");
44044404
4405-
b1.ToJson("EntityWithTwoProperties");
4405+
b1
4406+
.ToJson("EntityWithTwoProperties")
4407+
.HasColumnType("nvarchar(max)");
44064408
44074409
b1.WithOwner("EntityWithOneProperty")
44084410
.HasForeignKey("EntityWithOnePropertyId");
@@ -4472,6 +4474,7 @@ public virtual void Owned_types_mapped_to_json_are_stored_in_snapshot()
44724474

44734475
Assert.Equal(nameof(EntityWithOneProperty), ownedType1.GetTableName());
44744476
Assert.Equal("EntityWithTwoProperties", ownedType1.GetContainerColumnName());
4477+
Assert.Equal("nvarchar(max)", ownedType1.GetContainerColumnType());
44754478

44764479
var ownership2 = ownedType1.FindNavigation(nameof(EntityWithStringKey)).ForeignKey;
44774480
Assert.Equal("EntityWithTwoPropertiesEntityWithOnePropertyId", ownership2.Properties[0].Name);
@@ -4508,6 +4511,81 @@ public virtual void Owned_types_mapped_to_json_are_stored_in_snapshot()
45084511
Assert.Equal("Name", ownedProperties3[3].Name);
45094512
});
45104513

4514+
[ConditionalFact]
4515+
public virtual void Owned_types_mapped_to_json_with_explicit_column_type_are_stored_in_snapshot()
4516+
=> Test(
4517+
builder =>
4518+
{
4519+
builder.Entity<EntityWithOneProperty>(b =>
4520+
{
4521+
b.HasKey(x => x.Id).HasName("PK_Custom");
4522+
4523+
b.OwnsOne(
4524+
x => x.EntityWithTwoProperties, bb =>
4525+
{
4526+
bb.ToJson().HasColumnType("json");
4527+
bb.Ignore(x => x.Id);
4528+
bb.Property(x => x.AlternateId).HasJsonPropertyName("NotKey");
4529+
bb.WithOwner(e => e.EntityWithOneProperty);
4530+
});
4531+
});
4532+
},
4533+
AddBoilerPlate(
4534+
GetHeading()
4535+
+ """
4536+
modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", b =>
4537+
{
4538+
b.Property<int>("Id")
4539+
.ValueGeneratedOnAdd()
4540+
.HasColumnType("int");
4541+
4542+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
4543+
4544+
b.HasKey("Id")
4545+
.HasName("PK_Custom");
4546+
4547+
b.ToTable("EntityWithOneProperty", "DefaultSchema");
4548+
});
4549+
4550+
modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", b =>
4551+
{
4552+
b.OwnsOne("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithTwoProperties", "EntityWithTwoProperties", b1 =>
4553+
{
4554+
b1.Property<int>("EntityWithOnePropertyId");
4555+
4556+
b1.Property<int>("AlternateId")
4557+
.HasJsonPropertyName("NotKey");
4558+
4559+
b1.HasKey("EntityWithOnePropertyId");
4560+
4561+
b1.ToTable("EntityWithOneProperty", "DefaultSchema");
4562+
4563+
b1
4564+
.ToJson("EntityWithTwoProperties")
4565+
.HasColumnType("json");
4566+
4567+
b1.WithOwner("EntityWithOneProperty")
4568+
.HasForeignKey("EntityWithOnePropertyId");
4569+
4570+
b1.Navigation("EntityWithOneProperty");
4571+
});
4572+
4573+
b.Navigation("EntityWithTwoProperties");
4574+
});
4575+
""", usingSystem: false),
4576+
o =>
4577+
{
4578+
var entityWithOneProperty = o.FindEntityType(typeof(EntityWithOneProperty));
4579+
Assert.Equal("PK_Custom", entityWithOneProperty.GetKeys().Single().GetName());
4580+
4581+
var ownership1 = entityWithOneProperty.FindNavigation(nameof(EntityWithOneProperty.EntityWithTwoProperties))
4582+
.ForeignKey;
4583+
var ownedType1 = ownership1.DeclaringEntityType;
4584+
Assert.Equal(nameof(EntityWithOneProperty), ownedType1.GetTableName());
4585+
Assert.Equal("EntityWithTwoProperties", ownedType1.GetContainerColumnName());
4586+
Assert.Equal("json", ownedType1.GetContainerColumnType());
4587+
});
4588+
45114589
private class Order
45124590
{
45134591
public int Id { get; set; }
@@ -6197,6 +6275,125 @@ public readonly struct Coordinates(decimal latitude, decimal longitude)
61976275

61986276
[ConditionalFact]
61996277
public virtual void Complex_types_mapped_to_json_are_stored_in_snapshot()
6278+
=> Test(
6279+
builder =>
6280+
{
6281+
builder.Entity<EntityWithOneProperty>(b =>
6282+
{
6283+
b.HasKey(x => x.Id).HasName("PK_Custom");
6284+
6285+
b.ComplexProperty(
6286+
x => x.EntityWithTwoProperties, bb =>
6287+
{
6288+
bb.ToJson("TwoProps");
6289+
bb.Property(x => x.AlternateId).HasJsonPropertyName("NotKey");
6290+
bb.ComplexProperty(
6291+
x => x.EntityWithStringKey, bbb =>
6292+
{
6293+
bbb.ComplexCollection(x => x.Properties, bbbb => bbbb.HasJsonPropertyName("JsonProps"));
6294+
});
6295+
bb.ComplexProperty(
6296+
x => x.Coordinates, bbb =>
6297+
{
6298+
bbb.Property(c => c.Latitude).HasJsonPropertyName("Lat");
6299+
bbb.Property(c => c.Longitude).HasJsonPropertyName("Lon");
6300+
});
6301+
});
6302+
});
6303+
},
6304+
AddBoilerPlate(
6305+
GetHeading()
6306+
+ """
6307+
modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", b =>
6308+
{
6309+
b.Property<int>("Id")
6310+
.ValueGeneratedOnAdd()
6311+
.HasColumnType("int");
6312+
6313+
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
6314+
6315+
b.ComplexProperty(typeof(Dictionary<string, object>), "EntityWithTwoProperties", "Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty.EntityWithTwoProperties#EntityWithTwoProperties", b1 =>
6316+
{
6317+
b1.Property<int>("AlternateId")
6318+
.HasJsonPropertyName("NotKey");
6319+
6320+
b1.Property<int>("Id");
6321+
6322+
b1.ComplexProperty(typeof(Dictionary<string, object>), "Coordinates", "Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty.EntityWithTwoProperties#EntityWithTwoProperties.Coordinates#Coordinates", b2 =>
6323+
{
6324+
b2.IsRequired();
6325+
6326+
b2.Property<decimal>("Latitude")
6327+
.HasJsonPropertyName("Lat");
6328+
6329+
b2.Property<decimal>("Longitude")
6330+
.HasJsonPropertyName("Lon");
6331+
});
6332+
6333+
b1.ComplexProperty(typeof(Dictionary<string, object>), "EntityWithStringKey", "Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty.EntityWithTwoProperties#EntityWithTwoProperties.EntityWithStringKey#EntityWithStringKey", b2 =>
6334+
{
6335+
b2.Property<string>("Id");
6336+
6337+
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Properties", "Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty.EntityWithTwoProperties#EntityWithTwoProperties.EntityWithStringKey#EntityWithStringKey.Properties#EntityWithStringProperty", b3 =>
6338+
{
6339+
b3.Property<int>("Id");
6340+
6341+
b3.Property<string>("Name");
6342+
6343+
b3.HasJsonPropertyName("JsonProps");
6344+
});
6345+
});
6346+
6347+
b1
6348+
.ToJson("TwoProps")
6349+
.HasColumnType("nvarchar(max)");
6350+
});
6351+
6352+
b.HasKey("Id")
6353+
.HasName("PK_Custom");
6354+
6355+
b.ToTable("EntityWithOneProperty", "DefaultSchema");
6356+
});
6357+
""", usingSystem: false, usingCollections: true),
6358+
o =>
6359+
{
6360+
var entityWithOneProperty = o.FindEntityType(typeof(EntityWithOneProperty));
6361+
Assert.Equal("PK_Custom", entityWithOneProperty.GetKeys().Single().GetName());
6362+
6363+
var complexProperty1 = entityWithOneProperty.FindComplexProperty(nameof(EntityWithOneProperty.EntityWithTwoProperties));
6364+
Assert.False(complexProperty1.IsCollection);
6365+
Assert.True(complexProperty1.IsNullable);
6366+
var complexType1 = complexProperty1.ComplexType;
6367+
Assert.Equal("TwoProps", complexType1.GetContainerColumnName());
6368+
Assert.Equal("nvarchar(max)", complexType1.GetContainerColumnType());
6369+
6370+
var alternateIdProperty = complexType1.FindProperty(nameof(EntityWithTwoProperties.AlternateId));
6371+
Assert.Equal("NotKey", alternateIdProperty.GetJsonPropertyName());
6372+
6373+
var coordinatesComplexProperty = complexType1.FindComplexProperty(nameof(EntityWithTwoProperties.Coordinates));
6374+
Assert.False(coordinatesComplexProperty.IsCollection);
6375+
Assert.False(coordinatesComplexProperty.IsNullable);
6376+
var coordinatesComplexType = coordinatesComplexProperty.ComplexType;
6377+
var latitudeProperty = coordinatesComplexType.FindProperty(nameof(Coordinates.Latitude));
6378+
Assert.Equal("Lat", latitudeProperty.GetJsonPropertyName());
6379+
var longitudeProperty = coordinatesComplexType.FindProperty(nameof(Coordinates.Longitude));
6380+
Assert.Equal("Lon", longitudeProperty.GetJsonPropertyName());
6381+
6382+
var entityWithStringKeyComplexProperty =
6383+
complexType1.FindComplexProperty(nameof(EntityWithTwoProperties.EntityWithStringKey));
6384+
Assert.False(entityWithStringKeyComplexProperty.IsCollection);
6385+
Assert.True(entityWithStringKeyComplexProperty.IsNullable);
6386+
var entityWithStringKeyComplexType = entityWithStringKeyComplexProperty.ComplexType;
6387+
6388+
var propertiesComplexCollection =
6389+
entityWithStringKeyComplexType.FindComplexProperty(nameof(EntityWithStringKey.Properties));
6390+
Assert.True(propertiesComplexCollection.IsCollection);
6391+
Assert.Equal("JsonProps", propertiesComplexCollection.GetJsonPropertyName());
6392+
Assert.Equal(typeof(List<Dictionary<string, object>>), propertiesComplexCollection.ClrType);
6393+
});
6394+
6395+
[ConditionalFact]
6396+
public virtual void Complex_types_mapped_to_json_with_explicit_column_type_are_stored_in_snapshot()
62006397
=> Test(
62016398
builder =>
62026399
{

test/EFCore.SqlServer.Tests/Migrations/SqlServerModelDifferTest.cs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1894,4 +1894,110 @@ public void Rebuild_index_with_different_datacompression_value()
18941894

18951895
Assert.Equal(DataCompressionType.Page, annotationValue);
18961896
});
1897+
1898+
[ConditionalFact]
1899+
public void Alter_column_from_nvarchar_max_to_json_for_owned_type()
1900+
=> Execute(
1901+
_ => { },
1902+
source => source.Entity(
1903+
"Blog",
1904+
x =>
1905+
{
1906+
x.Property<int>("Id");
1907+
x.HasKey("Id");
1908+
x.OwnsOne(
1909+
"Details", "Details", d =>
1910+
{
1911+
d.Property<string>("Author");
1912+
d.Property<int>("Viewers");
1913+
d.ToJson();
1914+
});
1915+
}),
1916+
target => target.Entity(
1917+
"Blog",
1918+
x =>
1919+
{
1920+
x.Property<int>("Id");
1921+
x.HasKey("Id");
1922+
x.OwnsOne(
1923+
"Details", "Details", d =>
1924+
{
1925+
d.Property<string>("Author");
1926+
d.Property<int>("Viewers");
1927+
d.ToJson().HasColumnType("json");
1928+
});
1929+
}),
1930+
upOps =>
1931+
{
1932+
Assert.Equal(1, upOps.Count);
1933+
1934+
var operation = Assert.IsType<AlterColumnOperation>(upOps[0]);
1935+
Assert.Equal("Blog", operation.Table);
1936+
Assert.Equal("Details", operation.Name);
1937+
Assert.Equal("json", operation.ColumnType);
1938+
Assert.Equal("nvarchar(max)", operation.OldColumn.ColumnType);
1939+
},
1940+
downOps =>
1941+
{
1942+
Assert.Equal(1, downOps.Count);
1943+
1944+
var operation = Assert.IsType<AlterColumnOperation>(downOps[0]);
1945+
Assert.Equal("Blog", operation.Table);
1946+
Assert.Equal("Details", operation.Name);
1947+
Assert.Equal("nvarchar(max)", operation.ColumnType);
1948+
Assert.Equal("json", operation.OldColumn.ColumnType);
1949+
});
1950+
1951+
[ConditionalFact]
1952+
public void Alter_column_from_nvarchar_max_to_json_for_complex_type()
1953+
=> Execute(
1954+
_ => { },
1955+
source => source.Entity(
1956+
"Blog",
1957+
x =>
1958+
{
1959+
x.Property<int>("Id");
1960+
x.HasKey("Id");
1961+
x.ComplexProperty(
1962+
typeof(Dictionary<string, object>), "Details", d =>
1963+
{
1964+
d.Property<string>("Author");
1965+
d.Property<int>("Viewers");
1966+
d.ToJson();
1967+
});
1968+
}),
1969+
target => target.Entity(
1970+
"Blog",
1971+
x =>
1972+
{
1973+
x.Property<int>("Id");
1974+
x.HasKey("Id");
1975+
x.ComplexProperty(
1976+
typeof(Dictionary<string, object>), "Details", d =>
1977+
{
1978+
d.Property<string>("Author");
1979+
d.Property<int>("Viewers");
1980+
d.ToJson().HasColumnType("json");
1981+
});
1982+
}),
1983+
upOps =>
1984+
{
1985+
Assert.Equal(1, upOps.Count);
1986+
1987+
var operation = Assert.IsType<AlterColumnOperation>(upOps[0]);
1988+
Assert.Equal("Blog", operation.Table);
1989+
Assert.Equal("Details", operation.Name);
1990+
Assert.Equal("json", operation.ColumnType);
1991+
Assert.Equal("nvarchar(max)", operation.OldColumn.ColumnType);
1992+
},
1993+
downOps =>
1994+
{
1995+
Assert.Equal(1, downOps.Count);
1996+
1997+
var operation = Assert.IsType<AlterColumnOperation>(downOps[0]);
1998+
Assert.Equal("Blog", operation.Table);
1999+
Assert.Equal("Details", operation.Name);
2000+
Assert.Equal("nvarchar(max)", operation.ColumnType);
2001+
Assert.Equal("json", operation.OldColumn.ColumnType);
2002+
});
18972003
}

0 commit comments

Comments
 (0)