diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index 291952bca9d..d5efd7f6a81 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -443,7 +443,8 @@ protected virtual void GenerateProperty( var clrType = (FindValueConverter(property)?.ProviderClrType ?? property.ClrType) .MakeNullable(property.IsNullable); - var propertyBuilderName = $"{entityTypeBuilderName}.Property<{Code.Reference(clrType)}>({Code.Literal(property.Name)})"; + var propertyCall = property.IsPrimitiveCollection ? "PrimitiveCollection" : "Property"; + var propertyBuilderName = $"{entityTypeBuilderName}.{propertyCall}<{Code.Reference(clrType)}>({Code.Literal(property.Name)})"; stringBuilder .AppendLine() diff --git a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs index 875567bd1b7..01e6918c559 100644 --- a/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs +++ b/src/EFCore.SqlServer/Design/Internal/SqlServerAnnotationCodeGenerator.cs @@ -55,18 +55,42 @@ private static readonly MethodInfo PropertyIsSparseMethodInfo = typeof(SqlServerPropertyBuilderExtensions).GetRuntimeMethod( nameof(SqlServerPropertyBuilderExtensions.IsSparse), [typeof(PropertyBuilder), typeof(bool)])!; + private static readonly MethodInfo PrimitiveCollectionIsSparseMethodInfo + = typeof(SqlServerPrimitiveCollectionBuilderExtensions).GetRuntimeMethod( + nameof(SqlServerPrimitiveCollectionBuilderExtensions.IsSparse), [typeof(PrimitiveCollectionBuilder), typeof(bool)])!; + + private static readonly MethodInfo ComplexTypePropertyIsSparseMethodInfo + = typeof(SqlServerComplexTypePropertyBuilderExtensions).GetRuntimeMethod( + nameof(SqlServerComplexTypePropertyBuilderExtensions.IsSparse), [typeof(ComplexTypePropertyBuilder), typeof(bool)])!; + + private static readonly MethodInfo ComplexTypePrimitiveCollectionIsSparseMethodInfo + = typeof(SqlServerComplexTypePrimitiveCollectionBuilderExtensions).GetRuntimeMethod( + nameof(SqlServerComplexTypePrimitiveCollectionBuilderExtensions.IsSparse), [typeof(ComplexTypePrimitiveCollectionBuilder), typeof(bool)])!; + private static readonly MethodInfo PropertyUseIdentityColumnsMethodInfo = typeof(SqlServerPropertyBuilderExtensions).GetRuntimeMethod( nameof(SqlServerPropertyBuilderExtensions.UseIdentityColumn), [typeof(PropertyBuilder), typeof(long), typeof(int)])!; + private static readonly MethodInfo ComplexTypePropertyUseIdentityColumnsMethodInfo + = typeof(SqlServerComplexTypePropertyBuilderExtensions).GetRuntimeMethod( + nameof(SqlServerComplexTypePropertyBuilderExtensions.UseIdentityColumn), [typeof(ComplexTypePropertyBuilder), typeof(long), typeof(int)])!; + private static readonly MethodInfo PropertyUseHiLoMethodInfo = typeof(SqlServerPropertyBuilderExtensions).GetRuntimeMethod( nameof(SqlServerPropertyBuilderExtensions.UseHiLo), [typeof(PropertyBuilder), typeof(string), typeof(string)])!; + private static readonly MethodInfo ComplexTypePropertyUseHiLoMethodInfo + = typeof(SqlServerComplexTypePropertyBuilderExtensions).GetRuntimeMethod( + nameof(SqlServerComplexTypePropertyBuilderExtensions.UseHiLo), [typeof(ComplexTypePropertyBuilder), typeof(string), typeof(string)])!; + private static readonly MethodInfo PropertyUseSequenceMethodInfo = typeof(SqlServerPropertyBuilderExtensions).GetRuntimeMethod( nameof(SqlServerPropertyBuilderExtensions.UseSequence), [typeof(PropertyBuilder), typeof(string), typeof(string)])!; + private static readonly MethodInfo ComplexTypePropertyUseSequenceMethodInfo + = typeof(SqlServerComplexTypePropertyBuilderExtensions).GetRuntimeMethod( + nameof(SqlServerComplexTypePropertyBuilderExtensions.UseSequence), [typeof(ComplexTypePropertyBuilder), typeof(string), typeof(string)])!; + private static readonly MethodInfo IndexIsClusteredMethodInfo = typeof(SqlServerIndexBuilderExtensions).GetRuntimeMethod( nameof(SqlServerIndexBuilderExtensions.IsClustered), [typeof(IndexBuilder), typeof(bool)])!; @@ -144,7 +168,7 @@ public override IReadOnlyList GenerateFluentApiCalls( { var fragments = new List(base.GenerateFluentApiCalls(model, annotations)); - if (GenerateValueGenerationStrategy(annotations, model, onModel: true) is MethodCallCodeFragment valueGenerationStrategy) + if (GenerateValueGenerationStrategy(annotations, model, onModel: true, complexType: false) is MethodCallCodeFragment valueGenerationStrategy) { fragments.Add(valueGenerationStrategy); } @@ -179,7 +203,9 @@ public override IReadOnlyList GenerateFluentApiCalls( { var fragments = new List(base.GenerateFluentApiCalls(property, annotations)); - if (GenerateValueGenerationStrategy(annotations, property.DeclaringType.Model, onModel: false) is MethodCallCodeFragment + var isPrimitiveCollection = property.IsPrimitiveCollection; + + if (GenerateValueGenerationStrategy(annotations, property.DeclaringType.Model, onModel: false, complexType: property.DeclaringType is IComplexType) is MethodCallCodeFragment valueGenerationStrategy) { fragments.Add(valueGenerationStrategy); @@ -187,10 +213,17 @@ public override IReadOnlyList GenerateFluentApiCalls( if (GetAndRemove(annotations, SqlServerAnnotationNames.Sparse) is bool isSparse) { + var methodInfo = isPrimitiveCollection + ? property.DeclaringType is IComplexType + ? ComplexTypePrimitiveCollectionIsSparseMethodInfo + : PrimitiveCollectionIsSparseMethodInfo + : property.DeclaringType is IComplexType + ? ComplexTypePropertyIsSparseMethodInfo + : PropertyIsSparseMethodInfo; fragments.Add( isSparse - ? new MethodCallCodeFragment(PropertyIsSparseMethodInfo) - : new MethodCallCodeFragment(PropertyIsSparseMethodInfo, false)); + ? new MethodCallCodeFragment(methodInfo) + : new MethodCallCodeFragment(methodInfo, false)); } return fragments; @@ -367,7 +400,8 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an private static MethodCallCodeFragment? GenerateValueGenerationStrategy( IDictionary annotations, IModel model, - bool onModel) + bool onModel, + bool complexType) { SqlServerValueGenerationStrategy strategy; if (annotations.TryGetValue(SqlServerAnnotationNames.ValueGenerationStrategy, out var strategyAnnotation) @@ -405,7 +439,11 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an ?? model.FindAnnotation(SqlServerAnnotationNames.IdentityIncrement)?.Value as int? ?? 1; return new MethodCallCodeFragment( - onModel ? ModelUseIdentityColumnsMethodInfo : PropertyUseIdentityColumnsMethodInfo, + onModel + ? ModelUseIdentityColumnsMethodInfo + : complexType + ? ComplexTypePropertyUseIdentityColumnsMethodInfo + : PropertyUseIdentityColumnsMethodInfo, (seed, increment) switch { (1L, 1) => [], @@ -418,7 +456,11 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an var name = GetAndRemove(annotations, SqlServerAnnotationNames.HiLoSequenceName); var schema = GetAndRemove(annotations, SqlServerAnnotationNames.HiLoSequenceSchema); return new MethodCallCodeFragment( - onModel ? ModelUseHiLoMethodInfo : PropertyUseHiLoMethodInfo, + onModel + ? ModelUseHiLoMethodInfo + : complexType + ? ComplexTypePropertyUseHiLoMethodInfo + : PropertyUseHiLoMethodInfo, (name, schema) switch { (null, null) => [], @@ -435,7 +477,11 @@ protected override bool IsHandledByConvention(IProperty property, IAnnotation an var schema = GetAndRemove(annotations, SqlServerAnnotationNames.SequenceSchema); return new MethodCallCodeFragment( - onModel ? ModelUseKeySequencesMethodInfo : PropertyUseSequenceMethodInfo, + onModel + ? ModelUseKeySequencesMethodInfo + : complexType + ? ComplexTypePropertyUseSequenceMethodInfo + : PropertyUseSequenceMethodInfo, (name: nameOrSuffix, schema) switch { (null, null) => [], diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index a3ac3eff14b..d9216f9dfe5 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -5944,6 +5944,72 @@ public virtual void SQLServer_property_legacy_identity_seed_int_annotation() #endregion + #region Primitive collection + + [ConditionalFact] + public virtual void PrimitiveCollection_is_stored_in_snapshot() + => Test( + builder => + { + builder.Entity() + .PrimitiveCollection>("List") + .IsSparse() + .IsFixedLength() + .HasMaxLength(100) + .IsUnicode() + .UseCollation("ListCollation") + .HasSentinel([]) + .HasColumnName("ListColumn") + .HasColumnType("nvarchar") + .HasColumnOrder(1) + .HasComment("ListComment") + .HasComputedColumnSql("ListSql") + .HasJsonPropertyName("ListJson") + .ElementType(b => b.HasConversion()) + .ValueGeneratedOnUpdateSometimes() + .HasAnnotation("AnnotationName", "AnnotationValue"); + + builder.Ignore(); + }, + AddBoilerPlate( + GetHeading() + + """ + modelBuilder.Entity("Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.PrimitiveCollection("List") + .ValueGeneratedOnUpdateSometimes() + .HasMaxLength(100) + .IsUnicode(true) + .HasColumnType("nvarchar") + .HasColumnName("ListColumn") + .HasColumnOrder(1) + .HasComputedColumnSql("ListSql") + .IsFixedLength() + .HasComment("ListComment") + .UseCollation("ListCollation") + .HasAnnotation("AnnotationName", "AnnotationValue") + .HasAnnotation("Relational:JsonPropertyName", "ListJson"); + + SqlServerPrimitiveCollectionBuilderExtensions.IsSparse(b.PrimitiveCollection("List")); + + b.HasKey("Id"); + + b.ToTable("EntityWithOneProperty", "DefaultSchema"); + }); +"""), + o => + { + var property = o.GetEntityTypes().First().FindProperty("List"); + Assert.Equal("AnnotationValue", property["AnnotationName"]); + }); + #endregion + #region Complex types [ConditionalFact] @@ -5958,8 +6024,13 @@ public virtual void Complex_properties_are_stored_in_snapshot() eo => eo.EntityWithTwoProperties, eb => { eb.IsRequired(); - eb.Property(e => e.AlternateId).HasColumnOrder(1); - eb.ComplexProperty(e => e.EntityWithStringKey).IsRequired(); + eb.Property(e => e.AlternateId).HasColumnOrder(1).IsSparse(); + eb.PrimitiveCollection>("List") + .HasColumnType("nvarchar(max)") + .IsSparse(); + eb.ComplexProperty(e => e.EntityWithStringKey) + .IsRequired() + .Ignore(e => e.Properties); eb.HasPropertyAnnotation("PropertyAnnotation", 1); eb.HasTypeAnnotation("TypeAnnotation", 2); }); @@ -5984,9 +6055,16 @@ public virtual void Complex_properties_are_stored_in_snapshot() .HasColumnType("int") .HasColumnOrder(1); + SqlServerComplexTypePropertyBuilderExtensions.IsSparse(b1.Property("AlternateId")); + b1.Property("Id") .HasColumnType("int"); + b1.PrimitiveCollection("List") + .HasColumnType("nvarchar(max)"); + + SqlServerComplexTypePrimitiveCollectionBuilderExtensions.IsSparse(b1.PrimitiveCollection("List")); + b1.ComplexProperty>("EntityWithStringKey", "Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGeneratorTest+EntityWithOneProperty.EntityWithTwoProperties#EntityWithTwoProperties.EntityWithStringKey#EntityWithStringKey", b2 => { b2.IsRequired(); @@ -6005,7 +6083,7 @@ public virtual void Complex_properties_are_stored_in_snapshot() b.ToTable("EntityWithOneProperty", "DefaultSchema"); }); """, usingCollections: true), - o => + (_, o) => { var entityWithOneProperty = o.FindEntityType(typeof(EntityWithOneProperty)); Assert.Equal(nameof(EntityWithOneProperty), entityWithOneProperty.GetTableName()); @@ -6037,7 +6115,8 @@ public virtual void Complex_properties_are_stored_in_snapshot() Assert.Equal(nameof(EntityWithOneProperty), nestedComplexType.GetTableName()); var nestedIdProperty = nestedComplexType.FindProperty(nameof(EntityWithStringKey.Id)); Assert.True(nestedIdProperty.IsNullable); - }); + }, + validate: true); #endregion @@ -7981,7 +8060,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); - b.Property("BoolCollection") + b.PrimitiveCollection("BoolCollection") .HasColumnType("nvarchar(max)"); b.Property("Boolean") @@ -7993,7 +8072,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Bytes") .HasColumnType("varbinary(max)"); - b.Property("BytesCollection") + b.PrimitiveCollection("BytesCollection") .HasColumnType("nvarchar(max)"); b.Property("Character") @@ -8003,7 +8082,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DateTime") .HasColumnType("datetime2"); - b.Property("DateTimeCollection") + b.PrimitiveCollection("DateTimeCollection") .HasColumnType("nvarchar(max)"); b.Property("DateTimeOffset") @@ -8015,7 +8094,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Double") .HasColumnType("float"); - b.Property("DoubleCollection") + b.PrimitiveCollection("DoubleCollection") .HasColumnType("nvarchar(max)"); b.Property("Enum16") @@ -8048,7 +8127,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Int32") .HasColumnType("int"); - b.Property("Int32Collection") + b.PrimitiveCollection("Int32Collection") .HasColumnType("nvarchar(max)"); b.Property("Int64") @@ -8108,7 +8187,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("String") .HasColumnType("nvarchar(max)"); - b.Property("StringCollection") + b.PrimitiveCollection("StringCollection") .HasColumnType("nvarchar(max)"); b.Property("TimeSpan") @@ -8403,7 +8482,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) protected void Test(Action buildModel, string expectedCode, Action assert) => Test(buildModel, expectedCode, (m, _) => assert(m)); - protected void Test(Action buildModel, string expectedCode, Action assert) + protected void Test(Action buildModel, string expectedCode, Action assert, bool validate = false) { var modelBuilder = CreateConventionalModelBuilder(); modelBuilder.HasDefaultSchema("DefaultSchema"); @@ -8411,7 +8490,7 @@ protected void Test(Action buildModel, string expectedCode, Action modelBuilder.Model.RemoveAnnotation(CoreAnnotationNames.ProductVersion); buildModel(modelBuilder); - var model = modelBuilder.FinalizeModel(designTime: true, skipValidation: true); + var model = modelBuilder.FinalizeModel(designTime: true, skipValidation: !validate); Test(model, expectedCode, assert); }