Skip to content

Resolve string enum parameters on the raw-SQL path#3

Merged
dmytro-khmara merged 1 commit intomasterfrom
feature/raw-sql-mapping
Apr 29, 2026
Merged

Resolve string enum parameters on the raw-SQL path#3
dmytro-khmara merged 1 commit intomasterfrom
feature/raw-sql-mapping

Conversation

@dmytro-khmara
Copy link
Copy Markdown
Member

Summary

  • The relational type-mapping plugin only resolved a mapping when EF gave it both a CLR type and a store type. EF's raw-SQL parameter pipeline (DatabaseFacade.ExecuteSqlRaw{,Async}) supplies only the CLR type, so the plugin returned null, EF fell back to NpgsqlStringTypeMapping, and the parameter was pinned to NpgsqlDbType.Text — producing 42804: column "x" is of type my_enum but expression is of type text against any column of a native enum type.
  • Adds an internal StringEnumPgTypeRegistry that the plugin consults when storeType is null, plus a configure overload on UseStringEnumsAsPostgresEnums that pre-registers each string enum.
  • Pre-registration on the options builder is required because EF Core's relational type-mapping cache locks in the result of the very first FindMapping(typeof(TEnum), null) call, which fires during convention setup before OnModelCreating runs — model-level MapStringEnumAsPostgresEnum<TEnum>() is too late.

Usage

optionsBuilder
    .UseNpgsql(dataSource)
    .UseStringEnums()
    .UseStringEnumsAsPostgresEnums(r =>
        r.MapStringEnum<Sport>()
         .MapStringEnum<Status>(name: "status_kind", schema: "myapp"));

The configure delegate is optional — existing callers of UseStringEnumsAsPostgresEnums() keep working unchanged for the EF-translated path. It only needs to be supplied when raw-SQL parameter binding is required.

Test plan

  • New integration test RawSqlEnumParameterTests.Binds_a_string_enum_parameter_in_ExecuteSqlRawAsync round-trips a Sport value through ExecuteSqlRawAsync against a real Postgres enum column. Fails on master with 42804, passes with this change.
  • New unit test FindMapping_FallsBackToRegistry_WhenStoreTypeIsMissing pins the registry-fallback branch of the plugin.
  • Existing integration round-trip and unit tests still green (25 unit + 3 integration).

The relational type-mapping plugin only resolved a mapping when EF gave it
both a CLR type and a store type. EF's raw-SQL parameter pipeline
(DatabaseFacade.ExecuteSqlRaw{,Async}) supplies only the CLR type, so the
plugin returned null, EF fell back to NpgsqlStringTypeMapping, the parameter
was pinned to NpgsqlDbType.Text, and the server rejected the statement with
42804 against any column of a native enum type.

Adds a StringEnumPgTypeRegistry that the plugin consults when storeType is
null, and a configure overload on UseStringEnumsAsPostgresEnums that lets
callers pre-register their string enums:

    optionsBuilder
        .UseNpgsql(dataSource)
        .UseStringEnums()
        .UseStringEnumsAsPostgresEnums(r => r.MapStringEnum<Sport>());

Pre-registration is required because EF Core's relational type-mapping cache
locks in the result of the very first FindMapping(typeof(TEnum), null) call,
which fires during convention setup before OnModelCreating runs --
MapStringEnumAsPostgresEnum<TEnum>() in the model builder is too late.

Covered by a new integration test that round-trips a Sport value through
ExecuteSqlRawAsync against a real Postgres enum column, and by a unit test
that pins the registry-fallback branch of the plugin.
@dmytro-khmara dmytro-khmara merged commit a94c0b3 into master Apr 29, 2026
2 checks passed
@dmytro-khmara dmytro-khmara deleted the feature/raw-sql-mapping branch April 29, 2026 23:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant