diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextMySQLExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextMySQLExtensions.cs index f04f8fd4f3..2ec3383ddc 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextMySQLExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextMySQLExtensions.cs @@ -1,7 +1,9 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; using System; using Volo.Abp.EntityFrameworkCore.DependencyInjection; +using Volo.Abp.EntityFrameworkCore.MySQL; namespace Volo.Abp.EntityFrameworkCore; @@ -11,21 +13,21 @@ public static class AbpDbContextConfigurationContextMySQLExtensions [NotNull] this AbpDbContextConfigurationContext context, Action? mySQLOptionsAction = null) { - if (context.ExistingConnection != null) - { - return context.DbContextOptions.UseMySQL(context.ExistingConnection, optionsBuilder => + var dbContextOptionsBuilder = context.ExistingConnection != null + ? context.DbContextOptions.UseMySQL(context.ExistingConnection, optionsBuilder => { optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); mySQLOptionsAction?.Invoke(optionsBuilder); - }); - } - else - { - return context.DbContextOptions.UseMySQL(context.ConnectionString, optionsBuilder => + }) + : context.DbContextOptions.UseMySQL(context.ConnectionString, optionsBuilder => { optionsBuilder.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery); mySQLOptionsAction?.Invoke(optionsBuilder); }); - } + + ((IDbContextOptionsBuilderInfrastructure)dbContextOptionsBuilder) + .AddOrUpdateExtension(new AbpMySQLDbContextOptionsExtension()); + + return dbContextOptionsBuilder; } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/AbpMySQLDbContextOptionsExtension.cs b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/AbpMySQLDbContextOptionsExtension.cs new file mode 100644 index 0000000000..bea4ed8432 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/AbpMySQLDbContextOptionsExtension.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Volo.Abp.EntityFrameworkCore.MySQL; + +/* Registers ABP services into the EF Core internal service provider to patch + * MySQL provider issues (currently the Guid[] type mapping plugin). */ +internal sealed class AbpMySQLDbContextOptionsExtension : IDbContextOptionsExtension +{ + public DbContextOptionsExtensionInfo Info => new ExtensionInfo(this); + + public void ApplyServices(IServiceCollection services) + { + services.TryAddEnumerable( + ServiceDescriptor.Singleton()); + } + + public void Validate(IDbContextOptions options) + { + } + + private sealed class ExtensionInfo : DbContextOptionsExtensionInfo + { + public ExtensionInfo(IDbContextOptionsExtension extension) + : base(extension) + { + } + + public override bool IsDatabaseProvider => false; + + public override string LogFragment => "using AbpMySQL "; + + public override int GetServiceProviderHashCode() => 0; + + public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) + { + return other is ExtensionInfo; + } + + public override void PopulateDebugInfo(IDictionary debugInfo) + { + debugInfo["Volo.Abp.EntityFrameworkCore.MySQL"] = "1"; + } + } +} diff --git a/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/MySQLGuidArrayTypeMappingSourcePlugin.cs b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/MySQLGuidArrayTypeMappingSourcePlugin.cs new file mode 100644 index 0000000000..e3ac23563c --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/MySQLGuidArrayTypeMappingSourcePlugin.cs @@ -0,0 +1,25 @@ +using System; +using System.Data; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Volo.Abp.EntityFrameworkCore.MySQL; + +/* MySql.EntityFrameworkCore (up to 10.0.9) maps Guid[] query parameters to its + * scalar GUID mapping and throws NullReferenceException at parameter binding. + * This plugin runs before the provider's own lookup and returns the collection + * mapping the provider already builds for List. Remove once the provider + * handles Guid[] parameters. */ +internal sealed class MySQLGuidArrayTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin +{ + public RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo) + { + if (mappingInfo.ClrType == typeof(Guid[]) && mappingInfo.ElementTypeMapping is not null) + { + return new StringTypeMapping("longtext", DbType.String).Clone( + clrType: typeof(Guid[]), + elementMapping: mappingInfo.ElementTypeMapping); + } + + return null; + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo.Abp.EntityFrameworkCore.Tests.csproj b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo.Abp.EntityFrameworkCore.Tests.csproj index 77b1448ded..137dba20f4 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo.Abp.EntityFrameworkCore.Tests.csproj +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo.Abp.EntityFrameworkCore.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/MySQL/MySQLGuidArrayTypeMappingSourcePlugin_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/MySQL/MySQLGuidArrayTypeMappingSourcePlugin_Tests.cs new file mode 100644 index 0000000000..2114e87b0e --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/MySQL/MySQLGuidArrayTypeMappingSourcePlugin_Tests.cs @@ -0,0 +1,58 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.EntityFrameworkCore.DependencyInjection; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.MySQL; + +public class MySQLGuidArrayTypeMappingSourcePlugin_Tests +{ + /* Runs without a MySQL server: type mapping lookup is metadata-only, no + * connection is opened. When upgrading the provider, verify its native + * Guid[] mapping without the plugin registered; once the provider handles + * Guid[] itself, remove MySQLGuidArrayTypeMappingSourcePlugin together + * with this test. */ + [Fact] + public void UseMySQL_Should_Map_Guid_Array_Parameter_To_A_Collection_Mapping() + { + var services = new ServiceCollection(); + services.AddLogging(); + + var configurationContext = new AbpDbContextConfigurationContext( + "Server=localhost;Database=_;Uid=_;Pwd=_;", + services.BuildServiceProvider(), + null, + null); + configurationContext.UseMySQL(); + + using var dbContext = new PluginTestDbContext(configurationContext.DbContextOptions.Options); + var typeMappingSource = dbContext.GetService(); + var elementMapping = typeMappingSource.FindMapping(typeof(Guid))!; + + var mapping = typeMappingSource.FindMapping(typeof(Guid[]), dbContext.Model, elementMapping); + + mapping.ShouldNotBeNull(); + mapping.ClrType.ShouldBe(typeof(Guid[])); + mapping.StoreType.ShouldBe("longtext"); + mapping.ElementTypeMapping.ShouldBe(elementMapping); + } + + private class PluginTestDbContext : DbContext + { + public PluginTestDbContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Entities => Set(); + } + + private class PluginTestEntity + { + public Guid Id { get; set; } + } +}