diff --git a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs index 0eb58b004..259a278f1 100644 --- a/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs +++ b/src/EFCore.PG/Infrastructure/Internal/INpgsqlSingletonOptions.cs @@ -1,32 +1,39 @@ -namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; +using System.Data.Common; + +namespace Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; /// -/// Represents options for Npgsql that can only be set at the singleton level. +/// Represents options for Npgsql that can only be set at the singleton level. /// public interface INpgsqlSingletonOptions : ISingletonOptions { /// - /// The backend version to target. + /// The backend version to target. /// Version PostgresVersion { get; } /// - /// The backend version to target, but returns unless the user explicitly specified a version. + /// The backend version to target, but returns unless the user explicitly specified a version. /// Version? PostgresVersionWithoutDefault { get; } /// - /// Whether to target Redshift. + /// Whether to target Redshift. /// bool UseRedshift { get; } /// - /// True if reverse null ordering is enabled; otherwise, false. + /// Whether reverse null ordering is enabled. /// bool ReverseNullOrderingEnabled { get; } /// - /// The collection of range mappings. + /// 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; } } \ No newline at end of file diff --git a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs index 7216414e7..80077e628 100644 --- a/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs +++ b/src/EFCore.PG/Internal/NpgsqlSingletonOptions.cs @@ -1,4 +1,5 @@ -using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; +using System.Data.Common; +using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure; using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure.Internal; namespace Npgsql.EntityFrameworkCore.PostgreSQL.Internal; @@ -46,6 +47,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 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 @@ -72,6 +81,7 @@ public virtual void Initialize(IDbContextOptions options) PostgresVersion = npgsqlOptions.PostgresVersion ?? DefaultPostgresVersion; UseRedshift = npgsqlOptions.UseRedshift; ReverseNullOrderingEnabled = npgsqlOptions.ReverseNullOrdering; + DataSource = npgsqlOptions.DataSource; UserRangeDefinitions = npgsqlOptions.UserRangeDefinitions; } @@ -104,6 +114,12 @@ public virtual void Validate(IDbContextOptions options) nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); } + if (!ReferenceEquals(DataSource, npgsqlOptions.DataSource)) + { + throw new InvalidOperationException( + NpgsqlStrings.TwoDataSourcesInSameServiceProvider(nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } + if (UserRangeDefinitions.Count != npgsqlOptions.UserRangeDefinitions.Count || UserRangeDefinitions.Zip(npgsqlOptions.UserRangeDefinitions).Any(t => t.First != t.Second)) { diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs index 3c2b2f439..3bcd31e53 100644 --- a/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs +++ b/src/EFCore.PG/Properties/NpgsqlStrings.Designer.cs @@ -19,6 +19,14 @@ public static class NpgsqlStrings private static readonly ResourceManager _resourceManager = new ResourceManager("Npgsql.EntityFrameworkCore.PostgreSQL.Properties.NpgsqlStrings", typeof(NpgsqlStrings).Assembly); + /// + /// Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'. + /// + public static string TwoDataSourcesInSameServiceProvider(object? useInternalServiceProvider) + => string.Format( + GetString("TwoDataSourcesInSameServiceProvider", nameof(useInternalServiceProvider)), + useInternalServiceProvider); + /// /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}', but are configured with different compression methods. /// diff --git a/src/EFCore.PG/Properties/NpgsqlStrings.resx b/src/EFCore.PG/Properties/NpgsqlStrings.resx index 22cada8de..1759bc4a9 100644 --- a/src/EFCore.PG/Properties/NpgsqlStrings.resx +++ b/src/EFCore.PG/Properties/NpgsqlStrings.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Using two distinct data sources within a service provider is not supported, and Entity Framework is not building its own internal service provider. Either allow Entity Framework to build the service provider by removing the call to '{useInternalServiceProvider}', or ensure that the same data source is used for all uses of a given service provider passed to '{useInternalServiceProvider}'. + '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured with different compression methods. diff --git a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs index dce4ce40f..1d899cff1 100644 --- a/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs +++ b/src/EFCore.PG/Storage/Internal/NpgsqlTypeMappingSource.cs @@ -68,8 +68,6 @@ static NpgsqlTypeMappingSource() /// private readonly Dictionary> _multirangeTypeMappings; - private static MethodInfo? _adoUserTypeMappingsGetMethodInfo; - private readonly bool _supportsMultiranges; #region Mappings @@ -207,7 +205,8 @@ static NpgsqlTypeMappingSource() /// 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 NpgsqlTypeMappingSource(TypeMappingSourceDependencies dependencies, + public NpgsqlTypeMappingSource( + TypeMappingSourceDependencies dependencies, RelationalTypeMappingSourceDependencies relationalDependencies, ISqlGenerationHelper sqlGenerationHelper, INpgsqlSingletonOptions npgsqlSingletonOptions) @@ -443,7 +442,7 @@ public NpgsqlTypeMappingSource(TypeMappingSourceDependencies dependencies, StoreTypeMappings = new ConcurrentDictionary(storeTypeMappings, StringComparer.OrdinalIgnoreCase); ClrTypeMappings = new ConcurrentDictionary(clrTypeMappings); - LoadUserDefinedTypeMappings(sqlGenerationHelper); + LoadUserDefinedTypeMappings(sqlGenerationHelper, npgsqlSingletonOptions.DataSource as NpgsqlDataSource); _userRangeDefinitions = npgsqlSingletonOptions?.UserRangeDefinitions ?? Array.Empty(); } @@ -452,28 +451,38 @@ public NpgsqlTypeMappingSource(TypeMappingSourceDependencies dependencies, /// 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) - => SetupEnumMappings(sqlGenerationHelper); + 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) + protected virtual void SetupEnumMappings(ISqlGenerationHelper sqlGenerationHelper, NpgsqlDataSource? dataSource) { + var adoEnumMappings = new List(); + #pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - _adoUserTypeMappingsGetMethodInfo ??= NpgsqlConnection.GlobalTypeMapper.GetType().GetProperty("UserTypeMappings")?.GetMethod; + if (NpgsqlConnection.GlobalTypeMapper.GetType().GetProperty("UserTypeMappings")?.GetMethod is { } globalTypeMappingsMethodInfo + && globalTypeMappingsMethodInfo.Invoke(NpgsqlConnection.GlobalTypeMapper, Array.Empty()) is + IDictionary globalUserMappings) + { + adoEnumMappings.AddRange(globalUserMappings.Values.OfType()); + } #pragma warning restore CS0618 - if (_adoUserTypeMappingsGetMethodInfo is null) + // TODO: Think about what to do here. We could just require users to do the mapping at the EF level, and then EF would take care + // of the ADO mapping. + if (dataSource is not null + && typeof(NpgsqlDataSource).GetField("_userTypeMappings", BindingFlags.NonPublic | BindingFlags.Instance) is + { } dataSourceTypeMappingsFieldInfo + && dataSourceTypeMappingsFieldInfo.GetValue(dataSource) is IDictionary dataSourceUserMappings) { - return; + adoEnumMappings.AddRange(dataSourceUserMappings.Values.OfType()); } -#pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete - var adoUserTypeMappings = (IDictionary)_adoUserTypeMappingsGetMethodInfo.Invoke(NpgsqlConnection.GlobalTypeMapper, Array.Empty())!; -#pragma warning restore CS0618 - - foreach (var adoUserTypeMapping in adoUserTypeMappings.Values.OfType()) + foreach (var adoUserTypeMapping in adoEnumMappings) { // TODO: update with schema per https://github.com/npgsql/npgsql/issues/2121 var components = adoUserTypeMapping.PgTypeName.Split('.'); diff --git a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs index 4be7a2e23..8155d9cdf 100644 --- a/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs +++ b/test/EFCore.PG.FunctionalTests/BuiltInDataTypesNpgsqlTest.cs @@ -977,10 +977,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con { base.OnModelCreating(modelBuilder, context); + // TODO: Switch to using data source #pragma warning disable CS0618 // NpgsqlConnection.GlobalTypeMapper is obsolete NpgsqlConnection.GlobalTypeMapper.MapEnum(); #pragma warning restore CS0618 - ((NpgsqlTypeMappingSource)context.GetService()).LoadUserDefinedTypeMappings(context.GetService()); + ((NpgsqlTypeMappingSource)context.GetService()).LoadUserDefinedTypeMappings( + context.GetService(), dataSource: null); modelBuilder.HasPostgresEnum("mood", new[] { "happy", "sad" });