diff --git a/Dependencies.targets b/Dependencies.targets index 4db5d7707..45864c46e 100644 --- a/Dependencies.targets +++ b/Dependencies.targets @@ -13,7 +13,7 @@ - + @@ -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 c9bbd5d21..26a6da4d3 100644 --- a/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs +++ b/src/EFCore.MySql.NTS/Storage/Internal/MySqlNetTopologySuiteTypeMappingSourcePlugin.cs @@ -80,16 +80,28 @@ 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( - typeof(MySqlGeometryTypeMapping<>).MakeGenericType(clrType ?? defaultClrType ?? typeof(Geometry)), - _geometryServices, - storeTypeName ?? defaultStoreType ?? "geometry", - _options) - : null; + var hasDefaultStoreType = clrType != null + && TryGetDefaultStoreType(clrType, out defaultStoreType); + var hasDefaultClrType = storeTypeName != null + && _spatialStoreTypeMappings.TryGetValue(storeTypeName, out defaultClrType); + + // NOTE: If the incoming user-specified 'clrType' is of the known calculated 'defaultClrType', ONLY then proceeed + // with the creation of 'MySqlGeometryTypeMapping'. + var hasDefaultStoreOrClrType = hasDefaultStoreType || hasDefaultClrType; + var isClrTypeNotAssignable = clrType != null + && !clrType.IsAssignableFrom(defaultClrType); + + if (!hasDefaultStoreOrClrType + || (!hasDefaultStoreType && isClrTypeNotAssignable)) + { + return null; + } + + return (RelationalTypeMapping)Activator.CreateInstance( + typeof(MySqlGeometryTypeMapping<>).MakeGenericType(clrType ?? defaultClrType ?? typeof(Geometry)), + _geometryServices, + storeTypeName ?? defaultStoreType ?? "geometry", + _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 efe3e9273..3bbfea37f 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 000000000..8ec72dfd2 --- /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(); +}