Skip to content

Commit

Permalink
Register PG extensions in the model by convention (#2161)
Browse files Browse the repository at this point in the history
By scanning property types in the model.

Closes #2137
  • Loading branch information
roji committed Dec 30, 2021
1 parent 45d0739 commit f6e9789
Show file tree
Hide file tree
Showing 15 changed files with 205 additions and 27 deletions.
1 change: 1 addition & 0 deletions EFCore.PG.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ The .NET Foundation licenses this file to you under the MIT license.
<s:Boolean x:Key="/Default/UserDictionary/Words/=niladic/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pluralizer/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Poolable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=postgis/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=pushdown/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=regconfig/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=regdictionary/@EntryIndexedValue">True</s:Boolean>
Expand Down
2 changes: 1 addition & 1 deletion src/EFCore.PG.NTS/EFCore.PG.NTS.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

<ItemGroup>
<Compile Include="..\Shared\*.cs" />
<None Include="README.md" Pack="true" PackagePath="\"/>
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="build\**\*">
<Pack>True</Pack>
<PackagePath>build</PackagePath>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public static IServiceCollection AddEntityFrameworkNpgsqlNetTopologySuite(
.TryAdd<IRelationalTypeMappingSourcePlugin, NpgsqlNetTopologySuiteTypeMappingSourcePlugin>()
.TryAdd<IMethodCallTranslatorPlugin, NpgsqlNetTopologySuiteMethodCallTranslatorPlugin>()
.TryAdd<IMemberTranslatorPlugin, NpgsqlNetTopologySuiteMemberTranslatorPlugin>()
.TryAdd<IConventionSetPlugin, NpgsqlNetTopologySuiteConventionSetPlugin>()
.TryAddProviderSpecificServices(
x => x.TryAddSingleton<INpgsqlNetTopologySuiteOptions, NpgsqlNetTopologySuiteOptions>());

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}

Original file line number Diff line number Diff line change
@@ -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
{
/// <inheritdoc />
public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
=> modelBuilder.HasPostgresExtension("postgis");
}
3 changes: 0 additions & 3 deletions src/EFCore.PG.NTS/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,7 @@ public static bool CanSetValueGenerationStrategy(
/// <param name="schema">The schema in which to create the extension.</param>
/// <param name="name">The name of the extension to create.</param>
/// <param name="version">The version of the extension.</param>
/// <returns>
/// The updated <see cref="ModelBuilder"/>.
/// </returns>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
/// <remarks>
/// See: https://www.postgresql.org/docs/current/external-extensions.html
/// </remarks>
Expand All @@ -274,9 +272,7 @@ public static ModelBuilder HasPostgresExtension(
/// </summary>
/// <param name="modelBuilder">The model builder in which to define the extension.</param>
/// <param name="name">The name of the extension to create.</param>
/// <returns>
/// The updated <see cref="ModelBuilder"/>.
/// </returns>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
/// <remarks>
/// See: https://www.postgresql.org/docs/current/external-extensions.html
/// </remarks>
Expand All @@ -286,6 +282,78 @@ public static ModelBuilder HasPostgresExtension(
string name)
=> modelBuilder.HasPostgresExtension(null, name);

/// <summary>
/// Registers a PostgreSQL extension in the model.
/// </summary>
/// <param name="modelBuilder">The model builder in which to define the extension.</param>
/// <param name="schema">The schema in which to create the extension.</param>
/// <param name="name">The name of the extension to create.</param>
/// <param name="version">The version of the extension.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
/// <remarks>
/// See: https://www.postgresql.org/docs/current/external-extensions.html
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/></exception>
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;
}

/// <summary>
/// Registers a PostgreSQL extension in the model.
/// </summary>
/// <param name="modelBuilder">The model builder in which to define the extension.</param>
/// <param name="name">The name of the extension to create.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
/// <remarks>
/// See: https://www.postgresql.org/docs/current/external-extensions.html
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/></exception>
public static IConventionModelBuilder? HasPostgresExtension(
this IConventionModelBuilder modelBuilder,
string name,
bool fromDataAnnotation = false)
=> modelBuilder.HasPostgresExtension(schema: null, name, version: null, fromDataAnnotation);

/// <summary>
/// Returns a value indicating whether the given PostgreSQL extension can be registered in the model.
/// </summary>
/// <remarks>
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
/// for more information and examples.
/// </remarks>
/// <param name="modelBuilder">The model builder.</param>
/// <param name="schema">The schema in which to create the extension.</param>
/// <param name="name">The name of the extension to create.</param>
/// <param name="version">The version of the extension.</param>
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
/// <returns><see langword="true" /> if the given value can be set as the default increment for SQL Server IDENTITY.</returns>
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,13 @@ public static PostgresExtension GetOrAddPostgresExtension(
public static IReadOnlyList<PostgresExtension> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

/// <inheritdoc />
public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> 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;
}
}
}
}
}
35 changes: 34 additions & 1 deletion src/EFCore.PG/Metadata/PostgresExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,39 @@ public static PostgresExtension GetOrAddPostgresExtension(
return new PostgresExtension(annotatable, annotationName) { Version = version };
}

