diff --git a/Directory.Packages.props b/Directory.Packages.props
index ae6249cd21..5fc14be5c9 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 f9c753a6c1..216dcf6e05 100644
--- a/docs/en/package-version-changes.md
+++ b/docs/en/package-version-changes.md
@@ -82,6 +82,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/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs
index 32dfa93807..6febfe0511 100644
--- a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs
+++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs
@@ -7,6 +7,7 @@ using System.Linq.Expressions;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Data;
+using Volo.Abp.DynamicProxy;
using Volo.Abp.ObjectExtending;
using Volo.Abp.ObjectMapping;
@@ -44,9 +45,13 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider
var mapper = ServiceProvider.GetService>();
if (mapper != null)
{
+ var mapExtraPropertiesAttribute = mapper.GetType().GetSingleAttributeOrNull();
mapper.BeforeMap((TSource)source);
var destination = mapper.Map((TSource)source);
- TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, new ExtraPropertyDictionary());
+ var destinationExtraProperties = mapExtraPropertiesAttribute == null
+ ? new ExtraPropertyDictionary()
+ : GetDestinationExtraPropertiesSeed((TSource)source, destination);
+ TryMapExtraProperties(mapExtraPropertiesAttribute, (TSource)source, destination, destinationExtraProperties);
mapper.AfterMap((TSource)source, destination);
return destination;
}
@@ -54,9 +59,13 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider
var reverseMapper = ServiceProvider.GetService>();
if (reverseMapper != null)
{
+ var mapExtraPropertiesAttribute = reverseMapper.GetType().GetSingleAttributeOrNull();
reverseMapper.BeforeReverseMap((TSource)source);
var destination = reverseMapper.ReverseMap((TSource)source);
- TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, new ExtraPropertyDictionary());
+ var destinationExtraProperties = mapExtraPropertiesAttribute == null
+ ? new ExtraPropertyDictionary()
+ : GetDestinationExtraPropertiesSeed((TSource)source, destination);
+ TryMapExtraProperties(mapExtraPropertiesAttribute, (TSource)source, destination, destinationExtraProperties);
reverseMapper.AfterReverseMap((TSource)source, destination);
return destination;
}
@@ -219,6 +228,38 @@ public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider
return Expression.Lambda>(callConvert, instanceParam, sourceParam, destinationParam).Compile();
}
+ protected virtual ExtraPropertyDictionary GetDestinationExtraPropertiesSeed(TSource source, TDestination destination)
+ {
+ var extraProperties = new ExtraPropertyDictionary();
+ if (source is not IHasExtraProperties sourceHasExtraProperties ||
+ destination is not IHasExtraProperties destinationHasExtraProperties ||
+ destinationHasExtraProperties.ExtraProperties is null)
+ {
+ return extraProperties;
+ }
+
+ //Keys that don't exist in the source can only be set by the destination's constructor
+ foreach (var property in destinationHasExtraProperties.ExtraProperties)
+ {
+ if (sourceHasExtraProperties.ExtraProperties == null || !sourceHasExtraProperties.ExtraProperties.ContainsKey(property.Key))
+ {
+ extraProperties[property.Key] = property.Value;
+ }
+ }
+
+ //Source keys may be copied by the generated mapper, reset registered ones to their default value and let the filter map the source value
+ var destinationType = ProxyHelper.UnProxy(destinationHasExtraProperties).GetType();
+ foreach (var property in ObjectExtensionManager.Instance.GetProperties(destinationType))
+ {
+ if (!extraProperties.ContainsKey(property.Name) && destinationHasExtraProperties.ExtraProperties.ContainsKey(property.Name))
+ {
+ extraProperties[property.Name] = property.GetDefaultValue();
+ }
+ }
+
+ return extraProperties;
+ }
+
protected virtual ExtraPropertyDictionary GetExtraProperties(TDestination destination)
{
var extraProperties = new ExtraPropertyDictionary();
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/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs
index 9eca171e4e..7e6d4321a4 100644
--- a/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Mapperly/AbpAutoMapperExtensibleDtoExtensions_Tests.cs
@@ -53,8 +53,10 @@ public class AbpAutoMapperExtensibleDtoExtensions_Tests : AbpIntegratedTest(person);
personDto.GetProperty("Name").ShouldBe("John"); //Defined in both classes
- personDto.GetProperty("ChildCount").ShouldBe(0); //Not defined in the source, but was set to the default value by ExtensibleTestPersonDto constructor
- personDto.GetProperty("CityName").ShouldBeNull(); //Ignored, but was set to the default value by ExtensibleTestPersonDto constructor
+ personDto.HasProperty("ChildCount").ShouldBeTrue(); //Not defined in the source, but was set to the default value by ExtensibleTestPersonDto constructor
+ personDto.GetProperty("ChildCount").ShouldBe(0);
+ personDto.HasProperty("CityName").ShouldBeTrue(); //Ignored, but was set to the default value by ExtensibleTestPersonDto constructor
+ personDto.GetProperty("CityName").ShouldBeNull();
personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination
personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes
}
diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs
index 6dc7b5e2a0..ba3f3f3fe4 100644
--- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs
@@ -97,6 +97,7 @@ public class AbpReverseMapperly_Tests : AbpIntegratedTest
var entity = _objectMapper.Map(dto);
entity.GetProperty("Tag").ShouldBe("ok");
- entity.HasProperty("Secret").ShouldBeFalse();
+ entity.HasProperty("Secret").ShouldBeTrue(); //Ignored, but was set to the default value by the ExtensibleObject constructor
+ entity.GetProperty("Secret").ShouldBeNull();
}
}
diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapExtraPropertiesDefaultSeed_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapExtraPropertiesDefaultSeed_Tests.cs
new file mode 100644
index 0000000000..bc3ee341ae
--- /dev/null
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapExtraPropertiesDefaultSeed_Tests.cs
@@ -0,0 +1,87 @@
+using System;
+using Microsoft.Extensions.DependencyInjection;
+using Shouldly;
+using Volo.Abp.Data;
+using Volo.Abp.ObjectMapping;
+using Volo.Abp.Testing;
+using Xunit;
+
+namespace Volo.Abp.Mapperly;
+
+public class MapExtraPropertiesDefaultSeed_Tests : AbpIntegratedTest
+{
+ private readonly IObjectMapper _objectMapper;
+
+ public MapExtraPropertiesDefaultSeed_Tests()
+ {
+ _objectMapper = ServiceProvider.GetRequiredService();
+ }
+
+ [Fact]
+ public void Single_Parameter_Map_Should_Preserve_Constructor_Seeded_Properties()
+ {
+ var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() }
+ .SetProperty("Tag", "ok");
+
+ var dto = _objectMapper.Map(entity);
+
+ dto.GetProperty("Tag").ShouldBe("ok"); //Defined in both classes
+ dto.GetProperty("CreatedBy").ShouldBe("system"); //Set by the ExtensibleSeededDto constructor
+ dto.HasProperty("DtoOnly").ShouldBeTrue(); //Not defined in the source, but was set to the default value by the ExtensibleObject constructor
+ dto.GetProperty("DtoOnly").ShouldBeNull();
+ }
+
+ [Fact]
+ public void Single_Parameter_Map_Should_Not_Change_The_Source_Extra_Properties()
+ {
+ var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() }
+ .SetProperty("Tag", "ok")
+ .SetProperty("CreatedBy", "leaked");
+ var originalReference = entity.ExtraProperties;
+
+ _objectMapper.Map(entity);
+
+ ReferenceEquals(entity.ExtraProperties, originalReference).ShouldBeTrue();
+ entity.GetProperty("Tag").ShouldBe("ok");
+ entity.GetProperty("CreatedBy").ShouldBe("leaked");
+ }
+
+ [Fact]
+ public void Single_Parameter_Map_Should_Not_Seed_Defaults_When_Destination_Disables_Them()
+ {
+ var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() }
+ .SetProperty("Tag", "ok");
+
+ var dto = _objectMapper.Map(entity);
+
+ dto.GetProperty("Tag").ShouldBe("ok"); //Defined in both classes
+ dto.HasProperty("DtoOnly").ShouldBeFalse(); //ExtensibleNonSeededDto constructor disables the default values seeding
+ }
+
+ [Fact]
+ public void Single_Parameter_Map_Should_Not_Leak_Filtered_Source_Values_Into_Registered_Keys()
+ {
+ var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() }
+ .SetProperty("Tag", "ok")
+ .SetProperty("DtoOnly", "leaked");
+
+ var dto = _objectMapper.Map(entity);
+
+ dto.GetProperty("Tag").ShouldBe("ok"); //Defined in both classes
+ dto.GetProperty("DtoOnly").ShouldBeNull(); //Not defined in the source, the source value must not leak
+ }
+
+ [Fact]
+ public void Single_Parameter_Map_Should_Not_Invoke_Default_Value_Factories_Without_MapExtraProperties_Attribute()
+ {
+ var entity = new ExtensibleSeededEntity { Id = Guid.NewGuid() }
+ .SetProperty("Tag", "ok");
+
+ var callsBefore = ExtensibleNoAttributeDto.CountedDefaultValueFactoryCalls;
+
+ var dto = _objectMapper.Map(entity);
+
+ (ExtensibleNoAttributeDto.CountedDefaultValueFactoryCalls - callsBefore).ShouldBe(1); //Only the ExtensibleNoAttributeDto constructor seeding
+ dto.HasProperty("Counted").ShouldBeTrue();
+ }
+}
diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs
index 5153109472..eafce45f89 100644
--- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs
@@ -21,7 +21,20 @@ public class MapperlyTestModule : AbpModule
.AddOrUpdateProperty("Tag")
.AddOrUpdateProperty("Secret")
.AddOrUpdateProperty("Tag")
- .AddOrUpdateProperty("Secret");
+ .AddOrUpdateProperty("Secret")
+ .AddOrUpdateProperty("Tag")
+ .AddOrUpdateProperty("Tag")
+ .AddOrUpdateProperty("DtoOnly")
+ .AddOrUpdateProperty("Tag")
+ .AddOrUpdateProperty("DtoOnly")
+ .AddOrUpdateProperty("Counted", options =>
+ {
+ options.DefaultValueFactory = () =>
+ {
+ ExtensibleNoAttributeDto.CountedDefaultValueFactoryCalls++;
+ return null!;
+ };
+ });
});
}
}
diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs
index 9c31b853b3..d64788a4ea 100644
--- a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs
+++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs
@@ -105,4 +105,62 @@ public partial class ExtensibleReverseMapper : TwoWayMapperBase
+{
+ public override partial ExtensibleSeededDto Map(ExtensibleSeededEntity source);
+
+ public override partial void Map(ExtensibleSeededEntity source, ExtensibleSeededDto destination);
+}
+
+[Mapper]
+[MapExtraProperties]
+public partial class ExtensibleNonSeededMapper : MapperBase
+{
+ public override partial ExtensibleNonSeededDto Map(ExtensibleSeededEntity source);
+
+ public override partial void Map(ExtensibleSeededEntity source, ExtensibleNonSeededDto destination);
+}
+
+[Mapper]
+public partial class ExtensibleNoAttributeMapper : MapperBase
+{
+ public override partial ExtensibleNoAttributeDto Map(ExtensibleSeededEntity source);
+
+ public override partial void Map(ExtensibleSeededEntity source, ExtensibleNoAttributeDto destination);
}
\ No newline at end of file
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();
});