Background

I wrote about using Enumeration classes in a previous post.
Now I'll show you how I use them . I will show examples both for EF Core and Dapper. The goal is to be able to store only the Enumeration value in the database. When reading it back from the database, it should be mapped to the correct Enumeration type.

We have the following Enumeration type:

public record Hamburger : Enumeration<Hamburger>
{
    public static Hamburger Cheeseburger = new (1, "Cheeseburger");
    public static Hamburger BigMac = new(2, "Big Mac");
    public static Hamburger BigTasty = new(3, "Big Tasty");

    private Hamburger(int id, string displayName) : base(id, displayName)
    {
    }
}

And the following entity:

public class MyEntity
{
    public MyEntity(Guid id, Hamburger hamburger)
    {
        Id = id;
        Hamburger = hamburger;
    }

    public Guid Id { get; }
    public Hamburger Hamburger { get; }
}

EF Core

The goal is to make the following test pass:

[Fact]
public async Task CanSaveAndReadEntityWithEnumeration()
{
    var myEntity = new MyEntity(Guid.NewGuid(), Hamburger.BigMac);
    await using var arrangeDbContext = new MyDbContext(_fixture.PostgresDatabaseOptions);
    arrangeDbContext.MyEntities.Add(myEntity);
    await arrangeDbContext.SaveChangesAsync();
    await using var actDbContext = new MyDbContext(_fixture.PostgresDatabaseOptions);

    var result = await actDbContext.MyEntities.FirstAsync(x => x.Id == myEntity.Id);

    result.ShouldNotBeNull();
    result.Id.ShouldBe(myEntity.Id);
    result.Hamburger.ShouldBe(myEntity.Hamburger);
}

The MyDbContext looks like this:

public class MyDbContext : DbContext
{
    public MyDbContext(PostgresDatabaseOptions postgresDatabaseOptions)
        : base(postgresDatabaseOptions.DbContextOptions)
    {
    }

    public DbSet<MyEntity> MyEntities { get; set; } = null!;

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(MyDbContext).Assembly);
    }
}

MyEntity has it's own IEntityTypeConfiguration that looks like this:

public class MyEntityEntityTypeConfiguration : IEntityTypeConfiguration<MyEntity>
{
    public void Configure(EntityTypeBuilder<MyEntity> builder)
    {
        builder.HasKey(x => x.Id);
        builder.Property(x => x.Hamburger).ConfigureEnumeration().IsRequired();
    }
}

The ConfigureEnumeration method looks like this:

public static PropertyBuilder<TProperty> ConfigureEnumeration<TProperty>(
        this PropertyBuilder<TProperty> propertyBuilder) where TProperty : Enumeration<TProperty>
    {
        return propertyBuilder.HasConversion(
            enumeration => enumeration.Value, value => Enumeration<TProperty>.FromValue(value));
    }

Here I'm using a custom ValueConversion that configures the mapping for our Enumeration property. The HasConversion takes two funcs. The first one says how the value should be stored, and the second one how the value should be mapped when reading it from the database.

Dapper

Here we have the following test:

[Fact]
public async Task CanSaveAndReadEntityWithEnumeration()
{
    var myEntity = new MyEntity(Guid.NewGuid(), Hamburger.BigMac);
    await using var arrangeConnection = new NpgsqlConnection(_fixture.PostgresDatabaseOptions.ConnectionString);
    const string insertSql = """
        INSERT INTO my_entities
        VALUES (@id, @hamburger)
    """;
    await arrangeConnection.ExecuteAsync(insertSql, new {id = myEntity.Id, hamburger = myEntity.Hamburger});
    await using var actConnection = new NpgsqlConnection(_fixture.PostgresDatabaseOptions.ConnectionString);

    var results = (await actConnection.QueryAsync<MyEntity>(
        "SELECT id, hamburger from my_entities WHERE id = @id", new {id = myEntity.Id})).ToList();

    results.ShouldNotBeNull();
    results.Count.ShouldBe(1);
    var result = results.First();
    result.Id.ShouldBe(myEntity.Id);
    result.Hamburger.ShouldBe(myEntity.Hamburger);
}

The key here is to use a custom TypeHandler.

public class EnumerationTypeHandler<T> : SqlMapper.TypeHandler<T> where T : Enumeration<T>
{
    public override void SetValue(IDbDataParameter parameter, T value)
    {
        parameter.Value = value.Value;
    }

    public override T Parse(object value)
    {
        if(!int.TryParse(value.ToString(), out var intValue))
        {
            throw new ArgumentException($"Could not convert {value} to int", nameof(value));
        }

        return Enumeration<T>.FromValue(intValue);
    }
}

It has the same approach as the EF Core solution. The SetValue method decides how the value should be stored in the database. The Parse method parses the stored integer back to the correct Enumeration.

Only thing left to do is to update our test so that we actually use our TypeHandler.

[Fact]
public async Task CanSaveAndReadEntityWithEnumeration()
{
    var myEntity = new MyEntity(Guid.NewGuid(), Hamburger.BigMac);
    await using var arrangeConnection = new NpgsqlConnection(_fixture.PostgresDatabaseOptions.ConnectionString);
    SqlMapper.AddTypeHandler(new EnumerationTypeHandler<Hamburger>());
    const string insertSql = """
        INSERT INTO my_entities
        VALUES (@id, @hamburger)
    """;
    await arrangeConnection.ExecuteAsync(insertSql, new {id = myEntity.Id, hamburger = myEntity.Hamburger});
    await using var actConnection = new NpgsqlConnection(_fixture.PostgresDatabaseOptions.ConnectionString);

    var results = (await actConnection.QueryAsync<MyEntity>(
        "SELECT id, hamburger from my_entities WHERE id = @id", new {id = myEntity.Id})).ToList();

    results.ShouldNotBeNull();
    results.Count.ShouldBe(1);
    var result = results.First();
    result.Id.ShouldBe(myEntity.Id);
    result.Hamburger.ShouldBe(myEntity.Hamburger);
}