diff --git a/Directory.Packages.props b/Directory.Packages.props
index d0eb7e479f..f809304e31 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -142,7 +142,7 @@
-
+
diff --git a/docs/en/package-version-changes.md b/docs/en/package-version-changes.md
index aa1d4e4ed2..98ee15b919 100644
--- a/docs/en/package-version-changes.md
+++ b/docs/en/package-version-changes.md
@@ -11,6 +11,7 @@
| Package | Old Version | New Version | PR |
|---------|-------------|-------------|-----|
+| MySql.EntityFrameworkCore | 10.0.1 | 10.0.9 | #25896 |
| Scriban | 7.2.1 | 7.2.5 | #25862 |
| Swashbuckle.AspNetCore | 10.0.1 | 10.2.3 | #25759 |
| System.Security.Cryptography.Xml | 10.0.7 | 10.0.10 | #25862 |
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; }
+ }
+}
diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs
index 429863c5ae..c1a94abce6 100644
--- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs
+++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs
@@ -1,6 +1,8 @@
using System;
+using System.Text.Json;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.ChangeTracking;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.Users.EntityFrameworkCore;
@@ -187,7 +189,28 @@ public static class IdentityDbContextModelBuilderExtensions
b.HasKey(p => p.CredentialId);
b.Property(p => p.CredentialId).HasMaxLength(IdentityUserPasskeyConsts.MaxCredentialIdLength); // Defined in WebAuthn spec to be no longer than 1023 bytes
- b.OwnsOne(p => p.Data).ToJson();
+
+ if (builder.IsUsingMySQL())
+ {
+ /* MySQL providers do not support EF Core JSON columns (ToJson),
+ * so store Data as a serialized json column with the same column
+ * name and content. The comparer detects in-place mutations
+ * (e.g. sign count updates on login). */
+ b.Property(p => p.Data)
+ .HasColumnName(nameof(IdentityUserPasskey.Data))
+ .HasColumnType("json")
+ .HasConversion(
+ d => JsonSerializer.Serialize(d, (JsonSerializerOptions?)null),
+ s => JsonSerializer.Deserialize(s, (JsonSerializerOptions?)null)!,
+ new ValueComparer(
+ (l, r) => JsonSerializer.Serialize(l, (JsonSerializerOptions?)null) == JsonSerializer.Serialize(r, (JsonSerializerOptions?)null),
+ v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null).GetHashCode(),
+ v => JsonSerializer.Deserialize(JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), (JsonSerializerOptions?)null)!));
+ }
+ else
+ {
+ b.OwnsOne(p => p.Data).ToJson();
+ }
b.ApplyObjectExtensionMappings();
});