Skip to content

Unable to save entities with enum with a special string converter #3472

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Kampfmoehre opened this issue Feb 24, 2025 · 2 comments
Open

Unable to save entities with enum with a special string converter #3472

Kampfmoehre opened this issue Feb 24, 2025 · 2 comments

Comments

@Kampfmoehre
Copy link

Kampfmoehre commented Feb 24, 2025

I am trying to migrate a project to Npgsql (and EF Core) v9, however I cannot get our enums to work.

The enum looks like this:

public enum MyEnum
{
  [Description("VALUE_1")]
  Value1,

  [Description("VALUE_2")]
  Value2,
}

Previously we had this:

NpgsqlDataSourceBuilder dataSourceBuilder = new(connectionString);
dataSourceBuilder.MapEnum<MyEnum>();
NpgsqlDataSource dataSource = dataSourceBuilder.Build();

services.AddDbContextFactory<MyContext>(o =>
  {
    o.UseNpgsql(datasource);
    o.UseSnakeCaseNamingConvention();
  });

and in the Context we used this:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    ValueConverter<MyEnum, string> myEnumConverter = new(
      v => v.GetEnumDescription(),
      v => GetEnumFromDescriptionString<MyEnum>(v)); // takes the value form the Description attribute

    modelBuilder.Entity<MyEntity>(entity =>
    {
      // ...
      entity.Property(e => e.SomeThing).HasConversion(myEnumConverter);
    });
}

Following the docs I was trying to set up the following:

services.AddDbContextFactory<MyContext>(options =>
{
   options.UseNpgsql(connectionString, o => o.MapEnum<MyEnum>("my_enum"));
   options.UseSnakeCaseNamingConvention();
});

Loading entities works, but adding a new one (or changing the enum property of an existing one) fails with the following error:

---> Npgsql.PostgresException (0x80004005): 42804: column "someThing" is of type my_entity_my_enum but expression is of type text

  Exception data:
    Severity: ERROR
    SqlState: 42804
    MessageText: column "someThing" is of type my_entity_my_enum but expression is of type text
    Hint: You will need to rewrite or cast the expression.
    Position: 176
    File: parse_target.c
    Line: 584
    Routine: transformAssignedExpr

I tried different things, using DataSource, withtout Datasource, also with adding

modelBuilder.HasPostgresEnum<MyEnum>(name: "my_entity_my_enum");

but to no avail. What am I missing? I tried some things I saw in other issues around Enum mapping but failed to resolve this somehow.

My Versions are:

<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
<PackageReference Include="NetTopologySuite" Version="2.5.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL.NetTopologySuite" Version="9.0.3" />
@roji
Copy link
Member

roji commented Feb 24, 2025

