From dba630bee58fd71062c7bd1c037eb95ecf961c89 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Thu, 9 May 2024 18:02:02 +0200 Subject: [PATCH 1/3] Make DbDataSource scoped --- .../Internal/INpgsqlSingletonOptions.cs | 10 ------- .../Internal/NpgsqlOptionsExtension.cs | 1 - .../Internal/NpgsqlSingletonOptions.cs | 30 ------------------- .../Internal/NpgsqlRelationalConnection.cs | 4 +-- .../Internal/NpgsqlTypeMappingSource.cs | 2 +- .../NpgsqlRelationalConnectionTest.cs | 7 ++--- 6 files changed, 6 insertions(+), 48 deletions(-) diff --git a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs index 7618ae040f..73ec83307e 100644 --- a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs +++ b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs @@ -27,18 +27,8 @@ public interface INpgsqlSingletonOptions : ISingletonOptions /// bool ReverseNullOrderingEnabled { get; } - /// - /// The data source being used, or if a connection string or connection was provided directly. - /// - DbDataSource? DataSource { 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..86d117d98a 100644 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs +++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs @@ -453,7 +453,6 @@ 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 && Extension.UserRangeDefinitions.SequenceEqual(otherInfo.Extension.UserRangeDefinitions) && Extension.UseRedshift == otherInfo.Extension.UseRedshift; diff --git a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs index e0fa14260b..79c8329899 100644 --- a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs +++ b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs @@ -39,14 +39,6 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions /// public virtual bool ReverseNullOrderingEnabled { 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 DbDataSource? DataSource { 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 @@ -55,14 +47,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 @@ -78,20 +62,12 @@ 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; 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,12 +99,6 @@ public virtual void Validate(IDbContextOptions options) nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); } - if (npgsqlOptions.DataSource is not null && !ReferenceEquals(DataSource, npgsqlOptions.DataSource)) - { - throw new InvalidOperationException( - NpgsqlStrings.TwoDataSourcesInSameServiceProvider(nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); - } - if (!UserRangeDefinitions.SequenceEqual(npgsqlOptions.UserRangeDefinitions)) { throw new InvalidOperationException( 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..c721dfdada 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -348,7 +348,7 @@ public NpgsqlTypeMappingSource( StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings); - LoadUserDefinedTypeMappings(sqlGenerationHelper, options.DataSource as NpgsqlDataSource); + LoadUserDefinedTypeMappings(sqlGenerationHelper, dataSource: null); _userRangeDefinitions = options.UserRangeDefinitions; } 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"; From f0e8c61e88eff9175de95db85d27bf613b41c944 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Fri, 10 May 2024 15:25:37 +0200 Subject: [PATCH 2/3] Make enums configurable at the EF level --- .../Internal/INpgsqlSingletonOptions.cs | 6 ++ .../Internal/NpgsqlOptionsExtension.cs | 86 +++++++++---------- .../Internal/UserRangeDefinition.cs | 48 +++++++++++ .../NpgsqlDbContextOptionsBuilder.cs | 27 ++++++ .../Internal/NpgsqlSingletonOptions.cs | 11 +++ .../Mapping/NpgsqlArrayTypeMapping.cs | 17 +++- .../Internal/Mapping/NpgsqlEnumTypeMapping.cs | 35 +++++++- .../Mapping/NpgsqlRangeTypeMapping.cs | 8 +- .../Internal/NpgsqlTypeMappingSource.cs | 83 ++++++------------ .../BuiltInDataTypesNpgsqlTest.cs | 4 +- .../Query/EnumQueryTest.cs | 25 ++++-- .../Storage/NpgsqlTypeMappingTest.cs | 4 +- 12 files changed, 230 insertions(+), 124 deletions(-) create mode 100644 src/EFCore.PG/Infrastructure/Internal/UserRangeDefinition.cs diff --git a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs index 73ec83307e..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; @@ -27,6 +28,11 @@ public interface INpgsqlSingletonOptions : ISingletonOptions /// bool ReverseNullOrderingEnabled { get; } + /// + /// The collection of enum mappings. + /// + IReadOnlyList EnumTypeMappings { get; } + /// /// The collection of range mappings. /// diff --git a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs index 86d117d98a..065a4bdbe4 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. /// @@ -454,6 +491,8 @@ public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo => other is ExtensionInfo otherInfo && Extension.PostgresVersion == otherInfo.Extension.PostgresVersion && 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; @@ -495,50 +534,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 79c8329899..5a0603aeb8 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; @@ -39,6 +40,14 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions /// public virtual bool ReverseNullOrderingEnabled { 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 IReadOnlyList EnumTypeMappings { 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 @@ -55,6 +64,7 @@ public class NpgsqlSingletonOptions : INpgsqlSingletonOptions /// public NpgsqlSingletonOptions() { + EnumTypeMappings = []; UserRangeDefinitions = []; } @@ -67,6 +77,7 @@ public virtual void Initialize(IDbContextOptions options) IsPostgresVersionSet = npgsqlOptions.IsPostgresVersionSet; UseRedshift = npgsqlOptions.UseRedshift; ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering; + EnumTypeMappings = npgsqlOptions.EnumTypeMappings; UserRangeDefinitions = npgsqlOptions.UserRangeDefinitions; } 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..dfe80916d3 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,6 +12,11 @@ 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. /// @@ -38,7 +44,7 @@ public class NpgsqlEnumTypeMapping : RelationalTypeMapping /// 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 NpgsqlEnumTypeMapping(string storeType, Type enumType, INpgsqlNameTranslator? nameTranslator = null) + public NpgsqlEnumTypeMapping(string storeType, string unquotedStoreType, Type enumType, INpgsqlNameTranslator? nameTranslator = null) : base( storeType, enumType, @@ -50,6 +56,8 @@ public NpgsqlEnumTypeMapping(string storeType, Type enumType, INpgsqlNameTransla throw new ArgumentException($"Enum type mappings require a CLR enum. {enumType.FullName} is not an enum."); } + UnquotedStoreType = unquotedStoreType; + #pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete nameTranslator ??= NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator; #pragma warning restore CS0618 @@ -66,11 +74,12 @@ public NpgsqlEnumTypeMapping(string storeType, Type enumType, INpgsqlNameTransla /// 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 +90,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 +101,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/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index c721dfdada..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, dataSource: null); - + _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..5c835bfd1f 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; @@ -298,14 +299,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/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)); } From ae6c527a3daa9a32ca20b638d7e8bc45cfb10795 Mon Sep 17 00:00:00 2001 From: Nino Floris Date: Fri, 10 May 2024 15:57:40 +0200 Subject: [PATCH 3/3] Add convention to apply HasPostgresEnum based on type mappings --- .../NpgsqlModelBuilderExtensions.cs | 55 +++++++++++++++++++ .../NpgsqlModelExtensions.cs | 13 +++++ .../Internal/NpgsqlOptionsExtension.cs | 10 ++++ .../Internal/NpgsqlSingletonOptions.cs | 8 +++ .../Conventions/NpgsqlConventionSetBuilder.cs | 5 +- ...NpgsqlPostgresModelFinalizingConvention.cs | 26 ++++++++- src/EFCore.PG/Metadata/PostgresEnum.cs | 43 +++++++++++++++ .../Internal/Mapping/NpgsqlEnumTypeMapping.cs | 6 ++ .../Query/EnumQueryTest.cs | 11 ++-- 9 files changed, 170 insertions(+), 7 deletions(-) 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/NpgsqlOptionsExtension.cs b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs index 065a4bdbe4..4eb4574f82 100644 --- a/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs +++ b/src/EFCore.PG/Infrastructure/Internal/NpgsqlOptionsExtension.cs @@ -520,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[ diff --git a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs index 5a0603aeb8..3691b3707f 100644 --- a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs +++ b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs @@ -110,6 +110,14 @@ public virtual void Validate(IDbContextOptions options) nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); } + if (!EnumTypeMappings.SequenceEqual(npgsqlOptions.EnumTypeMappings)) + { + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(NpgsqlDbContextOptionsBuilder.MapEnum), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + if (!UserRangeDefinitions.SequenceEqual(npgsqlOptions.UserRangeDefinitions)) { throw new InvalidOperationException( 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/NpgsqlEnumTypeMapping.cs b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs index dfe80916d3..3c3bfa0d38 100644 --- a/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs +++ b/src/EFCore.PG/Storage/Internal/Mapping/NpgsqlEnumTypeMapping.cs @@ -22,6 +22,12 @@ public class NpgsqlEnumTypeMapping : RelationalTypeMapping /// private readonly Dictionary _members; + /// + /// + /// + public IEnumerable EnumValues + => _members.Values.Order(StringComparer.Ordinal); + /// /// 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/Query/EnumQueryTest.cs b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs index 5c835bfd1f..d4d6f77e51 100644 --- a/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs +++ b/test/EFCore.PG.FunctionalTests/Query/EnumQueryTest.cs @@ -223,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) {