How to persist a list of strings with Entity Framework Core?

UPDATE FOR EF CORE 8

.NET 8 has now built-in support to store lists of primitive types in a column. Read here about Primitive Collections.

PRIOR TO EF CORE 8 (or if you want to manually control the serialization instead of using JSON)

This can be achieved in a much more simple way starting with Entity Framework Core 2.1. EF now supports Value Conversions to specifically address scenarios like this where a property needs to be mapped to a different type for storage.

To persist a collection of strings, you could setup your DbContext in the following way:

protected override void OnModelCreating(ModelBuilder builder)
{
    var splitStringConverter = new ValueConverter<IEnumerable<string>, string>(v => string.Join(";", v), v => v.Split(new[] { ';' }));
    builder.Entity<Entity>()
           .Property(nameof(Entity.SomeListOfValues))
           .HasConversion(splitStringConverter);
} 

Note that this solution does not litter your business class with DB concerns.

Needless to say that this solution, one would have to make sure that the strings cannot contains the delimiter. But of course, any custom logic could be used to make the conversion (e.g. conversion from/to JSON).

Another interesting fact is that null values are not passed into the conversion routine but rather handled by the framework itself. So one does not need to worry about null checks inside the conversion routine. However, the whole property becomes null if the database contains a NULL value.

What about Value Comparers?

Creating a migration using this converter leads to the following warning:

The property ‘Entity.SomeListOfValues’ is a collection or enumeration type with a value converter but with no value comparer. Set a value comparer to ensure the collection/enumeration elements are compared correctly.

Setting the correct comparer for the suggested converter depends on the semantics of your list. For example, if you do not care about the order of its elements, you can use the following comparer:

new ValueComparer<IEnumerable<string>>(
    (c1, c2) => new HashSet<string>(c1!).SetEquals(new HashSet<string>(c2!)),
    c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
    c => c.ToList()
);

Using this comparer, a reordered list with the same elements would not be detected as changed an thus a roundtrip to the database can be avoided. For more information on the topic of Value Comparers, consider the docs.

UPDATE EF CORE 6.0

In order to benefit from Entity Framework Core 6.0 Compiled Models, we can use the generic overload of HasConversion. So the full picture becomes:

builder.Entity<Foo>()
    .Property(nameof(Foo.Bar))
    .HasConversion<SemicolonSplitStringConverter, SplitStringComparer>();

...

public class SplitStringComparer : ValueComparer<IEnumerable<string>>
{
    public SplitStringComparer() : base(
        (c1, c2) => new HashSet<string>(c1!).SetEquals(new HashSet<string>(c2!)),
        c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())))
    {
    }
}

public abstract class SplitStringConverter : ValueConverter<IEnumerable<string>, string>
{
    protected SplitStringConverter(char delimiter) : base(
        v => string.Join(delimiter.ToString(), v),
        v => v.Split(new[] { delimiter }, StringSplitOptions.RemoveEmptyEntries))
    {
    }
}

public class SemicolonSplitStringConverter : SplitStringConverter
{
    public SemicolonSplitStringConverter() : base(';')
    {
    }
}

Leave a Comment