services.AddDbContextFactory<MyContext>(options =>
{
   options.UseNpgsql(connectionString, o => o.MapEnum<MyEnum>("my_enum");
   options.UseSnakeCaseNamingConvention();
});

The above should work, and has been tested successfully in various scenarios; can you please make sure that you're always doing that (for example, when running design-time tools, you may need have different DbContext configuration than in regular operations). Also, the code you posted above is missing a closing parentheses, so it may not be the actual code you're using.

If you can't find any issue, please try putting together a minimal, runnable repro; this will likely work, and the difference between that and your real project should help you understand where the problem is in your code. Otherwise, if you get to a minimal repro which shows the problem, you can post it here and I'll investigate.

@Kampfmoehre
Copy link
Author

Kampfmoehre commented Feb 25, 2025

Hi @roji, thanks for the quick reply. Sorry I copied parts of the code to simplify it, sorry for the missing brace.

I am using database first approach and scaffolded the code years ago, however due to an oversight of me the write to database part was never used, instead the API of a different application that talks with the database was used to create entities. The database belongs to this other application which is using NodeJS. The column is an actual Postges Enum. I tested scaffolding again with dotnet-ef 9.0.2 and the column is just missing after that like the enum docs state.

When debugging I can see the entity instance has the enum correctly assigned to the property and the logs show it tries to insert the correct value the converter is giving: @p1='VALUE_1'

The full relevant code is

public enum MyEnum
{
  [Description("VALUE_1")]
  Value1,

  [Description("VALUE_2")]
  Value2,
}

public partial class MyEntity
{
  // ...  
  [Column("someThing", TypeName = "enum")]
  public MyEnum SomeThing { get; set; }
}

public static class EnumDescriptionExtensions
{
  public static string? GetEnumDescription(this Enum value)
  {
    Type type = value.GetType();
    string name = Enum.GetName(type, value);
    if (name != null)
    {
      FieldInfo field = type.GetField(name);
      if (field != null && Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute descriptionAttribute)
      {
        return descriptionAttribute.Description;
      }
    }

    return null;
  }
}

public partial class MyContext : DbContext
{
  public MyContext(DbContextOptions<MyContext(> options)
    : base(options)
  {}

  protected override void OnModelCreating(ModelBuilder modelBuilder)
  {
    modelBuilder
      .HasPostgresEnum<MyEnum>(name: "my_entity_my_enum", new[] { "VALUE_1", "VALUE_2" })
      .HasPostgresExtension("uuid-ossp");

    ValueConverter<MyEnum, string> myEnumTypeConverter = new(
      v => v.GetEnumDescription(),
      v => GetEnumFromDescriptionString<MyEnum>(v));

    modelBuilder.Entity<MyEntity>(entity =>
    {
      entity.Property(e => e.SomeThing).HasConversion(myEnumTypeConverter);
    });
  }

  private TEnum GetEnumFromDescriptionString<TEnum>(string value)
    where TEnum : Enum
  {
    foreach (TEnum enumValue in Enum.GetValues(typeof(TEnum)))
    {
      var description = enumValue.GetEnumDescription();
      if (description == value)
      {
        return (TEnum)enumValue;
      }
    }

    throw new InvalidOperationException($"Unable to resolve enum value for string \"{value}\".");
  }
}

// DI Setup

  public static IServiceCollection ConfigureDatabases(
    this IServiceCollection services,
    IConfiguration configuration,
    IHostEnvironment environment)
  {
    bool isDev = environment.IsDevelopment();

    ConfigureDbContext<MyContext>(
      services,
      configuration,
      "MyContext",
      isDev,
      (o) => o.MapEnum<MyEnum>("my_entity_my_enum"));

    // ...

    return services;
  }

  private static void ConfigureDbContext<TContext>(
    IServiceCollection services,
    IConfiguration configuration,
    string name,
    bool enableSensitiveDataLogging = false,
    Action<NpgsqlDbContextOptionsBuilder>? npgsqlOptionsAction = null)
    where TContext : DbContext
  {
    string? connectionString = configuration.GetConnectionString(name);

    if (string.IsNullOrEmpty(connectionString))
    {
      throw new InvalidOperationException($"Unable to create a datasource: Connection string '{name}' not found.");
    }

    services.AddDbContextFactory<TContext>(options =>
    {
      options.UseNpgsql(connectionString, npgsqlOptionsAction);
      options.EnableSensitiveDataLogging(enableSensitiveDataLogging);
      options.UseSnakeCaseNamingConvention();
    });
  }

The enum was created with

CREATE TYPE "public"."my_entity_my_enum" AS ENUM('VALUE_1', 'VALUE_2');
ALTER TABLE "my_entity"
ADD "someThing" "public"."my_entity_my_enum" NOT NULL DEFAULT 'VALUE_1';

Could you guide me on how to set this up properly? Am I missing something or is this scenario not supported at all?

Edit: If I add the following line to the Context I can save the entity when I don't change the enum propertie's value without an error. But when switching to any nnon default value it fails.

      entity
        .Property(e => e.SomeThing)
        .HasColumnType("enum")
        .HasDefaultValue(MyEnum.Value1)
        .HasConversion(myEnumTypeConverter);

That's probably because Postgres is then filling in the default value, I guess?

@Kampfmoehre Kampfmoehre changed the title Unable to Map enum with v9 Unable to save entities with enum with a special string converter Feb 27, 2025
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

No branches or pull requests

2 participants