Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 20 additions & 81 deletions src/EFCore.MySql/Design/Internal/MySqlAnnotationCodeGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,74 +20,19 @@ 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<string>("Name"), "latin1")).
// See: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/1990

private static readonly MethodInfo _modelHasAnnotationMethodInfo
= typeof(ModelBuilder).GetRequiredRuntimeMethod(
nameof(ModelBuilder.HasAnnotation),
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)
{
}
Expand Down Expand Up @@ -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());
Expand All @@ -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);
}
Expand All @@ -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());
Expand All @@ -152,15 +97,15 @@ protected override MethodCallCodeFragment GenerateFluentApi(IModel model, IAnnot
model[RelationalAnnotationNames.Collation] is null)
{
return new MethodCallCodeFragment(
_modelUseCollationMethodInfo,
nameof(MySqlModelBuilderExtensions.UseCollation),
null,
annotation.Value);
}

if (annotation.Name == MySqlAnnotationNames.GuidCollation)
{
return new MethodCallCodeFragment(
_modelUseGuidCollationMethodInfo,
nameof(MySqlModelBuilderExtensions.UseGuidCollation),
annotation.Value);
}

Expand All @@ -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());
Expand All @@ -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);
}
Expand All @@ -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());
Expand All @@ -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);
}
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -334,9 +272,10 @@ private MethodCallCodeFragment GenerateValueGenerationStrategy(IDictionary<strin
{
MySqlValueGenerationStrategy.IdentityColumn => 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,
Expand Down
119 changes: 119 additions & 0 deletions test/EFCore.MySql.Tests/Design/MySqlAnnotationCodeGeneratorTest.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Tests for <see cref="MySqlAnnotationCodeGenerator"/> ensuring that generated
/// MethodCallCodeFragment instances produce fluent chaining output instead of static method calls.
/// </summary>
/// <remarks>
/// Fixes: https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/1990
/// </remarks>
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<int>("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<int>("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<IRelationalTypeMappingSource>();
var dependencies = new AnnotationCodeGeneratorDependencies(typeMappingSourceMock.Object);

return new MySqlAnnotationCodeGenerator(dependencies);
}
}
}