From f6e9789fc6b6e16e1ca700c3d311bc23e27a7b91 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 30 Dec 2021 17:17:21 +0200 Subject: [PATCH] Register PG extensions in the model by convention (#2161) By scanning property types in the model. Closes #2137 --- EFCore.PG.sln.DotSettings | 1 + src/EFCore.PG.NTS/EFCore.PG.NTS.csproj | 2 +- ...opologySuiteServiceCollectionExtensions.cs | 1 + ...gsqlNetTopologySuiteConventionSetPlugin.cs | 16 ++++ ...tTopologySuiteExtensionAddingConvention.cs | 12 +++ src/EFCore.PG.NTS/README.md | 3 - .../NpgsqlModelBuilderExtensions.cs | 80 +++++++++++++++++-- .../NpgsqlModelExtensions.cs | 7 ++ .../Conventions/NpgsqlConventionSetBuilder.cs | 8 +- ...sqlPostgresExtensionDiscoveryConvention.cs | 47 +++++++++++ src/EFCore.PG/Metadata/PostgresExtension.cs | 35 +++++++- .../BuiltInDataTypesNpgsqlTest.cs | 3 +- .../Query/CitextQueryTest.cs | 5 +- .../Query/LTreeQueryTest.cs | 7 +- .../SpatialNpgsqlFixture.cs | 5 +- 15 files changed, 205 insertions(+), 27 deletions(-) create mode 100644 src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteConventionSetPlugin.cs create mode 100644 src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteExtensionAddingConvention.cs create mode 100644 src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresExtensionDiscoveryConvention.cs diff --git a/EFCore.PG.sln.DotSettings b/EFCore.PG.sln.DotSettings index 88cb60599..1d21712f2 100644 --- a/EFCore.PG.sln.DotSettings +++ b/EFCore.PG.sln.DotSettings @@ -307,6 +307,7 @@ The .NET Foundation licenses this file to you under the MIT license. True True True + True True True True diff --git a/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj b/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj index bc891422c..46e3bf109 100644 --- a/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj +++ b/src/EFCore.PG.NTS/EFCore.PG.NTS.csproj @@ -24,7 +24,7 @@ - + True build diff --git a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs index 872d54ae9..c53030b38 100644 --- a/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs +++ b/src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs @@ -26,6 +26,7 @@ public static IServiceCollection AddEntityFrameworkNpgsqlNetTopologySuite( .TryAdd() .TryAdd() .TryAdd() + .TryAdd() .TryAddProviderSpecificServices( x => x.TryAddSingleton()); diff --git a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteConventionSetPlugin.cs b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteConventionSetPlugin.cs new file mode 100644 index 000000000..5b3130e7b --- /dev/null +++ b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteConventionSetPlugin.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal; + +public class NpgsqlNetTopologySuiteConventionSetPlugin : IConventionSetPlugin +{ + public virtual ConventionSet ModifyConventions(ConventionSet conventionSet) + { + conventionSet.ModelFinalizingConventions.Add(new NpgsqlNetTopologySuiteExtensionAddingConvention()); + + return conventionSet; + } +} + diff --git a/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteExtensionAddingConvention.cs b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteExtensionAddingConvention.cs new file mode 100644 index 000000000..94db2aee9 --- /dev/null +++ b/src/EFCore.PG.NTS/Internal/NpgsqlNetTopologySuiteExtensionAddingConvention.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// ReSharper disable once CheckNamespace +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal; + +public class NpgsqlNetTopologySuiteExtensionAddingConvention : IModelFinalizingConvention +{ + /// + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + => modelBuilder.HasPostgresExtension("postgis"); +} diff --git a/src/EFCore.PG.NTS/README.md b/src/EFCore.PG.NTS/README.md index 770ea3e05..ee89907bc 100644 --- a/src/EFCore.PG.NTS/README.md +++ b/src/EFCore.PG.NTS/README.md @@ -30,9 +30,6 @@ public class BlogContext : DbContext => optionsBuilder.UseNpgsql( @"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase", o => o.UseNetTopologySuite()); - - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.HasPostgresExtension("postgis"); } public class City diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs index 7898b34c8..b8ed22cc5 100644 --- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs +++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs @@ -247,9 +247,7 @@ public static bool CanSetValueGenerationStrategy( /// The schema in which to create the extension. /// The name of the extension to create. /// The version of the extension. - /// - /// The updated . - /// + /// The same builder instance so that multiple calls can be chained. /// /// See: https://www.postgresql.org/docs/current/external-extensions.html /// @@ -274,9 +272,7 @@ public static ModelBuilder HasPostgresExtension( /// /// The model builder in which to define the extension. /// The name of the extension to create. - /// - /// The updated . - /// + /// The same builder instance so that multiple calls can be chained. /// /// See: https://www.postgresql.org/docs/current/external-extensions.html /// @@ -286,6 +282,78 @@ public static ModelBuilder HasPostgresExtension( string name) => modelBuilder.HasPostgresExtension(null, name); + /// + /// Registers a PostgreSQL extension in the model. + /// + /// The model builder in which to define the extension. + /// The schema in which to create the extension. + /// The name of the extension to create. + /// The version of the extension. + /// Indicates whether the configuration was specified using a data annotation. + /// The same builder instance so that multiple calls can be chained. + /// + /// See: https://www.postgresql.org/docs/current/external-extensions.html + /// + /// + public static IConventionModelBuilder? HasPostgresExtension( + this IConventionModelBuilder modelBuilder, + string? schema, + string name, + string? version = null, + bool fromDataAnnotation = false) + { + if (modelBuilder.CanSetPostgresExtension(schema, name, version, fromDataAnnotation)) + { + modelBuilder.Metadata.GetOrAddPostgresExtension(schema, name, version); + return modelBuilder; + } + + return null; + } + + /// + /// Registers a PostgreSQL extension in the model. + /// + /// The model builder in which to define the extension. + /// The name of the extension to create. + /// Indicates whether the configuration was specified using a data annotation. + /// The same builder instance so that multiple calls can be chained. + /// + /// See: https://www.postgresql.org/docs/current/external-extensions.html + /// + /// + public static IConventionModelBuilder? HasPostgresExtension( + this IConventionModelBuilder modelBuilder, + string name, + bool fromDataAnnotation = false) + => modelBuilder.HasPostgresExtension(schema: null, name, version: null, fromDataAnnotation); + + /// + /// Returns a value indicating whether the given PostgreSQL extension can be registered in the model. + /// + /// + /// See Modeling entity types and relationships, and + /// Accessing SQL Server and SQL Azure databases with EF Core + /// for more information and examples. + /// + /// The model builder. + /// The schema in which to create the extension. + /// The name of the extension to create. + /// The version of the extension. + /// Indicates whether the configuration was specified using a data annotation. + /// if the given value can be set as the default increment for SQL Server IDENTITY. + public static bool CanSetPostgresExtension( + this IConventionModelBuilder modelBuilder, + string? schema, + string name, + string? version = null, + bool fromDataAnnotation = false) + { + var annotationName = PostgresExtension.BuildAnnotationName(schema, name); + + return modelBuilder.CanSetAnnotation(annotationName, $"{schema},{name},{version}", fromDataAnnotation); + } + #endregion #region Enums diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs index 1ffbac998..b78fb92d0 100644 --- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs +++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs @@ -161,6 +161,13 @@ public static PostgresExtension GetOrAddPostgresExtension( public static IReadOnlyList GetPostgresExtensions(this IReadOnlyModel model) => PostgresExtension.GetPostgresExtensions(model).ToArray(); + public static PostgresExtension GetOrAddPostgresExtension( + this IConventionModel model, + string? schema, + string name, + string? version) + => PostgresExtension.GetOrAddPostgresExtension(model, schema, name, version); + #endregion #region Enum types diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs index 1055980d1..b018956bd 100644 --- a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs @@ -5,15 +5,20 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; [EntityFrameworkInternal] public class NpgsqlConventionSetBuilder : RelationalConventionSetBuilder { + private readonly IRelationalTypeMappingSource _typeMappingSource; private readonly Version _postgresVersion; [EntityFrameworkInternal] public NpgsqlConventionSetBuilder( ProviderConventionSetBuilderDependencies dependencies, RelationalConventionSetBuilderDependencies relationalDependencies, + IRelationalTypeMappingSource typeMappingSource, INpgsqlOptions npgsqlOptions) : base(dependencies, relationalDependencies) - => _postgresVersion = npgsqlOptions.PostgresVersion; + { + _typeMappingSource = typeMappingSource; + _postgresVersion = npgsqlOptions.PostgresVersion; + } [EntityFrameworkInternal] public override ConventionSet CreateConventionSet() @@ -43,6 +48,7 @@ public override ConventionSet CreateConventionSet() conventionSet.PropertyAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention); conventionSet.ModelFinalizingConventions.Add(valueGenerationStrategyConvention); + conventionSet.ModelFinalizingConventions.Add(new NpgsqlPostgresExtensionDiscoveryConvention(_typeMappingSource)); ReplaceConvention(conventionSet.ModelFinalizingConventions, storeGenerationConvention); ReplaceConvention( conventionSet.ModelFinalizingConventions, diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresExtensionDiscoveryConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresExtensionDiscoveryConvention.cs new file mode 100644 index 000000000..c61d9ebb9 --- /dev/null +++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresExtensionDiscoveryConvention.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions; + +public class NpgsqlPostgresExtensionDiscoveryConvention : IModelFinalizingConvention +{ + private readonly IRelationalTypeMappingSource _typeMappingSource; + + public NpgsqlPostgresExtensionDiscoveryConvention(IRelationalTypeMappingSource typeMappingSource) + { + _typeMappingSource = typeMappingSource; + } + + /// + public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + foreach (var property in entityType.GetDeclaredProperties()) + { + var typeMapping = (RelationalTypeMapping?)property.FindTypeMapping() + ?? _typeMappingSource.FindMapping((IProperty)property); + + if (typeMapping is null) + { + continue; + } + + switch (typeMapping.StoreType) + { + case "hstore": + modelBuilder.HasPostgresExtension("hstore"); + continue; + case "citext": + modelBuilder.HasPostgresExtension("citext"); + continue; + case "ltree": + case "lquery": + case "ltxtquery": + modelBuilder.HasPostgresExtension("ltree"); + continue; + } + } + } + } +} diff --git a/src/EFCore.PG/Metadata/PostgresExtension.cs b/src/EFCore.PG/Metadata/PostgresExtension.cs index d6142241c..7b0899054 100644 --- a/src/EFCore.PG/Metadata/PostgresExtension.cs +++ b/src/EFCore.PG/Metadata/PostgresExtension.cs @@ -56,6 +56,39 @@ public static PostgresExtension GetOrAddPostgresExtension( return new PostgresExtension(annotatable, annotationName) { Version = version }; } + /// + /// Gets or adds a from or to the . + /// + /// The annotatable from which to get or add the extension. + /// The extension schema or null to use the model's default schema. + /// The extension name. + /// The extension version. + /// + /// The from the . + /// + /// + /// + /// + public static PostgresExtension GetOrAddPostgresExtension( + IConventionAnnotatable annotatable, + string? schema, + string name, + string? version) + { + Check.NotNull(annotatable, nameof(annotatable)); + Check.NullButNotEmpty(schema, nameof(schema)); + Check.NotNull(name, nameof(name)); + + if (FindPostgresExtension(annotatable, schema, name) is { } postgresExtension) + { + return postgresExtension; + } + + var annotationName = BuildAnnotationName(schema, name); + + return new PostgresExtension(annotatable, annotationName) { Version = version }; + } + /// /// Gets or adds a from or to the . /// @@ -99,7 +132,7 @@ public static PostgresExtension GetOrAddPostgresExtension( return annotatable[annotationName] is null ? null : new PostgresExtension(annotatable, annotationName); } - private static string BuildAnnotationName(string? schema, string name) + internal static string BuildAnnotationName(string? schema, string name) => schema is not null ? $"{NpgsqlAnnotationNames.PostgresExtensionPrefix}{schema}.{name}" : $"{NpgsqlAnnotationNames.PostgresExtensionPrefix}{name}"; diff --git a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs index 880437aad..6ee5c2a1f 100644 --- a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs @@ -965,7 +965,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con NpgsqlConnection.GlobalTypeMapper.MapEnum(); ((NpgsqlTypeMappingSource)context.GetService()).LoadUserDefinedTypeMappings(context.GetService()); - modelBuilder.HasPostgresExtension("hstore"); modelBuilder.HasPostgresEnum("mood", new[] { "happy", "sad" }); MakeRequired(modelBuilder); @@ -1431,4 +1430,4 @@ protected class MappedNullableDataTypes } // ReSharper disable once UnusedMember.Global -public enum Mood { Happy, Sad } \ No newline at end of file +public enum Mood { Happy, Sad } diff --git a/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs index 326c46511..712989142 100644 --- a/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs @@ -269,9 +269,6 @@ public class CitextQueryContext : PoolableDbContext public CitextQueryContext(DbContextOptions options) : base(options) {} - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.HasPostgresExtension("citext"); - public static void Seed(CitextQueryContext context) { context.SomeEntities.AddRange( @@ -295,4 +292,4 @@ public class CitextQueryFixture : SharedStoreFixtureBase public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; protected override void Seed(CitextQueryContext context) => CitextQueryContext.Seed(context); } -} \ No newline at end of file +} diff --git a/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs index 1a5c04036..8815791f7 100644 --- a/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs @@ -427,9 +427,6 @@ public class LTreeQueryContext : PoolableDbContext public LTreeQueryContext(DbContextOptions options) : base(options) {} - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.HasPostgresExtension("ltree"); - public static void Seed(LTreeQueryContext context) { var ltreeEntities = new LTreeEntity[] @@ -471,9 +468,7 @@ public class LTreeQueryFixture : SharedStoreFixtureBase protected override ITestStoreFactory TestStoreFactory => NpgsqlTestStoreFactory.Instance; public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory; protected override void Seed(LTreeQueryContext context) => LTreeQueryContext.Seed(context); - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - => modelBuilder.HasPostgresExtension("ltree"); } #endregion -} \ No newline at end of file +} diff --git a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs index ee6de263c..ea5cbde63 100644 --- a/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs +++ b/test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs @@ -24,7 +24,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); - modelBuilder.HasPostgresExtension("postgis") - .HasPostgresExtension("uuid-ossp"); + modelBuilder.HasPostgresExtension("uuid-ossp"); } -} \ No newline at end of file +}