使用 efcore.pg 对枚举进行故障排除

Troubleshooting enums with efcore.pg

在与 Npgsql.EntityFrameworkCore.PostgreSQL 提供程序一起使用时,是否有解决 enum 问题的最佳实践?只是我不知道该去哪里找了。 以下是我如何注册它们:

public class Client
{
    [Key]
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    public SexType SexType { get; set; }
}

public enum SexType
{
    Male,
    Female,
}
public class MedServiceDbContext : DbContext
{
    public const string DatabaseSchema = "public";

    public DbSet<Client> Clients { get; protected set; }

    static MedServiceDbContext()
    {
        NpgsqlConnection.GlobalTypeMapper.MapEnum<SexType>($"{DatabaseSchema}.{NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator.TranslateTypeName(nameof(SexType))}", NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator);

        //NpgsqlConnection.GlobalTypeMapper.MapEnum<SexType>();
    }

    private static void MapTypes(ModelBuilder modelBuilder)
    {
        modelBuilder
            .HasPostgresEnum<SexType>(DatabaseSchema, NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator.TranslateTypeName(nameof(SexType)), NpgsqlConnection.GlobalTypeMapper.DefaultNameTranslator);
        ;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        MapTypes(modelBuilder);
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
        optionsBuilder
            .UseNpgsql(@"Host=localhost;Database=TestEnum;Username=postgres;Password=123")
            .LogTo(Console.WriteLine, LogLevel.Trace)
            .EnableSensitiveDataLogging()
        ;

    public MedServiceDbContext()
    {
    }
}

这是迁移的样子:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterDatabase()
        .Annotation("Npgsql:Enum:public.sex_type", "male,female");

    migrationBuilder.CreateTable(
        name: "Clients",
        columns: table => new
        {
            Id = table.Column<int>(type: "integer", nullable: false)
                .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
            Name = table.Column<string>(type: "text", nullable: false),
            SexType = table.Column<SexType>(type: "public.sex_type", nullable: false)
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_Clients", x => x.Id);
        });
}

在干净的复制项目中使用时,它可以完美运行。它也适用于测试。但是相同的代码(复制粘贴)在给我的主项目中不起作用:

System.InvalidOperationException: An error occurred while reading a database value for property 'Client.SexType'. The expected type was 'MedService.API.Database.Entities.Enums.SexType' but the actual value was of type 'MedService.API.Database.Entities.Enums.SexType'.
 ---> System.InvalidCastException: Can't cast database type public.sex_type to Int32
   at Npgsql.Internal.TypeHandling.NpgsqlTypeHandler.ReadCustom[TAny](NpgsqlReadBuffer buf, Int32 len, Boolean async, FieldDescription fieldDescription)
   at Npgsql.NpgsqlDataReader.GetFieldValue[T](Int32 ordinal)
   at Npgsql.NpgsqlDataReader.GetInt32(Int32 ordinal)
   at lambda_method256(Closure , QueryContext , DbDataReader , ResultContext , SingleQueryResultCoordinator )
   --- End of inner exception stack trace ---
   at lambda_method256(Closure , QueryContext , DbDataReader , ResultContext , SingleQueryResultCoordinator )
   at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()

我检查了所有相关的代码,但唯一的区别是数据库上下文的创建方式。主项目使用 DI 的方式与测试项目相同,而 repro 直接创建上下文。所以从逻辑上讲,如果主项目和测试项目工作相同,它们的行为应该相同,但它们不是。

我检查了每个生成的 SQL 脚本、LINQ 表达式等。一切似乎都一样,但我一直收到此异常。

我还应该在哪里寻找问题? 我已确认 sex_type 已在主项目和复制项目中创建。它们是相同的(使用 DBeaver 检查)。不仅如此,数据库结构也完全相同。数据库本身是使用 dotnet ef database update.

创建的

P.S。 不,ReloadTypes() 在这里也无济于事。已经检查过了。

使用的提供商版本:6.0.3 使用的 PostgreSQL 版本:14.2

UPDATE_01

做了一些实验,这个有效:

var connection = (Npgsql.NpgsqlConnection)_medServiceDbContext.Database.GetDbConnection();
connection.Open();

using (var cmd = new Npgsql.NpgsqlCommand("SELECT c.\"Id\", c.\"Name\", c.\"SexType\" FROM public.\"Clients\" as c", connection))
using (var reader = cmd.ExecuteReader())
{
    while (reader.Read())
    {
        var id = reader.GetFieldValue<int>(0);
        var surname = reader.GetFieldValue<string>(1);
        var sex = reader.GetFieldValue<SexType>(2);
        Console.WriteLine("{0}, {1}, {2}", id, surname, sex);
    }
}

而这不是:

_medServiceDbContext.Clients.AsNoTracking().ToArray()

后者给出了上述异常 (Can't cast database type public.sex_type to Int32)。 我的猜测是出于某种原因 EF 提供程序使用 GetInt32() override of the NpgsqlDataReader implementation instead of GetFieldValue<T>().

UPDATE_02

实际上我终于 assemble repro. It seems the issue is related to a usage of multiple different contexts targeting the same database connection. Described my findings here

对于任何偶然发现此问题的人,当前的解决方法(直到 issue is addressed) is to manually MapEnum all enums from all database contexts somewhere else, not inside of each DbContext static constructor as currently suggested in docs. 例如,在应用程序的 Startup class 的静态构造函数中。或者类似的东西。