From 9dbad69834722487f9d1a30a8b4a609295699a53 Mon Sep 17 00:00:00 2001 From: ANASGAUBA Date: Wed, 31 Jul 2024 09:17:32 -0600 Subject: [PATCH] Fix regression to allow custom value conversion mapping when the ClrType is of non-NTS type. plus added unit test. - regression introduced in commit [3ef628f](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/commit/3ef628ff7dfc38b8a4a8075fd9d7c5677f48829a#diff-4f1d25d8c94e4764d1beef032d7ecc17efaf6a2f592db3dab748bac1d7fc0446R87) Refs #1921 --- Dependencies.targets | 1 + ...NetTopologySuiteTypeMappingSourcePlugin.cs | 28 ++++-- .../EFCore.MySql.FunctionalTests.csproj | 1 + ...lueConvertersForNonNTSClrTypesMySqlTest.cs | 96 +++++++++++++++++++ 4 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 test/EFCore.MySql.FunctionalTests/NTS/CustomValueConvertersForNonNTSClrTypesMySqlTest.cs diff --git a/Dependencies.targets b/Dependencies.targets index 4db5d770..021f2946 100644 --- a/Dependencies.targets +++ b/Dependencies.targets @@ -46,6 +46,7 @@ + diff --git a/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs b/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs index c9bbd5d2..53b2fe98 100644 --- a/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs +++ b/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs @@ -80,16 +80,30 @@ public virtual RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo ma string defaultStoreType = null; Type defaultClrType = null; - return (clrType != null - && TryGetDefaultStoreType(clrType, out defaultStoreType)) - || (storeTypeName != null - && _spatialStoreTypeMappings.TryGetValue(storeTypeName, out defaultClrType)) - ? (RelationalTypeMapping)Activator.CreateInstance( + var hasDefaultStoreType = clrType != null + && TryGetDefaultStoreType(clrType, out defaultStoreType); + var hasDefaultClrType = storeTypeName != null + && _spatialStoreTypeMappings.TryGetValue(storeTypeName, out defaultClrType); + + if (!(hasDefaultStoreType || hasDefaultClrType)) + { + return null; + } + + // NOTE: If the incoming user-specified 'clrType' is of the known calculated 'defaultClrType', ONLY then proceeed + // with the creation of 'MySqlGeometryTypeMapping'. + clrType = clrType == null + ? defaultClrType + : clrType.IsAssignableFrom(defaultClrType) + ? clrType + : null; + return clrType == null + ? null + : (RelationalTypeMapping)Activator.CreateInstance( typeof(MySqlGeometryTypeMapping<>).MakeGenericType(clrType ?? defaultClrType ?? typeof(Geometry)), _geometryServices, storeTypeName ?? defaultStoreType ?? "geometry", - _options) - : null; + _options); } private static bool TryGetDefaultStoreType(Type type, out string defaultStoreType) diff --git a/test/EFCore.MySql.FunctionalTests/EFCore.MySql.FunctionalTests.csproj b/test/EFCore.MySql.FunctionalTests/EFCore.MySql.FunctionalTests.csproj index efe3e927..3bbfea37 100644 --- a/test/EFCore.MySql.FunctionalTests/EFCore.MySql.FunctionalTests.csproj +++ b/test/EFCore.MySql.FunctionalTests/EFCore.MySql.FunctionalTests.csproj @@ -32,6 +32,7 @@ + diff --git a/test/EFCore.MySql.FunctionalTests/NTS/CustomValueConvertersForNonNTSClrTypesMySqlTest.cs b/test/EFCore.MySql.FunctionalTests/NTS/CustomValueConvertersForNonNTSClrTypesMySqlTest.cs new file mode 100644 index 00000000..8ec72dfd --- /dev/null +++ b/test/EFCore.MySql.FunctionalTests/NTS/CustomValueConvertersForNonNTSClrTypesMySqlTest.cs @@ -0,0 +1,96 @@ +namespace Pomelo.EntityFrameworkCore.MySql.FunctionalTests.NTS; + +using System; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NetTopologySuite.Geometries; +using Pomelo.EntityFrameworkCore.MySql.Tests; +using Shouldly; +using Xunit; + +public class CustomValueConvertersForNonNTSClrTypesMySqlTest +{ + [Fact] + public async Task NonNTS_ClrType_CanBe_Mapped_to_CustomValueConverters() + { + var services = new ServiceCollection() + .AddDbContext() + .AddSingleton( + new DbContextOptionsBuilder() + .UseMySql( + AppConfig.ConnectionString, + AppConfig.ServerVersion, + options => + { + options.UseNetTopologySuite(); + }) + .EnableSensitiveDataLogging() + .LogTo(Console.WriteLine, LogLevel.Debug) + .Options); + + var provider = services.BuildServiceProvider(); + await using var context = provider.GetRequiredService(); + + // Validate `MySqlNetTopologySuiteTypeMappingSourcePlugin.FindMapping()` doesn't throw. + await Should.NotThrowAsync(context.Database.EnsureDeletedAsync()); + await Should.NotThrowAsync(context.Database.EnsureCreatedAsync()); + } +} + +public sealed class CustomDbContext(DbContextOptions options) : DbContext(options) +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var testClass = modelBuilder.Entity(); + testClass.Property(t => t.Vertices) + .HasColumnType("GEOMETRY") + .HasConversion(new MysqlGeometryWkbValueConverter()); + + var testClass2 = modelBuilder.Entity(); + testClass2.Property(t => t.Vertices) + .HasColumnType("GEOMETRY"); + } +} + +public class TestClass +{ + public int Id { get; set; } +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public byte[] Vertices { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +} + +public class TestClass2 +{ + public int Id { get; set; } + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. + public Geometry Vertices { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. +} + +/// +/// MySql's internal geometry format is WKB with an initial 4 +/// bytes for the SRID: +/// https://dev.mysql.com/doc/refman/5.7/en/gis-data-formats.html +/// +public class MysqlGeometryWkbValueConverter : ValueConverter +{ + public MysqlGeometryWkbValueConverter() + : base( + clr => AddSRID(clr), + col => StripSRID(col)) + { + } + + private static byte[] AddSRID(byte[] wkb) => + new byte[] { 0, 0, 0, 0, }.Concat(wkb).ToArray(); + + private static byte[] StripSRID(byte[] col) => + col.Skip(4).ToArray(); +}