Skip to content

Commit f6e9789

Browse files
authored
Register PG extensions in the model by convention (#2161)
By scanning property types in the model. Closes #2137
1 parent 45d0739 commit f6e9789

15 files changed

+205
-27
lines changed

EFCore.PG.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ The .NET Foundation licenses this file to you under the MIT license.
307307
<s:Boolean x:Key="/Default/UserDictionary/Words/=niladic/@EntryIndexedValue">True</s:Boolean>
308308
<s:Boolean x:Key="/Default/UserDictionary/Words/=pluralizer/@EntryIndexedValue">True</s:Boolean>
309309
<s:Boolean x:Key="/Default/UserDictionary/Words/=Poolable/@EntryIndexedValue">True</s:Boolean>
310+
<s:Boolean x:Key="/Default/UserDictionary/Words/=postgis/@EntryIndexedValue">True</s:Boolean>
310311
<s:Boolean x:Key="/Default/UserDictionary/Words/=pushdown/@EntryIndexedValue">True</s:Boolean>
311312
<s:Boolean x:Key="/Default/UserDictionary/Words/=regconfig/@EntryIndexedValue">True</s:Boolean>
312313
<s:Boolean x:Key="/Default/UserDictionary/Words/=regdictionary/@EntryIndexedValue">True</s:Boolean>

src/EFCore.PG.NTS/EFCore.PG.NTS.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
<ItemGroup>
2626
<Compile Include="..\Shared\*.cs" />
27-
<None Include="README.md" Pack="true" PackagePath="\"/>
27+
<None Include="README.md" Pack="true" PackagePath="\" />
2828
<None Include="build\**\*">
2929
<Pack>True</Pack>
3030
<PackagePath>build</PackagePath>

src/EFCore.PG.NTS/Extensions/NpgsqlNetTopologySuiteServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public static IServiceCollection AddEntityFrameworkNpgsqlNetTopologySuite(
2626
.TryAdd<IRelationalTypeMappingSourcePlugin, NpgsqlNetTopologySuiteTypeMappingSourcePlugin>()
2727
.TryAdd<IMethodCallTranslatorPlugin, NpgsqlNetTopologySuiteMethodCallTranslatorPlugin>()
2828
.TryAdd<IMemberTranslatorPlugin, NpgsqlNetTopologySuiteMemberTranslatorPlugin>()
29+
.TryAdd<IConventionSetPlugin, NpgsqlNetTopologySuiteConventionSetPlugin>()
2930
.TryAddProviderSpecificServices(
3031
x => x.TryAddSingleton<INpgsqlNetTopologySuiteOptions, NpgsqlNetTopologySuiteOptions>());
3132

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
// ReSharper disable once CheckNamespace
5+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
6+
7+
public class NpgsqlNetTopologySuiteConventionSetPlugin : IConventionSetPlugin
8+
{
9+
public virtual ConventionSet ModifyConventions(ConventionSet conventionSet)
10+
{
11+
conventionSet.ModelFinalizingConventions.Add(new NpgsqlNetTopologySuiteExtensionAddingConvention());
12+
13+
return conventionSet;
14+
}
15+
}
16+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
// ReSharper disable once CheckNamespace
5+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
6+
7+
public class NpgsqlNetTopologySuiteExtensionAddingConvention : IModelFinalizingConvention
8+
{
9+
/// <inheritdoc />
10+
public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
11+
=> modelBuilder.HasPostgresExtension("postgis");
12+
}

src/EFCore.PG.NTS/README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ public class BlogContext : DbContext
3030
=> optionsBuilder.UseNpgsql(
3131
@"Host=myserver;Username=mylogin;Password=mypass;Database=mydatabase",
3232
o => o.UseNetTopologySuite());
33-
34-
protected override void OnModelCreating(ModelBuilder modelBuilder)
35-
=> modelBuilder.HasPostgresExtension("postgis");
3633
}
3734

3835
public class City

src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,7 @@ public static bool CanSetValueGenerationStrategy(
247247
/// <param name="schema">The schema in which to create the extension.</param>
248248
/// <param name="name">The name of the extension to create.</param>
249249
/// <param name="version">The version of the extension.</param>
250-
/// <returns>
251-
/// The updated <see cref="ModelBuilder"/>.
252-
/// </returns>
250+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
253251
/// <remarks>
254252
/// See: https://www.postgresql.org/docs/current/external-extensions.html
255253
/// </remarks>
@@ -274,9 +272,7 @@ public static ModelBuilder HasPostgresExtension(
274272
/// </summary>
275273
/// <param name="modelBuilder">The model builder in which to define the extension.</param>
276274
/// <param name="name">The name of the extension to create.</param>
277-
/// <returns>
278-
/// The updated <see cref="ModelBuilder"/>.
279-
/// </returns>
275+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
280276
/// <remarks>
281277
/// See: https://www.postgresql.org/docs/current/external-extensions.html
282278
/// </remarks>
@@ -286,6 +282,78 @@ public static ModelBuilder HasPostgresExtension(
286282
string name)
287283
=> modelBuilder.HasPostgresExtension(null, name);
288284

285+
/// <summary>
286+
/// Registers a PostgreSQL extension in the model.
287+
/// </summary>
288+
/// <param name="modelBuilder">The model builder in which to define the extension.</param>
289+
/// <param name="schema">The schema in which to create the extension.</param>
290+
/// <param name="name">The name of the extension to create.</param>
291+
/// <param name="version">The version of the extension.</param>
292+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
293+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
294+
/// <remarks>
295+
/// See: https://www.postgresql.org/docs/current/external-extensions.html
296+
/// </remarks>
297+
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/></exception>
298+
public static IConventionModelBuilder? HasPostgresExtension(
299+
this IConventionModelBuilder modelBuilder,
300+
string? schema,
301+
string name,
302+
string? version = null,
303+
bool fromDataAnnotation = false)
304+
{
305+
if (modelBuilder.CanSetPostgresExtension(schema, name, version, fromDataAnnotation))
306+
{
307+
modelBuilder.Metadata.GetOrAddPostgresExtension(schema, name, version);
308+
return modelBuilder;
309+
}
310+
311+
return null;
312+
}
313+
314+
/// <summary>
315+
/// Registers a PostgreSQL extension in the model.
316+
/// </summary>
317+
/// <param name="modelBuilder">The model builder in which to define the extension.</param>
318+
/// <param name="name">The name of the extension to create.</param>
319+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
320+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
321+
/// <remarks>
322+
/// See: https://www.postgresql.org/docs/current/external-extensions.html
323+
/// </remarks>
324+
/// <exception cref="ArgumentNullException"><paramref name="modelBuilder"/></exception>
325+
public static IConventionModelBuilder? HasPostgresExtension(
326+
this IConventionModelBuilder modelBuilder,
327+
string name,
328+
bool fromDataAnnotation = false)
329+
=> modelBuilder.HasPostgresExtension(schema: null, name, version: null, fromDataAnnotation);
330+
331+
/// <summary>
332+
/// Returns a value indicating whether the given PostgreSQL extension can be registered in the model.
333+
/// </summary>
334+
/// <remarks>
335+
/// See <see href="https://aka.ms/efcore-docs-modeling">Modeling entity types and relationships</see>, and
336+
/// <see href="https://aka.ms/efcore-docs-sqlserver">Accessing SQL Server and SQL Azure databases with EF Core</see>
337+
/// for more information and examples.
338+
/// </remarks>
339+
/// <param name="modelBuilder">The model builder.</param>
340+
/// <param name="schema">The schema in which to create the extension.</param>
341+
/// <param name="name">The name of the extension to create.</param>
342+
/// <param name="version">The version of the extension.</param>
343+
/// <param name="fromDataAnnotation">Indicates whether the configuration was specified using a data annotation.</param>
344+
/// <returns><see langword="true" /> if the given value can be set as the default increment for SQL Server IDENTITY.</returns>
345+
public static bool CanSetPostgresExtension(
346+
this IConventionModelBuilder modelBuilder,
347+
string? schema,
348+
string name,
349+
string? version = null,
350+
bool fromDataAnnotation = false)
351+
{
352+
var annotationName = PostgresExtension.BuildAnnotationName(schema, name);
353+
354+
return modelBuilder.CanSetAnnotation(annotationName, $"{schema},{name},{version}", fromDataAnnotation);
355+
}
356+
289357
#endregion
290358

291359
#region Enums

src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ public static PostgresExtension GetOrAddPostgresExtension(
161161
public static IReadOnlyList<PostgresExtension> GetPostgresExtensions(this IReadOnlyModel model)
162162
=> PostgresExtension.GetPostgresExtensions(model).ToArray();
163163

164+
public static PostgresExtension GetOrAddPostgresExtension(
165+
this IConventionModel model,
166+
string? schema,
167+
string name,
168+
string? version)
169+
=> PostgresExtension.GetOrAddPostgresExtension(model, schema, name, version);
170+
164171
#endregion
165172

166173
#region Enum types

src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions;
55
[EntityFrameworkInternal]
66
public class NpgsqlConventionSetBuilder : RelationalConventionSetBuilder
77
{
8+
private readonly IRelationalTypeMappingSource _typeMappingSource;
89
private readonly Version _postgresVersion;
910

1011
[EntityFrameworkInternal]
1112
public NpgsqlConventionSetBuilder(
1213
ProviderConventionSetBuilderDependencies dependencies,
1314
RelationalConventionSetBuilderDependencies relationalDependencies,
15+
IRelationalTypeMappingSource typeMappingSource,
1416
INpgsqlOptions npgsqlOptions)
1517
: base(dependencies, relationalDependencies)
16-
=> _postgresVersion = npgsqlOptions.PostgresVersion;
18+
{
19+
_typeMappingSource = typeMappingSource;
20+
_postgresVersion = npgsqlOptions.PostgresVersion;
21+
}
1722

1823
[EntityFrameworkInternal]
1924
public override ConventionSet CreateConventionSet()
@@ -43,6 +48,7 @@ public override ConventionSet CreateConventionSet()
4348
conventionSet.PropertyAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention);
4449

4550
conventionSet.ModelFinalizingConventions.Add(valueGenerationStrategyConvention);
51+
conventionSet.ModelFinalizingConventions.Add(new NpgsqlPostgresExtensionDiscoveryConvention(_typeMappingSource));
4652
ReplaceConvention(conventionSet.ModelFinalizingConventions, storeGenerationConvention);
4753
ReplaceConvention(
4854
conventionSet.ModelFinalizingConventions,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions;
5+
6+
public class NpgsqlPostgresExtensionDiscoveryConvention : IModelFinalizingConvention
7+
{
8+
private readonly IRelationalTypeMappingSource _typeMappingSource;
9+
10+
public NpgsqlPostgresExtensionDiscoveryConvention(IRelationalTypeMappingSource typeMappingSource)
11+
{
12+
_typeMappingSource = typeMappingSource;
13+
}
14+
15+
/// <inheritdoc />
16+
public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder, IConventionContext<IConventionModelBuilder> context)
17+
{
18+
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes())
19+
{
20+
foreach (var property in entityType.GetDeclaredProperties())
21+
{
22+
var typeMapping = (RelationalTypeMapping?)property.FindTypeMapping()
23+
?? _typeMappingSource.FindMapping((IProperty)property);
24+
25+
if (typeMapping is null)
26+
{
27+
continue;
28+
}
29+
30+
switch (typeMapping.StoreType)
31+
{
32+
case "hstore":
33+
modelBuilder.HasPostgresExtension("hstore");
34+
continue;
35+
case "citext":
36+
modelBuilder.HasPostgresExtension("citext");
37+
continue;
38+
case "ltree":
39+
case "lquery":
40+
case "ltxtquery":
41+
modelBuilder.HasPostgresExtension("ltree");
42+
continue;
43+
}
44+
}
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)