Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,29 @@ namespace StrEnum.Npgsql.EntityFrameworkCore.Internal;
/// <item>property metadata — when EF passes a <c>storeType</c> (set by
/// <c>HasPostgresStringEnum&lt;TEnum&gt;()</c> on the property or
/// <c>MapStringEnumAsPostgresEnum&lt;TEnum&gt;()</c> on the model);</item>
/// <item><see cref="StringEnumPgTypeRegistry"/> — on the raw-SQL parameter path
/// (<c>DatabaseFacade.ExecuteSqlRawAsync(sql, object[])</c>), where EF asks for a mapping by
/// CLR type alone and there is no property to carry the store type.</item>
/// <item>the enums registered up-front through <see cref="StringEnumPostgresEnumRegistrar"/> — on
/// the raw-SQL parameter path (<c>DatabaseFacade.ExecuteSqlRawAsync(sql, object[])</c>), where EF
/// asks for a mapping by CLR type alone and there is no property to carry the store type.</item>
/// </list>
/// Plugins are consulted *before* EFCore.PG's built-in <c>NpgsqlTypeMappingSource</c>, so this is
/// the hook that prevents EF from falling back to <c>NpgsqlStringTypeMapping</c> (which would force
/// <c>NpgsqlDbType.Text</c> on the parameter and break the wire-level enum binding).
/// </summary>
/// <remarks>
/// Registrations are held per service provider, not process-wide. EF Core memoises the first
/// <c>FindMapping(typeof(TEnum), storeType: null)</c> 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.
/// </remarks>
internal sealed class NpgsqlStringEnumTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin
{
private readonly IReadOnlyDictionary<string, string> _registrations;

public NpgsqlStringEnumTypeMappingSourcePlugin(IReadOnlyDictionary<string, string> registrations)
{
_registrations = registrations;
}

public RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
{
var clrType = mappingInfo.ClrType;
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ public StrEnumNpgsqlOptionsExtension(IReadOnlyDictionary<string, string> registr

public void ApplyServices(IServiceCollection services)
{
services.AddSingleton<IRelationalTypeMappingSourcePlugin, NpgsqlStringEnumTypeMappingSourcePlugin>();
services.AddSingleton<IRelationalTypeMappingSourcePlugin>(
new NpgsqlStringEnumTypeMappingSourcePlugin(Registrations));
}

public void Validate(IDbContextOptions options) { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace StrEnum.Npgsql.EntityFrameworkCore.Internal;

/// <summary>
/// Keys a <see cref="StringEnum{TEnum}"/> CLR type in the per-service-provider registration map.
/// </summary>
internal static class StringEnumPgTypeKey
{
public static string For(Type clrType) => clrType.AssemblyQualifiedName ?? clrType.FullName!;
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,6 @@ public static ModelBuilder HasPostgresStringEnum<TEnum>(this ModelBuilder modelB

var labels = StringEnumLabels.For<TEnum>();
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ public StringEnumPostgresEnumRegistrar MapStringEnum<TEnum>(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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace StrEnum.Npgsql.EntityFrameworkCore.IntegrationTests;
/// Verifies that <see cref="StringEnum{TEnum}"/> parameters bind to native Postgres enum columns on
/// the raw-SQL path — <c>DatabaseFacade.ExecuteSqlRawAsync(sql, object[])</c> — where EF Core has
/// no property metadata to carry the column type. Without the
/// <see cref="Internal.StringEnumPgTypeRegistry"/> fallback, EF would fall through to
/// up-front registration, EF would fall through to
/// <c>NpgsqlStringTypeMapping</c>, pin the parameter to <c>NpgsqlDbType.Text</c>, and the server
/// would reject the UPDATE with <c>42804: column "x" is of type sport but expression is of type
/// text</c>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ public class UnregisteredEnum : StringEnum<UnregisteredEnum>
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);
Expand All @@ -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);
Expand All @@ -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();
Expand All @@ -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();
Expand Down
Loading