diff --git a/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/NpgsqlStringEnumTypeMappingSourcePlugin.cs b/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/NpgsqlStringEnumTypeMappingSourcePlugin.cs
index da37450..1ba0445 100644
--- a/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/NpgsqlStringEnumTypeMappingSourcePlugin.cs
+++ b/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/NpgsqlStringEnumTypeMappingSourcePlugin.cs
@@ -10,16 +10,29 @@ namespace StrEnum.Npgsql.EntityFrameworkCore.Internal;
/// - property metadata — when EF passes a storeType (set by
/// HasPostgresStringEnum<TEnum>() on the property or
/// MapStringEnumAsPostgresEnum<TEnum>() on the model);
-/// - — on the raw-SQL parameter path
-/// (DatabaseFacade.ExecuteSqlRawAsync(sql, object[])), where EF asks for a mapping by
-/// CLR type alone and there is no property to carry the store type.
+/// - the enums registered up-front through — on
+/// the raw-SQL parameter path (DatabaseFacade.ExecuteSqlRawAsync(sql, object[])), where EF
+/// asks for a mapping by CLR type alone and there is no property to carry the store type.
///
/// Plugins are consulted *before* EFCore.PG's built-in NpgsqlTypeMappingSource, so this is
/// the hook that prevents EF from falling back to NpgsqlStringTypeMapping (which would force
/// NpgsqlDbType.Text on the parameter and break the wire-level enum binding).
///
+///
+/// Registrations are held per service provider, not process-wide. EF Core memoises the first
+/// FindMapping(typeof(TEnum), storeType: null) result in a cache owned by the service provider,
+/// so shared state lets one context's miss — or another context's differently named enum — be served
+/// to a context that registered its own.
+///
internal sealed class NpgsqlStringEnumTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin
{
+ private readonly IReadOnlyDictionary _registrations;
+
+ public NpgsqlStringEnumTypeMappingSourcePlugin(IReadOnlyDictionary registrations)
+ {
+ _registrations = registrations;
+ }
+
public RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
{
var clrType = mappingInfo.ClrType;
@@ -31,7 +44,7 @@ internal sealed class NpgsqlStringEnumTypeMappingSourcePlugin : IRelationalTypeM
return null;
var storeType = mappingInfo.StoreTypeName;
- if (storeType is null && !StringEnumPgTypeRegistry.TryGetPgTypeName(clrType, out storeType))
+ if (storeType is null && !_registrations.TryGetValue(StringEnumPgTypeKey.For(clrType), out storeType))
return null;
var mappingType = typeof(NpgsqlStringEnumTypeMapping<>).MakeGenericType(clrType);
diff --git a/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StrEnumNpgsqlOptionsExtension.cs b/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StrEnumNpgsqlOptionsExtension.cs
index 8505b64..1926af4 100644
--- a/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StrEnumNpgsqlOptionsExtension.cs
+++ b/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StrEnumNpgsqlOptionsExtension.cs
@@ -35,7 +35,8 @@ public StrEnumNpgsqlOptionsExtension(IReadOnlyDictionary registr
public void ApplyServices(IServiceCollection services)
{
- services.AddSingleton();
+ services.AddSingleton(
+ new NpgsqlStringEnumTypeMappingSourcePlugin(Registrations));
}
public void Validate(IDbContextOptions options) { }
diff --git a/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StringEnumPgTypeKey.cs b/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StringEnumPgTypeKey.cs
new file mode 100644
index 0000000..652088b
--- /dev/null
+++ b/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StringEnumPgTypeKey.cs
@@ -0,0 +1,9 @@
+namespace StrEnum.Npgsql.EntityFrameworkCore.Internal;
+
+///
+/// Keys a CLR type in the per-service-provider registration map.
+///
+internal static class StringEnumPgTypeKey
+{
+ public static string For(Type clrType) => clrType.AssemblyQualifiedName ?? clrType.FullName!;
+}
diff --git a/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StringEnumPgTypeRegistry.cs b/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StringEnumPgTypeRegistry.cs
deleted file mode 100644
index 4de0685..0000000
--- a/src/StrEnum.Npgsql.EntityFrameworkCore/Internal/StringEnumPgTypeRegistry.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-using System.Collections.Concurrent;
-
-namespace StrEnum.Npgsql.EntityFrameworkCore.Internal;
-
-///
-/// Process-wide registry mapping a CLR type to the Postgres enum
-/// type name that or the
-/// model-level registered for it.
-/// Consulted by when EF Core asks for a
-/// mapping by CLR type alone — i.e. on the raw-SQL parameter path
-/// (DatabaseFacade.ExecuteSqlRawAsync(sql, object[])), where there is no property metadata
-/// and therefore no storeType to drive the mapping.
-///
-///
-/// State is process-wide because EF Core type-mapping plugins are stateless from EF's perspective
-/// and have no access to the model. Within a process, a given type
-/// is expected to map to a single Postgres enum — mapping the same CLR type to different enum
-/// names across multiple DbContexts in one process is not supported.
-///
-internal static class StringEnumPgTypeRegistry
-{
- private static readonly ConcurrentDictionary _map = new();
-
- public static void Register(Type clrType, string pgTypeName)
- {
- if (clrType is null) throw new ArgumentNullException(nameof(clrType));
- if (pgTypeName is null) throw new ArgumentNullException(nameof(pgTypeName));
-
- _map[clrType] = pgTypeName;
- }
-
- public static bool TryGetPgTypeName(Type clrType, out string? pgTypeName)
- {
- if (_map.TryGetValue(clrType, out var found))
- {
- pgTypeName = found;
- return true;
- }
-
- pgTypeName = null;
- return false;
- }
-}
diff --git a/src/StrEnum.Npgsql.EntityFrameworkCore/ModelBuilderExtensions.cs b/src/StrEnum.Npgsql.EntityFrameworkCore/ModelBuilderExtensions.cs
index 33e5546..712b85e 100644
--- a/src/StrEnum.Npgsql.EntityFrameworkCore/ModelBuilderExtensions.cs
+++ b/src/StrEnum.Npgsql.EntityFrameworkCore/ModelBuilderExtensions.cs
@@ -22,12 +22,6 @@ public static ModelBuilder HasPostgresStringEnum(this ModelBuilder modelB
var labels = StringEnumLabels.For();
var enumName = name ?? PostgresNaming.ToSnakeCase(typeof(TEnum).Name);
- var pgTypeName = schema is null ? enumName : $"{schema}.{enumName}";
-
- // Register in the process-wide CLR-type → PG-type-name map so the EF Core type-mapping
- // plugin can resolve the store type on the raw-SQL parameter path, where there's no
- // property metadata to carry it.
- StringEnumPgTypeRegistry.Register(typeof(TEnum), pgTypeName);
return modelBuilder.HasPostgresEnum(schema, enumName, labels);
}
diff --git a/src/StrEnum.Npgsql.EntityFrameworkCore/StringEnumPostgresEnumRegistrar.cs b/src/StrEnum.Npgsql.EntityFrameworkCore/StringEnumPostgresEnumRegistrar.cs
index f73d262..b0cf17a 100644
--- a/src/StrEnum.Npgsql.EntityFrameworkCore/StringEnumPostgresEnumRegistrar.cs
+++ b/src/StrEnum.Npgsql.EntityFrameworkCore/StringEnumPostgresEnumRegistrar.cs
@@ -38,8 +38,9 @@ public StringEnumPostgresEnumRegistrar MapStringEnum(string? name = null,
{
var enumName = name ?? PostgresNaming.ToSnakeCase(typeof(TEnum).Name);
var pgTypeName = schema is null ? enumName : $"{schema}.{enumName}";
- StringEnumPgTypeRegistry.Register(typeof(TEnum), pgTypeName);
- _registrations[typeof(TEnum).AssemblyQualifiedName ?? typeof(TEnum).FullName!] = pgTypeName;
+
+ _registrations[StringEnumPgTypeKey.For(typeof(TEnum))] = pgTypeName;
+
return this;
}
}
diff --git a/test/StrEnum.Npgsql.EntityFrameworkCore.IntegrationTests/RawSqlEnumParameterTests.cs b/test/StrEnum.Npgsql.EntityFrameworkCore.IntegrationTests/RawSqlEnumParameterTests.cs
index f3e23bc..b3aec93 100644
--- a/test/StrEnum.Npgsql.EntityFrameworkCore.IntegrationTests/RawSqlEnumParameterTests.cs
+++ b/test/StrEnum.Npgsql.EntityFrameworkCore.IntegrationTests/RawSqlEnumParameterTests.cs
@@ -10,7 +10,7 @@ namespace StrEnum.Npgsql.EntityFrameworkCore.IntegrationTests;
/// Verifies that parameters bind to native Postgres enum columns on
/// the raw-SQL path — DatabaseFacade.ExecuteSqlRawAsync(sql, object[]) — where EF Core has
/// no property metadata to carry the column type. Without the
-/// fallback, EF would fall through to
+/// up-front registration, EF would fall through to
/// NpgsqlStringTypeMapping, pin the parameter to NpgsqlDbType.Text, and the server
/// would reject the UPDATE with 42804: column "x" is of type sport but expression is of type
/// text.
diff --git a/test/StrEnum.Npgsql.EntityFrameworkCore.UnitTests/NpgsqlStringEnumTypeMappingSourcePluginTests.cs b/test/StrEnum.Npgsql.EntityFrameworkCore.UnitTests/NpgsqlStringEnumTypeMappingSourcePluginTests.cs
index 3e38661..bce3382 100644
--- a/test/StrEnum.Npgsql.EntityFrameworkCore.UnitTests/NpgsqlStringEnumTypeMappingSourcePluginTests.cs
+++ b/test/StrEnum.Npgsql.EntityFrameworkCore.UnitTests/NpgsqlStringEnumTypeMappingSourcePluginTests.cs
@@ -18,10 +18,13 @@ public class UnregisteredEnum : StringEnum
public static readonly UnregisteredEnum One = Define("ONE");
}
+ private static NpgsqlStringEnumTypeMappingSourcePlugin CreatePlugin(params (Type ClrType, string PgTypeName)[] registrations)
+ => new(registrations.ToDictionary(r => StringEnumPgTypeKey.For(r.ClrType), r => r.PgTypeName));
+
[Fact]
public void FindMapping_ReturnsNpgsqlStringEnumTypeMapping_ForStringEnumWithStoreType()
{
- var plugin = new NpgsqlStringEnumTypeMappingSourcePlugin();
+ var plugin = CreatePlugin();
var info = new RelationalTypeMappingInfo(typeof(Sport), storeTypeName: "sport", unicode: null, size: null, precision: null, scale: null);
var mapping = plugin.FindMapping(info);
@@ -32,11 +35,9 @@ public void FindMapping_ReturnsNpgsqlStringEnumTypeMapping_ForStringEnumWithStor
}
[Fact]
- public void FindMapping_FallsBackToRegistry_WhenStoreTypeIsMissing()
+ public void FindMapping_FallsBackToTheRegisteredEnums_WhenStoreTypeIsMissing()
{
- StringEnumPgTypeRegistry.Register(typeof(Sport), "races.sport_kind");
-
- var plugin = new NpgsqlStringEnumTypeMappingSourcePlugin();
+ var plugin = CreatePlugin((typeof(Sport), "races.sport_kind"));
var info = new RelationalTypeMappingInfo(typeof(Sport));
var mapping = plugin.FindMapping(info);
@@ -49,7 +50,7 @@ public void FindMapping_FallsBackToRegistry_WhenStoreTypeIsMissing()
[Fact]
public void FindMapping_ReturnsNull_WhenStoreTypeIsMissingAndNoRegistration()
{
- var plugin = new NpgsqlStringEnumTypeMappingSourcePlugin();
+ var plugin = CreatePlugin();
var info = new RelationalTypeMappingInfo(typeof(UnregisteredEnum));
plugin.FindMapping(info).Should().BeNull();
@@ -58,7 +59,7 @@ public void FindMapping_ReturnsNull_WhenStoreTypeIsMissingAndNoRegistration()
[Fact]
public void FindMapping_ReturnsNull_ForNonStringEnumClrType()
{
- var plugin = new NpgsqlStringEnumTypeMappingSourcePlugin();
+ var plugin = CreatePlugin();
var info = new RelationalTypeMappingInfo(typeof(string), storeTypeName: "sport", unicode: null, size: null, precision: null, scale: null);
plugin.FindMapping(info).Should().BeNull();