PAYATHON 2026

Npgsql EF Core 将枚举作为整数发送,如何映射 PostgreSQL enum?

数据小陈

结论:需要在 UseNpgsql 中补上 MapEnum

从现有配置看,缺少 EF provider 层的 MapEnum<EventType>()。枚举已经在 NpgsqlDataSourceBuilder 上注册,也通过 HasPostgresEnum 声明了数据库类型,但 UseNpgsql 的配置回调中只有 MigrationsAssembly。

Npgsql EF provider 从 9.0 起要求显式配置 EF 层的枚举映射,题目使用的 10.x 也应采用这种方式。使用外部 NpgsqlDataSource 时,还要保留数据源上的 MapEnum。这一变更见官方 9.0 版本说明。

修改运行时配置

保留现有数据源注册,在 UseNpgsql 回调中添加枚举映射:

var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);

dataSourceBuilder.MapEnum<EventType>(
    pgName: "event_type",
    nameTranslator: new NpgsqlSnakeCaseNameTranslator());

var dataSource = dataSourceBuilder.Build();
services.AddSingleton(dataSource);

services.AddDbContext<AppDbContext>((sp, options) =>
    options.UseNpgsql(
        sp.GetRequiredService<NpgsqlDataSource>(),
        b =>
        {
            b.MigrationsAssembly("*******.Infrastructure");
            b.MapEnum<EventType>(
                enumName: "event_type",
                nameTranslator: new NpgsqlSnakeCaseNameTranslator());
        })
        .UseSnakeCaseNamingConvention()
        .EnableSensitiveDataLogging()
        .EnableDetailedErrors()
        .AddInterceptors(new AuditInterceptor()));

示例中的 *******.Infrastructure 需要替换为实际的 migration 程序集名称。

两个 API 的命名参数不同:数据源上的参数叫 pgName,EF 配置中的参数叫 enumName。EF 层的完整签名可查阅 NpgsqlDbContextOptionsBuilder API。

两处使用相同的类型名和 NpgsqlSnakeCaseNameTranslator,以保持 Purchase → purchase、Signup → signup、OfferTrading → offer_trading 的对应关系。底层枚举标签的转换规则见 NpgsqlDataSourceBuilder API。

为什么现有配置仍然发送 integer 或 text

这几项配置各有不同的职责:

  • HasPostgresEnum<EventType>() 声明模型中的 PostgreSQL enum,供 migration 创建数据库类型。
  • NpgsqlDataSourceBuilder.MapEnum<EventType>() 配置底层驱动对 CLR enum 的读写。
  • UseNpgsql(..., b => b.MapEnum<EventType>(...)) 配置 EF provider 使用原生枚举映射。

数据库中已经存在 event_type,不代表 EF 已经按该类型绑定参数。官方枚举映射文档给出了使用外部数据源时同时配置这两层映射的方式。

.HasColumnType("event_type") 也不能代替 MapEnum。如果为解决该故障添加了字符串转换器,应将其移除:它会先把 CLR enum 转成字符串,无法补齐缺少的原生枚举映射。属性配置可以保持为:

builder.Property(c => c.EventType)
    .HasColumnType("event_type")
    .IsRequired();

使用 EF 层的 MapEnum 后,它也会注册供 migration 使用的枚举定义,因此可以移除原先对应的 HasPostgresEnum<EventType>(),避免重复维护。其他实体配置保留即可。

同步设计时配置并检查 migration

IDesignTimeDbContextFactory<> 中也必须在 UseNpgsql 回调里补上 MapEnum<EventType>()。如果工厂自行构建外部数据源,该数据源仍需配置底层 MapEnum。仅复制原来的配置,缺失项也会被复制过去。

建议让运行时和设计时复用同一段 provider 配置,再按以下顺序确认结果:

  1. 重新构建并重启应用,使用新创建的数据源和 DbContext。
  2. 检查 EventType 属性仍为 CLR enum,且没有残留的属性转换器、全局转换约定或后续覆盖配置。
  3. 在正确的设计时配置下生成 migration,检查模型快照和生成的 SQL。
  4. 在开发或测试数据库中验证一次插入和读取,确认不再出现 42804,且读回的枚举值正确。

原 migration 中的 table.Column<int>(type: "event_type", ...) 值得检查,但其中显式指定的数据库类型仍是 event_type,不能只凭泛型参数断定实际列是 integer。数据库错误已经指出,列是 enum,而当前写入表达式是 integer。

不要直接删除或改写已应用的 migration。如果数据库类型和列定义已经正确,这次修复未必需要修改数据库结构;应根据实际生成的差异决定后续 migration。

参数日志应怎样判断

日志中的 @p2 = '0' 只是值的展示,单看这一项不足以判断 PostgreSQL 实际收到的参数类型。题目中的 42804 错误才是当前类型不匹配的直接证据。

也不必要求日志一定显示为 DbType = event_type。DbType 是通用类型枚举,PostgreSQL 自定义类型名不是其中的成员。应重点验证实际参数映射、插入是否成功,以及枚举值能否正确读回。

如果补齐两层映射后仍然失败,再检查运行时是否确实使用了这份 DbContext 配置,以及数据库类型是否位于自定义 schema。如果位于自定义 schema,两层映射都应指向同一个实际类型。

备注:内容仅供参考。