Skip to content

Enum collection serialization in .NET Core and Entity Framework Core

  • C#
  • .NET
  • EFCore
  • database

Most .NET Core and C# developers have used enum values in their applications. They're a great way to represent a choice from a set of mutually exclusive values or a combination of choices, while ensuring the compilation-time safety and code readability. And as you probably know, each enum value has an underlying integral numeric type that actually gets written into allocated memory. You can define a simple enum like this:

public enum UserRole : ushort
{
  None,
  Guest,
  Member,
  Contributor,
  Manager,
}

And assigning a value to a variable is as simple as:

var role = UserRole.Member;

C# knows that the role variable has the type of UserRole and won't let you assign anything else to it, even though it's represented as an unsigned integer behind the scenes. Let's try it out:

ushort i = 1;
role = i;

This leads to a compiler error:

Cannot convert source type 'int' to target type 'EntityCollectionSerializerExample.Enums.UserRole'

So far so good! But what happens if you'd like to be able to assign multiple enum values to a single variable? You can use a Flags attribute on the enum to indicate that it should be treated as a bit field. (aka a set of flags)

[Flags]
public enum UserRole : ushort
{
  None = 0b_0000,
  Guest = 0b_0001,
  Member = 0b_0010,
  Contributor = 0b0100,
  Manager = 0b1000,
}

The binary representation is completely optional, we could've used base-10 integers (0, 1, 2, 4, 8) but the binary system better represents the example.

Also worth noting that the Flags attribute is technically optional. This enum would work fine without it but its presence lets compiler know what our intentions are. It then understands that if the enum values is 6 (or 0b_0110), it represents a union of Member (0b_0010) and Contributor (0b_0100).

Enums and Entity Framework Core

What we've talked about so far works great if you keep the enums within your codebase. That is rarely the case in web development. Even in the example above, we want to persist the user's role in the database. So how do we do that?

There are two ways to approach this. The first, naïve one, would be to have an integer field in your database table. It's very simple to implement and there are cases in which it's a perfectly suitable thing to do. You need to be aware however of the drawbacks of that method. The main one being the fact that these values become useless without the context of your application. If you see a 3 in the database row, without looking at your application's code, would you be able to say what enumerable value (or values) it represents? Querying the data also becomes tricky. While all SQL dialects include bitwise operators, you're still operating on unrelated bit field flags. How unreadable this SQL query would be?

SELECT * from [Users] WHERE 1 & [UserRoles] > 0;

This simple query will retrieve guest users from the Users table. We can definitely do better.

Some languages, like TypeScript for instance, support string enums.

enum UserRole {
  None = 'None',
  Guest = 'Guest',
  Member = 'Member',
  Contributor = 'Contributor',
  Manager = 'Manager',
}

What's more, they serialize and deserialize automatically. Imagine you have a following JSON response from your back-end API:

{
  "userRole": "Guest"
}

If you fetch the data from your front-end TypeScript application and assign into a variable with the relevant enum type field, like this example interface:

interface User {
  userRole: UserRole;
}

Then this field will contain a value of the proper type, which allows us to do assignments and conditionals using the defined type system. For instance user.userRole === UserRole.Guest will return true. Beautiful!

Can we do the same in our C# Web API and Entity Framework Core? The answer is yes! while not trivial and not fully documented (for our use case) it's definitely possible and only requires a little bit of custom, readable code.

EF Core introduces a concept called value conversions. It allows us, for any property on our DbContext entities, to define a custom serialization and deserialization logic. Let's say, as a first step, we would like to store the UserRole values as a string in the database and we only allow a single value at a time. We can use the following logic in the OnModelCreating method:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder
    .Entity<User>()
    .Property(e => e.UserRole)
    .HasConversion(
      v => v.ToString(),
      v => (UserRole)Enum.Parse(typeof(UserRole), v));
}

That's it! User.UserRole stays as an enum type in our C# codebase while in the database it gets saved as corresponding string values. UserRole.Member for instance will be stored "Member" string.

What's more, this is such a common scenario, EF Core includes a conversion class called EnumToStringConverter that does this for us. So instead of defining two lambdas ourself, we can pass the conversion class instance to the HasConversion method instead.

var converter = new EnumToStringConverter<UserRole>();

modelBuilder
  .Entity<User>()
  .Property(e => e.UserRole)
  .HasConversion(converter);

