Browse Source

Merge pull request #25896 from abpframework/maliming/upgrade-mysql-entityframeworkcore-10-0-9

Fix MySQL provider issues on EF Core 10
pull/25900/head
Engincan VESKE 3 weeks ago
committed by GitHub
parent
commit
f78d9c26a1
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      Directory.Packages.props
  2. 1
      docs/en/package-version-changes.md
  3. 20
      framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/AbpDbContextConfigurationContextMySQLExtensions.cs
  4. 48
      framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/AbpMySQLDbContextOptionsExtension.cs
  5. 25
      framework/src/Volo.Abp.EntityFrameworkCore.MySQL/Volo/Abp/EntityFrameworkCore/MySQL/MySQLGuidArrayTypeMappingSourcePlugin.cs
  6. 1
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo.Abp.EntityFrameworkCore.Tests.csproj
  7. 58
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/MySQL/MySQLGuidArrayTypeMappingSourcePlugin_Tests.cs
  8. 25
      modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/IdentityDbContextModelBuilderExtensions.cs

2
Directory.Packages.props

@ -142,7 +142,7 @@
<PackageVersion Include="Polly" Version="8.6.3" />
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="10.0.1" />
<PackageVersion Include="MySql.EntityFrameworkCore" Version="10.0.9" />
<PackageVersion Include="Quartz" Version="3.15.0" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.15.0" />
<PackageVersion Include="Quartz.Plugins.TimeZoneConverter" Version="3.15.0" />

1
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 |

20
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<MySql.EntityFrameworkCore.Infrastructure.MySQLDbContextOptionsBuilder>? 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;
}
}

48
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<IRelationalTypeMappingSourcePlugin, MySQLGuidArrayTypeMappingSourcePlugin>());
}
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<string, string> debugInfo)
{
debugInfo["Volo.Abp.EntityFrameworkCore.MySQL"] = "1";
}
}
}

25
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<Guid>. 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;
}
}

1
framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo.Abp.EntityFrameworkCore.Tests.csproj

@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\Volo.Abp.Autofac\Volo.Abp.Autofac.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.EntityFrameworkCore.MySQL\Volo.Abp.EntityFrameworkCore.MySQL.csproj" />
<ProjectReference Include="..\..\src\Volo.Abp.EntityFrameworkCore.Sqlite\Volo.Abp.EntityFrameworkCore.Sqlite.csproj" />
<ProjectReference Include="..\AbpTestBase\AbpTestBase.csproj" />
<ProjectReference Include="..\Volo.Abp.EntityFrameworkCore.Tests.SecondContext\Volo.Abp.EntityFrameworkCore.Tests.SecondContext.csproj" />

58
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<IRelationalTypeMappingSource>();
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<PluginTestEntity> Entities => Set<PluginTestEntity>();
}
private class PluginTestEntity
{
public Guid Id { get; set; }
}
}

25
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<IdentityPasskeyData>(s, (JsonSerializerOptions?)null)!,
new ValueComparer<IdentityPasskeyData>(
(l, r) => JsonSerializer.Serialize(l, (JsonSerializerOptions?)null) == JsonSerializer.Serialize(r, (JsonSerializerOptions?)null),
v => JsonSerializer.Serialize(v, (JsonSerializerOptions?)null).GetHashCode(),
v => JsonSerializer.Deserialize<IdentityPasskeyData>(JsonSerializer.Serialize(v, (JsonSerializerOptions?)null), (JsonSerializerOptions?)null)!));
}
else
{
b.OwnsOne(p => p.Data).ToJson();
}
b.ApplyObjectExtensionMappings();
});

Loading…
Cancel
Save