diff --git a/src/EFCore.MySql/Design/Internal/MySqlAnnotationCodeGenerator.cs b/src/EFCore.MySql/Design/Internal/MySqlAnnotationCodeGenerator.cs index ff1fb04a2..81d12b5c5 100644 --- a/src/EFCore.MySql/Design/Internal/MySqlAnnotationCodeGenerator.cs +++ b/src/EFCore.MySql/Design/Internal/MySqlAnnotationCodeGenerator.cs @@ -20,30 +20,11 @@ namespace Pomelo.EntityFrameworkCore.MySql.Design.Internal { public class MySqlAnnotationCodeGenerator : AnnotationCodeGenerator { - private static readonly MethodInfo _modelUseIdentityColumnsMethodInfo - = typeof(MySqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlModelBuilderExtensions.AutoIncrementColumns), - typeof(ModelBuilder)); - - private static readonly MethodInfo _modelHasCharSetMethodInfo - = typeof(MySqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlModelBuilderExtensions.HasCharSet), - typeof(ModelBuilder), - typeof(string), - typeof(DelegationModes?)); - - private static readonly MethodInfo _modelUseCollationMethodInfo - = typeof(MySqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlModelBuilderExtensions.UseCollation), - typeof(ModelBuilder), - typeof(string), - typeof(DelegationModes?)); - - private static readonly MethodInfo _modelUseGuidCollationMethodInfo - = typeof(MySqlModelBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlModelBuilderExtensions.UseGuidCollation), - typeof(ModelBuilder), - typeof(string)); + // NOTE: Using method name strings instead of MethodInfo for MySql extension methods. + // This ensures EF Core's CSharpSnapshotGenerator outputs fluent chaining instead of + // static method calls (e.g., .HasCharSet("latin1") instead of + // MySqlPropertyBuilderExtensions.HasCharSet(b.Property("Name"), "latin1")). + // See: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/1990 private static readonly MethodInfo _modelHasAnnotationMethodInfo = typeof(ModelBuilder).GetRequiredRuntimeMethod( @@ -51,43 +32,7 @@ private static readonly MethodInfo _modelHasAnnotationMethodInfo typeof(string), typeof(object)); - private static readonly MethodInfo _entityTypeHasCharSetMethodInfo - = typeof(MySqlEntityTypeBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlEntityTypeBuilderExtensions.HasCharSet), - typeof(EntityTypeBuilder), - typeof(string), - typeof(DelegationModes?)); - - private static readonly MethodInfo _entityTypeUseCollationMethodInfo - = typeof(MySqlEntityTypeBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlEntityTypeBuilderExtensions.UseCollation), - typeof(EntityTypeBuilder), - typeof(string), - typeof(DelegationModes?)); - - private static readonly MethodInfo _propertyUseIdentityColumnMethodInfo - = typeof(MySqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn), - typeof(PropertyBuilder)); - - private static readonly MethodInfo _propertyUseComputedColumnMethodInfo - = typeof(MySqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlPropertyBuilderExtensions.UseMySqlComputedColumn), - typeof(PropertyBuilder)); - - private static readonly MethodInfo _propertyHasCharSetMethodInfo - = typeof(MySqlPropertyBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlPropertyBuilderExtensions.HasCharSet), - typeof(PropertyBuilder), - typeof(string)); - - private static readonly MethodInfo _complexTypePropertyHasCharSetMethodInfo - = typeof(MySqlComplexTypePropertyBuilderExtensions).GetRequiredRuntimeMethod( - nameof(MySqlComplexTypePropertyBuilderExtensions.HasCharSet), - typeof(ComplexTypePropertyBuilder), - typeof(string)); - - public MySqlAnnotationCodeGenerator([JetBrains.Annotations.NotNull] AnnotationCodeGeneratorDependencies dependencies) + public MySqlAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies dependencies) : base(dependencies) { } @@ -121,7 +66,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnot { var delegationModes = model[MySqlAnnotationNames.CharSetDelegation] as DelegationModes?; return new MethodCallCodeFragment( - _modelHasCharSetMethodInfo, + nameof(MySqlModelBuilderExtensions.HasCharSet), new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); @@ -131,7 +76,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnot model[MySqlAnnotationNames.CharSet] is null) { return new MethodCallCodeFragment( - _modelHasCharSetMethodInfo, + nameof(MySqlModelBuilderExtensions.HasCharSet), null, annotation.Value); } @@ -142,7 +87,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnot { var delegationModes = model[MySqlAnnotationNames.CollationDelegation] as DelegationModes?; return new MethodCallCodeFragment( - _modelUseCollationMethodInfo, + nameof(MySqlModelBuilderExtensions.UseCollation), new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); @@ -152,7 +97,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnot model[RelationalAnnotationNames.Collation] is null) { return new MethodCallCodeFragment( - _modelUseCollationMethodInfo, + nameof(MySqlModelBuilderExtensions.UseCollation), null, annotation.Value); } @@ -160,7 +105,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnot if (annotation.Name == MySqlAnnotationNames.GuidCollation) { return new MethodCallCodeFragment( - _modelUseGuidCollationMethodInfo, + nameof(MySqlModelBuilderExtensions.UseGuidCollation), annotation.Value); } @@ -173,7 +118,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IEntityType entityTy { var delegationModes = entityType[MySqlAnnotationNames.CharSetDelegation] as DelegationModes?; return new MethodCallCodeFragment( - _entityTypeHasCharSetMethodInfo, + nameof(MySqlEntityTypeBuilderExtensions.HasCharSet), new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); @@ -183,7 +128,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IEntityType entityTy entityType[MySqlAnnotationNames.CharSet] is null) { return new MethodCallCodeFragment( - _entityTypeHasCharSetMethodInfo, + nameof(MySqlEntityTypeBuilderExtensions.HasCharSet), null, annotation.Value); } @@ -192,7 +137,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IEntityType entityTy { var delegationModes = entityType[MySqlAnnotationNames.CollationDelegation] as DelegationModes?; return new MethodCallCodeFragment( - _entityTypeUseCollationMethodInfo, + nameof(MySqlEntityTypeBuilderExtensions.UseCollation), new[] { annotation.Value } .AppendIfTrue(delegationModes.HasValue, delegationModes) .ToArray()); @@ -202,7 +147,7 @@ protected override MethodCallCodeFragment GenerateFluentApi(IEntityType entityTy entityType[RelationalAnnotationNames.Collation] is null) { return new MethodCallCodeFragment( - _entityTypeUseCollationMethodInfo, + nameof(MySqlEntityTypeBuilderExtensions.UseCollation), null, annotation.Value); } @@ -269,15 +214,8 @@ protected override MethodCallCodeFragment GenerateFluentApi(IProperty property, switch (annotation.Name) { case MySqlAnnotationNames.CharSet when annotation.Value is string { Length: > 0 } charSet: - if (property.DeclaringType is IComplexType) - { - return new MethodCallCodeFragment( - _complexTypePropertyHasCharSetMethodInfo, - charSet); - } - return new MethodCallCodeFragment( - _propertyHasCharSetMethodInfo, + nameof(MySqlPropertyBuilderExtensions.HasCharSet), charSet); default: @@ -334,9 +272,10 @@ private MethodCallCodeFragment GenerateValueGenerationStrategy(IDictionary new MethodCallCodeFragment( onModel - ? _modelUseIdentityColumnsMethodInfo - : _propertyUseIdentityColumnMethodInfo), - MySqlValueGenerationStrategy.ComputedColumn => new MethodCallCodeFragment(_propertyUseComputedColumnMethodInfo), + ? nameof(MySqlModelBuilderExtensions.AutoIncrementColumns) + : nameof(MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn)), + MySqlValueGenerationStrategy.ComputedColumn => new MethodCallCodeFragment( + nameof(MySqlPropertyBuilderExtensions.UseMySqlComputedColumn)), MySqlValueGenerationStrategy.None => new MethodCallCodeFragment( _modelHasAnnotationMethodInfo, MySqlAnnotationNames.ValueGenerationStrategy, diff --git a/test/EFCore.MySql.Tests/Design/MySqlAnnotationCodeGeneratorTest.cs b/test/EFCore.MySql.Tests/Design/MySqlAnnotationCodeGeneratorTest.cs new file mode 100644 index 000000000..e73e69ee2 --- /dev/null +++ b/test/EFCore.MySql.Tests/Design/MySqlAnnotationCodeGeneratorTest.cs @@ -0,0 +1,119 @@ +// Copyright (c) Pomelo Foundation. All rights reserved. +// Licensed under the MIT. See LICENSE in the project root for license information. + +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; +using Moq; +using Pomelo.EntityFrameworkCore.MySql.Design.Internal; +using Pomelo.EntityFrameworkCore.MySql.Metadata.Internal; +using Xunit; + +namespace Pomelo.EntityFrameworkCore.MySql.Design +{ + /// + /// Tests for ensuring that generated + /// MethodCallCodeFragment instances produce fluent chaining output instead of static method calls. + /// + /// + /// Fixes: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/1990 + /// + public class MySqlAnnotationCodeGeneratorTest + { + [Fact] + public void GenerateFluentApiCalls_for_property_identity_column_has_null_MethodInfo() + { + // Arrange + var modelBuilder = new ModelBuilder(); + modelBuilder.Entity("TestEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation(MySqlAnnotationNames.ValueGenerationStrategy, MySqlValueGenerationStrategy.IdentityColumn); + }); + + var model = modelBuilder.FinalizeModel(); + var property = model.FindEntityType("TestEntity")!.FindProperty("Id")!; + + var annotations = property.GetAnnotations() + .ToDictionary(a => a.Name, a => a); + + var generator = CreateGenerator(); + + // Act + var fragments = generator.GenerateFluentApiCalls(property, annotations); + + // Assert + var identityFragment = fragments.FirstOrDefault(f => f.Method == "UseMySqlIdentityColumn"); + Assert.NotNull(identityFragment); + + // Key assertion: MethodInfo must be null for fluent chaining + // When MethodInfo is null, EF Core's CSharpSnapshotGenerator outputs fluent chaining + // instead of static method calls like MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(...) + Assert.Null(identityFragment.MethodInfo); + } + + [Fact] + public void GenerateFluentApiCalls_for_property_computed_column_has_null_MethodInfo() + { + // Arrange + var modelBuilder = new ModelBuilder(); + modelBuilder.Entity("TestEntity", b => + { + b.Property("ComputedValue") + .HasAnnotation(MySqlAnnotationNames.ValueGenerationStrategy, MySqlValueGenerationStrategy.ComputedColumn); + }); + + var model = modelBuilder.FinalizeModel(); + var property = model.FindEntityType("TestEntity")!.FindProperty("ComputedValue")!; + + var annotations = property.GetAnnotations() + .ToDictionary(a => a.Name, a => a); + + var generator = CreateGenerator(); + + // Act + var fragments = generator.GenerateFluentApiCalls(property, annotations); + + // Assert + var computedFragment = fragments.FirstOrDefault(f => f.Method == "UseMySqlComputedColumn"); + Assert.NotNull(computedFragment); + Assert.Null(computedFragment.MethodInfo); + } + + [Fact] + public void GenerateFluentApiCalls_for_model_auto_increment_has_null_MethodInfo() + { + // Arrange + var modelBuilder = new ModelBuilder(); + modelBuilder.HasAnnotation( + MySqlAnnotationNames.ValueGenerationStrategy, + MySqlValueGenerationStrategy.IdentityColumn); + + var model = modelBuilder.FinalizeModel(); + + var annotations = model.GetAnnotations() + .ToDictionary(a => a.Name, a => a); + + var generator = CreateGenerator(); + + // Act + var fragments = generator.GenerateFluentApiCalls(model, annotations); + + // Assert + var autoIncrementFragment = fragments.FirstOrDefault(f => f.Method == "AutoIncrementColumns"); + Assert.NotNull(autoIncrementFragment); + Assert.Null(autoIncrementFragment.MethodInfo); + } + + private static MySqlAnnotationCodeGenerator CreateGenerator() + { + var typeMappingSourceMock = new Mock(); + var dependencies = new AnnotationCodeGeneratorDependencies(typeMappingSourceMock.Object); + + return new MySqlAnnotationCodeGenerator(dependencies); + } + } +}