Things do get more interesting however once we decide that a user can have one or more roles assigned at the same time. EF Core doesn't support enum collection conversions unfortunately. But since we've just learned that a converter is just a C# class that implements a specific interface, we don't we try to write one ourselves?

We'll assume that we want to keep the UserRole property in the Users table. As an alternative, we could create a separate table with a many-to-many relationship between User and Role entities and for a more complex scenario it may have been a good idea but in this case we'll stick to a simpler solution.

Let's start by writing a generic class that will serialize and deserialize any value into and from JSON strings. We can use a Newtonsoft.Json package which you're probably already familiar with. As a prerequisite, install two Nuget packages: Newtonsoft.Json and Microsoft.EntityFrameworkCore if you don't have it in your project already.

All custom converters need to implement Microsoft.EntityFrameworkCore.Storage.ValueConversion.ValueConverter<TModel, TProvider> interface. TModel defines the type of the property that we will want to store in the database and TProvider is a type we'll serialize into. In our case it's going to be a string and we'll leave TModel as a generic. Let's look at the implementation:

using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Newtonsoft.Json;

public class JsonValueConverter<T> : ValueConverter<T, string>
{
  public JsonValueConverter() : base(v => JsonConvert.SerializeObject(v),
    v => JsonConvert.DeserializeObject<T>(v))
  {
  }
}

ValueConverter's base constructor takes exactly the same arguments as the HasConversion method that we used earlier. First param is a function that serializes given argument and the second param is an inverse action. Using the base keyword in the constructor makes the implementation more concise.

Let's join both implementations now: the JSON value conversion and the Enum serialization in order to implement our final solution. We want our User entity to look like this:

public class User
{
  public Guid Id { get; set;}
  // ... any other relevant fields
  public ICollection<UserRole> UserRoles { get; set;}
}

Here's the logic we need to write:

  • In order to serialize, we take each of the enumerable values from the collection, serialize it into a string and then take the resulting list and serialize into a JSON.
  • To deserialize we need to do the opposite. We'll deserialize the JSON string into a collection of strings, then iterate over each string and parse it into an Enum.
namespace EntityCollectionSerializerExample.Converters
{
  using System;
  using System.Collections.Generic;
  using System.Linq;
  using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
  using Newtonsoft.Json;

  public class EnumCollectionJsonValueConverter<T> : ValueConverter<ICollection<T>, string> where T : Enum
  {
    public EnumCollectionJsonValueConverter() : base(
      v => JsonConvert
        .SerializeObject(v.Select(e => e.ToString()).ToList()),
      v => JsonConvert
        .DeserializeObject<ICollection<string>>(v)
        .Select(e => (T) Enum.Parse(typeof(T), e)).ToList())
    {
    }
  }
}

We can now reuse this single class for any enum collection property we have. where T : Enum is another recent C# feature which ensure the generic type is an enum. (which is in turn required by Enum.Parse) Then in the DbContext class:

var converter = new EnumCollectionJsonValueConverter<UserRole>();

modelBuilder
  .Entity<User>()
  .Property(e => e.UserRoles)
  .HasConversion(converter);

And as a final step, we have to write a comparer class, to ensure the enum collection elements are compared correctly. Here's an example implementation:

public class CollectionValueComparer<T> : ValueComparer<ICollection<T>>
{
  public CollectionValueComparer() : base((c1, c2) => c1.SequenceEqual(c2),
    c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), c => (ICollection<T>) c.ToHashSet())
  {
  }
}

And add it to the entity in ApplicationDbContext:

var converter = new EnumCollectionJsonValueConverter<UserRole>();
var comparer = new CollectionValueComparer<UserRole>();

builder.Entity<User>()
  .Property(e => e.UserRoles)
  .HasConversion(converter)
  .Metadata.SetValueComparer(comparer);

Source Code

You can find the final solution codebase on GitHub: https://github.com/gkedzierski/entity-collection-serializer-example

It's a fully working web application that seeds the SQLite database with some data and exposes a controller endpoint returning a JSON serialized list of users.


PS. If you liked this article, please share to spread the word.

Share

Looking for a handy server monitoring tool?

Check out StackScout, a project I've been working on for the past few years. It's a user-friendly web app that keeps an eye on your servers across different clouds and data centers. With StackScout, you can set custom alerts, access informative reports, and use some neat analytics tools. Feel free to give it a spin and let me know what you think!

Learn more about StackScout

StackScout server monitoring tool screenshot