diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 04a6940a970..a933fe05781 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -452,7 +452,11 @@ protected virtual void GenerateProperty( // Note that GenerateAnnotations below does the corresponding decrement stringBuilder.IncrementIndent(); - if (property.IsConcurrencyToken) + // ComplexCollectionTypePropertyBuilder doesn't have IsConcurrencyToken method + var isInComplexCollection = property.DeclaringType is IComplexType complexType + && complexType.ComplexProperty.IsCollection; + + if (!isInComplexCollection && property.IsConcurrencyToken) { stringBuilder .AppendLine() @@ -466,7 +470,8 @@ protected virtual void GenerateProperty( .Append(".IsRequired()"); } - if (property.ValueGenerated != ValueGenerated.Never) + // ComplexCollectionTypePropertyBuilder doesn't have ValueGenerated* methods + if (!isInComplexCollection && property.ValueGenerated != ValueGenerated.Never) { stringBuilder .AppendLine() @@ -498,8 +503,16 @@ protected virtual void GeneratePropertyAnnotations( .FilterIgnoredAnnotations(property.GetAnnotations()) .ToDictionary(a => a.Name, a => a); - GenerateFluentApiForMaxLength(property, stringBuilder); - GenerateFluentApiForPrecisionAndScale(property, stringBuilder); + // ComplexCollectionTypePropertyBuilder doesn't have HasMaxLength or HasPrecision methods + var isInComplexCollection = property.DeclaringType is IComplexType complexType + && complexType.ComplexProperty.IsCollection; + + if (!isInComplexCollection) + { + GenerateFluentApiForMaxLength(property, stringBuilder); + GenerateFluentApiForPrecisionAndScale(property, stringBuilder); + } + GenerateFluentApiForIsUnicode(property, stringBuilder); if (!annotations.ContainsKey(RelationalAnnotationNames.ColumnType) diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index dade6edb6ff..26af035e75d 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -6259,6 +6259,101 @@ public virtual void Complex_types_mapped_to_json_are_stored_in_snapshot() Assert.Equal(typeof(List>), propertiesComplexCollection.ClrType); }); + [ConditionalFact] + public virtual void Complex_collection_property_annotations_not_supported_by_builder_are_ignored_in_snapshot() + => Test( + builder => + { + builder.Entity(b => + { + b.HasKey(x => x.Id).HasName("PK_Custom"); + + b.ComplexProperty( + x => x.EntityWithTwoProperties, bb => + { + bb.ToJson("TwoProps").HasColumnType("json"); + bb.ComplexProperty( + x => x.EntityWithStringKey, bbb => + { + bbb.ComplexCollection(x => x.Properties, bbbb => + { + bbbb.HasJsonPropertyName("JsonProps"); + // Set annotations directly on the model to simulate convention behavior + // These should NOT appear in snapshot because ComplexCollectionTypePropertyBuilder + // doesn't support these methods + var complexType = bbbb.Metadata.ComplexType; + var nameProperty = (IMutableProperty)complexType.FindProperty("Name")!; + nameProperty.SetMaxLength(100); + nameProperty.SetPrecision(10); + nameProperty.SetScale(2); + nameProperty.IsConcurrencyToken = true; + nameProperty.ValueGenerated = ValueGenerated.OnAdd; + }); + }); + }); + }); + }, + 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"); + + b1.Property("Id"); + + 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("json"); + }); + + b.HasKey("Id") + .HasName("PK_Custom"); + + b.ToTable("EntityWithOneProperty", "DefaultSchema"); + }); +""", usingSystem: false, usingCollections: true), + o => + { + var entityWithOneProperty = o.FindEntityType(typeof(EntityWithOneProperty)); + var complexProperty1 = entityWithOneProperty.FindComplexProperty(nameof(EntityWithOneProperty.EntityWithTwoProperties)); + var complexType1 = complexProperty1.ComplexType; + var entityWithStringKeyComplexProperty = + complexType1.FindComplexProperty(nameof(EntityWithTwoProperties.EntityWithStringKey)); + var entityWithStringKeyComplexType = entityWithStringKeyComplexProperty.ComplexType; + + var propertiesComplexCollection = + entityWithStringKeyComplexType.FindComplexProperty(nameof(EntityWithStringKey.Properties)); + Assert.True(propertiesComplexCollection.IsCollection); + + // MaxLength is NOT in the snapshot, so it won't be set on the model created from snapshot + // This verifies that the snapshot doesn't contain HasMaxLength which would cause a compile error + var nameProperty = propertiesComplexCollection.ComplexType.FindProperty("Name"); + Assert.Null(nameProperty.GetMaxLength()); + }); + #endregion #region HasKey diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index 4553967aeb3..1472464bcc2 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -9922,6 +9922,39 @@ public virtual void Noop_on_complex_properties() target => { }, Assert.Empty); + [ConditionalFact] + public void Noop_on_complex_collection_property_annotations_not_in_snapshot() + => Execute( + builder => + { + builder.Entity( + "Entity", e => + { + e.Property("Id").ValueGeneratedOnAdd(); + e.HasKey("Id"); + + e.ComplexCollection, MyJsonComplex>( + "ComplexCollection", cp => + { + cp.ToJson(); + cp.Property(x => x.Value); + cp.Property(x => x.Date); + }); + }); + }, + source => + { + // Simulate convention setting MaxLength on string property in complex collection + // This annotation is NOT emitted in the snapshot because ComplexCollectionTypePropertyBuilder + // doesn't support HasMaxLength + var entity = source.Model.FindEntityType("Entity"); + var complexProperty = entity!.FindComplexProperty("ComplexCollection")!; + var valueProperty = complexProperty.ComplexType.FindProperty("Value")!; + ((IMutableProperty)valueProperty).SetMaxLength(255); + }, + target => { }, + Assert.Empty); + protected class MyJsonComplex { public string Value { get; set; }