/// <summary>
/// Gets or adds a <see cref="PostgresExtension"/> from or to the <see cref="IMutableAnnotatable"/>.
/// </summary>
/// <param name="annotatable">The annotatable from which to get or add the extension.</param>
/// <param name="schema">The extension schema or null to use the model's default schema.</param>
/// <param name="name">The extension name.</param>
/// <param name="version">The extension version.</param>
/// <returns>
/// The <see cref="PostgresExtension"/> from the <see cref="IMutableAnnotatable"/>.
/// </returns>
/// <exception cref="ArgumentException"><paramref name="schema"/></exception>
/// <exception cref="ArgumentNullException"><paramref name="annotatable"/></exception>
/// <exception cref="ArgumentNullException"><paramref name="name"/></exception>
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 };
}

/// <summary>
/// Gets or adds a <see cref="PostgresExtension"/> from or to the <see cref="IMutableAnnotatable"/>.
/// </summary>
Expand Down Expand Up @@ -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}";
Expand Down
3 changes: 1 addition & 2 deletions test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -965,7 +965,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
NpgsqlConnection.GlobalTypeMapper.MapEnum<Mood>();
((NpgsqlTypeMappingSource)context.GetService<ITypeMappingSource>()).LoadUserDefinedTypeMappings(context.GetService<ISqlGenerationHelper>());

modelBuilder.HasPostgresExtension("hstore");
modelBuilder.HasPostgresEnum("mood", new[] { "happy", "sad" });

MakeRequired<MappedDataTypes>(modelBuilder);
Expand Down Expand Up @@ -1431,4 +1430,4 @@ protected class MappedNullableDataTypes
}

// ReSharper disable once UnusedMember.Global
public enum Mood { Happy, Sad }
public enum Mood { Happy, Sad }
5 changes: 1 addition & 4 deletions test/EFCore.PG.FunctionalTests/Query/CitextQueryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -295,4 +292,4 @@ public class CitextQueryFixture : SharedStoreFixtureBase<CitextQueryContext>
public TestSqlLoggerFactory TestSqlLoggerFactory => (TestSqlLoggerFactory)ListLoggerFactory;
protected override void Seed(CitextQueryContext context) => CitextQueryContext.Seed(context);
}
}
}
7 changes: 1 addition & 6 deletions test/EFCore.PG.FunctionalTests/Query/LTreeQueryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -471,9 +468,7 @@ public class LTreeQueryFixture : SharedStoreFixtureBase<LTreeQueryContext>
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
}
}
5 changes: 2 additions & 3 deletions test/EFCore.PG.FunctionalTests/SpatialNpgsqlFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
}

0 comments on commit f6e9789

Please sign in to comment.