I’ve been diving into Entity Framework Core 5 and learning all I can about the object-relational mapper’s (ORM) inner-workings. With EF Core 5, the newer ORM has learned from its namesake Entity Framework’s past sins. With extensibility points and smarter defaults right out of the box, EF Core 5 puts us in a position to succeed, and we’re less likely to make mistakes that only reveal themselves in mission-critical situations.

This post will look at an extensibility point that many folks will appreciate, especially when dealing with C# objects that we can manage with identifiers. Let’s dive right into Value Converters for EF Core 5.

What Is A Value Converter?

When dealing with EF Core data modeling, we may store data differently than how we handle it within the context of our application.

Value converters allow property values to be converted when reading from or writing to the database. This conversion can be from one value to another of the same type (for example, encrypting strings) or from a value of one type to a value of another type (for example, converting enum values to and from strings in the database.) Microsoft Docs

We can use value converters to optimize database storage, write performance, and read latency, in addition to optimizing memory in our application.

Let’s look at how we use a value converter to optimize what would otherwise be duplicate information in our database.

Working With Value Converters

Let’s start with a database model of Movie, which will contain a StreamingService record property. While it may seem like a new streaming service is launching every few minutes, we can pretend that this data set is finite.

public class Movie
{
    public int Id { get; set; }
    public string Name { get; set; } 
    public StreamingService StreamingService { get; set; }
}

public record StreamingService(string Id, string Description)
{
    public static StreamingService Netflix { get; } 
        = new("netflix", "Netflix streaming service");
    public static StreamingService Hulu { get; }
        = new("hulu", "Hulu");
    public static StreamingService HBOMax { get; }
        = new("hbo-max", "HBO Max");
    public static StreamingService DisneyPlus { get; }
        = new("disney-plus", "Disney+");
    public static StreamingService AppleTvPlus { get; }
        = new("apple-tv-plus", "Apple TV+");

    public static IReadOnlyList<StreamingService> All = new[] {
        Netflix, 
        Hulu,
        HBOMax,
        DisneyPlus,
        AppleTvPlus
    };
}

It would be wasteful on the network and storage to transmit more than the Id to our database engine. We can store the rest of the information in our C# record instances. Not to mention that duplicating these read-only entities would add to our application’s memory usage.

Let’s see how to tell EF Core to translate our record instances into string values that we can store in our database.

public class Database
    : DbContext
{
    public DbSet<Movie> Movies { get; set; }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .EnableSensitiveDataLogging()
            .LogTo(Console.WriteLine)
            .UseSqlite("Data Source=movies.db");

        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var converter = new ValueConverter<StreamingService, string>(
            from => from.Id,
            to => StreamingService.All.FirstOrDefault(s => s.Id == to)
        );

        modelBuilder
            .Entity<Movie>()
            .Property(m => m.StreamingService)
            .HasConversion(converter);
        
        base.OnModelCreating(modelBuilder);
    }
}

In the OnModelCreating, we can define a new ValueConverter instance with two Func definitions. EF Core 5 will use these two functions to convert to and from our a StreamingService record to our target column, which is of type string. Using our ModelBuilder, we can tell EF Core that our StreamingService property can convert using our new ValueConverter instance.

Let’s generate our migration to see that EF Core managed to understand our mapping correctly.

public partial class initial : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Movies",
            columns: table => new
            {
                Id = table.Column<int>(type: "INTEGER", nullable: false)
                    .Annotation("Sqlite:Autoincrement", true),
                Name = table.Column<string>(type: "TEXT", nullable: true),
                StreamingService = table.Column<string>(type: "TEXT", nullable: true)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Movies", x => x.Id);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(
            name: "Movies");
    }
}

As we can see, the StreamingService column is a type of TEXT since we are using SQLite in this case.

Writing Queries With Value Converters

Our value converter expects us to use StreamingService instances in our LINQ queries.

// 👎 wrong
var results =
    await catalog
        .Movies
        .Where(m => m.StreamingService.Id == StreamingService.Netflix.Id)
        .ToListAsync(); 

// 👍 correct
var results =
    await catalog
        .Movies
        .Where(m => m.StreamingService == StreamingService.Netflix)
        .ToListAsync(); 

It is essential to remember that EF Core only knows about our top-level entity. It doesn’t know that StreamingService has an Id or Description property. Trying to use any of the StreamingService properties in a query will end in an exception.

Let’s see the complete running sample.

using System;
using System.Linq;
using EFCoreValueConverters;
using Microsoft.EntityFrameworkCore;

// save new movie
await using var catalog = new Database();
catalog.Movies.Add(new Movie {
    Name = "Birdbox", 
    StreamingService = StreamingService.Netflix
});
await catalog.SaveChangesAsync();

// retrieve
await using var browser = new Database();

var results =
    await catalog
        .Movies
        .Where(m => m.StreamingService == StreamingService.Netflix)
        .ToListAsync(); 

foreach (var result in results) {
    Console.WriteLine($"{result.Name} now streaming on {result.StreamingService.Description}!");        
}

Parsing through the logging of EF Core, we can see a few things. First, let’s look at how we saved our movie.

Executing DbCommand [Parameters=[@p0='Birdbox' (Size = 7), @p1='netflix' (Size = 7)], CommandType='Text', CommandTimeout='30']
INSERT INTO "Movies" ("Name", "StreamingService")
VALUES (@p0, @p1);
SELECT "Id"
FROM "Movies"
WHERE changes() = 1 AND "rowid" = last_insert_rowid();

We can see that netflix was used as a parameter, meaning EF Core mapped our StreamingService record correctly. Next, let’s see our read query.

Executed DbCommand (1ms) [Parameters=[@__Netflix_0='netflix' (Size = 7)], CommandType='Text', CommandTimeout='30']
SELECT "m"."Id", "m"."Name", "m"."StreamingService"
FROM "Movies" AS "m"
WHERE "m"."StreamingService" = @__Netflix_0

Remember, our LINQ where criteria used an instance of StreamingService.

var results =
    await catalog
        .Movies
        .Where(m => m.StreamingService == StreamingService.Netflix)
        .ToListAsync(); 

Finally, we see our result.

Birdbox now streaming on Netflix streaming service!

Awesome!

Conclusion

Value Converters allow us to map relatively complex types that might not be supported by our database engines. In the example above, we were able to store important user information in our application layer while storing the minimum value necessary to reconstitute our models. Reusing our read-only records also gains us memory savings in mission-critical situations. As we saw in the sample, defining value converters is straightforward.

Value Converters’ added benefit is that it allows third-party vendors to integrate directly into EF Core, allowing the ecosystem to thrive more than the previous Entity Framework 6 architecture.

If you’re using Value Converters today, please leave a comment below and let me know how.

And if you found this post helpful, please share it with coworkers and friends. As always, thank you.