F# Options with EF Core

Posted by Daniel on Sunday, July 8, 2018 at 5:00 pm

Categorized under ,

UPDATE: The code described below is now available as a NuGet package.

The 2.1 release of Entity Framework Core brought the ability to do value conversions. This is implemented through an abstract class, ValueConverter, which you can implement to convert a data type. They also provided several built-in converters that you don't have to write, such as storing enums as strings. To use a value converter, you provide a new instance of it and attach it to a property in your model's OnModelCreating event.

F# provides an Option<'T> type as a way to represent a value that may or may not be present. There are many benefits to defining optional values as 'T option rather than checking for null; you can read all about it if you'd like.

As I was working on a project, I already used Option.ofObj to convert my possibly-null results from queries to options; at the field level, though, I was working with default values. Could I use this new feature to handle nullable columns as well? As it turns out, yes!

Here is the code for the value converter.

module Conversion =
  open Microsoft.FSharp.Linq.RuntimeHelpers
  open System
  open System.Linq.Expressions

  let toOption<'T> =
    <@ Func<'T, 'T option>(fun (x : 'T) -> match box x with null -> None | _ -> Some x) @>
    |> LeafExpressionConverter.QuotationToExpression
    |> unbox<Expression<Func<'T, 'T option>>>
  let fromOption<'T> =
    <@ Func<'T option, 'T>(fun (x : 'T option) -> match x with Some y -> y | None -> Unchecked.defaultof<'T>) @>
    |> LeafExpressionConverter.QuotationToExpression
    |> unbox<Expression<Func<'T option, 'T>>>

type OptionConverter<'T> () =
  inherit ValueConverter<'T option, 'T> (Conversion.fromOption, Conversion.toOption)

The Conversion module contains the functions that we'll need to provide in the ValueConverter constructor. (With the way class inheritance is coded in F#, and the way ValueConverter wants its expressions in its constructor, this is a necessary step. I would have liked to have seen a no-argument constructor and overridable properties as an option, but I'm not complaining; this is a really great feature.) Within those functions, we make use of code quotations, then convert the quotation expressions to Linq expressions.

One other note; in the toOption function, if we used Option.ofObj instead of box x, the code would not support value types. This means that things like an int option field wouldn't be supported.

Now that we have our option converter, let's hook it into our model. In my project, each entity type has a static configureEF function, and I call those from OnModelCreating. Here's an abridged version of one of my entity types:

  Member =
  { /// ...
    /// E-mail format
    format : string option
    /// ...
    /// ...
    static member configureEF (mb : ModelBuilder) =
      /// ... HasColumnName statements, etc.
      mb.Model.FindEntityType(typeof<Member>).FindProperty("format").SetValueConverter(OptionConverter<string> ())
      |> ignore

This line of code finds the type within the model, the property within the type, and provides the new instance of our option converter to it. In this entity, a None here indicates that the member uses the group's default e-mail format; Some would indicate that they've specified which format they prefer.

That's all there is to it! Define the converter once, and plug it in to all the optional fields; now we have nullable fields translated to options by EF Core. "Magic unicorn," indeed!

(Credits: Many thanks to Jiří Činčura for the excellent value conversion blog post and Tomas Petricek for his Stack Overflow answer on converting quotation expressions to Linq expressions.)