Store and retrieve enums as strings with Entity Framework Core

C# enumeration types (enums) provide a highly extensible and type-safe way of storing values and they are used frequently in .NET software development.

When enums are saved to a SQL database with Entity Framework Core, by default they are stored using their underlying integer values rather than as strings. There are several reasons for this default behaviour, including storage and performance considerations.

However, sometimes you may want to consider storing enums as strings. Since this isn’t the default behaviour, in this article I will walk through how to convert enums to strings, both on a per-property basis and on a ‘global’ basis for your database context.

Considerations

Before deciding to store enums as strings, it’s a good idea to start by considering why you may want to take this approach and determine if it makes sense for your particular scenario.

Note that I am assuming that your database engine is either Azure SQL or SQL Server. However, the code solutions that are provided later in the article should also work for SQLite databases.

Pros

One of the primary reasons for storing enums as strings is readability. When you are looking at records within a database, it is not possible to understand what the enum values mean when they are stored as integer values without referring to external documentation or the codebase that contains the enum definition. When enums are stored as strings it becomes much easier to understand the context and become more productive as a result.

Another reason for choosing to store enums as strings is in relation to data integrity. If a developer happens to reorder enums that haven’t been assigned a specific integer value or one or more integer values for an enum in the codebase are accidentally changed, this could cause major problems and no warning would be raised. On the other hand, when storing enums as strings, if an enum value is renamed accidentally this will be flagged with a suitable error when attempting to retrieve existing data at runtime.

Cons

While there are advantages to storing enums as strings, it’s also important to be aware of the trade-offs.

The first trade-off to consider is storage. Storing enums as strings will undoubtedly result in more storage space being used compared to integers. For large datasets, the extra storage space can be significant, however, this isn’t a big concern for all applications.

The other main trade-off is performance. Comparing and querying string values will typically be slower than with integers and strings are not as efficient to index as integers. However, it is essential to measure performance before drawing any conclusions.

Aside from storage and performance, there are some other things to consider such as error handling and localization, but these considerations will not apply to all applications.

Moving forward

Now that we’ve considered some of the pros and cons, let’s move on to the code solution and see how the conversion of enums to strings works in a database context.

Conversion

When it comes to converting enums to strings (and back again) with Entity Framework Core, there are a couple of solutions we can employ depending on the requirements.

Per-Property

If you only need to target a few specific enums in your data model, the most appropriate solution would be to specify the conversion on a per-property basis. You can achieve this using the approach demonstrated in the following code.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 
public class TodoContext : DbContext
{
    public DbSet<Todo> Todos { getset; }
    public DbSet<User> Users { getset; }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
modelBuilder.
.Entity<Todo>()
.Property(t => t.Status)
.HasConversion(new EnumToStringConverter<TodoStatus>());     } }

In the above example, there is a sample TodoContext database context class that has a set of Todo and User entities, represented by the Todos and Users properties.

In this case, the Todo class has a Status enum property that is of type TodoStatus, defined as follows.

public enum TodoStatus
{
    New = 0,
    InProgress = 1,
    Completed = 2
}

Within the overridden OnModelCreating method, the code is using the ModelBuilder instance to target the Todo entity and configure its Status property.

Entity Framework Core ships with a predefined EnumToStringConverter class that can handle the enum-to-string conversion for us. The code registers this value converter using the HasConversion method.

Note that it’s a good practice to group your model configurations by using separate entity configuration classes.

Per-Context

If you want to ensure that all enums in your data model are stored as enums, it is possible to apply this behaviour at the database context level using Reflection. Below is the code you will need to achieve this.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
 
public class TodoContext : DbContext
{
    public DbSet<Todo> Todos { getset; }
    public DbSet<User> Users { getset; }
 
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            foreach (var property in entityType.GetProperties())
            {
                if (property.ClrType.IsEnum)
                {
                    var type = typeof(EnumToStringConverter<>).MakeGenericType(property.ClrType);
                    var converter = Activator.CreateInstance(typenew ConverterMappingHints()) as ValueConverter;
 
                    property.SetValueConverter(converter);
                }
                else if (Nullable.GetUnderlyingType(property.ClrType)?.IsEnum == true)
                {
                    var type = typeof(EnumToStringConverter<>).MakeGenericType(Nullable.GetUnderlyingType(property.ClrType)!);
                    var converter = Activator.CreateInstance(typenew ConverterMappingHints()) as ValueConverter;
 
                    property.SetValueConverter(converter);
                }
            }
        }
    }
}

The above example is doing essentially the same thing as the prior ‘per-property’ example, however, this time the process of assigning the value converter to a property is repeated for all enums within the database context.

The code within the OnModelCreating method gets all the entity types defined in the model and iterates through them. For each entity type, all the properties of that type are retrieved and the code iterates through each of the properties in turn.

The code then checks if the property is an enum using the IsEnum property. If so, an instance of the EnumAsStringConverter class is created using Reflection and is then passed to the SetValueConverter method to apply the converter to the property.

To account for nullable enums, the Nullable.GetUnderlyingType is used as part of a separate conditional check.

After the code has finished iterating through all types, it will have applied the EnumAsStringConverter value converter to all enum and nullable enum properties in your data model.

Migrations

If you are using Entity Framework Core Migrations, don’t forget to add a migration at this point!

Another thing to consider is indexing. By default, for Azure SQL and SQL Server, Entity Framework Core will specify nvarchar(max) as the data type. If you want to index columns that are mapped to enum properties, you’ll need to configure a suitable column size limit to allow indexing.

Backwards compatibility

One of the nice things about the EnumAsStringConverter is its backwards-compatibility story when you are working with an existing database that has been storing enums as integers up to this point.

Lazy data transition

The behaviour of the EnumAsStringConverter lends itself well to existing data, as it will continue to read old integer values without any issues. After migrating enum columns from int to nvarchar the old values will be retained and converted to strings e.g. a value of 1 will be converted to the string “1” as part of the migration. This is standard Azure SQL and SQL Server behaviour when you alter the column type.

When Entity Framework Core reads the value of “1” it will recognise that this is a number and attempt to convert it to the enum based on the underlying integer value of the string. This behaviour allows you to switch seamlessly from integers to strings.

With the EnumAsStringConverter applied, when existing database records are updated the values in the database will be replaced with the string representation of the enum values, transitioning the data over time.

Eager data migration

If you prefer, you can apply migrations to update existing values as part of the original migration process, using code that is similar to the following within the Entity Framework Core migration class that is generated for your migration.

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.Sql("UPDATE Todos SET Status = 'New' WHERE Status = 0");     migrationBuilder.Sql("UPDATE Todos SET Status = 'InProgress' WHERE Status = 1");
    migrationBuilder.Sql("UPDATE Todos SET Status = 'Completed' WHERE Status = 2"); }

Of course, you will need to update the SQL statement string according to your database schema.

Alternatively, you can run some SQL manually or integrate the update statements into your preferred migration tool.

In either case, it’s good to know that your application will continue to work after adding the conversion code, whether you choose to update data as you go along or up-front.

Summary

In this article, I have walked through how to store and retrieve enums as strings with Entity Framework Core.

I started by looking at some of the considerations you should make before confirming your decision to store enums as strings in an Azure SQL or SQL Server database.

I then demonstrated how to map individual enum properties as strings and followed this up by looking at how to configure the behaviour across the board at the database context level.

Lastly, I provided a brief overview of the backwards compatibility story when dealing with a database that contains existing data, where enums have previously been stored as integers.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

Comments