From 84fb709053e1de64555b5abf2d93cd407fc1e0ef Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sun, 23 Sep 2018 09:56:40 +0100 Subject: [PATCH] Switch to new EF Core plugin model Notes: * Added some missing NTS translations * The new EF Core plugin model doesn't yet support specifying evaluatable (https://github.com/aspnet/EntityFrameworkCore/issues/13454), so we currently hack that up inside the main provider using type names as strings. Fixes #658 --- .gitignore | 1 - ...pgsqlNetTopologySuiteDesignTimeServices.cs | 19 + src/EFCore.PG.NTS/EFCore.PG.NTS.csproj | 6 + ...ySuiteDbContextOptionsBuilderExtensions.cs | 48 ++ ...opologySuiteServiceCollectionExtensions.cs | 43 ++ .../INpgsqlNetTopologySuiteOptions.cs | 15 + .../NpgsqlNetTopologySuiteOptionsExtension.cs | 94 +++ .../Internal/NpgsqlNetTopologySuiteOptions.cs | 24 + ...TopologySuiteDbContextOptionsExtensions.cs | 32 -- .../NetTopologySuiteGeographyTypeMapping.cs | 46 -- .../NetTopologySuiteGeometryTypeMapping.cs | 46 -- src/EFCore.PG.NTS/NetTopologySuitePlugin.cs | 108 ---- ...NetTopologySuiteMemberTranslatorPlugin.cs} | 47 +- ...opologySuiteMethodCallTranslatorPlugin.cs} | 63 +- .../Internal/NpgsqlGeometryTypeMapping.cs | 73 +++ ...NetTopologySuiteTypeMappingSourcePlugin.cs | 37 ++ ...rkCore.PostgreSQL.NetTopologySuite.targets | 46 ++ .../NpgsqlNodaTimeDesignTimeServices.cs | 14 + .../EFCore.PG.NodaTime.csproj | 6 + ...daTimeDbContextOptionsBuilderExtensions.cs | 37 ++ ...gsqlNodaTimeServiceCollectionExtensions.cs | 36 ++ .../NpgsqlNodaTimeOptionsExtension.cs | 41 ++ .../NodaTimeDbContextOptionsExtensions.cs | 62 -- src/EFCore.PG.NodaTime/NodaTimePlugin.cs | 160 ------ .../NpgsqlNodaTimeMemberTranslatorPlugin.cs} | 17 +- ...gsqlNodaTimeMethodCallTranslatorPlugin.cs} | 16 +- .../Internal}/NodaTimeMappings.cs | 2 +- .../NpgsqlNodaTimeTypeMappingSourcePlugin.cs | 198 +++++++ ...yFrameworkCore.PostgreSQL.NodaTime.targets | 46 ++ .../Infrastructure/Internal/INpgsqlOptions.cs | 7 - .../Internal/NpgsqlOptionsExtension.cs | 31 +- .../NpgsqlDbContextOptionsBuilder.cs | 7 - .../NpgsqlEntityFrameworkPlugin.cs | 49 -- src/EFCore.PG/Internal/NpgsqlOptions.cs | 10 +- ...sqlCompositeEvaluatableExpressionFilter.cs | 9 +- ...sqlNodaTimeEvaluatableExpressionFilter.cs} | 33 +- .../NpgsqlCompositeMemberTranslator.cs | 3 - .../NpgsqlCompositeMethodCallTranslator.cs | 5 +- .../Internal/NpgsqlTypeMappingSource.cs | 34 +- src/Shared/MemberInfoExtensions.cs | 73 +++ .../EFCore.PG.FunctionalTests.csproj | 1 + .../NpgsqlComplianceTest.cs | 4 +- .../SpatialQueryNpgsqlGeographyFixture.cs | 61 ++ .../Query/SpatialQueryNpgsqlGeographyTest.cs | 201 +++++++ .../SpatialQueryNpgsqlGeometryFixture.cs | 39 ++ .../Query/SpatialQueryNpgsqlGeometryTest.cs | 544 ++++++++++++++++++ .../SpatialNpgsqlFixture.cs | 35 ++ .../SpatialNpgsqlTest.cs | 17 + .../NetTopologySuiteTest.cs | 512 ----------------- ...TimeTest.cs => NodaTimeQueryNpgsqlTest.cs} | 34 +- .../NpgsqlNodaTimeTypeMappingTest.cs | 25 +- 51 files changed, 1937 insertions(+), 1180 deletions(-) create mode 100644 src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs create mode 100644 src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs create mode 100644 src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs create mode 100644 src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteOptions.cs create mode 100644 src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs create mode 100644 src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteOptions.cs delete mode 100644 src/EFCore.PG.NTS/NetTopologySuiteDbContextOptionsExtensions.cs delete mode 100644 src/EFCore.PG.NTS/NetTopologySuiteGeographyTypeMapping.cs delete mode 100644 src/EFCore.PG.NTS/NetTopologySuiteGeometryTypeMapping.cs delete mode 100644 src/EFCore.PG.NTS/NetTopologySuitePlugin.cs rename src/EFCore.PG.NTS/{NetTopologySuiteMemberTranslator.cs => Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs} (54%) rename src/EFCore.PG.NTS/{NetTopologySuiteMethodCallTranslator.cs => Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs} (58%) create mode 100644 src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs create mode 100644 src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs create mode 100644 src/EFCore.PG.NTS/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.targets create mode 100644 src/EFCore.PG.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs create mode 100644 src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs create mode 100644 src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs create mode 100644 src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs delete mode 100644 src/EFCore.PG.NodaTime/NodaTimeDbContextOptionsExtensions.cs delete mode 100644 src/EFCore.PG.NodaTime/NodaTimePlugin.cs rename src/EFCore.PG.NodaTime/{NodaTimeMemberTranslator.cs => Query/ExpressionTranslators/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs} (91%) rename src/EFCore.PG.NodaTime/{NodaTimeMethodCallTranslator.cs => Query/ExpressionTranslators/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs} (87%) rename src/EFCore.PG.NodaTime/{ => Storage/Internal}/NodaTimeMappings.cs (99%) create mode 100644 src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs create mode 100644 src/EFCore.PG.NodaTime/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.targets delete mode 100644 src/EFCore.PG/Infrastructure/NpgsqlEntityFrameworkPlugin.cs rename src/{EFCore.PG.NodaTime/NodaTimeEvaluatableExpressionFilter.cs => EFCore.PG/Query/EvaluatableExpressionFilters/Internal/NpgsqlNodaTimeEvaluatableExpressionFilter.cs} (79%) create mode 100644 src/Shared/MemberInfoExtensions.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyFixture.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryFixture.cs create mode 100644 test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs create mode 100644 test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs create mode 100644 test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs delete mode 100644 test/EFCore.PG.Plugins.FunctionalTests/NetTopologySuiteTest.cs rename test/EFCore.PG.Plugins.FunctionalTests/{NodaTimeTest.cs => NodaTimeQueryNpgsqlTest.cs} (97%) diff --git a/.gitignore b/.gitignore index d490ec5ed..6d66814ec 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ .idea/ .vs/ [Bb]in/ -[Bb]uild/ [Oo]bj/ [Oo]bj/ artifacts/ diff --git a/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs b/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs new file mode 100644 index 000000000..8f7577bf6 --- /dev/null +++ b/src/EFCore.PG.NTS/Design/Internal/NpgsqlNetTopologySuiteDesignTimeServices.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal +{ + public class NpgsqlNetTopologySuiteDesignTimeServices : IDesignTimeServices + { + public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollection) + => serviceCollection + .AddSingleton() + .TryAddSingleton(); + + } +} diff --git a/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj b/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj index b6e5c7e7c..56034f83b 100644 --- a/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj +++ b/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj @@ -22,6 +22,12 @@ + + + True + build + + diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs new file mode 100644 index 000000000..ab75466ec --- /dev/null +++ b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.cs @@ -0,0 +1,48 @@ +using GeoAPI; +using GeoAPI.Geometries; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Npgsql; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +namespace Microsoft.EntityFrameworkCore +{ + /// + /// NetTopologySuite specific extension methods for . + /// + public static class NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions + { + /// + /// Use NetTopologySuite to access SQL Server spatial data. + /// + /// + /// The options builder so that further configuration can be chained. + /// + public static NpgsqlDbContextOptionsBuilder UseNetTopologySuite( + [NotNull] this NpgsqlDbContextOptionsBuilder optionsBuilder, + ICoordinateSequenceFactory coordinateSequenceFactory = null, + IPrecisionModel precisionModel = null, + Ordinates handleOrdinates = Ordinates.None, + bool geographyAsDefault = false) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + + // TODO: Global-only setup at the ADO.NET level for now, optionally allow per-connection? + NpgsqlConnection.GlobalTypeMapper.UseNetTopologySuite(coordinateSequenceFactory, precisionModel, handleOrdinates, geographyAsDefault); + + var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder; + + var extension = coreOptionsBuilder.Options.FindExtension() + ?? new NpgsqlNetTopologySuiteOptionsExtension(); + + if (geographyAsDefault) + extension = extension.WithGeographyDefault(); + + ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } + } +} diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs new file mode 100644 index 000000000..78ed68734 --- /dev/null +++ b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs @@ -0,0 +1,43 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection.Extensions; +using NetTopologySuite; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite extension methods for . + /// + public static class NpgsqlNetTopologySuiteServiceCollectionExtensions + { + /// + /// Adds the services required for NetTopologySuite support in the Npgsql provider for Entity Framework. + /// + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddEntityFrameworkNpgsqlNetTopologySuite( + [NotNull] this IServiceCollection serviceCollection) + { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + + new EntityFrameworkRelationalServicesBuilder(serviceCollection) + .TryAdd(p => p.GetService()) + .TryAddProviderSpecificServices( + x => x + .TryAddSingletonEnumerable() + .TryAddSingletonEnumerable() + .TryAddSingletonEnumerable() + .TryAddSingleton()); + + + return serviceCollection; + } + } +} diff --git a/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteOptions.cs b/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteOptions.cs new file mode 100644 index 000000000..950174130 --- /dev/null +++ b/src/EFCore.PG.NTS/Infrastructure/Internal/INpgsqlNetTopologySuiteOptions.cs @@ -0,0 +1,15 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal +{ + /// + /// Represents options for Npgsql NetTopologySuite that can only be set at the singleton level. + /// + public interface INpgsqlNetTopologySuiteOptions : ISingletonOptions + { + /// + /// True if geography is to be used by default instead of geometry + /// + bool IsGeographyDefault { get; } + } +} diff --git a/src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs b/src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs new file mode 100644 index 000000000..4e178788d --- /dev/null +++ b/src/EFCore.PG.NTS/Infrastructure/Internal/NpgsqlNetTopologySuiteOptionsExtension.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal +{ + public class NpgsqlNetTopologySuiteOptionsExtension : IDbContextOptionsExtensionWithDebugInfo + { + bool _isGeographyDefault; + string _logFragment; + + public NpgsqlNetTopologySuiteOptionsExtension() {} + + protected NpgsqlNetTopologySuiteOptionsExtension([NotNull] NpgsqlNetTopologySuiteOptionsExtension copyFrom) + => _isGeographyDefault = copyFrom._isGeographyDefault; + + protected virtual NpgsqlNetTopologySuiteOptionsExtension Clone() => new NpgsqlNetTopologySuiteOptionsExtension(this); + + public virtual bool ApplyServices(IServiceCollection services) + { + services.AddEntityFrameworkNpgsqlNetTopologySuite(); + + return false; + } + + public virtual bool IsGeographyDefault => _isGeographyDefault; + + public virtual NpgsqlNetTopologySuiteOptionsExtension WithGeographyDefault(bool isGeographyDefault = true) + { + var clone = Clone(); + + clone._isGeographyDefault = isGeographyDefault; + + return clone; + } + + public virtual long GetServiceProviderHashCode() => _isGeographyDefault ? 541 : 0; + + public virtual void PopulateDebugInfo(IDictionary debugInfo) + { + Check.NotNull(debugInfo, nameof(debugInfo)); + + debugInfo["NpgsqlNetTopologySuite:" + nameof(IsGeographyDefault)] + = (_isGeographyDefault ? 541 : 0).ToString(CultureInfo.InvariantCulture); + } + + public virtual void Validate(IDbContextOptions options) + { + Check.NotNull(options, nameof(options)); + + var internalServiceProvider = options.FindExtension()?.InternalServiceProvider; + if (internalServiceProvider != null) + { + using (var scope = internalServiceProvider.CreateScope()) + { + if (scope.ServiceProvider.GetService>() + ?.Any(s => s is NpgsqlNetTopologySuiteTypeMappingSourcePlugin) != true) + { + throw new InvalidOperationException($"{nameof(NpgsqlNetTopologySuiteDbContextOptionsBuilderExtensions.UseNetTopologySuite)} requires {nameof(NpgsqlNetTopologySuiteServiceCollectionExtensions.AddEntityFrameworkNpgsqlNetTopologySuite)} to be called on the internal service provider used."); + } + } + } + } + + [NotNull] + public virtual string LogFragment + { + get + { + if (_logFragment == null) + { + var builder = new StringBuilder("using NetTopologySuite"); + if (_isGeographyDefault) + builder.Append(" (geography by default)"); + builder.Append(' '); + + _logFragment = builder.ToString(); + } + + return _logFragment; + } + } + } +} diff --git a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteOptions.cs b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteOptions.cs new file mode 100644 index 000000000..2f4195095 --- /dev/null +++ b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteOptions.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal +{ + /// + public class NpgsqlNetTopologySuiteOptions : INpgsqlNetTopologySuiteOptions + { + /// + public bool IsGeographyDefault { get; set; } + + /// + public void Initialize(IDbContextOptions options) + { + var npgsqlNtsOptions = options.FindExtension() ?? new NpgsqlNetTopologySuiteOptionsExtension(); + + IsGeographyDefault = npgsqlNtsOptions.IsGeographyDefault; + } + + /// + public void Validate(IDbContextOptions options) {} + } +} diff --git a/src/EFCore.PG.NTS/NetTopologySuiteDbContextOptionsExtensions.cs b/src/EFCore.PG.NTS/NetTopologySuiteDbContextOptionsExtensions.cs deleted file mode 100644 index f88f31c44..000000000 --- a/src/EFCore.PG.NTS/NetTopologySuiteDbContextOptionsExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -using GeoAPI; -using GeoAPI.Geometries; -using JetBrains.Annotations; -using Npgsql; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore -{ - public static class NetTopologySuiteDbContextOptionsExtensions - { - public static NpgsqlDbContextOptionsBuilder UseNetTopologySuite( - [NotNull] this NpgsqlDbContextOptionsBuilder optionsBuilder, - ICoordinateSequenceFactory coordinateSequenceFactory = null, - IPrecisionModel precisionModel = null, - Ordinates handleOrdinates = Ordinates.None) - { - Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - - NetTopologySuiteBootstrapper.Bootstrap(); - - // TODO: Global-only setup at the ADO.NET level for now, optionally allow per-connection? - NpgsqlConnection.GlobalTypeMapper.UseNetTopologySuite(coordinateSequenceFactory, precisionModel, handleOrdinates); - - optionsBuilder.UsePlugin(new NetTopologySuitePlugin()); - - return optionsBuilder; - } - } -} diff --git a/src/EFCore.PG.NTS/NetTopologySuiteGeographyTypeMapping.cs b/src/EFCore.PG.NTS/NetTopologySuiteGeographyTypeMapping.cs deleted file mode 100644 index 748f40bbb..000000000 --- a/src/EFCore.PG.NTS/NetTopologySuiteGeographyTypeMapping.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Text; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using GeoAPI.Geometries; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using NpgsqlTypes; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite -{ - public class NetTopologySuiteGeographyTypeMapping : NpgsqlTypeMapping - { - public NetTopologySuiteGeographyTypeMapping(Type clrType) : base("geography", clrType, NpgsqlDbType.Geography) {} - - protected NetTopologySuiteGeographyTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType) - : base(parameters, npgsqlDbType) {} - - public override RelationalTypeMapping Clone(string storeType, int? size) - => new NetTopologySuiteGeographyTypeMapping(Parameters.WithStoreTypeAndSize(storeType, size), NpgsqlDbType); - - public override CoreTypeMapping Clone(ValueConverter converter) - => new NetTopologySuiteGeographyTypeMapping(Parameters.WithComposedConverter(converter), NpgsqlDbType); - - protected override string GenerateNonNullSqlLiteral(object value) - { - var geometry = (IGeometry)value; - var builder = new StringBuilder(); - - builder.Append("GEOGRAPHY '"); - - if (geometry.SRID > 0) - { - builder - .Append("SRID=") - .Append(geometry.SRID) - .Append(";"); - } - - builder - .Append(geometry.AsText()) - .Append("'"); - - return builder.ToString(); - } - } -} diff --git a/src/EFCore.PG.NTS/NetTopologySuiteGeometryTypeMapping.cs b/src/EFCore.PG.NTS/NetTopologySuiteGeometryTypeMapping.cs deleted file mode 100644 index 9bd7bba51..000000000 --- a/src/EFCore.PG.NTS/NetTopologySuiteGeometryTypeMapping.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Text; -using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using GeoAPI.Geometries; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using NpgsqlTypes; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite -{ - public class NetTopologySuiteGeometryTypeMapping : NpgsqlTypeMapping - { - public NetTopologySuiteGeometryTypeMapping(Type clrType) : base("geometry", clrType, NpgsqlDbType.Geometry) {} - - protected NetTopologySuiteGeometryTypeMapping(RelationalTypeMappingParameters parameters, NpgsqlDbType npgsqlDbType) - : base(parameters, npgsqlDbType) {} - - public override RelationalTypeMapping Clone(string storeType, int? size) - => new NetTopologySuiteGeometryTypeMapping(Parameters.WithStoreTypeAndSize(storeType, size), NpgsqlDbType); - - public override CoreTypeMapping Clone(ValueConverter converter) - => new NetTopologySuiteGeometryTypeMapping(Parameters.WithComposedConverter(converter), NpgsqlDbType); - - protected override string GenerateNonNullSqlLiteral(object value) - { - var geometry = (IGeometry)value; - var builder = new StringBuilder(); - - builder.Append("GEOMETRY '"); - - if (geometry.SRID > 0) - { - builder - .Append("SRID=") - .Append(geometry.SRID) - .Append(";"); - } - - builder - .Append(geometry.AsText()) - .Append("'"); - - return builder.ToString(); - } - } -} diff --git a/src/EFCore.PG.NTS/NetTopologySuitePlugin.cs b/src/EFCore.PG.NTS/NetTopologySuitePlugin.cs deleted file mode 100644 index 94105e25a..000000000 --- a/src/EFCore.PG.NTS/NetTopologySuitePlugin.cs +++ /dev/null @@ -1,108 +0,0 @@ -using GeoAPI.Geometries; -using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; -using Microsoft.EntityFrameworkCore.Storage; -using NetTopologySuite.Geometries; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite -{ - public class NetTopologySuitePlugin : NpgsqlEntityFrameworkPlugin - { - public override string Name => "NetTopologySuite"; - public override string Description => "Plugin to map PostGIS types to NetTopologySuite"; - - #region Geometry mappings - - readonly NetTopologySuiteGeometryTypeMapping _point = new NetTopologySuiteGeometryTypeMapping(typeof(Point)); - readonly NetTopologySuiteGeometryTypeMapping _lineString = new NetTopologySuiteGeometryTypeMapping(typeof(LineString)); - readonly NetTopologySuiteGeometryTypeMapping _polygon = new NetTopologySuiteGeometryTypeMapping(typeof(Polygon)); - readonly NetTopologySuiteGeometryTypeMapping _multiPoint = new NetTopologySuiteGeometryTypeMapping(typeof(MultiPoint)); - readonly NetTopologySuiteGeometryTypeMapping _multiLineString = new NetTopologySuiteGeometryTypeMapping(typeof(MultiLineString)); - readonly NetTopologySuiteGeometryTypeMapping _multiPolygon = new NetTopologySuiteGeometryTypeMapping(typeof(MultiPolygon)); - readonly NetTopologySuiteGeometryTypeMapping _collection = new NetTopologySuiteGeometryTypeMapping(typeof(GeometryCollection)); - readonly NetTopologySuiteGeometryTypeMapping _geometry = new NetTopologySuiteGeometryTypeMapping(typeof(Geometry)); - - readonly NetTopologySuiteGeometryTypeMapping _ipoint = new NetTopologySuiteGeometryTypeMapping(typeof(IPoint)); - readonly NetTopologySuiteGeometryTypeMapping _ilineString = new NetTopologySuiteGeometryTypeMapping(typeof(ILineString)); - readonly NetTopologySuiteGeometryTypeMapping _ipolygon = new NetTopologySuiteGeometryTypeMapping(typeof(IPolygon)); - readonly NetTopologySuiteGeometryTypeMapping _imultiPoint = new NetTopologySuiteGeometryTypeMapping(typeof(IMultiPoint)); - readonly NetTopologySuiteGeometryTypeMapping _imultiLineString = new NetTopologySuiteGeometryTypeMapping(typeof(IMultiLineString)); - readonly NetTopologySuiteGeometryTypeMapping _imultiPolygon = new NetTopologySuiteGeometryTypeMapping(typeof(IMultiPolygon)); - readonly NetTopologySuiteGeometryTypeMapping _icollection = new NetTopologySuiteGeometryTypeMapping(typeof(IGeometryCollection)); - readonly NetTopologySuiteGeometryTypeMapping _igeometry = new NetTopologySuiteGeometryTypeMapping(typeof(IGeometry)); - - #endregion Geometry mappings - - #region Geography mappings - - readonly NetTopologySuiteGeographyTypeMapping _geogPoint = new NetTopologySuiteGeographyTypeMapping(typeof(Point)); - readonly NetTopologySuiteGeographyTypeMapping _geogLineString = new NetTopologySuiteGeographyTypeMapping(typeof(LineString)); - readonly NetTopologySuiteGeographyTypeMapping _geogPolygon = new NetTopologySuiteGeographyTypeMapping(typeof(Polygon)); - readonly NetTopologySuiteGeographyTypeMapping _geogMultiPoint = new NetTopologySuiteGeographyTypeMapping(typeof(MultiPoint)); - readonly NetTopologySuiteGeographyTypeMapping _geogMultiLineString = new NetTopologySuiteGeographyTypeMapping(typeof(MultiLineString)); - readonly NetTopologySuiteGeographyTypeMapping _geogMultiPolygon = new NetTopologySuiteGeographyTypeMapping(typeof(MultiPolygon)); - readonly NetTopologySuiteGeographyTypeMapping _geogCollection = new NetTopologySuiteGeographyTypeMapping(typeof(GeometryCollection)); - readonly NetTopologySuiteGeographyTypeMapping _geography = new NetTopologySuiteGeographyTypeMapping(typeof(Geometry)); - - readonly NetTopologySuiteGeographyTypeMapping _igeogPoint = new NetTopologySuiteGeographyTypeMapping(typeof(IPoint)); - readonly NetTopologySuiteGeographyTypeMapping _igeogLineString = new NetTopologySuiteGeographyTypeMapping(typeof(ILineString)); - readonly NetTopologySuiteGeographyTypeMapping _igeogPolygon = new NetTopologySuiteGeographyTypeMapping(typeof(IPolygon)); - readonly NetTopologySuiteGeographyTypeMapping _igeogMultiPoint = new NetTopologySuiteGeographyTypeMapping(typeof(IMultiPoint)); - readonly NetTopologySuiteGeographyTypeMapping _igeogMultiLineString = new NetTopologySuiteGeographyTypeMapping(typeof(IMultiLineString)); - readonly NetTopologySuiteGeographyTypeMapping _igeogMultiPolygon = new NetTopologySuiteGeographyTypeMapping(typeof(IMultiPolygon)); - readonly NetTopologySuiteGeographyTypeMapping _igeogCollection = new NetTopologySuiteGeographyTypeMapping(typeof(IGeometryCollection)); - readonly NetTopologySuiteGeographyTypeMapping _igeography = new NetTopologySuiteGeographyTypeMapping(typeof(IGeometry)); - - #endregion Geography mappings - - public override void AddMappings(NpgsqlTypeMappingSource typeMappingSource) - { - typeMappingSource.ClrTypeMappings[typeof(Geometry)] = _geometry; - typeMappingSource.ClrTypeMappings[typeof(Point)] = _point; - typeMappingSource.ClrTypeMappings[typeof(LineString)] = _lineString; - typeMappingSource.ClrTypeMappings[typeof(Polygon)] = _polygon; - typeMappingSource.ClrTypeMappings[typeof(MultiPoint)] = _multiPoint; - typeMappingSource.ClrTypeMappings[typeof(MultiLineString)] = _multiLineString; - typeMappingSource.ClrTypeMappings[typeof(MultiPolygon)] = _multiPolygon; - typeMappingSource.ClrTypeMappings[typeof(GeometryCollection)] = _collection; - - typeMappingSource.ClrTypeMappings[typeof(IGeometry)] = _igeometry; - typeMappingSource.ClrTypeMappings[typeof(IPoint)] = _ipoint; - typeMappingSource.ClrTypeMappings[typeof(ILineString)] = _ilineString; - typeMappingSource.ClrTypeMappings[typeof(IPolygon)] = _ipolygon; - typeMappingSource.ClrTypeMappings[typeof(IMultiPoint)] = _imultiPoint; - typeMappingSource.ClrTypeMappings[typeof(IMultiLineString)] = _imultiLineString; - typeMappingSource.ClrTypeMappings[typeof(IMultiPolygon)] = _imultiPolygon; - typeMappingSource.ClrTypeMappings[typeof(IGeometryCollection)] = _icollection; - - typeMappingSource.StoreTypeMappings["geometry"] = new RelationalTypeMapping[] - { - _geometry, _point, _lineString, _polygon, _multiPoint, _multiLineString, _multiPolygon, _collection, - _igeometry, _ipoint, _ilineString, _ipolygon, _imultiPoint, _imultiLineString, _imultiPolygon, _icollection - }; - typeMappingSource.StoreTypeMappings["geography"] = new RelationalTypeMapping[] - { - _geography, _geogPoint, _geogLineString, _geogPolygon, _geogMultiPoint, _geogMultiLineString, _geogMultiPolygon, _geogCollection, - _igeography, _igeogPoint, _igeogLineString, _igeogPolygon, _igeogMultiPoint, _igeogMultiLineString, _igeogMultiPolygon, _igeogCollection - }; - } - - static readonly IMethodCallTranslator[] MethodCallTranslators = - { - new NetTopologySuiteMethodCallTranslator(), - }; - - static readonly IMemberTranslator[] MemberTranslators = - { - new NetTopologySuiteMemberTranslator(), - }; - - public override void AddMethodCallTranslators(NpgsqlCompositeMethodCallTranslator compositeMethodCallTranslator) - => compositeMethodCallTranslator.AddTranslators(MethodCallTranslators); - - public override void AddMemberTranslators(NpgsqlCompositeMemberTranslator compositeMemberTranslator) - => compositeMemberTranslator.AddTranslators(MemberTranslators); - } -} diff --git a/src/EFCore.PG.NTS/NetTopologySuiteMemberTranslator.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs similarity index 54% rename from src/EFCore.PG.NTS/NetTopologySuiteMemberTranslator.cs rename to src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs index b8f4b000c..675908503 100644 --- a/src/EFCore.PG.NTS/NetTopologySuiteMemberTranslator.cs +++ b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMemberTranslatorPlugin.cs @@ -1,18 +1,28 @@ -using System.Linq.Expressions; +using System; +using System.Collections.Generic; +using System.Linq.Expressions; using GeoAPI.Geometries; using Microsoft.EntityFrameworkCore.Query.Expressions; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; using NetTopologySuite.Geometries; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal { - public class NetTopologySuiteMemberTranslator : IMemberTranslator + public class NpgsqlNetTopologySuiteMemberTranslatorPlugin : IMemberTranslatorPlugin + { + public virtual IEnumerable Translators { get; } = new IMemberTranslator[] + { + new NpgsqlGeometryMemberTranslator() + }; + } + + public class NpgsqlGeometryMemberTranslator : IMemberTranslator { public Expression Translate(MemberExpression e) { var declaringType = e.Member.DeclaringType; - if (declaringType == typeof(Point)) + if (typeof(IPoint).IsAssignableFrom(declaringType)) { switch (e.Member.Name) { @@ -27,6 +37,15 @@ public Expression Translate(MemberExpression e) } } + if (typeof(ILineString).IsAssignableFrom(declaringType)) + { + switch (e.Member.Name) + { + case "Count": + return new SqlFunctionExpression("ST_NumPoints", typeof(int), new[] { e.Expression }); + } + } + if (typeof(IGeometry).IsAssignableFrom(declaringType)) { switch (e.Member.Name) @@ -37,12 +56,24 @@ public Expression Translate(MemberExpression e) return new SqlFunctionExpression("ST_Boundary", typeof(IGeometry), new[] { e.Expression }); case "Centroid": return new SqlFunctionExpression("ST_Centroid", typeof(Point), new[] { e.Expression }); + case "Count": + return new SqlFunctionExpression("ST_NumGeometries", typeof(int), new[] { e.Expression }); + case "Dimension": + return new SqlFunctionExpression("ST_Dimension", typeof(Dimension), new[] { e.Expression }); + case "EndPoint": + return new SqlFunctionExpression("ST_EndPoint", typeof(Point), new[] { e.Expression }); + case "Envelope": + return new SqlFunctionExpression("ST_Envelope", typeof(IGeometry), new[] { e.Expression }); + case "ExteriorRing": + return new SqlFunctionExpression("ST_ExteriorRing", typeof(ILineString), new[] { e.Expression }); case "GeometryType": return new SqlFunctionExpression("GeometryType", typeof(string), new[] { e.Expression }); case "IsClosed": return new SqlFunctionExpression("ST_IsClosed", typeof(bool), new[] { e.Expression }); case "IsEmpty": return new SqlFunctionExpression("ST_IsEmpty", typeof(bool), new[] { e.Expression }); + case "IsRing": + return new SqlFunctionExpression("ST_IsRing", typeof(bool), new[] { e.Expression }); case "IsSimple": return new SqlFunctionExpression("ST_IsSimple", typeof(bool), new[] { e.Expression }); case "IsValid": @@ -51,8 +82,16 @@ public Expression Translate(MemberExpression e) return new SqlFunctionExpression("ST_Length", typeof(double), new[] { e.Expression }); case "NumGeometries": return new SqlFunctionExpression("ST_NumGeometries", typeof(int), new[] { e.Expression }); + case "NumInteriorRings": + return new SqlFunctionExpression("ST_NumInteriorRings", typeof(int), new[] { e.Expression }); case "NumPoints": return new SqlFunctionExpression("ST_NumPoints", typeof(int), new[] { e.Expression }); + case "PointOnSurface": + return new SqlFunctionExpression("ST_PointOnSurface", typeof(IPoint), new[] { e.Expression }); + case "SRID": + return new SqlFunctionExpression("ST_SRID", typeof(int), new[] { e.Expression }); + case "StartPoint": + return new SqlFunctionExpression("ST_StartPoint", typeof(IPoint), new[] { e.Expression }); default: return null; } diff --git a/src/EFCore.PG.NTS/NetTopologySuiteMethodCallTranslator.cs b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs similarity index 58% rename from src/EFCore.PG.NTS/NetTopologySuiteMethodCallTranslator.cs rename to src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs index 5e3a274cc..6977d9591 100644 --- a/src/EFCore.PG.NTS/NetTopologySuiteMethodCallTranslator.cs +++ b/src/EFCore.PG.NTS/Query/ExpressionTranslators/Internal/NpgsqlNetTopologySuiteMethodCallTranslatorPlugin.cs @@ -1,17 +1,30 @@ -using System.Linq.Expressions; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; using GeoAPI.Geometries; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query.Expressions; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; using NetTopologySuite.Geometries; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal { + public class NpgsqlNetTopologySuiteMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin + { + public virtual IEnumerable Translators { get; } = new IMethodCallTranslator[] + { + new NpgsqlGeometryMethodTranslator() + }; + } + /// /// Translates methods operating on types implementing the interface. /// - public class NetTopologySuiteMethodCallTranslator : IMethodCallTranslator + public class NpgsqlGeometryMethodTranslator : IMethodCallTranslator { + static readonly MethodInfo _collectionItem = + typeof(IGeometryCollection).GetRuntimeProperty("Item").GetMethod; + /// [CanBeNull] public virtual Expression Translate(MethodCallExpression e) @@ -21,10 +34,16 @@ public virtual Expression Translate(MethodCallExpression e) switch (e.Method.Name) { + case "AsBinary": + return new SqlFunctionExpression("ST_AsBinary", typeof(byte[]), new[] { e.Object }); case "AsText": return new SqlFunctionExpression("ST_AsText", typeof(string), new[] { e.Object }); + case "Buffer": + return new SqlFunctionExpression("ST_Buffer", typeof(Geometry), new[] { e.Object, e.Arguments[0] }); case "Contains": return new SqlFunctionExpression("ST_Contains", typeof(bool), new[] { e.Object, e.Arguments[0] }); + case "ConvexHull": + return new SqlFunctionExpression("ST_ConvexHull", typeof(Geometry), new[] { e.Object }); case "CoveredBy": return new SqlFunctionExpression("ST_CoveredBy", typeof(bool), new[] { e.Object, e.Arguments[0] }); case "Covers": @@ -42,14 +61,11 @@ public virtual Expression Translate(MethodCallExpression e) case "EqualsTopologically": return new SqlFunctionExpression("ST_Equals", typeof(bool), new[] { e.Object, e.Arguments[0] }); case "GetGeometryN": - // NetTopologySuite uses 0-based indexing, but PostGIS uses 1-based - return new SqlFunctionExpression("ST_GeometryN", typeof(Geometry), new[] - { - e.Object, - e.Arguments[0] is ConstantExpression constant - ? (Expression)Expression.Constant((int)constant.Value + 1) - : Expression.Add(e.Arguments[0], Expression.Constant(1)) - }); + return GenerateOneBasedFunctionExpression("ST_GeometryN", e.Object, e.Arguments[0]); + case "GetInteriorRingN": + return GenerateOneBasedFunctionExpression("ST_InteriorRingN", e.Object, e.Arguments[0]); + case "GetPointN": + return GenerateOneBasedFunctionExpression("ST_PointN", e.Object, e.Arguments[0]); case "Intersection": return new SqlFunctionExpression("ST_Intersection", typeof(Geometry), new[] { e.Object, e.Arguments[0] }); case "Intersects": @@ -62,17 +78,36 @@ e.Arguments[0] is ConstantExpression constant return new SqlFunctionExpression("ST_Reverse", typeof(Geometry), new[] { e.Object }); case "SymmetricDifference": return new SqlFunctionExpression("ST_SymDifference", typeof(Geometry), new[] { e.Object, e.Arguments[0] }); + case "ToBinary": + return new SqlFunctionExpression("ST_AsBinary", typeof(byte[]), new[] { e.Object }); case "ToText": return new SqlFunctionExpression("ST_AsText", typeof(string), new[] { e.Object }); case "Touches": return new SqlFunctionExpression("ST_Touches", typeof(bool), new[] { e.Object, e.Arguments[0] }); - case "Union": + case "Union" when e.Arguments.Count == 0: + return null; // ST_Union() with only one parameter is an aggregate function in PostGIS + case "Union" when e.Arguments.Count == 1: return new SqlFunctionExpression("ST_Union", typeof(Geometry), new[] { e.Object, e.Arguments[0] }); case "Within": return new SqlFunctionExpression("ST_Within", typeof(bool), new[] { e.Object, e.Arguments[0] }); - default: - return null; } + + // IGeometryCollection[index] + var method = e.Method.OnInterface(typeof(IGeometryCollection)); + if (Equals(method, _collectionItem)) + return GenerateOneBasedFunctionExpression("ST_GeometryN", e.Object, e.Arguments[0]); + + return null; } + + // NetTopologySuite uses 0-based indexing, but PostGIS uses 1-based + static SqlFunctionExpression GenerateOneBasedFunctionExpression(string functionName, Expression obj, Expression arg) + => new SqlFunctionExpression(functionName, typeof(Geometry), new[] + { + obj, + arg is ConstantExpression constant + ? (Expression)Expression.Constant((int)constant.Value + 1) + : Expression.Add(arg, Expression.Constant(1)) + }); } } diff --git a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs new file mode 100644 index 000000000..8464bdc68 --- /dev/null +++ b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlGeometryTypeMapping.cs @@ -0,0 +1,73 @@ +using System; +using System.Data.Common; +using System.Text; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using GeoAPI.Geometries; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using NetTopologySuite.IO; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using NpgsqlTypes; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal +{ + public class NpgsqlGeometryTypeMapping : RelationalGeometryTypeMapping + { + readonly bool _isGeography; + + public NpgsqlGeometryTypeMapping(string storeType) : base(new NullValueConverter(), storeType) + => _isGeography = IsGeography(storeType); + + protected NpgsqlGeometryTypeMapping(RelationalTypeMappingParameters parameters) + : base(parameters) {} + + protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters) + => new NpgsqlGeometryTypeMapping(parameters); + + protected override void ConfigureParameter([NotNull] DbParameter parameter) + { + base.ConfigureParameter(parameter); + + ((NpgsqlParameter)parameter).NpgsqlDbType = _isGeography ? NpgsqlDbType.Geography : NpgsqlDbType.Geometry; + } + + protected override string GenerateNonNullSqlLiteral(object value) + { + var geometry = (IGeometry)value; + var builder = new StringBuilder(); + + builder + .Append(_isGeography ? "GEOGRAPHY" : "GEOMETRY") + .Append(" '"); + + if (geometry.SRID > 0) + { + builder + .Append("SRID=") + .Append(geometry.SRID) + .Append(';'); + } + + builder + .Append(geometry.AsText()) + .Append('\''); + + return builder.ToString(); + } + + static bool IsGeography(string storeType) + => string.Equals(storeType, "geography", StringComparison.OrdinalIgnoreCase); + + class NullValueConverter : ValueConverter + { + public NullValueConverter() : base(t => t, t => t) {} + } + + protected override string AsText(object value) => ((IGeometry)value).AsText(); + + protected override int GetSrid(object value) => ((IGeometry)value).SRID; + + protected override Type WKTReaderType => typeof(WKTReader); + } +} diff --git a/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs new file mode 100644 index 000000000..09b557bc9 --- /dev/null +++ b/src/EFCore.PG.NTS/Storage/Internal/NpgsqlNetTopologySuiteTypeMappingSourcePlugin.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using GeoAPI.Geometries; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal +{ + public class NpgsqlNetTopologySuiteTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin + { + // Note: we reference the options rather than copying IsGeographyDefault out, because that field is initialized + // rather late by SingletonOptionsInitializer + readonly INpgsqlNetTopologySuiteOptions _options; + + public NpgsqlNetTopologySuiteTypeMappingSourcePlugin([NotNull] INpgsqlNetTopologySuiteOptions options) + => _options = Check.NotNull(options, nameof(options)); + + public virtual RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) + { + var clrType = mappingInfo.ClrType; + var storeTypeName = mappingInfo.StoreTypeName; + + // TODO: Array + return clrType != null && typeof(IGeometry).IsAssignableFrom(clrType) || + storeTypeName != null && ( + storeTypeName.Equals("geometry", StringComparison.OrdinalIgnoreCase) || + storeTypeName.Equals("geography", StringComparison.OrdinalIgnoreCase)) + ? (RelationalTypeMapping)Activator.CreateInstance( + typeof(NpgsqlGeometryTypeMapping<>).MakeGenericType(clrType ?? typeof(IGeometry)), + storeTypeName ?? (_options.IsGeographyDefault ? "geography" : "geometry")) + : null; + } + } +} diff --git a/src/EFCore.PG.NTS/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.targets b/src/EFCore.PG.NTS/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.targets new file mode 100644 index 000000000..e5f3b396c --- /dev/null +++ b/src/EFCore.PG.NTS/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite.targets @@ -0,0 +1,46 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + $(IntermediateOutputPath)EFCoreNpgsqlNetTopologySuite$(DefaultLanguageSourceExtension) + + + + + + + CompileBefore + + + + + CompileAfter + + + + + + + Compile + + + + + + + <_Parameter1>Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal.NpgsqlNetTopologySuiteDesignTimeServices, Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite + <_Parameter2>Npgsql.EntityFrameworkCore.PostgreSQL + + + + + + + + diff --git a/src/EFCore.PG.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs b/src/EFCore.PG.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs new file mode 100644 index 000000000..d9d4d3628 --- /dev/null +++ b/src/EFCore.PG.NodaTime/Design/Internal/NpgsqlNodaTimeDesignTimeServices.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal +{ + public class NpgsqlNodaTimeDesignTimeServices : IDesignTimeServices + { + public virtual void ConfigureDesignTimeServices(IServiceCollection serviceCollection) + => serviceCollection + .AddSingleton(); + } +} diff --git a/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj b/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj index 25c71f2e5..b00a1f2cf 100644 --- a/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj +++ b/src/EFCore.PG.NodaTime/EFCore.PG.NodaTime.csproj @@ -22,6 +22,12 @@ + + + True + build + + diff --git a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs new file mode 100644 index 000000000..fbeec8e1f --- /dev/null +++ b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeDbContextOptionsBuilderExtensions.cs @@ -0,0 +1,37 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Npgsql; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +namespace Microsoft.EntityFrameworkCore +{ + /// + /// NodaTime specific extension methods for . + /// + public static class NpgsqlNodaTimeDbContextOptionsBuilderExtensions + { + /// + /// Use NetTopologySuite to access SQL Server spatial data. + /// + /// The options builder so that further configuration can be chained. + public static NpgsqlDbContextOptionsBuilder UseNodaTime( + [NotNull] this NpgsqlDbContextOptionsBuilder optionsBuilder) + { + Check.NotNull(optionsBuilder, nameof(optionsBuilder)); + + // TODO: Global-only setup at the ADO.NET level for now, optionally allow per-connection? + NpgsqlConnection.GlobalTypeMapper.UseNodaTime(); + + var coreOptionsBuilder = ((IRelationalDbContextOptionsBuilderInfrastructure)optionsBuilder).OptionsBuilder; + + var extension = coreOptionsBuilder.Options.FindExtension() + ?? new NpgsqlNodaTimeOptionsExtension(); + + ((IDbContextOptionsBuilderInfrastructure)coreOptionsBuilder).AddOrUpdateExtension(extension); + + return optionsBuilder; + } + } +} diff --git a/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs new file mode 100644 index 000000000..a85be9544 --- /dev/null +++ b/src/EFCore.PG.NodaTime/Extensions/NpgsqlNodaTimeServiceCollectionExtensions.cs @@ -0,0 +1,36 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; +using Microsoft.EntityFrameworkCore.Storage; +using Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; +using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime extension methods for . + /// + public static class NpgsqlNodaTimeServiceCollectionExtensions + { + /// + /// Adds the services required for NodaTime support in the Npgsql provider for Entity Framework. + /// + /// The to add services to. + /// The same service collection so that multiple calls can be chained. + public static IServiceCollection AddEntityFrameworkNpgsqlNodaTime( + [NotNull] this IServiceCollection serviceCollection) + { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + + new EntityFrameworkRelationalServicesBuilder(serviceCollection) + .TryAddProviderSpecificServices( + x => x + .TryAddSingletonEnumerable() + .TryAddSingletonEnumerable() + .TryAddSingletonEnumerable()); + + return serviceCollection; + } + } +} diff --git a/src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs b/src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs new file mode 100644 index 000000000..336f9e051 --- /dev/null +++ b/src/EFCore.PG.NodaTime/Infrastructure/Internal/NpgsqlNodaTimeOptionsExtension.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal +{ + public class NpgsqlNodaTimeOptionsExtension : IDbContextOptionsExtension + { + public virtual string LogFragment => "using NodaTime "; + + public virtual bool ApplyServices(IServiceCollection services) + { + services.AddEntityFrameworkNpgsqlNodaTime(); + + return false; + } + + public virtual long GetServiceProviderHashCode() => 0; + + public virtual void Validate(IDbContextOptions options) + { + var internalServiceProvider = options.FindExtension()?.InternalServiceProvider; + if (internalServiceProvider != null) + { + using (var scope = internalServiceProvider.CreateScope()) + { + if (scope.ServiceProvider.GetService>() + ?.Any(s => s is NpgsqlNodaTimeTypeMappingSourcePlugin) != true) + { + throw new InvalidOperationException($"{nameof(NpgsqlNodaTimeDbContextOptionsBuilderExtensions.UseNodaTime)} requires {nameof(NpgsqlNodaTimeServiceCollectionExtensions.AddEntityFrameworkNpgsqlNodaTime)} to be called on the internal service provider used."); + } + } + } + } + } +} diff --git a/src/EFCore.PG.NodaTime/NodaTimeDbContextOptionsExtensions.cs b/src/EFCore.PG.NodaTime/NodaTimeDbContextOptionsExtensions.cs deleted file mode 100644 index 10abbc383..000000000 --- a/src/EFCore.PG.NodaTime/NodaTimeDbContextOptionsExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using System; -using JetBrains.Annotations; -using Npgsql; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Utilities; - -// ReSharper disable once CheckNamespace -namespace Microsoft.EntityFrameworkCore -{ - /// - /// Provides extension methods for the NodaTime plugin for the Npgsql Entity Framework Core provider. - /// - public static class NodaTimeDbContextOptionsExtensions - { - /// - /// Registers the NodaTime plugin for the Npgsql Entity Framework Core provider. - /// - /// The options builder. - /// - /// The options builder configured with the NodaTime plugin. - /// - /// - [NotNull] - public static NpgsqlDbContextOptionsBuilder UseNodaTime([NotNull] this NpgsqlDbContextOptionsBuilder optionsBuilder) - { - Check.NotNull(optionsBuilder, nameof(optionsBuilder)); - - // TODO: Global-only setup at the ADO.NET level for now, optionally allow per-connection? - NpgsqlConnection.GlobalTypeMapper.UseNodaTime(); - - optionsBuilder.UsePlugin(new NodaTimePlugin()); - - return optionsBuilder; - } - } -} diff --git a/src/EFCore.PG.NodaTime/NodaTimePlugin.cs b/src/EFCore.PG.NodaTime/NodaTimePlugin.cs deleted file mode 100644 index 3e3764b3f..000000000 --- a/src/EFCore.PG.NodaTime/NodaTimePlugin.cs +++ /dev/null @@ -1,160 +0,0 @@ -#region License - -// The PostgreSQL License -// -// Copyright (C) 2016 The Npgsql Development Team -// -// Permission to use, copy, modify, and distribute this software and its -// documentation for any purpose, without fee, and without a written -// agreement is hereby granted, provided that the above copyright notice -// and this paragraph and the following two paragraphs appear in all copies. -// -// IN NO EVENT SHALL THE NPGSQL DEVELOPMENT TEAM BE LIABLE TO ANY PARTY -// FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -// INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS -// DOCUMENTATION, EVEN IF THE NPGSQL DEVELOPMENT TEAM HAS BEEN ADVISED OF -// THE POSSIBILITY OF SUCH DAMAGE. -// -// THE NPGSQL DEVELOPMENT TEAM SPECIFICALLY DISCLAIMS ANY WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY -// AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS -// ON AN "AS IS" BASIS, AND THE NPGSQL DEVELOPMENT TEAM HAS NO OBLIGATIONS -// TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. - -#endregion - -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; -using Microsoft.EntityFrameworkCore.Storage; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.EvaluatableExpressionFilters.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; -using NpgsqlTypes; -using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime -{ - /// - /// The NodaTime plugin for the Npgsql Entity Framework Core provider. - /// - public class NodaTimePlugin : NpgsqlEntityFrameworkPlugin - { - #region TypeMapping - - [NotNull] readonly TimestampInstantMapping _timestampInstant = new TimestampInstantMapping(); - [NotNull] readonly TimestampLocalDateTimeMapping _timestampLocalDateTime = new TimestampLocalDateTimeMapping(); - - [NotNull] readonly TimestampTzInstantMapping _timestamptzInstant = new TimestampTzInstantMapping(); - [NotNull] readonly TimestampTzZonedDateTimeMapping _timestamptzZonedDateTime = new TimestampTzZonedDateTimeMapping(); - [NotNull] readonly TimestampTzOffsetDateTimeMapping _timestamptzOffsetDateTime = new TimestampTzOffsetDateTimeMapping(); - - [NotNull] readonly DateMapping _date = new DateMapping(); - [NotNull] readonly TimeMapping _time = new TimeMapping(); - [NotNull] readonly TimeTzMapping _timetz = new TimeTzMapping(); - [NotNull] readonly IntervalMapping _period = new IntervalMapping(); - - [NotNull] readonly NpgsqlRangeTypeMapping _timestampLocalDateTimeRange; - [NotNull] readonly NpgsqlRangeTypeMapping _timestampInstantRange; - [NotNull] readonly NpgsqlRangeTypeMapping _timestamptzInstantRange; - [NotNull] readonly NpgsqlRangeTypeMapping _timestamptzZonedDateTimeRange; - [NotNull] readonly NpgsqlRangeTypeMapping _timestamptzOffsetDateTimeRange; - [NotNull] readonly NpgsqlRangeTypeMapping _dateRange; - - #endregion - - /// - /// The default member translators registered by the Npgsql NodaTime plugin. - /// - [NotNull] static readonly IMemberTranslator[] MemberTranslators = { new NodaTimeMemberTranslator() }; - - /// - /// The default method call translators registered by the Npgsql NodaTime plugin. - /// - [NotNull] static readonly IMethodCallTranslator[] MethodCallTranslators = { new NodaTimeMethodCallTranslator() }; - - /// - /// The default evaluatable expression filters registered by the Npgsql NodaTime plugin. - /// - [NotNull] static readonly IEvaluatableExpressionFilter[] EvaluatableExpressionFilters = { new NodaTimeEvaluatableExpressionFilter() }; - - /// - public override string Name => "NodaTime"; - - /// - public override string Description => "Plugin to map NodaTime types to PostgreSQL date/time datatypes"; - - /// - /// Constructs an instance of the class. - /// - public NodaTimePlugin() - { - _timestampLocalDateTimeRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange), _timestampLocalDateTime); - _timestampInstantRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange), _timestampInstant); - _timestamptzInstantRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzInstant); - _timestamptzZonedDateTimeRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzZonedDateTime); - _timestamptzOffsetDateTimeRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzOffsetDateTime); - _dateRange = new NpgsqlRangeTypeMapping("daterange", typeof(NpgsqlRange), _date); - } - - /// - public override void AddMappings(NpgsqlTypeMappingSource typeMappingSource) - { - typeMappingSource.ClrTypeMappings[typeof(Instant)] = _timestampInstant; - typeMappingSource.ClrTypeMappings[typeof(LocalDateTime)] = _timestampLocalDateTime; - - typeMappingSource.StoreTypeMappings["timestamp"] = - typeMappingSource.StoreTypeMappings["timestamp without time zone"] = - new RelationalTypeMapping[] { _timestampInstant, _timestampLocalDateTime }; - - typeMappingSource.ClrTypeMappings[typeof(ZonedDateTime)] = _timestamptzZonedDateTime; - typeMappingSource.ClrTypeMappings[typeof(OffsetDateTime)] = _timestamptzOffsetDateTime; - - typeMappingSource.StoreTypeMappings["timestamptz"] = - typeMappingSource.StoreTypeMappings["timestamp with time zone"] = - new RelationalTypeMapping[] { _timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime }; - - typeMappingSource.ClrTypeMappings[typeof(LocalDate)] = _date; - typeMappingSource.StoreTypeMappings["date"] = new RelationalTypeMapping[] { _date }; - - typeMappingSource.ClrTypeMappings[typeof(LocalTime)] = _time; - typeMappingSource.StoreTypeMappings["time"] = new RelationalTypeMapping[] { _time }; - - typeMappingSource.ClrTypeMappings[typeof(OffsetTime)] = _timetz; - typeMappingSource.StoreTypeMappings["timetz"] = new RelationalTypeMapping[] { _timetz }; - - typeMappingSource.ClrTypeMappings[typeof(Period)] = _period; - typeMappingSource.StoreTypeMappings["interval"] = new RelationalTypeMapping[] { _period }; - - // Ranges - typeMappingSource.ClrTypeMappings[typeof(NpgsqlRange)] = _timestampInstantRange; - typeMappingSource.ClrTypeMappings[typeof(NpgsqlRange)] = _timestampLocalDateTimeRange; - - typeMappingSource.StoreTypeMappings["tsrange"] = - new RelationalTypeMapping[] { _timestampInstantRange, _timestampLocalDateTimeRange }; - - typeMappingSource.ClrTypeMappings[typeof(NpgsqlRange)] = _timestamptzZonedDateTimeRange; - typeMappingSource.ClrTypeMappings[typeof(NpgsqlRange)] = _timestamptzOffsetDateTimeRange; - - typeMappingSource.StoreTypeMappings["tstzrange"] = - new RelationalTypeMapping[] { _timestamptzInstantRange, _timestamptzZonedDateTimeRange, _timestamptzOffsetDateTimeRange }; - - typeMappingSource.ClrTypeMappings[typeof(NpgsqlRange)] = _dateRange; - typeMappingSource.StoreTypeMappings["daterange"] = new RelationalTypeMapping[] { _dateRange }; - } - - /// - public override void AddMemberTranslators(NpgsqlCompositeMemberTranslator compositeMemberTranslator) - => compositeMemberTranslator.AddTranslators(MemberTranslators); - - /// - public override void AddMethodCallTranslators(NpgsqlCompositeMethodCallTranslator compositeMethodCallTranslator) - => compositeMethodCallTranslator.AddTranslators(MethodCallTranslators); - - /// - public override void AddEvaluatableExpressionFilters(NpgsqlCompositeEvaluatableExpressionFilter compositeEvaluatableExpressionFilter) - => compositeEvaluatableExpressionFilter.AddFilters(EvaluatableExpressionFilters); - } -} diff --git a/src/EFCore.PG.NodaTime/NodaTimeMemberTranslator.cs b/src/EFCore.PG.NodaTime/Query/ExpressionTranslators/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs similarity index 91% rename from src/EFCore.PG.NodaTime/NodaTimeMemberTranslator.cs rename to src/EFCore.PG.NodaTime/Query/ExpressionTranslators/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs index 2311b0b77..0bc96a485 100644 --- a/src/EFCore.PG.NodaTime/NodaTimeMemberTranslator.cs +++ b/src/EFCore.PG.NodaTime/Query/ExpressionTranslators/Internal/NpgsqlNodaTimeMemberTranslatorPlugin.cs @@ -23,6 +23,7 @@ #endregion +using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; @@ -38,7 +39,21 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime /// /// See: https://www.postgresql.org/docs/current/static/functions-datetime.html /// - public class NodaTimeMemberTranslator : IMemberTranslator + public class NpgsqlNodaTimeMemberTranslatorPlugin : IMemberTranslatorPlugin + { + public virtual IEnumerable Translators { get; } = new IMemberTranslator[] + { + new NpgsqlNodaTimeMemberTranslator() + }; + } + + /// + /// Provides translation services for members. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/functions-datetime.html + /// + public class NpgsqlNodaTimeMemberTranslator : IMemberTranslator { /// /// The static member info for . diff --git a/src/EFCore.PG.NodaTime/NodaTimeMethodCallTranslator.cs b/src/EFCore.PG.NodaTime/Query/ExpressionTranslators/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs similarity index 87% rename from src/EFCore.PG.NodaTime/NodaTimeMethodCallTranslator.cs rename to src/EFCore.PG.NodaTime/Query/ExpressionTranslators/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs index 44a26c3e5..1aa6fc149 100644 --- a/src/EFCore.PG.NodaTime/NodaTimeMethodCallTranslator.cs +++ b/src/EFCore.PG.NodaTime/Query/ExpressionTranslators/Internal/NpgsqlNodaTimeMethodCallTranslatorPlugin.cs @@ -35,10 +35,24 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime { + /// + /// Provides translation services for members. + /// + /// + /// See: https://www.postgresql.org/docs/current/static/functions-datetime.html + /// + public class NpgsqlNodaTimeMethodCallTranslatorPlugin : IMethodCallTranslatorPlugin + { + public virtual IEnumerable Translators { get; } = new IMethodCallTranslator[] + { + new NpgsqlNodaTimeMethodCallTranslator() + }; + } + /// /// Provides translation services for NodaTime method calls. /// - public class NodaTimeMethodCallTranslator : IMethodCallTranslator + public class NpgsqlNodaTimeMethodCallTranslator : IMethodCallTranslator { /// /// The static method info for . diff --git a/src/EFCore.PG.NodaTime/NodaTimeMappings.cs b/src/EFCore.PG.NodaTime/Storage/Internal/NodaTimeMappings.cs similarity index 99% rename from src/EFCore.PG.NodaTime/NodaTimeMappings.cs rename to src/EFCore.PG.NodaTime/Storage/Internal/NodaTimeMappings.cs index 10d1f6a70..7d0b03d7c 100644 --- a/src/EFCore.PG.NodaTime/NodaTimeMappings.cs +++ b/src/EFCore.PG.NodaTime/Storage/Internal/NodaTimeMappings.cs @@ -30,7 +30,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; using NpgsqlTypes; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal { #region timestamp diff --git a/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs b/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs new file mode 100644 index 000000000..629792b9a --- /dev/null +++ b/src/EFCore.PG.NodaTime/Storage/Internal/NpgsqlNodaTimeTypeMappingSourcePlugin.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Storage; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping; +using NpgsqlTypes; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal +{ + public class NpgsqlNodaTimeTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin + { + public ConcurrentDictionary StoreTypeMappings { get; } + public ConcurrentDictionary ClrTypeMappings { get; } + + #region TypeMapping + + [NotNull] readonly TimestampInstantMapping _timestampInstant = new TimestampInstantMapping(); + [NotNull] readonly TimestampLocalDateTimeMapping _timestampLocalDateTime = new TimestampLocalDateTimeMapping(); + + [NotNull] readonly TimestampTzInstantMapping _timestamptzInstant = new TimestampTzInstantMapping(); + [NotNull] readonly TimestampTzZonedDateTimeMapping _timestamptzZonedDateTime = new TimestampTzZonedDateTimeMapping(); + [NotNull] readonly TimestampTzOffsetDateTimeMapping _timestamptzOffsetDateTime = new TimestampTzOffsetDateTimeMapping(); + + [NotNull] readonly DateMapping _date = new DateMapping(); + [NotNull] readonly TimeMapping _time = new TimeMapping(); + [NotNull] readonly TimeTzMapping _timetz = new TimeTzMapping(); + [NotNull] readonly IntervalMapping _period = new IntervalMapping(); + + [NotNull] readonly NpgsqlRangeTypeMapping _timestampLocalDateTimeRange; + [NotNull] readonly NpgsqlRangeTypeMapping _timestampInstantRange; + [NotNull] readonly NpgsqlRangeTypeMapping _timestamptzInstantRange; + [NotNull] readonly NpgsqlRangeTypeMapping _timestamptzZonedDateTimeRange; + [NotNull] readonly NpgsqlRangeTypeMapping _timestamptzOffsetDateTimeRange; + [NotNull] readonly NpgsqlRangeTypeMapping _dateRange; + + #endregion + + /// + /// Constructs an instance of the class. + /// + public NpgsqlNodaTimeTypeMappingSourcePlugin() + { + _timestampLocalDateTimeRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange), _timestampLocalDateTime); + _timestampInstantRange = new NpgsqlRangeTypeMapping("tsrange", typeof(NpgsqlRange), _timestampInstant); + _timestamptzInstantRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzInstant); + _timestamptzZonedDateTimeRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzZonedDateTime); + _timestamptzOffsetDateTimeRange = new NpgsqlRangeTypeMapping("tstzrange", typeof(NpgsqlRange), _timestamptzOffsetDateTime); + _dateRange = new NpgsqlRangeTypeMapping("daterange", typeof(NpgsqlRange), _date); + + var storeTypeMappings = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "timestamp", new RelationalTypeMapping[] { _timestampInstant, _timestampLocalDateTime } }, + { "timestamp without time zone", new RelationalTypeMapping[] { _timestampInstant, _timestampLocalDateTime } }, + { "timestamptz", new RelationalTypeMapping[] { _timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime } }, + { "timestamp with time zone", new RelationalTypeMapping[] { _timestamptzInstant, _timestamptzZonedDateTime, _timestamptzOffsetDateTime } }, + { "date", new RelationalTypeMapping[] { _date } }, + { "time", new RelationalTypeMapping[] { _time } }, + { "time without time zone", new RelationalTypeMapping[] { _time } }, + { "timetz", new RelationalTypeMapping[] { _timetz } }, + { "time with time zone", new RelationalTypeMapping[] { _timetz } }, + { "interval", new RelationalTypeMapping[] { _period } }, + + { "tsrange", new RelationalTypeMapping[] { _timestampInstantRange, _timestampLocalDateTimeRange } }, + { "tstzrange", new RelationalTypeMapping[] { _timestamptzInstantRange, _timestamptzZonedDateTimeRange, _timestamptzOffsetDateTimeRange } }, + { "daterange", new RelationalTypeMapping[] { _dateRange} } + }; + + var clrTypeMappings = new Dictionary + { + { typeof(Instant), _timestampInstant }, + { typeof(LocalDateTime), _timestampLocalDateTime }, + { typeof(ZonedDateTime), _timestamptzZonedDateTime }, + { typeof(OffsetDateTime), _timestamptzOffsetDateTime }, + { typeof(LocalDate), _date }, + { typeof(LocalTime), _time }, + { typeof(OffsetTime), _timetz }, + { typeof(Period), _period }, + + { typeof(NpgsqlRange), _timestampInstantRange }, + { typeof(NpgsqlRange), _timestampLocalDateTimeRange }, + { typeof(NpgsqlRange), _timestamptzZonedDateTimeRange }, + { typeof(NpgsqlRange), _dateRange }, + { typeof(NpgsqlRange), _timestamptzOffsetDateTimeRange } + }; + + StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); + ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings); + } + + public RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) + => FindExistingMapping(mappingInfo) ?? FindArrayMapping(mappingInfo); + + protected virtual RelationalTypeMapping FindExistingMapping(in RelationalTypeMappingInfo mappingInfo) + { + var clrType = mappingInfo.ClrType; + var storeTypeName = mappingInfo.StoreTypeName; + var storeTypeNameBase = mappingInfo.StoreTypeNameBase; + + if (storeTypeName != null) + { + if (StoreTypeMappings.TryGetValue(storeTypeName, out var mappings)) + { + if (clrType == null) + return mappings[0]; + + foreach (var m in mappings) + if (m.ClrType == clrType) + return m; + + return null; + } + + if (StoreTypeMappings.TryGetValue(storeTypeNameBase, out mappings)) + { + if (clrType == null) + return mappings[0].Clone(in mappingInfo); + + foreach (var m in mappings) + if (m.ClrType == clrType) + return m.Clone(in mappingInfo); + + return null; + } + } + + if (clrType == null || !ClrTypeMappings.TryGetValue(clrType, out var mapping)) + return null; + + // All PostgreSQL date/time types accept a precision except for date + // TODO: Cache size/precision/scale mappings? + return mappingInfo.Precision.HasValue && mapping.ClrType != typeof(LocalDate) + ? mapping.Clone($"{mapping.StoreType}({mappingInfo.Precision.Value})", null) + : mapping; + } + + RelationalTypeMapping FindArrayMapping(in RelationalTypeMappingInfo mappingInfo) + { + // PostgreSQL array type names are the element plus [] + var storeType = mappingInfo.StoreTypeName; + if (storeType != null) + { + if (!storeType.EndsWith("[]")) + return null; + + // Note that we scaffold PostgreSQL arrays to C# arrays, not lists (which are also supported) + + // TODO: In theory support the multiple mappings just like we do with scalars above + // (e.g. DateTimeOffset[] vs. DateTime[] + var elementMapping = FindExistingMapping(new RelationalTypeMappingInfo(storeType.Substring(0, storeType.Length - 2))); + if (elementMapping != null) + return StoreTypeMappings.GetOrAdd(storeType, + new RelationalTypeMapping[] { new NpgsqlArrayTypeMapping(storeType, elementMapping) })[0]; + } + + var clrType = mappingInfo.ClrType; + if (clrType == null) + return null; + + if (clrType.IsArray) + { + var elementType = clrType.GetElementType(); + Debug.Assert(elementType != null, "Detected array type but element type is null"); + + // If an element isn't supported, neither is its array + var elementMapping = FindExistingMapping(new RelationalTypeMappingInfo(elementType)); + if (elementMapping == null) + return null; + + // Arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL + if (elementMapping is NpgsqlArrayTypeMapping) + return null; + + return ClrTypeMappings.GetOrAdd(clrType, new NpgsqlArrayTypeMapping(elementMapping, clrType)); + } + + if (clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(List<>)) + { + var elementType = clrType.GetGenericArguments()[0]; + + // If an element isn't supported, neither is its array + var elementMapping = FindExistingMapping(new RelationalTypeMappingInfo(elementType)); + if (elementMapping == null) + return null; + + // Arrays of arrays aren't supported (as opposed to multidimensional arrays) by PostgreSQL + if (elementMapping is NpgsqlArrayTypeMapping) + return null; + + return ClrTypeMappings.GetOrAdd(clrType, new NpgsqlListTypeMapping(elementMapping, clrType)); + } + + return null; + } + } +} diff --git a/src/EFCore.PG.NodaTime/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.targets b/src/EFCore.PG.NodaTime/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.targets new file mode 100644 index 000000000..2bbe4bc5d --- /dev/null +++ b/src/EFCore.PG.NodaTime/build/netstandard2.0/Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime.targets @@ -0,0 +1,46 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + $(IntermediateOutputPath)EFCoreNpgsqlNodaTime$(DefaultLanguageSourceExtension) + + + + + + + CompileBefore + + + + + CompileAfter + + + + + + + Compile + + + + + + + <_Parameter1>Npgsql.EntityFrameworkCore.PostgreSQL.Design.Internal.NpgsqlNodaTimeDesignTimeServices, Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime + <_Parameter2>Npgsql.EntityFrameworkCore.PostgreSQL + + + + + + + + diff --git a/src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs b/src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs index 6352ca8ae..680bbea9e 100644 --- a/src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs +++ b/src/EFCore.PG/Infrastructure/Internal/INpgsqlOptions.cs @@ -26,12 +26,5 @@ public interface INpgsqlOptions : ISingletonOptions /// [NotNull] IReadOnlyList RangeMappings { get; } - - /// - /// The collection of database plugins. - /// - [NotNull] - [ItemNotNull] - IReadOnlyList Plugins { get; } } } diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs index ab1bf9ca4..f1db48908 100644 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs +++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs @@ -16,8 +16,6 @@ public class NpgsqlOptionsExtension : RelationalOptionsExtension { [NotNull] readonly List _rangeMappings; - [NotNull] readonly List _plugins; - /// /// The name of the database for administrative operations. /// @@ -36,13 +34,6 @@ public class NpgsqlOptionsExtension : RelationalOptionsExtension [NotNull] public IReadOnlyList RangeMappings => _rangeMappings; - /// - /// The collection of database plugins. - /// - [NotNull] - [ItemNotNull] - public IReadOnlyList Plugins => _plugins; - /// /// The specified . /// @@ -64,10 +55,7 @@ public class NpgsqlOptionsExtension : RelationalOptionsExtension /// Initializes an instance of with the default settings. /// public NpgsqlOptionsExtension() - { - _rangeMappings = new List(); - _plugins = new List(); - } + => _rangeMappings = new List(); // NB: When adding new options, make sure to update the copy ctor below. /// @@ -78,7 +66,6 @@ public NpgsqlOptionsExtension([NotNull] NpgsqlOptionsExtension copyFrom) : base( { AdminDatabase = copyFrom.AdminDatabase; _rangeMappings = new List(copyFrom._rangeMappings); - _plugins = new List(copyFrom._plugins); PostgresVersion = copyFrom.PostgresVersion; ProvideClientCertificatesCallback = copyFrom.ProvideClientCertificatesCallback; RemoteCertificateValidationCallback = copyFrom.RemoteCertificateValidationCallback; @@ -129,22 +116,6 @@ public virtual NpgsqlOptionsExtension WithRangeMapping(string rangeName, Type su return clone; } - /// - /// Returns a copy of the current instance configured to use the specified . - /// - /// The plugin to configure. - [NotNull] - public virtual NpgsqlOptionsExtension WithPlugin([NotNull] NpgsqlEntityFrameworkPlugin plugin) - { - Check.NotNull(plugin, nameof(plugin)); - - var clone = (NpgsqlOptionsExtension)Clone(); - - clone._plugins.Add(plugin); - - return clone; - } - /// /// Returns a copy of the current instance configured to use the specified administrative database. /// diff --git a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs index b560bb207..44f8fa768 100644 --- a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs +++ b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs @@ -23,13 +23,6 @@ public class NpgsqlDbContextOptionsBuilder public NpgsqlDbContextOptionsBuilder([NotNull] DbContextOptionsBuilder optionsBuilder) : base(optionsBuilder) {} - /// - /// Configures the to use the specified . - /// - /// The plugin to configure. - public virtual void UsePlugin([NotNull] NpgsqlEntityFrameworkPlugin plugin) - => WithOption(e => e.WithPlugin(plugin)); - /// /// Connect to this database for administrative operations (creating/dropping databases). /// diff --git a/src/EFCore.PG/Infrastructure/NpgsqlEntityFrameworkPlugin.cs b/src/EFCore.PG/Infrastructure/NpgsqlEntityFrameworkPlugin.cs deleted file mode 100644 index 2f33e591a..000000000 --- a/src/EFCore.PG/Infrastructure/NpgsqlEntityFrameworkPlugin.cs +++ /dev/null @@ -1,49 +0,0 @@ -using JetBrains.Annotations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.EvaluatableExpressionFilters.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Query.ExpressionTranslators.Internal; -using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure -{ - /// - /// Represents a plugin to the Npgsql provider for Entity Framework Core. - /// - public abstract class NpgsqlEntityFrameworkPlugin - { - /// - /// The name of the plugin. - /// - [NotNull] - public abstract string Name { get; } - - /// - /// The description of the plugin. - /// - [NotNull] - public abstract string Description { get; } - - /// - /// Adds plugin-specific type mappings to the . - /// - /// The default type mapping source for the Npgsql provider. - public virtual void AddMappings([NotNull] NpgsqlTypeMappingSource typeMappingSource) {} - - /// - /// Adds plugin-specific method call translators to the . - /// - /// - public virtual void AddMethodCallTranslators([NotNull] NpgsqlCompositeMethodCallTranslator compositeMethodCallTranslator) {} - - /// - /// Adds plugin-specific member translators to the . - /// - /// - public virtual void AddMemberTranslators([NotNull] NpgsqlCompositeMemberTranslator compositeMemberTranslator) {} - - /// - /// Adds plugin-specific evaluatable expression filters to the . - /// - /// - public virtual void AddEvaluatableExpressionFilters([NotNull] NpgsqlCompositeEvaluatableExpressionFilter compositeEvaluatableExpressionFilter) {} - } -} diff --git a/src/EFCore.PG/Internal/NpgsqlOptions.cs b/src/EFCore.PG/Internal/NpgsqlOptions.cs index f91e48a26..b18f2fcea 100644 --- a/src/EFCore.PG/Internal/NpgsqlOptions.cs +++ b/src/EFCore.PG/Internal/NpgsqlOptions.cs @@ -22,15 +22,8 @@ public class NpgsqlOptions : INpgsqlOptions [NotNull] public virtual IReadOnlyList RangeMappings { get; private set; } - /// - [NotNull] - public virtual IReadOnlyList Plugins { get; private set; } - public NpgsqlOptions() - { - RangeMappings = new RangeMappingInfo[0]; - Plugins = new NpgsqlEntityFrameworkPlugin[0]; - } + => RangeMappings = new RangeMappingInfo[0]; /// public void Initialize(IDbContextOptions options) @@ -39,7 +32,6 @@ public void Initialize(IDbContextOptions options) PostgresVersion = npgsqlOptions.PostgresVersion; ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering; - Plugins = npgsqlOptions.Plugins; RangeMappings = npgsqlOptions.RangeMappings; } diff --git a/src/EFCore.PG/Query/EvaluatableExpressionFilters/Internal/NpgsqlCompositeEvaluatableExpressionFilter.cs b/src/EFCore.PG/Query/EvaluatableExpressionFilters/Internal/NpgsqlCompositeEvaluatableExpressionFilter.cs index 4fc3db5e7..865636e23 100644 --- a/src/EFCore.PG/Query/EvaluatableExpressionFilters/Internal/NpgsqlCompositeEvaluatableExpressionFilter.cs +++ b/src/EFCore.PG/Query/EvaluatableExpressionFilters/Internal/NpgsqlCompositeEvaluatableExpressionFilter.cs @@ -21,15 +21,12 @@ public class NpgsqlCompositeEvaluatableExpressionFilter : RelationalEvaluatableE [NotNull] [ItemNotNull] readonly List _filters = new List { - new NpgsqlFullTextSearchEvaluatableExpressionFilter() + new NpgsqlFullTextSearchEvaluatableExpressionFilter(), + new NpgsqlNodaTimeEvaluatableExpressionFilter() }; /// - public NpgsqlCompositeEvaluatableExpressionFilter([NotNull] IModel model, [NotNull] INpgsqlOptions npgsqlOptions) : base(model) - { - foreach (var plugin in npgsqlOptions.Plugins) - plugin.AddEvaluatableExpressionFilters(this); - } + public NpgsqlCompositeEvaluatableExpressionFilter([NotNull] IModel model) : base(model) {} /// public override bool IsEvaluatableMember(MemberExpression expression) diff --git a/src/EFCore.PG.NodaTime/NodaTimeEvaluatableExpressionFilter.cs b/src/EFCore.PG/Query/EvaluatableExpressionFilters/Internal/NpgsqlNodaTimeEvaluatableExpressionFilter.cs similarity index 79% rename from src/EFCore.PG.NodaTime/NodaTimeEvaluatableExpressionFilter.cs rename to src/EFCore.PG/Query/EvaluatableExpressionFilters/Internal/NpgsqlNodaTimeEvaluatableExpressionFilter.cs index c98004899..014acef72 100644 --- a/src/EFCore.PG.NodaTime/NodaTimeEvaluatableExpressionFilter.cs +++ b/src/EFCore.PG/Query/EvaluatableExpressionFilters/Internal/NpgsqlNodaTimeEvaluatableExpressionFilter.cs @@ -23,37 +23,26 @@ #endregion -using System; using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; -using NodaTime; +using Microsoft.EntityFrameworkCore; +using NpgsqlTypes; using Remotion.Linq.Parsing.ExpressionVisitors.TreeEvaluation; -namespace Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query.EvaluatableExpressionFilters.Internal { - /// - /// Represents an Npgsql-specific filter for NodaTime to identify expressions that are evaluatable. - /// - public class NodaTimeEvaluatableExpressionFilter : IEvaluatableExpressionFilter + // TODO: This is a hack until https://github.com/aspnet/EntityFrameworkCore/issues/13454 is done + public class NpgsqlNodaTimeEvaluatableExpressionFilter : IEvaluatableExpressionFilter { - /// - /// The static member info for . - /// - [NotNull] static readonly MemberInfo Instance = - typeof(SystemClock).GetRuntimeProperty(nameof(SystemClock.Instance)); - - /// - /// The static method info for . - /// - [NotNull] static readonly MethodInfo GetCurrentInstant = - typeof(SystemClock).GetRuntimeMethod(nameof(SystemClock.GetCurrentInstant), new Type[0]); - /// - public bool IsEvaluatableMember(MemberExpression node) => node.Member != Instance; + public bool IsEvaluatableMethodCall(MethodCallExpression node) + => node.Method.DeclaringType?.FullName != "NodaTime.SystemClock" || + node.Method.Name != "GetCurrentInstant"; - /// - public bool IsEvaluatableMethodCall(MethodCallExpression node) => node.Method != GetCurrentInstant; + bool IEvaluatableExpressionFilter.IsEvaluatableMember(MemberExpression node) + => node.Member.DeclaringType?.FullName != "NodaTime.SystemClock" || + node.Member.Name != "Instance"; #region unused interface methods diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs index e9f681dff..3accfdc90 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMemberTranslator.cs @@ -28,9 +28,6 @@ public NpgsqlCompositeMemberTranslator( { // ReSharper disable once VirtualMemberCallInConstructor AddTranslators(MemberTranslators); - - foreach (var plugin in npgsqlOptions.Plugins) - plugin.AddMemberTranslators(this); } /// diff --git a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs index 1329c4ad6..c40cbaab1 100644 --- a/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs +++ b/src/EFCore.PG/Query/ExpressionTranslators/Internal/NpgsqlCompositeMethodCallTranslator.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query.ExpressionTranslators; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; @@ -45,9 +45,6 @@ public NpgsqlCompositeMethodCallTranslator( // ReSharper disable once DoNotCallOverridableMethodsInConstructor AddTranslators(versionDependentTranslators); - - foreach (var plugin in npgsqlOptions.Plugins) - plugin.AddMethodCallTranslators(this); } /// diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index 207e93e81..b13983c32 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -256,9 +256,6 @@ public NpgsqlTypeMappingSource([NotNull] TypeMappingSourceDependencies dependenc StoreTypeMappings[rangeName] = new RelationalTypeMapping[] { rangeMapping }; ClrTypeMappings[rangeClrType] = rangeMapping; } - - foreach (var plugin in npgsqlOptions.Plugins) - plugin.AddMappings(this); } /// @@ -298,18 +295,15 @@ void SetupEnumMappings() } } - protected override RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) - { - var baseMapping = FindBaseTypeMapping(mappingInfo); - if (baseMapping != null) - return baseMapping; - - // We couldn't find a base (simple) type mapping. Try to find an array. - var arrayMapping = FindArrayMapping(mappingInfo); - return arrayMapping ?? null; - } + protected override RelationalTypeMapping FindMapping(in RelationalTypeMappingInfo mappingInfo) => + // First, try any plugins, allowing them to override built-in mappings (e.g. NodaTime) + base.FindMapping(mappingInfo) ?? + // Then, any mappings that have already been set up + FindExistingMapping(mappingInfo) ?? + // Finally, try any array mappings which have not yet been set up + FindArrayMapping(mappingInfo); - protected virtual RelationalTypeMapping FindBaseTypeMapping(in RelationalTypeMappingInfo mappingInfo) + protected virtual RelationalTypeMapping FindExistingMapping(in RelationalTypeMappingInfo mappingInfo) { var clrType = mappingInfo.ClrType; var storeTypeName = mappingInfo.StoreTypeName; @@ -319,9 +313,13 @@ protected virtual RelationalTypeMapping FindBaseTypeMapping(in RelationalTypeMap { if (StoreTypeMappings.TryGetValue(storeTypeName, out var mappings)) { + // We found the user-specified store type. No CLR type was provided - we're probably + // scaffolding from an existing database, take the first mapping as the default. if (clrType == null) return mappings[0]; + // A CLR type was provided - look for a mapping between the store and CLR types. If not found, fail + // immediately. foreach (var m in mappings) if (m.ClrType == clrType) return m; @@ -340,12 +338,12 @@ protected virtual RelationalTypeMapping FindBaseTypeMapping(in RelationalTypeMap return null; } - } - if (clrType == null) - return null; + // A store type name was provided, but is unknown. This could be a domain (alias) type, in which case + // we proceed with a CLR type lookup (if the type doesn't exist at all the failure will come later). + } - if (!ClrTypeMappings.TryGetValue(clrType, out var mapping)) + if (clrType == null || !ClrTypeMappings.TryGetValue(clrType, out var mapping)) return null; // If needed, clone the mapping with the configured length/precision/scale diff --git a/src/Shared/MemberInfoExtensions.cs b/src/Shared/MemberInfoExtensions.cs new file mode 100644 index 000000000..23231d7e3 --- /dev/null +++ b/src/Shared/MemberInfoExtensions.cs @@ -0,0 +1,73 @@ +using System.Diagnostics; +using System.Linq; +using Microsoft.EntityFrameworkCore.Internal; + +namespace System.Reflection +{ + internal static class MemberInfoExtensions + { + public static Type GetMemberType(this MemberInfo memberInfo) + => (memberInfo as PropertyInfo)?.PropertyType ?? ((FieldInfo)memberInfo)?.FieldType; + + public static bool IsSameAs(this MemberInfo propertyInfo, MemberInfo otherPropertyInfo) + => propertyInfo == null + ? otherPropertyInfo == null + : (otherPropertyInfo == null + ? false + : Equals(propertyInfo, otherPropertyInfo) + || (propertyInfo.Name == otherPropertyInfo.Name + && (propertyInfo.DeclaringType == otherPropertyInfo.DeclaringType + || propertyInfo.DeclaringType.GetTypeInfo().IsSubclassOf(otherPropertyInfo.DeclaringType) + || otherPropertyInfo.DeclaringType.GetTypeInfo().IsSubclassOf(propertyInfo.DeclaringType) + || propertyInfo.DeclaringType.GetTypeInfo().ImplementedInterfaces.Contains(otherPropertyInfo.DeclaringType) + || otherPropertyInfo.DeclaringType.GetTypeInfo().ImplementedInterfaces.Contains(propertyInfo.DeclaringType)))); + + public static MemberInfo OnInterface(this MemberInfo targetMember, Type interfaceType) + { + var declaringType = targetMember.DeclaringType; + if (declaringType == interfaceType + || declaringType.IsInterface + || !declaringType.GetInterfaces().Any(i => i == interfaceType)) + { + return targetMember; + } + if (targetMember is MethodInfo targetMethod) + { + return targetMethod.OnInterface(interfaceType); + } + if (targetMember is PropertyInfo targetProperty) + { + var targetGetMethod = targetProperty.GetMethod; + var interfaceGetMethod = targetGetMethod.OnInterface(interfaceType); + if (interfaceGetMethod == targetGetMethod) + { + return targetProperty; + } + + return interfaceType.GetProperties().First(p => Equals(p.GetMethod, interfaceGetMethod)); + } + + Debug.Fail("Unexpected member type: " + targetMember.MemberType); + + return targetMember; + } + + public static MethodInfo OnInterface(this MethodInfo targetMethod, Type interfaceType) + { + var declaringType = targetMethod.DeclaringType; + if (declaringType == interfaceType + || declaringType.IsInterface + || !declaringType.GetInterfaces().Any(i => i == interfaceType)) + { + return targetMethod; + } + + var map = targetMethod.DeclaringType.GetInterfaceMap(interfaceType); + var index = map.TargetMethods.IndexOf(targetMethod); + + return index != -1 + ? map.InterfaceMethods[index] + : targetMethod; + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj index 5ae3fc86a..06c837bd6 100644 --- a/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj +++ b/test/EFCore.PG.FunctionalTests/EFCore.PG.FunctionalTests.csproj @@ -19,6 +19,7 @@ + diff --git a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs b/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs index 8d380f64b..3840f868c 100644 --- a/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs +++ b/test/EFCore.PG.FunctionalTests/NpgsqlComplianceTest.cs @@ -16,9 +16,7 @@ public class NpgsqlComplianceTest : RelationalComplianceTestBase typeof(FunkyDataQueryTestBase<>), typeof(LoggingRelationalTestBase<,>), typeof(AsyncFromSqlSprocQueryTestBase<>), - typeof(FromSqlSprocQueryTestBase<>), - typeof(SpatialTestBase<>), - typeof(SpatialQueryTestBase<>) + typeof(FromSqlSprocQueryTestBase<>) }; protected override Assembly TargetAssembly { get; } = typeof(NpgsqlComplianceTest).Assembly; diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyFixture.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyFixture.cs new file mode 100644 index 000000000..bd4e6b4c9 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyFixture.cs @@ -0,0 +1,61 @@ +using System.Threading; +using GeoAPI; +using GeoAPI.Geometries; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using NetTopologySuite; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + public class SpatialQueryNpgsqlGeographyFixture : SpatialQueryRelationalFixture + { + IGeometryServices _geometryServices; + IGeometryFactory _geometryFactory; + + public IGeometryServices GeometryServices + => LazyInitializer.EnsureInitialized( + ref _geometryServices, + () => new NtsGeometryServices( + NtsGeometryServices.Instance.DefaultCoordinateSequenceFactory, + NtsGeometryServices.Instance.DefaultPrecisionModel, + 4326)); + + public override IGeometryFactory GeometryFactory + => LazyInitializer.EnsureInitialized( + ref _geometryFactory, + () => GeometryServices.CreateGeometryFactory()); + + protected override string StoreName + => "SpatialQueryGeographyTest"; + + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + { + NpgsqlConnection.GlobalTypeMapper.UseNetTopologySuite(); + + return base.AddServices(serviceCollection) + .AddEntityFrameworkNpgsqlNetTopologySuite(); + } + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNetTopologySuite(null, null, Ordinates.None, true); + + return optionsBuilder; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("postgis"); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs new file mode 100644 index 000000000..ba97e8c35 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeographyTest.cs @@ -0,0 +1,201 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; +using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; +using NetTopologySuite.Geometries; +using Xunit; +using Xunit.Abstractions; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + public class SpatialQueryNpgsqlGeographyTest : SpatialQueryTestBase + { + public SpatialQueryNpgsqlGeographyTest(SpatialQueryNpgsqlGeographyFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + protected override bool AssertDistances + => false; + + public override async Task Area(bool isAsync) + { + await base.Area(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Area(e.""Polygon"") AS ""Area"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task AsBinary(bool isAsync) + { + await base.AsBinary(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_AsBinary(e.""Point"") AS ""Binary"" +FROM ""PointEntity"" AS e"); + } + + public override async Task AsText(bool isAsync) + { + await base.AsText(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_AsText(e.""Point"") AS ""Text"" +FROM ""PointEntity"" AS e"); + } + + public override async Task Buffer(bool isAsync) + { + await base.Buffer(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Buffer(e.""Polygon"", 1.0) AS ""Buffer"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Centroid(bool isAsync) + { + await base.Centroid(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Centroid(e.""Polygon"") AS ""Centroid"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Distance(bool isAsync) + { + await base.Distance(isAsync); + + AssertSql( + @"@__point_0='POINT (0 1)' (DbType = Object) + +SELECT e.""Id"", ST_Distance(e.""Point"", @__point_0) AS ""Distance"" +FROM ""PointEntity"" AS e"); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public override async Task GeometryType(bool isAsync) + { + // PostGIS returns "POINT", NTS returns "Point" + await AssertQuery(isAsync, es => es.Select(e => new { e.Id, Type=e.Point.GeometryType.ToLower() })); + + AssertSql( + @"SELECT e.""Id"", LOWER(GeometryType(e.""Point"")) AS ""Type"" +FROM ""PointEntity"" AS e"); + } + + public override async Task Intersection(bool isAsync) + { + await base.Intersection(isAsync); + + AssertSql( + @"@__polygon_0='POLYGON ((0 0 +1 0 +1 1 +0 0))' (DbType = Object) + +SELECT e.""Id"", ST_Intersection(e.""Polygon"", @__polygon_0) AS ""Intersection"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Intersects(bool isAsync) + { + await base.Intersects(isAsync); + + AssertSql( + @"@__lineString_0='LINESTRING (0.5 -0.5 +0.5 0.5)' (DbType = Object) + +SELECT e.""Id"", ST_Intersects(e.""LineString"", @__lineString_0) AS ""Intersects"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task Length(bool isAsync) + { + await base.Length(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Length(e.""LineString"") AS ""Length"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task SRID(bool isAsync) + { + await base.SRID(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_SRID(e.""Point"") AS ""SRID"" +FROM ""PointEntity"" AS e"); + } + + public override async Task ToBinary(bool isAsync) + { + await base.ToBinary(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_AsBinary(e.""Point"") AS ""Binary"" +FROM ""PointEntity"" AS e"); + } + + public override async Task ToText(bool isAsync) + { + await base.ToText(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_AsText(e.""Point"") AS ""Text"" +FROM ""PointEntity"" AS e"); + } + + #region Not supported on geography + + public override Task Boundary(bool isAsync) => Task.CompletedTask; + public override Task Contains(bool isAsync) => Task.CompletedTask; + public override Task ConvexHull(bool isAsync) => Task.CompletedTask; + public override Task Crosses(bool isAsync) => Task.CompletedTask; + public override Task Difference(bool isAsync) => Task.CompletedTask; + public override Task Dimension(bool isAsync) => Task.CompletedTask; + public override Task Disjoint(bool isAsync) => Task.CompletedTask; + public override Task EndPoint(bool isAsync) => Task.CompletedTask; + public override Task Envelope(bool isAsync) => Task.CompletedTask; + public override Task EqualsTopologically(bool isAsync) => Task.CompletedTask; + public override Task ExteriorRing(bool isAsync) => Task.CompletedTask; + public override Task GetGeometryN(bool isAsync) => Task.CompletedTask; + public override Task GetInteriorRingN(bool isAsync) => Task.CompletedTask; + public override Task GetPointN(bool isAsync) => Task.CompletedTask; + public override Task ICurve_IsClosed(bool isAsync) => Task.CompletedTask; + public override Task IGeometryCollection_Count(bool isAsync) => Task.CompletedTask; + public override Task IMultiCurve_IsClosed(bool isAsync) => Task.CompletedTask; + public override Task IsEmpty(bool isAsync) => Task.CompletedTask; + public override Task IsRing(bool isAsync) => Task.CompletedTask; + public override Task IsSimple(bool isAsync) => Task.CompletedTask; + public override Task IsValid(bool isAsync) => Task.CompletedTask; + public override Task Item(bool isAsync) => Task.CompletedTask; + public override Task LineString_Count(bool isAsync) => Task.CompletedTask; + public override Task M(bool isAsync) => Task.CompletedTask; + public override Task NumGeometries(bool isAsync) => Task.CompletedTask; + public override Task NumInteriorRings(bool isAsync) => Task.CompletedTask; + public override Task NumPoints(bool isAsync) => Task.CompletedTask; + public override Task Overlaps(bool isAsync) => Task.CompletedTask; + public override Task PointOnSurface(bool isAsync) => Task.CompletedTask; + public override Task Relate(bool isAsync) => Task.CompletedTask; + public override Task Reverse(bool isAsync) => Task.CompletedTask; + public override Task StartPoint(bool isAsync) => Task.CompletedTask; + public override Task SymmetricDifference(bool isAsync) => Task.CompletedTask; + public override Task Touches(bool isAsync) => Task.CompletedTask; + public override Task Union(bool isAsync) => Task.CompletedTask; + public override Task Union_void(bool isAsync) => Task.CompletedTask; + public override Task Within(bool isAsync) => Task.CompletedTask; + public override Task X(bool isAsync) => Task.CompletedTask; + public override Task Y(bool isAsync) => Task.CompletedTask; + public override Task Z(bool isAsync) => Task.CompletedTask; + + #endregion + + void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryFixture.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryFixture.cs new file mode 100644 index 000000000..dde620bc4 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryFixture.cs @@ -0,0 +1,39 @@ +using GeoAPI.Geometries; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + public class SpatialQueryNpgsqlGeometryFixture : SpatialQueryRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + { + NpgsqlConnection.GlobalTypeMapper.UseNetTopologySuite(); + + return base.AddServices(serviceCollection) + .AddEntityFrameworkNpgsqlNetTopologySuite(); + } + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNetTopologySuite(); + + return optionsBuilder; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("postgis"); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs new file mode 100644 index 000000000..a2bcec777 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/Query/SpatialQueryNpgsqlGeometryTest.cs @@ -0,0 +1,544 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; +using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; +using NetTopologySuite.Geometries; +using Xunit; +using Xunit.Abstractions; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query +{ + public class SpatialQueryNpgsqlGeometryTest : SpatialQueryTestBase + { + public SpatialQueryNpgsqlGeometryTest(SpatialQueryNpgsqlGeometryFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + } + + public override async Task Area(bool isAsync) + { + await base.Area(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Area(e.""Polygon"") AS ""Area"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task AsBinary(bool isAsync) + { + await base.AsBinary(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_AsBinary(e.""Point"") AS ""Binary"" +FROM ""PointEntity"" AS e"); + } + + public override async Task AsText(bool isAsync) + { + await base.AsText(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_AsText(e.""Point"") AS ""Text"" +FROM ""PointEntity"" AS e"); + } + + public override async Task Boundary(bool isAsync) + { + await base.Boundary(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Boundary(e.""Polygon"") AS ""Boundary"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Buffer(bool isAsync) + { + await base.Buffer(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Buffer(e.""Polygon"", 1.0) AS ""Buffer"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Centroid(bool isAsync) + { + await base.Centroid(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Centroid(e.""Polygon"") AS ""Centroid"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Contains(bool isAsync) + { + await base.Contains(isAsync); + + AssertSql( + @"@__point_0='POINT (0.25 0.25)' (DbType = Object) + +SELECT e.""Id"", ST_Contains(e.""Polygon"", @__point_0) AS ""Contains"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task ConvexHull(bool isAsync) + { + await base.ConvexHull(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_ConvexHull(e.""Polygon"") AS ""ConvexHull"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task IGeometryCollection_Count(bool isAsync) + { + await base.IGeometryCollection_Count(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_NumGeometries(e.""MultiLineString"") AS ""Count"" +FROM ""MultiLineStringEntity"" AS e"); + } + + public override async Task LineString_Count(bool isAsync) + { + await base.LineString_Count(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_NumPoints(e.""LineString"") AS ""Count"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task Crosses(bool isAsync) + { + await base.Crosses(isAsync); + + AssertSql( + @"@__lineString_0='LINESTRING (0.5 -0.5 +0.5 0.5)' (DbType = Object) + +SELECT e.""Id"", ST_Crosses(e.""LineString"", @__lineString_0) AS ""Crosses"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task Difference(bool isAsync) + { + await base.Difference(isAsync); + + AssertSql( + @"@__polygon_0='POLYGON ((0 0 +1 0 +1 1 +0 0))' (DbType = Object) + +SELECT e.""Id"", ST_Difference(e.""Polygon"", @__polygon_0) AS ""Difference"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Dimension(bool isAsync) + { + await base.Dimension(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Dimension(e.""Point"") AS ""Dimension"" +FROM ""PointEntity"" AS e"); + } + + public override async Task Disjoint(bool isAsync) + { + await base.Disjoint(isAsync); + + AssertSql( + @"@__point_0='POINT (1 1)' (DbType = Object) + +SELECT e.""Id"", ST_Disjoint(e.""Polygon"", @__point_0) AS ""Disjoint"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Distance(bool isAsync) + { + await base.Distance(isAsync); + + AssertSql( + @"@__point_0='POINT (0 1)' (DbType = Object) + +SELECT e.""Id"", ST_Distance(e.""Point"", @__point_0) AS ""Distance"" +FROM ""PointEntity"" AS e"); + } + + public override async Task EndPoint(bool isAsync) + { + await base.EndPoint(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_EndPoint(e.""LineString"") AS ""EndPoint"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task Envelope(bool isAsync) + { + await base.Envelope(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Envelope(e.""Polygon"") AS ""Envelope"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task EqualsTopologically(bool isAsync) + { + await base.EqualsTopologically(isAsync); + + AssertSql( + @"@__point_0='POINT (0 0)' (DbType = Object) + +SELECT e.""Id"", ST_Equals(e.""Point"", @__point_0) AS ""EqualsTopologically"" +FROM ""PointEntity"" AS e"); + } + + public override async Task ExteriorRing(bool isAsync) + { + await base.ExteriorRing(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_ExteriorRing(e.""Polygon"") AS ""ExteriorRing"" +FROM ""PolygonEntity"" AS e"); + } + + [Theory] + [MemberData(nameof(IsAsyncData))] + public override async Task GeometryType(bool isAsync) + { + // PostGIS returns "POINT", NTS returns "Point" + await AssertQuery(isAsync, es => es.Select(e => new { e.Id, Type=e.Point.GeometryType.ToLower() })); + + AssertSql( + @"SELECT e.""Id"", LOWER(GeometryType(e.""Point"")) AS ""Type"" +FROM ""PointEntity"" AS e"); + } + + public override async Task GetGeometryN(bool isAsync) + { + await base.GetGeometryN(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_GeometryN(e.""MultiLineString"", 1) AS ""Geometry0"" +FROM ""MultiLineStringEntity"" AS e"); + } + + public override async Task GetInteriorRingN(bool isAsync) + { + await base.GetInteriorRingN(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_InteriorRingN(e.""Polygon"", 1) AS ""InteriorRing0"" +FROM ""PolygonEntity"" AS e +WHERE ST_NumInteriorRings(e.""Polygon"") > 0"); + } + + public override async Task GetPointN(bool isAsync) + { + await base.GetPointN(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_PointN(e.""LineString"", 1) AS ""Point0"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task Intersection(bool isAsync) + { + await base.Intersection(isAsync); + + AssertSql( + @"@__polygon_0='POLYGON ((0 0 +1 0 +1 1 +0 0))' (DbType = Object) + +SELECT e.""Id"", ST_Intersection(e.""Polygon"", @__polygon_0) AS ""Intersection"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Intersects(bool isAsync) + { + await base.Intersects(isAsync); + + AssertSql( + @"@__lineString_0='LINESTRING (0.5 -0.5 +0.5 0.5)' (DbType = Object) + +SELECT e.""Id"", ST_Intersects(e.""LineString"", @__lineString_0) AS ""Intersects"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task ICurve_IsClosed(bool isAsync) + { + await base.ICurve_IsClosed(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_IsClosed(e.""LineString"") AS ""IsClosed"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task IMultiCurve_IsClosed(bool isAsync) + { + await base.IMultiCurve_IsClosed(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_IsClosed(e.""MultiLineString"") AS ""IsClosed"" +FROM ""MultiLineStringEntity"" AS e"); + } + + public override async Task IsEmpty(bool isAsync) + { + await base.IsEmpty(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_IsEmpty(e.""MultiLineString"") AS ""IsEmpty"" +FROM ""MultiLineStringEntity"" AS e"); + } + + public override async Task IsRing(bool isAsync) + { + await base.IsRing(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_IsRing(e.""LineString"") AS ""IsRing"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task IsSimple(bool isAsync) + { + await base.IsSimple(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_IsSimple(e.""LineString"") AS ""IsSimple"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task IsValid(bool isAsync) + { + await base.IsValid(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_IsValid(e.""Point"") AS ""IsValid"" +FROM ""PointEntity"" AS e"); + } + + public override async Task Item(bool isAsync) + { + await base.Item(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_GeometryN(e.""MultiLineString"", 1) AS ""Item0"" +FROM ""MultiLineStringEntity"" AS e"); + } + + public override async Task Length(bool isAsync) + { + await base.Length(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Length(e.""LineString"") AS ""Length"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task M(bool isAsync) + { + await base.M(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_M(e.""Point"") AS ""M"" +FROM ""PointEntity"" AS e"); + } + + public override async Task NumGeometries(bool isAsync) + { + await base.NumGeometries(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_NumGeometries(e.""MultiLineString"") AS ""NumGeometries"" +FROM ""MultiLineStringEntity"" AS e"); + } + + public override async Task NumInteriorRings(bool isAsync) + { + await base.NumInteriorRings(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_NumInteriorRings(e.""Polygon"") AS ""NumInteriorRings"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task NumPoints(bool isAsync) + { + await base.NumPoints(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_NumPoints(e.""LineString"") AS ""NumPoints"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task Overlaps(bool isAsync) + { + await base.Overlaps(isAsync); + + AssertSql( + @"@__polygon_0='POLYGON ((0 0 +1 0 +1 1 +0 0))' (DbType = Object) + +SELECT e.""Id"", ST_Overlaps(e.""Polygon"", @__polygon_0) AS ""Overlaps"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task PointOnSurface(bool isAsync) + { + await base.PointOnSurface(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_PointOnSurface(e.""Polygon"") AS ""PointOnSurface"", e.""Polygon"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Relate(bool isAsync) + { + await base.Relate(isAsync); + + AssertSql( + @"@__polygon_0='POLYGON ((0 0 +1 0 +1 1 +0 0))' (DbType = Object) + +SELECT e.""Id"", ST_Relate(e.""Polygon"", @__polygon_0, '212111212') AS ""Relate"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task SRID(bool isAsync) + { + await base.SRID(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_SRID(e.""Point"") AS ""SRID"" +FROM ""PointEntity"" AS e"); + } + + public override async Task StartPoint(bool isAsync) + { + await base.StartPoint(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_StartPoint(e.""LineString"") AS ""StartPoint"" +FROM ""LineStringEntity"" AS e"); + } + + public override async Task SymmetricDifference(bool isAsync) + { + await base.SymmetricDifference(isAsync); + + AssertSql( + @"@__polygon_0='POLYGON ((0 0 +1 0 +1 1 +0 0))' (DbType = Object) + +SELECT e.""Id"", ST_SymDifference(e.""Polygon"", @__polygon_0) AS ""SymmetricDifference"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task ToBinary(bool isAsync) + { + await base.ToBinary(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_AsBinary(e.""Point"") AS ""Binary"" +FROM ""PointEntity"" AS e"); + } + + public override async Task ToText(bool isAsync) + { + await base.ToText(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_AsText(e.""Point"") AS ""Text"" +FROM ""PointEntity"" AS e"); + } + + public override async Task Touches(bool isAsync) + { + await base.Touches(isAsync); + + AssertSql( + @"@__polygon_0='POLYGON ((0 1 +1 0 +1 1 +0 1))' (DbType = Object) + +SELECT e.""Id"", ST_Touches(e.""Polygon"", @__polygon_0) AS ""Touches"" +FROM ""PolygonEntity"" AS e"); + } + + public override async Task Union(bool isAsync) + { + await base.Union(isAsync); + + AssertSql( + @"@__polygon_0='POLYGON ((0 0 +1 0 +1 1 +0 0))' (DbType = Object) + +SELECT e.""Id"", ST_Union(e.""Polygon"", @__polygon_0) AS ""Union"" +FROM ""PolygonEntity"" AS e"); + } + + [ConditionalTheory(Skip="ST_Union() with only one parameter is an aggregate function in PostGIS")] + public override Task Union_void(bool isAsync) => null; + + public override async Task Within(bool isAsync) + { + await base.Within(isAsync); + + AssertSql( + @"@__polygon_0='POLYGON ((-1 -1 +2 -1 +2 2 +-1 2 +-1 -1))' (DbType = Object) + +SELECT e.""Id"", ST_Within(e.""Point"", @__polygon_0) AS ""Within"" +FROM ""PointEntity"" AS e"); + } + + public override async Task X(bool isAsync) + { + await base.X(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_X(e.""Point"") AS ""X"" +FROM ""PointEntity"" AS e"); + } + + public override async Task Y(bool isAsync) + { + await base.Y(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Y(e.""Point"") AS ""Y"" +FROM ""PointEntity"" AS e"); + } + + public override async Task Z(bool isAsync) + { + await base.Z(isAsync); + + AssertSql( + @"SELECT e.""Id"", ST_Z(e.""Point"") AS ""Z"" +FROM ""PointEntity"" AS e"); + } + + void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } +} diff --git a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs new file mode 100644 index 000000000..e49703d86 --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs @@ -0,0 +1,35 @@ +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.TestModels.SpatialModel; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL +{ + public class SpatialNpgsqlFixture : SpatialFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => NpgsqlTestStoreFactory.Instance; + + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection) + .AddEntityFrameworkNpgsqlNetTopologySuite(); + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + { + var optionsBuilder = base.AddOptions(builder); + new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNetTopologySuite(); + + return optionsBuilder; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.HasPostgresExtension("postgis"); + } + } +} diff --git a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs new file mode 100644 index 000000000..88f4df92f --- /dev/null +++ b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlTest.cs @@ -0,0 +1,17 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL +{ + public class SpatialNpgsqlTest : SpatialTestBase + { + public SpatialNpgsqlTest(SpatialNpgsqlFixture fixture) + : base(fixture) + { + } + + protected override void UseTransaction(DatabaseFacade facade, IDbContextTransaction transaction) + => facade.UseTransaction(transaction.GetDbTransaction()); + } +} diff --git a/test/EFCore.PG.Plugins.FunctionalTests/NetTopologySuiteTest.cs b/test/EFCore.PG.Plugins.FunctionalTests/NetTopologySuiteTest.cs deleted file mode 100644 index a5912920f..000000000 --- a/test/EFCore.PG.Plugins.FunctionalTests/NetTopologySuiteTest.cs +++ /dev/null @@ -1,512 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; -using GeoAPI.Geometries; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using NetTopologySuite.Geometries; -using NetTopologySuite.IO; -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; -using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities; -using Xunit; - -namespace Npgsql.EntityFrameworkCore.PostgreSQL -{ - public class NetTopologySuiteTest : IClassFixture - { - public NetTopologySuiteTest(NetTopologySuiteFixture fixture) - { - Fixture = fixture; - Fixture.TestSqlLoggerFactory.Clear(); - } - - NetTopologySuiteFixture Fixture { get; } - - #region Method/Member translation - - [Fact] - public void Area() - { - AssertQuery(st => st.Where(s => s.Polygon.Area == 16 && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Area(s.""Polygon"")", Sql); - } - - [Fact] - public void AsText() - { - // Note: PostgreSQL returns 'POINT (3 4)' while NetTopologySuite returns 'POINT(3 4)' (without the space) - // So we don't use AssertQuery - using (var ctx = CreateContext()) - { - Assert.Equal(1, ctx.SpatialTypes.Single(s => s.Point.AsText() == "POINT(3 4)" && s.Id == 1).Id); - Assert.Contains(@"ST_AsText(s.""Point"")", Sql); - } - } - - [Fact] - public void Boundary() - { - var boundary = (MultiPoint)Reader.Read("MULTIPOINT(0 0,1 1)"); - AssertQuery(st => st.Where(s => s.LineString.Boundary.EqualsExact(boundary) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Boundary(s.""LineString"") = @__boundary_0", Sql); - } - - [Fact] - public void Contains() - { - AssertQuery(st => st.Where(s => s.Polygon.Contains(new Point(0, 0)) && s.Id == 1), entryCount: 1); - Assert.Contains(@"WHERE (ST_Contains(s.""Polygon"", GEOMETRY 'POINT (0 0)') = TRUE)", Sql); - } - - [Fact] - public void Covers() - { - AssertQuery(st => st.Where(s => - new LineString(new[] { new Coordinate(1, 2), new Coordinate(3, 4) }).Covers(s.Point) && s.Id == 1), - entryCount: 1); - Assert.Contains(@"ST_Covers(GEOMETRY 'LINESTRING (1 2, 3 4)', s.""Point"")", Sql); - } - - [Fact] - public void CoveredBy() - { - AssertQuery(st => st.Where(s => - s.Point.CoveredBy(new LineString(new[] { new Coordinate(1, 2), new Coordinate(3, 4) })) && s.Id == 1), - entryCount: 1); - Assert.Contains(@"ST_CoveredBy(s.""Point"", GEOMETRY 'LINESTRING (1 2, 3 4)')", Sql); - } - - [Fact] - public void Crosses() - { - var doesCross = new LineString(new[] { new Coordinate(0, 1), new Coordinate(1, 0) }); - var doesNotCross = new LineString(new[] { new Coordinate(2, 2), new Coordinate(3, 3) }); - - AssertQuery(st => st.Where(s => s.LineString.Crosses(doesCross) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Crosses(s.""LineString"", @__doesCross_0)", Sql); - AssertQuery(st => st.Where(s => s.LineString.Crosses(doesNotCross) && s.Id == 1), entryCount: 0); - } - [Fact] - public void Difference() - { - var polygon = (Polygon)Reader.Read("POLYGON((-1 -1,-1 3,3 3,3 -1,-1 -1))"); - var difference = (Polygon)Reader.Read("POLYGON((-2 -2,-2 2,-1 2,-1 -1,2 -1,2 -2,-2 -2))"); - AssertQuery(st => st.Where(s => s.Polygon.Difference(polygon).EqualsExact(difference) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Difference(s.""Polygon"", @__polygon_0)", Sql); - } - - [Fact] - public void Disjoint() - { - AssertQuery(st => st.Where(s => s.LineString.Disjoint(new Point(2, 2)) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Disjoint(s.""LineString"", GEOMETRY 'POINT (2 2)')", Sql); - AssertQuery(st => st.Where(s => s.LineString.Disjoint(new Point(0.5, 0.5)) && s.Id == 1), entryCount: 0); - } - - [Fact] - public void Distance() - { - AssertQuery(st => st.Where(s => s.Point.Distance(new Point(4, 4)) == 1 && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Distance(s.""Point"", GEOMETRY 'POINT (4 4)')", Sql); - } - - [Fact] - public void Equals() - { - AssertQuery(st => st.Where(s => s.LineString.Equals(new LineString(new[] { new Coordinate(0, 0), new Coordinate(1, 1) })) && s.Id == 1), entryCount: 1); - Assert.Contains(@"s.""LineString"" = GEOMETRY 'LINESTRING (0 0, 1 1)'", Sql); - AssertQuery(st => st.Where(s => s.LineString.EqualsExact(new LineString(new[] { new Coordinate(1, 1), new Coordinate(0, 0) })) && s.Id == 1), entryCount: 0); - } - - [Fact] - public void EqualsExact() - { - AssertQuery(st => st.Where(s => s.LineString.EqualsExact(new LineString(new[] { new Coordinate(0, 0), new Coordinate(1, 1) })) && s.Id == 1), entryCount: 1); - Assert.Contains(@"s.""LineString"" = GEOMETRY 'LINESTRING (0 0, 1 1)'", Sql); - AssertQuery(st => st.Where(s => s.LineString.EqualsExact(new LineString(new[] { new Coordinate(1, 1), new Coordinate(0, 0) })) && s.Id == 1), entryCount: 0); - } - - [Fact] - public void EqualsTopologically() - { - AssertQuery(st => st.Where(s => s.LineString.EqualsTopologically(new LineString(new[] { new Coordinate(1, 1), new Coordinate(0, 0) })) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Equals(s.""LineString"", GEOMETRY 'LINESTRING (1 1, 0 0)')", Sql); - AssertQuery(st => st.Where(s => s.LineString.EqualsTopologically(new LineString(new[] { new Coordinate(2, 2), new Coordinate(0, 0) })) && s.Id == 1), entryCount: 0); - } - - [Fact] - public void GeometryType() - { - // It would have been nice to be able to do this with the C# is operators :) - - // Note that NTS returns Point whereas PostGIS returns POINT, hence ToUpper() here - AssertQuery(st => st.Where(s => s.Geometry.GeometryType.ToUpper() == "POINT" && s.Id == 1), entryCount: 1); - Assert.Contains(@"GeometryType(s.""Geometry"")", Sql); - } - - [Fact] - public void GetGeometryN() - { - // Constant index - AssertQuery(st => st.Where(s => s.Id == 2 && s.Collection.GetGeometryN(0).EqualsExact(new Point(3, 4))), entryCount: 1); - Assert.Contains(@"ST_GeometryN(s.""Collection"", 1)", Sql); - // Parameter index - var i = 0; - AssertQuery(st => st.Where(s => s.Id == 2 && s.Collection.GetGeometryN(i).EqualsExact(new Point(3, 4))), entryCount: 1); - Assert.Contains(@"ST_GeometryN(s.""Collection"", @__i_0 + 1)", Sql); - - AssertQuery(st => st.Where(s => s.Id == 2 && ((Point)s.Collection.GetGeometryN(0)).X == 3), entryCount: 1); - } - - [Fact] - public void Intersection() - { - var polygon = (Polygon)Reader.Read("POLYGON((-1 -1,-1 3,3 3,3 -1,-1 -1))"); - var intersection = (Polygon)Reader.Read("POLYGON((-1 2,2 2,2 -1,-1 -1,-1 2))"); - AssertQuery(st => st.Where(s => s.Polygon.Intersection(polygon).EqualsExact(intersection) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Intersection(s.""Polygon"", @__polygon_0)", Sql); - } - - [Fact] - public void Intersects() - { - AssertQuery(st => st.Where(s => s.LineString.Intersects(new Point(0.5, 0.5)) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Intersects(s.""LineString"", GEOMETRY 'POINT (0.5 0.5)')", Sql); - AssertQuery(st => st.Where(s => s.LineString.Intersects(new Point(2, 2)) && s.Id == 1), entryCount: 0); - } - - [Fact] - public void IsClosed() - { - AssertQuery(st => st.Where(s => s.LineString.IsClosed && s.Id == 2), entryCount: 1); - Assert.Contains(@"ST_IsClosed(s.""LineString"")", Sql); - AssertQuery(st => st.Where(s => !s.LineString.IsClosed && s.Id == 1), entryCount: 1); - } - - [Fact] - public void IsEmpty() - { - AssertQuery(st => st.Where(s => s.Collection.IsEmpty && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_IsEmpty(s.""Collection"")", Sql); - AssertQuery(st => st.Where(s => s.Collection.IsEmpty && s.Id == 2), entryCount: 0); - } - - [Fact] - public void IsSimple() - { - AssertQuery(st => st.Where(s => s.LineString.IsSimple && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_IsSimple(s.""LineString"")", Sql); - } - - [Fact] - public void IsValid() - { - AssertQuery(st => st.Where(s => s.LineString.IsValid && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_IsValid(s.""LineString"")", Sql); - } - - [Fact] - public void Length() - { - AssertQuery(st => st.Where(s => s.LineString.Length - 1.4142135623731 < 0.01 && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Length(s.""LineString"")", Sql); - } - - [Fact(Skip="No support currently in Npgsql.NetTopologySuite")] - public void M() {} - - [Fact] - public void NumGeometries() - { - AssertQuery(st => st.Where(s => s.Collection.NumGeometries == 0 && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_NumGeometries(s.""Collection"")", Sql); - AssertQuery(st => st.Where(s => s.Collection.NumGeometries == 2 && s.Id == 2), entryCount: 1); - } - - [Fact] - public void NumPoints() - { - AssertQuery(st => st.Where(s => s.LineString.NumPoints == 2 && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_NumPoints(s.""LineString"") = 2", Sql); - AssertQuery(st => st.Where(s => s.LineString.NumPoints == 4 && s.Id == 2), entryCount: 1); - } - - [Fact] - public void Overlaps() - { - var polygon = (Polygon)Reader.Read("POLYGON((-1 -1,-1 3,3 3,3 -1,-1 -1))"); - AssertQuery(st => st.Where(s => s.Id == 1 && s.Polygon.Overlaps(polygon)), entryCount: 1); - Assert.Contains(@"ST_Overlaps(s.""Polygon"", @__polygon_0)", Sql); - } - - [Fact] - public void Relate() - { - AssertQuery(st => st.Where(s => s.Point.Relate(new Point(3, 4), "0FFFFFFF2") && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Relate(s.""Point"", GEOMETRY 'POINT (3 4)', '0FFFFFFF2')", Sql); - } - - [Fact] - public void Reverse() - { - AssertQuery(st => st.Where(s => s.LineString.Reverse().EqualsExact(new LineString(new[] { new Coordinate(1, 1), new Coordinate(0, 0) })) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Reverse(s.""LineString"")", Sql); - } - - [Fact] - public void SymmetricDifference() - { - var polygon = (Polygon)Reader.Read("POLYGON((-1 -1,-1 3,3 3,3 -1,-1 -1))"); - var symDifference = (MultiPolygon)Reader.Read("MULTIPOLYGON(((-2 -2,-2 2,-1 2,-1 -1,2 -1,2 -2,-2 -2)),((2 -1,2 2,-1 2,-1 3,3 3,3 -1,2 -1)))"); - AssertQuery(st => st.Where(s => s.Polygon.SymmetricDifference(polygon).EqualsExact(symDifference) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_SymDifference(s.""Polygon"", @__polygon_0)", Sql); - } - - [Fact] - public void Touches() - { - AssertQuery(st => st.Where(s => s.LineString.Touches(new Point(1, 1)) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Touches(s.""LineString"", GEOMETRY 'POINT (1 1)')", Sql); - AssertQuery(st => st.Where(s => s.LineString.Touches(new Point(2, 2)) && s.Id == 1), entryCount: 0); - } - - [Fact] - public void ToText() - { - // Note: PostgreSQL returns 'POINT (3 4)' while NetTopologySuite returns 'POINT(3 4)' (without the space) - // So we don't use AssertQuery - using (var ctx = CreateContext()) - { - Assert.Equal(1, ctx.SpatialTypes.Single(s => s.Point.ToText() == "POINT(3 4)" && s.Id == 1).Id); - Assert.Contains(@"ST_AsText(s.""Point"")", Sql); - } - } - - [Fact] - public void Union() - { - var polygon = (Polygon)Reader.Read("POLYGON((-1 -1,-1 3,3 3,3 -1,-1 -1))"); - var union = (Polygon)Reader.Read("POLYGON((-2 -2,-2 2,-1 2,-1 3,3 3,3 -1,2 -1,2 -2,-2 -2))"); - AssertQuery(st => st.Where(s => s.Polygon.Union(polygon).EqualsExact(union) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Union(s.""Polygon"", @__polygon_0)", Sql); - } - - [Fact] - public void Within() - { - var polygon = (Polygon)Reader.Read("POLYGON((-2 -2,-2 2,2 2,2 -2,-2 -2))"); - - AssertQuery(st => st.Where(s => s.LineString.Within(polygon) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Within(s.""LineString"", @__polygon_0)", Sql); - AssertQuery(st => st.Where(s => s.LineString.Within(polygon) && s.Id == 2), entryCount: 0); - } - - [Fact] - public void X() - { - AssertQuery(st => st.Where(s => s.Point.X == 3 && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_X(s.""Point"")", Sql); - } - - [Fact] - public void Y() - { - AssertQuery(st => st.Where(s => s.Point.Y == 4 && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Y(s.""Point"")", Sql); - } - - [Fact(Skip="https://github.com/npgsql/npgsql/issues/1906")] - public void Z() - { - AssertQuery(st => st.Where(s => s.Point.Z == 5 && s.Id == 2), entryCount: 1); - Assert.Contains(@"ST_Z(s.""Point"")", Sql); - } - - #endregion Method/Member translation - - [Fact] - public void Polymorphism() - { - AssertQuery(st => st.Where(s => s.Geometry.EqualsExact(new Point(3,4)) && s.Id == 1), entryCount: 1); - Assert.Contains(@"WHERE (s.""Geometry"" = GEOMETRY 'POINT (3 4)')", Sql); - } - - [Fact] - public void Geography() - { - // Makes sure that when NTS types are mapped to PostGIS geography, the proper overload of ST_Distance() gets called - // See http://workshops.boundlessgeo.com/postgis-intro/geography.html - using (var ctx = CreateContext()) - { - var paris = new Point(2.5559, 49.0083); - Assert.Equal(1, ctx.SpatialTypes.Single(s => s.Geography.Distance(paris) - 9124665.26917268 < 1 && s.Id == 1).Id); - Assert.Contains(@"ST_Distance(s.""Geography"", @__paris_0)", Sql); - } - } - - [Fact] - public void IGeometry() - { - var igeometry = (IGeometry)new Point(0.5, 0.5); - AssertQuery(st => st.Where(s => s.LineString.Intersects(igeometry) && s.Id == 1), entryCount: 1); - Assert.Contains(@"ST_Intersects(s.""LineString"", @__igeometry_0)", Sql); - AssertQuery(st => st.Where(s => s.IGeometry.Intersects(new Point(6, 6)) && s.Id == 1), entryCount: 0); - Assert.Contains(@"ST_Intersects(s.""IGeometry"", GEOMETRY 'POINT (6 6)')", Sql); - } - - #region Support - - static WKTReader Reader = new WKTReader(new GeometryFactory(new PrecisionModel(1), 0)); - - NetTopologySuiteContext CreateContext() => Fixture.CreateContext(); - - public void AssertQuery( - Func, IQueryable> query, - Func elementSorter = null, - Action elementAsserter = null, - bool assertOrder = false, - int entryCount = 0) - where TItem1 : class - => Fixture.QueryAsserter.AssertQuery(query, query, elementSorter, elementAsserter, assertOrder, entryCount).GetAwaiter().GetResult(); - - public void AssertQuery( - Func, IQueryable> query, - Func elementSorter = null, - Action elementAsserter = null, - bool assertOrder = false, - int entryCount = 0) - => AssertQuery(query, elementSorter, elementAsserter, assertOrder, entryCount); - - string Sql => Fixture.TestSqlLoggerFactory.Sql; - - public class NetTopologySuiteFixture : SharedStoreFixtureBase, IQueryFixtureBase - { - public NetTopologySuiteFixture() - { - var entitySorters = new Dictionary> - { - { typeof(SpatialTypes), e => e?.Id } - }; - - var entityAsserters = new Dictionary> - { - { - typeof(SpatialTypes), - (e, a) => - { - Assert.Equal(e == null, a == null); - - if (a != null) - { - Assert.Equal(e.Id, a.Id); - } - } - } - }; - - QueryAsserter = new QueryAsserter( - CreateContext, - new SpatialData(), - entitySorters, - entityAsserters); - } - - protected override string StoreName { get; } = "NetTopologySuiteTest"; - - public QueryAsserterBase QueryAsserter { get; set; } - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - { - var npgsqlBuilder = new NpgsqlDbContextOptionsBuilder(builder).UseNetTopologySuite(); - return builder; - } - - protected override void Seed(NetTopologySuiteContext context) => SpatialData.Seed(context); - - protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - } - - public class NetTopologySuiteContext : PoolableDbContext - { - public NetTopologySuiteContext(DbContextOptions options) : base(options) {} - - protected override void OnModelCreating(ModelBuilder builder) - { - builder.HasPostgresExtension("postgis"); - } - - public DbSet SpatialTypes { get; set; } - } - - public class SpatialTypes - { - public int Id { get; set; } - public Point Point { get; set; } - public LineString LineString { get; set; } - public Polygon Polygon { get; set; } - public GeometryCollection Collection { get; set; } - public Geometry Geometry { get; set; } - [Column(TypeName="geography")] - public Geometry Geography { get; set; } - public IGeometry IGeometry { get; set; } - } - - public class SpatialData : IExpectedData - { - readonly SpatialTypes[] _spatialTypes = CreateSpatialTypes(); - - static SpatialTypes[] CreateSpatialTypes() - { - return new[] - { - new SpatialTypes - { - Id = 1, - Point = new Point(3, 4), - LineString = new LineString(new[] { new Coordinate(0, 0), new Coordinate(1, 1) }), // Open - Polygon = (Polygon)Reader.Read("POLYGON((-2 -2,-2 2,2 2,2 -2,-2 -2))"), - Collection = new GeometryCollection(new IGeometry[0]), - Geometry = new Point(3, 4), - Geography = new Point(-118.4079, 33.9434), // Los Angeles - IGeometry = new Point(3, 4) - }, - new SpatialTypes - { - Id = 2, - Point = new Point(3, 4, 5), - LineString = (LineString)Reader.Read("LINESTRING(0 0,0 3,3 3,0 0)"), // Closed - Polygon = (Polygon)Reader.Read("POLYGON((-2 -2,-2 2,2 2,2 -2,-2 -2))"), - Collection = new GeometryCollection(new IGeometry[] { new Point(3, 4), new Point(4, 5) }), - Geometry = (LineString)Reader.Read("LINESTRING(0 0,0 3,3 3,0 0)"), - Geography = (LineString)Reader.Read("LINESTRING(0 0,0 3,3 3,0 0)"), - IGeometry = new Point(5, 6) - } - }; - } - - public IQueryable Set() where TEntity : class - { - if (typeof(TEntity) == typeof(SpatialTypes)) - { - return (IQueryable)_spatialTypes.AsQueryable(); - } - - throw new InvalidOperationException("Invalid entity type: " + typeof(TEntity)); - } - - public static void Seed(NetTopologySuiteContext context) - { - context.SpatialTypes.AddRange(CreateSpatialTypes()); - context.SaveChanges(); - } - } - - #endregion Support - } -} diff --git a/test/EFCore.PG.Plugins.FunctionalTests/NodaTimeTest.cs b/test/EFCore.PG.Plugins.FunctionalTests/NodaTimeQueryNpgsqlTest.cs similarity index 97% rename from test/EFCore.PG.Plugins.FunctionalTests/NodaTimeTest.cs rename to test/EFCore.PG.Plugins.FunctionalTests/NodaTimeQueryNpgsqlTest.cs index 29dfb7275..16654301d 100644 --- a/test/EFCore.PG.Plugins.FunctionalTests/NodaTimeTest.cs +++ b/test/EFCore.PG.Plugins.FunctionalTests/NodaTimeQueryNpgsqlTest.cs @@ -391,13 +391,32 @@ public class NodaTimeFixture : SharedStoreFixtureBase { protected override string StoreName { get; } = "NodaTimeTest"; + protected override IServiceCollection AddServices(IServiceCollection serviceCollection) + => base.AddServices(serviceCollection).AddEntityFrameworkNpgsqlNodaTime(); + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) { - var npgsqlBuilder = new NpgsqlDbContextOptionsBuilder(builder).UseNodaTime(); - return builder; + var optionsBuilder = base.AddOptions(builder); + new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNodaTime(); + + return optionsBuilder; } protected override void Seed(NodaTimeContext context) + => NodaTimeContext.Seed(context); + + protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); + } + + public class NodaTimeContext : PoolableDbContext + { + public NodaTimeContext(DbContextOptions options) : base(options) {} + + public DbSet NodaTimeTypes { get; set; } + + public static void Seed(NodaTimeContext context) { var localDateTime = new LocalDateTime(2018, 4, 20, 10, 31, 33, 666); var zonedDateTime = localDateTime.InUtc(); @@ -420,17 +439,6 @@ protected override void Seed(NodaTimeContext context) }); context.SaveChanges(); } - - protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; - - public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ServiceProvider.GetRequiredService(); - } - - public class NodaTimeContext : PoolableDbContext - { - public NodaTimeContext(DbContextOptions options) : base(options) {} - - public DbSet NodaTimeTypes { get; set; } } public class NodaTimeTypes diff --git a/test/EFCore.PG.Plugins.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs b/test/EFCore.PG.Plugins.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs index 7b014ef3c..92265127e 100644 --- a/test/EFCore.PG.Plugins.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs +++ b/test/EFCore.PG.Plugins.FunctionalTests/NpgsqlNodaTimeTypeMappingTest.cs @@ -118,33 +118,16 @@ public void GenerateSqlLiteral_returns_period_literal() #region Support - static NpgsqlNodaTimeTypeMappingTest() - { - var optionsBuilder = new DbContextOptionsBuilder(); - var npgsqlBuilder = new NpgsqlDbContextOptionsBuilder(optionsBuilder).UseNodaTime(); - var options = new NpgsqlOptions(); - options.Initialize(optionsBuilder.Options); - - Mapper = new NpgsqlTypeMappingSource( - new TypeMappingSourceDependencies( - new ValueConverterSelector(new ValueConverterSelectorDependencies()), - Array.Empty() - ), - new RelationalTypeMappingSourceDependencies(Array.Empty()), - options - ); - } - - static readonly NpgsqlTypeMappingSource Mapper; + static readonly IRelationalTypeMappingSourcePlugin Mapper = new NpgsqlNodaTimeTypeMappingSourcePlugin(); static RelationalTypeMapping GetMapping(string storeType) - => Mapper.FindMapping(storeType); + => Mapper.FindMapping(new RelationalTypeMappingInfo(storeType)); public static RelationalTypeMapping GetMapping(Type clrType) - => (RelationalTypeMapping)Mapper.FindMapping(clrType); + => Mapper.FindMapping(new RelationalTypeMappingInfo(clrType)); public static RelationalTypeMapping GetMapping(Type clrType, string storeType) - => Mapper.FindMapping(clrType, storeType); + => Mapper.FindMapping(new RelationalTypeMappingInfo(clrType, storeType, false, null, null, null, null, null, null)); #endregion Support }