diff --git a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs
index 6cfd490c1f..f38fda9af7 100644
--- a/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs
+++ b/src/EFCore.PG/Extensions/BuilderExtensions/NpgsqlModelBuilderExtensions.cs
@@ -443,6 +443,61 @@ public static ModelBuilder HasPostgresEnum(
return modelBuilder;
}
+ ///
+ /// Registers a user-defined enum type in the model.
+ ///
+ /// The model builder in which to create the enum type.
+ /// The schema in which to create the enum type.
+ /// The name of the enum type to create.
+ /// The enum label values.
+ ///
+ /// The updated .
+ ///
+ ///
+ /// See: https://www.postgresql.org/docs/current/static/datatype-enum.html
+ ///
+ /// builder
+ public static IConventionModelBuilder HasPostgresEnum(
+ this IConventionModelBuilder modelBuilder,
+ string? schema,
+ string name,
+ string[] labels)
+ {
+ Check.NotNull(modelBuilder, nameof(modelBuilder));
+ Check.NotEmpty(name, nameof(name));
+ Check.NotNull(labels, nameof(labels));
+
+ if (modelBuilder.CanSetPostgresEnum(schema, name))
+ {
+ modelBuilder.Metadata.GetOrAddPostgresEnum(schema, name, labels);
+ }
+ return modelBuilder;
+ }
+
+ ///
+ /// 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.
+ /// 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 CanSetPostgresEnum(
+ this IConventionModelBuilder modelBuilder,
+ string? schema,
+ string name,
+ bool fromDataAnnotation = false)
+ {
+ var annotationName = PostgresExtension.BuildAnnotationName(schema, name);
+
+ return modelBuilder.CanSetAnnotation(annotationName, $"{schema},{name}", fromDataAnnotation);
+ }
+
///
/// Registers a user-defined enum type in the model.
///
diff --git a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs
index 018c77f5f6..35228a5060 100644
--- a/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs
+++ b/src/EFCore.PG/Extensions/MetadataExtensions/NpgsqlModelExtensions.cs
@@ -321,6 +321,19 @@ public static PostgresEnum GetOrAddPostgresEnum(
string[] labels)
=> PostgresEnum.GetOrAddPostgresEnum(model, schema, name, labels);
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public static PostgresEnum GetOrAddPostgresEnum(
+ this IConventionModel model,
+ string? schema,
+ string name,
+ string[] labels)
+ => PostgresEnum.GetOrAddPostgresEnum(model, schema, name, labels);
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs
index 7618ae040f..364ed7c291 100644
--- a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs
+++ b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs
@@ -1,4 +1,5 @@
using System.Data.Common;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
@@ -28,17 +29,12 @@ public interface INpgsqlSingletonOptions : ISingletonOptions
bool ReverseNullOrderingEnabled { get; }
///
- /// The data source being used, or if a connection string or connection was provided directly.
+ /// The collection of enum mappings.
///
- DbDataSource? DataSource { get; }
+ IReadOnlyList EnumTypeMappings { get; }
///
/// The collection of range mappings.
///
IReadOnlyList UserRangeDefinitions { get; }
-
- ///
- /// The root service provider for the application, if available. />.
- ///
- IServiceProvider? ApplicationServiceProvider { get; }
}
diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
index 1ed164bb4f..4eb4574f82 100644
--- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
+++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs
@@ -2,6 +2,8 @@
using System.Globalization;
using System.Net.Security;
using System.Text;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
@@ -12,6 +14,7 @@ public class NpgsqlOptionsExtension : RelationalOptionsExtension
{
private DbContextOptionsExtensionInfo? _info;
private readonly List _userRangeDefinitions;
+ private readonly List _enumMappings;
private Version? _postgresVersion;
@@ -57,6 +60,12 @@ public virtual bool IsPostgresVersionSet
public virtual IReadOnlyList UserRangeDefinitions
=> _userRangeDefinitions;
+ ///
+ /// The list of range mappings specified by the user.
+ ///
+ public virtual IReadOnlyList EnumTypeMappings
+ => _enumMappings;
+
///
/// The specified .
///
@@ -85,6 +94,7 @@ public virtual IReadOnlyList UserRangeDefinitions
public NpgsqlOptionsExtension()
{
_userRangeDefinitions = [];
+ _enumMappings = [];
}
// NB: When adding new options, make sure to update the copy ctor below.
@@ -100,6 +110,7 @@ public NpgsqlOptionsExtension(NpgsqlOptionsExtension copyFrom)
_postgresVersion = copyFrom._postgresVersion;
UseRedshift = copyFrom.UseRedshift;
_userRangeDefinitions = [..copyFrom._userRangeDefinitions];
+ _enumMappings = [..copyFrom._enumMappings];
ProvideClientCertificatesCallback = copyFrom.ProvideClientCertificatesCallback;
RemoteCertificateValidationCallback = copyFrom.RemoteCertificateValidationCallback;
ProvidePasswordCallback = copyFrom.ProvidePasswordCallback;
@@ -180,6 +191,32 @@ public virtual NpgsqlOptionsExtension WithUserRangeDefinition(
return clone;
}
+ ///
+ /// Returns a copy of the current instance configured with the specified range mapping.
+ ///
+ public virtual NpgsqlOptionsExtension WithEnumMapping(
+ Type clrType,
+ string enumName,
+ string? schemaName,
+ INpgsqlNameTranslator? nameTranslator)
+ {
+ if (clrType is not { IsEnum: true, IsClass: false})
+ {
+ throw new ArgumentException($"Enum type mappings require a CLR enum. {clrType.FullName} is not an enum.");
+ }
+
+ var clone = (NpgsqlOptionsExtension)Clone();
+
+ // TODO
+#pragma warning disable EF1001
+ var storeType = new NpgsqlSqlGenerationHelper(new()).DelimitIdentifier(enumName, schemaName);
+#pragma warning restore EF1001
+
+ clone._enumMappings.Add(new NpgsqlEnumTypeMapping(storeType, schemaName + "." + enumName, clrType, nameTranslator));
+
+ return clone;
+ }
+
///
/// Returns a copy of the current instance configured to use the specified administrative database.
///
@@ -453,8 +490,9 @@ public override int GetServiceProviderHashCode()
public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other)
=> other is ExtensionInfo otherInfo
&& Extension.PostgresVersion == otherInfo.Extension.PostgresVersion
- && ReferenceEquals(Extension.DataSource, otherInfo.Extension.DataSource)
&& Extension.ReverseNullOrdering == otherInfo.Extension.ReverseNullOrdering
+ // TODO does not work, should be comparing a record, enum type definition instead of mapping.
+ && Extension.EnumTypeMappings.SequenceEqual(otherInfo.Extension.EnumTypeMappings)
&& Extension.UserRangeDefinitions.SequenceEqual(otherInfo.Extension.UserRangeDefinitions)
&& Extension.UseRedshift == otherInfo.Extension.UseRedshift;
@@ -482,6 +520,16 @@ public override void PopulateDebugInfo(IDictionary debugInfo)
debugInfo["Npgsql.EntityFrameworkCore.PostgreSQL:" + nameof(NpgsqlDbContextOptionsBuilder.ProvidePasswordCallback)]
= (Extension.ProvidePasswordCallback?.GetHashCode() ?? 0).ToString(CultureInfo.InvariantCulture);
+ foreach (var enumDefinition in Extension._enumMappings)
+ {
+ debugInfo[
+ "Npgsql.EntityFrameworkCore.PostgreSQL:"
+ + nameof(NpgsqlDbContextOptionsBuilder.MapEnum)
+ + ":"
+ + enumDefinition.ClrType.Name]
+ = enumDefinition.GetHashCode().ToString(CultureInfo.InvariantCulture);
+ }
+
foreach (var rangeDefinition in Extension._userRangeDefinitions)
{
debugInfo[
@@ -496,50 +544,3 @@ public override void PopulateDebugInfo(IDictionary debugInfo)
#endregion Infrastructure
}
-
-///
-/// A definition for a user-defined PostgreSQL range to be mapped.
-///
-public record UserRangeDefinition
-{
- ///
- /// The name of the PostgreSQL range type to be mapped.
- ///
- public virtual string RangeName { get; }
-
- ///
- /// The PostgreSQL schema in which the range is defined. If null, the default schema is used
- /// (which is public unless changed on the model).
- ///
- public virtual string? SchemaName { get; }
-
- ///
- /// The CLR type of the range's subtype (or element).
- /// The actual mapped type will be an over this type.
- ///
- public virtual Type SubtypeClrType { get; }
-
- ///
- /// Optionally, the name of the range's PostgreSQL subtype (or element).
- /// This is usually not needed - the subtype will be inferred based on .
- ///
- public virtual string? SubtypeName { get; }
-
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- public UserRangeDefinition(
- string rangeName,
- string? schemaName,
- Type subtypeClrType,
- string? subtypeName)
- {
- RangeName = Check.NotEmpty(rangeName, nameof(rangeName));
- SchemaName = schemaName;
- SubtypeClrType = Check.NotNull(subtypeClrType, nameof(subtypeClrType));
- SubtypeName = subtypeName;
- }
-}
diff --git a/src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs b/src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs
new file mode 100644
index 0000000000..51870f7a11
--- /dev/null
+++ b/src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs
@@ -0,0 +1,48 @@
+namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
+
+///
+/// A definition for a user-defined PostgreSQL range to be mapped.
+///
+public sealed record UserRangeDefinition
+{
+ ///
+ /// The name of the PostgreSQL range type to be mapped.
+ ///
+ public string RangeName { get; }
+
+ ///
+ /// The PostgreSQL schema in which the range is defined. If null, the default schema is used
+ /// (which is public unless changed on the model).
+ ///
+ public string? SchemaName { get; }
+
+ ///
+ /// The CLR type of the range's subtype (or element).
+ /// The actual mapped type will be an over this type.
+ ///
+ public Type SubtypeClrType { get; }
+
+ ///
+ /// Optionally, the name of the range's PostgreSQL subtype (or element).
+ /// This is usually not needed - the subtype will be inferred based on .
+ ///
+ public string? SubtypeName { get; }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public UserRangeDefinition(
+ string rangeName,
+ string? schemaName,
+ Type subtypeClrType,
+ string? subtypeName)
+ {
+ RangeName = Check.NotEmpty(rangeName, nameof(rangeName));
+ SchemaName = schemaName;
+ SubtypeClrType = Check.NotNull(subtypeClrType, nameof(subtypeClrType));
+ SubtypeName = subtypeName;
+ }
+}
diff --git a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
index 4a43c55b0d..b0a8e99210 100644
--- a/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
+++ b/src/EFCore.PG/Infrastructure/NpgsqlDbContextOptionsBuilder.cs
@@ -95,6 +95,33 @@ public virtual NpgsqlDbContextOptionsBuilder MapRange(
string? subtypeName = null)
=> WithOption(e => e.WithUserRangeDefinition(rangeName, schemaName, subtypeClrType, subtypeName));
+ ///
+ /// Maps a PostgreSQL enum type for use.
+ ///
+ /// The name of the PostgreSQL enum type to be mapped.
+ /// The name of the PostgreSQL schema in which the range is defined.
+ /// The name translator used to map enum value names to PostgreSQL enum values.
+ public virtual NpgsqlDbContextOptionsBuilder MapEnum(
+ string enumName,
+ string? schemaName = null,
+ INpgsqlNameTranslator? nameTranslator = null)
+ where T : struct, Enum
+ => MapEnum(typeof(T), enumName, schemaName, nameTranslator);
+
+ ///
+ /// Maps a PostgreSQL enum type for use.
+ ///
+ /// The CLR type of the enum.
+ /// The name of the PostgreSQL enum type to be mapped.
+ /// The name of the PostgreSQL schema in which the range is defined.
+ /// The name translator used to map enum value names to PostgreSQL enum values.
+ public virtual NpgsqlDbContextOptionsBuilder MapEnum(
+ Type clrType,
+ string enumName,
+ string? schemaName = null,
+ INpgsqlNameTranslator? nameTranslator = null)
+ => WithOption(e => e.WithEnumMapping(clrType, enumName, schemaName, nameTranslator));
+
///
/// Appends NULLS FIRST to all ORDER BY clauses. This is important for the tests which were written
/// for SQL Server. Note that to fully implement null-first ordering indexes also need to be generated
diff --git a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs
index e0fa14260b..3691b3707f 100644
--- a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs
+++ b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs
@@ -1,6 +1,7 @@
using System.Data.Common;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal;
@@ -45,7 +46,7 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public virtual DbDataSource? DataSource { get; private set; }
+ public IReadOnlyList EnumTypeMappings { get; private set; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -55,14 +56,6 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions
///
public virtual IReadOnlyList UserRangeDefinitions { get; private set; }
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- public virtual IServiceProvider? ApplicationServiceProvider { get; private set; }
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -71,6 +64,7 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions
///
public NpgsqlSingletonOptions()
{
+ EnumTypeMappings = [];
UserRangeDefinitions = [];
}
@@ -78,20 +72,13 @@ public NpgsqlSingletonOptions()
public virtual void Initialize(IDbContextOptions options)
{
var npgsqlOptions = options.FindExtension() ?? new NpgsqlOptionsExtension();
- var coreOptions = options.FindExtension() ?? new CoreOptionsExtension();
PostgresVersion = npgsqlOptions.PostgresVersion;
IsPostgresVersionSet = npgsqlOptions.IsPostgresVersionSet;
UseRedshift = npgsqlOptions.UseRedshift;
ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering;
+ EnumTypeMappings = npgsqlOptions.EnumTypeMappings;
UserRangeDefinitions = npgsqlOptions.UserRangeDefinitions;
-
- // TODO: Remove after https://github.com/dotnet/efcore/pull/29950
- ApplicationServiceProvider = coreOptions.ApplicationServiceProvider;
-
- DataSource = npgsqlOptions.DataSource ?? (npgsqlOptions.ConnectionString is null && npgsqlOptions.Connection is null
- ? coreOptions.ApplicationServiceProvider?.GetService()
- : null);
}
///
@@ -123,10 +110,12 @@ public virtual void Validate(IDbContextOptions options)
nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
}
- if (npgsqlOptions.DataSource is not null && !ReferenceEquals(DataSource, npgsqlOptions.DataSource))
+ if (!EnumTypeMappings.SequenceEqual(npgsqlOptions.EnumTypeMappings))
{
throw new InvalidOperationException(
- NpgsqlStrings.TwoDataSourcesInSameServiceProvider(nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
+ CoreStrings.SingletonOptionChanged(
+ nameof(NpgsqlDbContextOptionsBuilder.MapEnum),
+ nameof(DbContextOptionsBuilder.UseInternalServiceProvider)));
}
if (!UserRangeDefinitions.SequenceEqual(npgsqlOptions.UserRangeDefinitions))
diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs
index 8b9098fbd6..602e9db3d2 100644
--- a/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs
+++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlConventionSetBuilder.cs
@@ -1,4 +1,5 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions;
@@ -19,6 +20,7 @@ public class NpgsqlConventionSetBuilder : RelationalConventionSetBuilder
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
private readonly Version _postgresVersion;
+ private readonly IReadOnlyList _enumTypeMappings;
///
/// Creates a new instance.
@@ -36,6 +38,7 @@ public NpgsqlConventionSetBuilder(
{
_typeMappingSource = typeMappingSource;
_postgresVersion = npgsqlSingletonOptions.PostgresVersion;
+ _enumTypeMappings = npgsqlSingletonOptions.EnumTypeMappings;
}
///
@@ -70,7 +73,7 @@ public override ConventionSet CreateConventionSet()
conventionSet.PropertyAnnotationChangedConventions, (RelationalValueGenerationConvention)valueGenerationConvention);
conventionSet.ModelFinalizingConventions.Add(valueGenerationStrategyConvention);
- conventionSet.ModelFinalizingConventions.Add(new NpgsqlPostgresModelFinalizingConvention(_typeMappingSource));
+ conventionSet.ModelFinalizingConventions.Add(new NpgsqlPostgresModelFinalizingConvention(_typeMappingSource, _enumTypeMappings));
ReplaceConvention(conventionSet.ModelFinalizingConventions, storeGenerationConvention);
ReplaceConvention(
conventionSet.ModelFinalizingConventions,
diff --git a/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs b/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs
index b7ed9e0ae3..3ce71c3663 100644
--- a/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs
+++ b/src/EFCore.PG/Metadata/Conventions/NpgsqlPostgresModelFinalizingConvention.cs
@@ -1,3 +1,5 @@
+using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
+
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions;
///
@@ -9,14 +11,19 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.Conventions;
public class NpgsqlPostgresModelFinalizingConvention : IModelFinalizingConvention
{
private readonly IRelationalTypeMappingSource _typeMappingSource;
+ private readonly IReadOnlyList _enumTypeMappings;
///
/// Creates a new instance of .
///
/// The type mapping source to use.
- public NpgsqlPostgresModelFinalizingConvention(IRelationalTypeMappingSource typeMappingSource)
+ ///
+ public NpgsqlPostgresModelFinalizingConvention(
+ IRelationalTypeMappingSource typeMappingSource,
+ IReadOnlyList enumTypeMappings)
{
_typeMappingSource = typeMappingSource;
+ _enumTypeMappings = enumTypeMappings;
}
///
@@ -36,6 +43,22 @@ public virtual void ProcessModelFinalizing(IConventionModelBuilder modelBuilder,
}
}
}
+
+ SetupEnums(modelBuilder);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ protected virtual void SetupEnums(IConventionModelBuilder modelBuilder)
+ {
+ foreach (var enumMapping in _enumTypeMappings)
+ {
+ // TODO schema and name needs to flow from definition.
+ var nameParts = enumMapping.UnquotedStoreType.Split(".");
+ modelBuilder.HasPostgresEnum(nameParts[0], nameParts[1], enumMapping.EnumValues.ToArray());
+ }
}
///
@@ -46,6 +69,7 @@ protected virtual void DiscoverPostgresExtensions(
RelationalTypeMapping typeMapping,
IConventionModelBuilder modelBuilder)
{
+ // TODO does not work if CREATE EXTENSION was done on a non-default schema.
switch (typeMapping.StoreType)
{
case "hstore":
diff --git a/src/EFCore.PG/Metadata/PostgresEnum.cs b/src/EFCore.PG/Metadata/PostgresEnum.cs
index f31cd651a0..0d373c0cc3 100644
--- a/src/EFCore.PG/Metadata/PostgresEnum.cs
+++ b/src/EFCore.PG/Metadata/PostgresEnum.cs
@@ -70,6 +70,49 @@ public static PostgresEnum GetOrAddPostgresEnum(
return new PostgresEnum(annotatable, annotationName) { Labels = labels };
}
+ ///
+ /// Gets or adds a from or to the .
+ ///
+ /// The annotatable from which to get or add the enum.
+ /// The enum schema or null to use the model's default schema.
+ /// The enum name.
+ /// The enum labels.
+ ///
+ /// The from the .
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static PostgresEnum GetOrAddPostgresEnum(
+ IConventionAnnotatable annotatable,
+ string? schema,
+ string name,
+ string[] labels)
+ {
+ Check.NotNull(annotatable, nameof(annotatable));
+ Check.NullButNotEmpty(schema, nameof(schema));
+ Check.NotEmpty(name, nameof(name));
+ Check.NotNull(labels, nameof(labels));
+
+ if (FindPostgresEnum(annotatable, schema, name) is { } enumType)
+ {
+ return enumType;
+ }
+
+ var annotationName = BuildAnnotationName(schema, name);
+
+ return new PostgresEnum(annotatable, annotationName) { Labels = labels };
+ }
+
///
/// Gets or adds a from or to the .
///
diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
index b986d9122b..4b01441d87 100644
--- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
+++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlArrayTypeMapping.cs
@@ -234,7 +234,22 @@ public override DbParameter CreateParameter(
}
}
- return base.CreateParameter(command, name, value, nullable, direction);
+ var param = base.CreateParameter(command, name, value, nullable, direction);
+ if (param is not NpgsqlParameter npgsqlParameter)
+ {
+ throw new InvalidOperationException(
+ $"Npgsql-specific type mapping {GetType().Name} being used with non-Npgsql parameter type {param.GetType().Name}");
+ }
+ switch (ElementTypeMapping)
+ {
+ case NpgsqlEnumTypeMapping enumTypeMapping:
+ npgsqlParameter.DataTypeName = enumTypeMapping.UnquotedStoreType + "[]";
+ break;
+ case NpgsqlRangeTypeMapping rangeTypeMapping:
+ npgsqlParameter.DataTypeName = rangeTypeMapping.UnquotedStoreType + "[]";
+ break;
+ }
+ return param;
}
///
diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs
index 94f1ead196..3c3bfa0d38 100644
--- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs
+++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs
@@ -1,4 +1,5 @@
-using System.Text.Json;
+using System.Data.Common;
+using System.Text.Json;
using Microsoft.EntityFrameworkCore.Storage.Json;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
@@ -11,11 +12,22 @@ namespace Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;
///
public class NpgsqlEnumTypeMapping : RelationalTypeMapping
{
+ ///
+ ///
+ ///
+ public string UnquotedStoreType { get; }
+
///
/// Translates the CLR member value to the PostgreSQL value label.
///
private readonly Dictionary
protected NpgsqlEnumTypeMapping(
RelationalTypeMappingParameters parameters,
- INpgsqlNameTranslator nameTranslator)
+ string unquotedStoreType, INpgsqlNameTranslator nameTranslator)
: base(parameters)
{
NameTranslator = nameTranslator;
_members = CreateValueMapping(parameters.CoreParameters.ClrType, nameTranslator);
+ UnquotedStoreType = unquotedStoreType;
}
// This constructor exists only to support the static Default property above, which is necessary to allow code generation for compiled
@@ -81,6 +96,7 @@ private NpgsqlEnumTypeMapping()
#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete
NameTranslator = NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator;
#pragma warning restore CS0618
+ UnquotedStoreType = null!;
_members = null!;
}
@@ -91,7 +107,24 @@ private NpgsqlEnumTypeMapping()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters parameters)
- => new NpgsqlEnumTypeMapping(parameters, NameTranslator);
+ => new NpgsqlEnumTypeMapping(parameters, UnquotedStoreType, NameTranslator);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override void ConfigureParameter(DbParameter parameter)
+ {
+ if (parameter is not NpgsqlParameter npgsqlParameter)
+ {
+ throw new InvalidOperationException(
+ $"Npgsql-specific type mapping {GetType().Name} being used with non-Npgsql parameter type {parameter.GetType().Name}");
+ }
+
+ npgsqlParameter.DataTypeName = UnquotedStoreType;
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs
index d8e5d62aed..8beeae1fac 100644
--- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs
+++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlRangeTypeMapping.cs
@@ -42,7 +42,7 @@ public class NpgsqlRangeTypeMapping : NpgsqlTypeMapping
/// For user-defined ranges, we have no and so the PG type name is set on
/// instead.
///
- private string? PgDataTypeName { get; init; }
+ public string? UnquotedStoreType { get; init; }
///
/// Constructs an instance of the class for a built-in range type which has a
@@ -74,7 +74,7 @@ public static NpgsqlRangeTypeMapping CreatUserDefinedRangeMapping(
RelationalTypeMapping subtypeMapping)
=> new(quotedRangeStoreType, rangeClrType, rangeNpgsqlDbType: NpgsqlDbType.Unknown, subtypeMapping)
{
- PgDataTypeName = unquotedRangeStoreType
+ UnquotedStoreType = unquotedRangeStoreType
};
private NpgsqlRangeTypeMapping(
@@ -139,7 +139,7 @@ protected override RelationalTypeMapping Clone(RelationalTypeMappingParameters p
protected override void ConfigureParameter(DbParameter parameter)
{
// Built-in range types have an NpgsqlDbType, so we just do the normal thing.
- if (PgDataTypeName is null)
+ if (UnquotedStoreType is null)
{
Check.DebugAssert(NpgsqlDbType is not NpgsqlDbType.Unknown, "NpgsqlDbType is Unknown but no PgDataTypeName is configured");
base.ConfigureParameter(parameter);
@@ -154,7 +154,7 @@ protected override void ConfigureParameter(DbParameter parameter)
$"Npgsql-specific type mapping {GetType().Name} being used with non-Npgsql parameter type {parameter.GetType().Name}");
}
- npgsqlParameter.DataTypeName = PgDataTypeName;
+ npgsqlParameter.DataTypeName = UnquotedStoreType;
}
///
diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs b/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs
index 77b3f81e22..d5900cdbd1 100644
--- a/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs
+++ b/src/EFCore.PG/Storage/Internal/NpgsqlRelationalConnection.cs
@@ -35,8 +35,8 @@ protected override bool SupportsAmbientTransactions
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- public NpgsqlRelationalConnection(RelationalConnectionDependencies dependencies, INpgsqlSingletonOptions options)
- : this(dependencies, options.DataSource)
+ public NpgsqlRelationalConnection(RelationalConnectionDependencies dependencies, IDbContextOptions options)
+ : this(dependencies, options.FindExtension()?.DataSource)
{
}
diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
index d6661e7fbe..b6239c4ead 100644
--- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
+++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs
@@ -57,6 +57,7 @@ static NpgsqlTypeMappingSource()
///
protected virtual ConcurrentDictionary ClrTypeMappings { get; }
+ private readonly IReadOnlyList _enumMappings;
private readonly IReadOnlyList _userRangeDefinitions;
private readonly bool _supportsMultiranges;
@@ -348,66 +349,10 @@ public NpgsqlTypeMappingSource(
StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase);
ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings);
- LoadUserDefinedTypeMappings(sqlGenerationHelper, options.DataSource as NpgsqlDataSource);
-
+ _enumMappings = options.EnumTypeMappings;
_userRangeDefinitions = options.UserRangeDefinitions;
}
- ///
- /// To be used in case user-defined mappings are added late, after this TypeMappingSource has already been initialized.
- /// This is basically only for test usage.
- ///
- public virtual void LoadUserDefinedTypeMappings(
- ISqlGenerationHelper sqlGenerationHelper,
- NpgsqlDataSource? dataSource)
- => SetupEnumMappings(sqlGenerationHelper, dataSource);
-
- ///
- /// Gets all global enum mappings from the ADO.NET layer and creates mappings for them
- ///
- protected virtual void SetupEnumMappings(ISqlGenerationHelper sqlGenerationHelper, NpgsqlDataSource? dataSource)
- {
- List? adoEnumMappings = null;
-
- if (dataSource is not null
- && typeof(NpgsqlDataSource).GetField("_hackyEnumTypeMappings", BindingFlags.NonPublic | BindingFlags.Instance) is
- { } dataSourceTypeMappingsFieldInfo
- && dataSourceTypeMappingsFieldInfo.GetValue(dataSource) is List dataSourceEnumMappings)
- {
- // Note that the data source's enum mappings also include any global ones that were configured when the data source was created.
- // So we don't need to also collect mappings from GlobalTypeMapper below.
- adoEnumMappings = dataSourceEnumMappings;
- }
-#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete
- else if (NpgsqlConnection.GlobalTypeMapper.GetType().GetProperty(
- "HackyEnumTypeMappings", BindingFlags.NonPublic | BindingFlags.Instance)
- is PropertyInfo globalEnumTypeMappingsProperty
- && globalEnumTypeMappingsProperty.GetValue(NpgsqlConnection.GlobalTypeMapper) is List
- globalEnumMappings)
- {
- adoEnumMappings = globalEnumMappings;
- }
-#pragma warning restore CS0618
-
- if (adoEnumMappings is not null)
- {
- foreach (var adoEnumMapping in adoEnumMappings)
- {
- // TODO: update with schema per https://github.com/npgsql/npgsql/issues/2121
- var components = adoEnumMapping.PgTypeName.Split('.');
- var schema = components.Length > 1 ? components.First() : null;
- var name = components.Length > 1 ? string.Join(null, components.Skip(1)) : adoEnumMapping.PgTypeName;
-
- var mapping = new NpgsqlEnumTypeMapping(
- sqlGenerationHelper.DelimitIdentifier(name, schema),
- adoEnumMapping.EnumClrType,
- adoEnumMapping.NameTranslator);
- ClrTypeMappings[adoEnumMapping.EnumClrType] = mapping;
- StoreTypeMappings[mapping.StoreType] = [mapping];
- }
- }
- }
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -418,6 +363,7 @@ is PropertyInfo globalEnumTypeMappingsProperty
// First, try any plugins, allowing them to override built-in mappings (e.g. NodaTime)
=> base.FindMapping(mappingInfo)
?? FindBaseMapping(mappingInfo)?.Clone(mappingInfo)
+ ?? FindEnumMapping(mappingInfo)
?? FindRowValueMapping(mappingInfo)?.Clone(mappingInfo)
?? FindUserRangeMapping(mappingInfo);
@@ -814,6 +760,29 @@ static Type FindTypeToInstantiate(Type collectionType, Type elementType)
? new NpgsqlRowValueTypeMapping(clrType)
: null;
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected virtual RelationalTypeMapping? FindEnumMapping(in RelationalTypeMappingInfo mappingInfo)
+ {
+ var storeType = mappingInfo.StoreTypeName;
+ var clrType = mappingInfo.ClrType;
+
+ if (clrType is not { IsEnum: true, IsClass: false})
+ {
+ return null;
+ }
+
+ // Try to find an enum definition (defined by the user on their context options), based on the
+ // incoming MappingInfo's StoreType or ClrType
+ return storeType is not null
+ ? _enumMappings.SingleOrDefault(m => m.StoreType == storeType)
+ : _enumMappings.SingleOrDefault(m => m.ClrType == clrType);
+ }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs
index 4d372c8e15..34d7978ebc 100644
--- a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs
+++ b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs
@@ -983,8 +983,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con
base.OnModelCreating(modelBuilder, context);
// TODO: Switch to using data source
- ((NpgsqlTypeMappingSource)context.GetService()).LoadUserDefinedTypeMappings(
- context.GetService(), dataSource: null);
+ // ((NpgsqlTypeMappingSource)context.GetService()).LoadUserDefinedTypeMappings(
+ // context.GetService(), dataSource: null);
modelBuilder.HasPostgresEnum("mood", ["happy", "sad"]);
diff --git a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs
index 479506646e..d4d6f77e51 100644
--- a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs
+++ b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs
@@ -1,4 +1,5 @@
-using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
+using Npgsql.EntityFrameworkCore.PostgreSQL.TestUtilities;
namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;
@@ -222,13 +223,14 @@ public class EnumContext(DbContextOptions options) : PoolableDbContext(options)
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public DbSet SomeEntities { get; set; }
+ // TODO remove after fixing definition
protected override void OnModelCreating(ModelBuilder builder)
=> builder
- .HasPostgresEnum("mapped_enum", ["happy", "sad"])
- .HasPostgresEnum()
- .HasPostgresEnum()
- .HasDefaultSchema("test")
- .HasPostgresEnum();
+ // .HasPostgresEnum();
+ // .HasPostgresEnum("mapped_enum", ["happy", "sad"])
+ // .HasPostgresEnum()
+ // .HasPostgresEnum()
+ .HasDefaultSchema("test");
public static void Seed(EnumContext context)
{
@@ -298,14 +300,24 @@ protected override ITestStoreFactory TestStoreFactory
public TestSqlLoggerFactory TestSqlLoggerFactory
=> (TestSqlLoggerFactory)ListLoggerFactory;
- static EnumFixture()
- {
+ static EnumFixture()
+ {
#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete
- NpgsqlConnection.GlobalTypeMapper.MapEnum("test.mapped_enum");
- NpgsqlConnection.GlobalTypeMapper.MapEnum("test.inferred_enum");
- NpgsqlConnection.GlobalTypeMapper.MapEnum("test.byte_enum");
- NpgsqlConnection.GlobalTypeMapper.MapEnum("test.schema_qualified_enum");
+ NpgsqlConnection.GlobalTypeMapper.EnableUnmappedTypes();
#pragma warning restore CS0618
+ }
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ {
+ var optionsBuilder = base.AddOptions(builder);
+
+ new NpgsqlDbContextOptionsBuilder(optionsBuilder)
+ .MapEnum("mapped_enum", "test")
+ .MapEnum("inferred_enum", "test")
+ .MapEnum("byte_enum", "test")
+ .MapEnum("schema_qualified_enum", "test");
+
+ return optionsBuilder;
}
private EnumData _expectedData;
diff --git a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs
index 7f64213381..6e2d35a436 100644
--- a/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs
+++ b/test/EFCore.PG.Tests/NpgsqlRelationalConnectionTest.cs
@@ -193,8 +193,7 @@ public static NpgsqlRelationalConnection CreateConnection(DbContextOptions optio
extension.Validate(options);
}
- var singletonOptions = new NpgsqlSingletonOptions();
- singletonOptions.Initialize(options);
+ var dbContextOptions = CreateOptions();
return new NpgsqlRelationalConnection(
new RelationalConnectionDependencies(
@@ -211,7 +210,7 @@ public static NpgsqlRelationalConnection CreateConnection(DbContextOptions optio
new DiagnosticListener("FakeDiagnosticListener"),
new NpgsqlLoggingDefinitions(),
new NullDbContextLogger(),
- CreateOptions()),
+ dbContextOptions),
new NamedConnectionStringResolver(options),
new RelationalTransactionFactory(
new RelationalTransactionFactoryDependencies(
@@ -226,7 +225,7 @@ public static NpgsqlRelationalConnection CreateConnection(DbContextOptions optio
new NpgsqlSqlGenerationHelper(new RelationalSqlGenerationHelperDependencies()),
new NpgsqlSingletonOptions()),
new ExceptionDetector()))),
- singletonOptions);
+ dbContextOptions);
}
private const string ConnectionString = "Fake Connection String";
diff --git a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs
index 77f45d8bfa..bb9c819a69 100644
--- a/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs
+++ b/test/EFCore.PG.Tests/Storage/NpgsqlTypeMappingTest.cs
@@ -489,7 +489,7 @@ public void ValueComparer_hstore_as_ImmutableDictionary()
[Fact]
public void GenerateSqlLiteral_returns_enum_literal()
{
- var mapping = new NpgsqlEnumTypeMapping("dummy_enum", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator());
+ var mapping = new NpgsqlEnumTypeMapping("dummy_enum", "dummy_enum", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator());
Assert.Equal("'sad'::dummy_enum", mapping.GenerateSqlLiteral(DummyEnum.Sad));
}
@@ -497,7 +497,7 @@ public void GenerateSqlLiteral_returns_enum_literal()
[Fact]
public void GenerateSqlLiteral_returns_enum_uppercase_literal()
{
- var mapping = new NpgsqlEnumTypeMapping(@"""DummyEnum""", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator());
+ var mapping = new NpgsqlEnumTypeMapping(@"""DummyEnum""", "DummyEnum", typeof(DummyEnum), new NpgsqlSnakeCaseNameTranslator());
Assert.Equal(@"'sad'::""DummyEnum""", mapping.GenerateSqlLiteral(DummyEnum.Sad));
}