Single case union types (aka Simple types)

It's common to use single case discriminated unions to meaningfully represent data values. But EF does not know anything about this kind of type. Luckily this repository has some ways to help you to deal with they.

Configuring

We have two approaches to deal with single case union types which are a converter or an extension that searches for all Single Case Unions in your entities.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
open EntityFrameworkCore.FSharp.Extensions

type PositiveInteger = PositiveInteger of int

[<CLIMutable>]
type Blog = {
    [<Key>]
    Id : Guid
    Title : string
    Votes: PositiveInteger
}

type MyContext () =
    inherit DbContext()

    [<DefaultValue>]
    val mutable private _blogs : DbSet<Blog>
    member this.Blogs with get() = this._blogs and set v = this._blogs <- v

    override _.OnModelCreating builder =
       
        // setting manually each property
        builder.Entity<Blog>()
            .Property(fun x -> x.Votes)
            .HasConversion(SingleCaseUnionConverter<int, PositiveInteger>())
        |> ignore
        
        // OR
        
        // enables single clase unions for all entities
        builder.RegisterSingleUnionCases() 

    override _.OnConfiguring(options: DbContextOptionsBuilder) : unit =
           options
             .UseSqlite( "Data Source=dbName.db")
             .UseFSharpTypes() // enable queries for F# types
           |> ignore

Querying

You can query for equality without any problem

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let blog =
   query {
       for blog in ctx.Blogs do
       where (blog.Votes = PositiveInteger 10)
       select blog
       headOrDefault
   }
   
 // or  
let blog' = ctx.Blogs.Where(fun b -> b.Votes = PositiveInteger 10).FirstOrDefault()

For querying with other types of operation you will need to unwrap the value inside the query

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let blogQuery =
   query {
       for blog in ctx.Blogs do
       let (PositiveInteger votes) = blog.Votes
       where (votes > 0)
       select blog
       headOrDefault
   }

Private Constructor

This extension doesn't support private union case constructors.

namespace EntityFrameworkCore
namespace EntityFrameworkCore.FSharp
module Extensions

from EntityFrameworkCore.FSharp
Multiple items
union case PositiveInteger.PositiveInteger: int -> PositiveInteger

--------------------
type PositiveInteger = | PositiveInteger of int
Multiple items
val int : value:'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
Multiple items
type CLIMutableAttribute =
  inherit Attribute
  new : unit -> CLIMutableAttribute

--------------------
new : unit -> CLIMutableAttribute
type Blog =
  { Id: Guid
    Title: string
    Votes: PositiveInteger }
Multiple items
type KeyAttribute =
  inherit Attribute
  new : unit -> KeyAttribute

--------------------
KeyAttribute() : KeyAttribute
Blog.Id: Guid
Multiple items
type Guid =
  struct
    new : b:byte[] -> Guid + 5 overloads
    member CompareTo : value:obj -> int + 1 overload
    member Equals : o:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member ToByteArray : unit -> byte[]
    member ToString : unit -> string + 2 overloads
    member TryFormat : destination:Span<char> * charsWritten:int * ?format:ReadOnlySpan<char> -> bool
    member TryWriteBytes : destination:Span<byte> -> bool
    static val Empty : Guid
    static member NewGuid : unit -> Guid
    ...
  end

--------------------
Guid ()
Guid(b: byte []) : Guid
Guid(b: ReadOnlySpan<byte>) : Guid
Guid(g: string) : Guid
Guid(a: int, b: int16, c: int16, d: byte []) : Guid
Guid(a: uint32, b: uint16, c: uint16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Guid(a: int, b: int16, c: int16, d: byte, e: byte, f: byte, g: byte, h: byte, i: byte, j: byte, k: byte) : Guid
Blog.Title: string
Multiple items
val string : value:'T -> string

--------------------
type string = String
Blog.Votes: PositiveInteger
Multiple items
type MyContext =
  inherit DbContext
  new : unit -> MyContext
  val mutable private _blogs: DbSet<Blog>
  override OnConfiguring : options:DbContextOptionsBuilder -> unit
  override OnModelCreating : builder:ModelBuilder -> unit
  member Blogs : DbSet<Blog>
  member Blogs : DbSet<Blog> with set

--------------------
new : unit -> MyContext
Multiple items
type DbContext =
  new : options:DbContextOptions -> DbContext
  member Add<'TEntity> : entity:'TEntity -> EntityEntry<'TEntity> + 1 overload
  member AddAsync<'TEntity> : entity:'TEntity * ?cancellationToken:CancellationToken -> ValueTask<EntityEntry<'TEntity>> + 1 overload
  member AddRange : [<ParamArray>] entities:obj[] -> unit + 1 overload
  member AddRangeAsync : [<ParamArray>] entities:obj[] -> Task + 1 overload
  member Attach<'TEntity> : entity:'TEntity -> EntityEntry<'TEntity> + 1 overload
  member AttachRange : [<ParamArray>] entities:obj[] -> unit + 1 overload
  member ChangeTracker : ChangeTracker
  member ContextId : DbContextId
  member Database : DatabaseFacade
  ...

--------------------
DbContext() : DbContext
DbContext(options: DbContextOptions) : DbContext
Multiple items
type DefaultValueAttribute =
  inherit Attribute
  new : unit -> DefaultValueAttribute
  new : check:bool -> DefaultValueAttribute
  member Check : bool

--------------------
new : unit -> DefaultValueAttribute
new : check:bool -> DefaultValueAttribute
MyContext._blogs: DbSet<Blog>
type DbSet<'TEntity (requires reference type)> =
  member Add : entity:'TEntity -> EntityEntry<'TEntity>
  member AddAsync : entity:'TEntity * ?cancellationToken:CancellationToken -> ValueTask<EntityEntry<'TEntity>>
  member AddRange : [<ParamArray>] entities:'TEntity[] -> unit + 1 overload
  member AddRangeAsync : [<ParamArray>] entities:'TEntity[] -> Task + 1 overload
  member AsAsyncEnumerable : unit -> IAsyncEnumerable<'TEntity>
  member AsQueryable : unit -> IQueryable<'TEntity>
  member Attach : entity:'TEntity -> EntityEntry<'TEntity>
  member AttachRange : [<ParamArray>] entities:'TEntity[] -> unit + 1 overload
  member EntityType : IEntityType
  member Equals : obj:obj -> bool
  ...
val this : MyContext
val set : elements:seq<'T> -> Set<'T> (requires comparison)
val v : DbSet<Blog>
val builder : ModelBuilder
ModelBuilder.Entity<'TEntity (requires reference type)>() : Metadata.Builders.EntityTypeBuilder<'TEntity>
ModelBuilder.Entity<'TEntity (requires reference type)>(buildAction: Action<Metadata.Builders.EntityTypeBuilder<'TEntity>>) : ModelBuilder
ModelBuilder.Entity(name: string) : Metadata.Builders.EntityTypeBuilder
ModelBuilder.Entity(type: Type) : Metadata.Builders.EntityTypeBuilder
ModelBuilder.Entity(name: string, buildAction: Action<Metadata.Builders.EntityTypeBuilder>) : ModelBuilder
ModelBuilder.Entity(type: Type, buildAction: Action<Metadata.Builders.EntityTypeBuilder>) : ModelBuilder
val x : Blog
Multiple items
type SingleCaseUnionConverter<'T,'U> =
  inherit ValueConverter<'U,'T>
  new : unit -> SingleCaseUnionConverter<'T,'U>

--------------------
new : unit -> SingleCaseUnionConverter<'T,'U>
val ignore : value:'T -> unit
member ModelBuilder.RegisterSingleUnionCases : unit -> unit
val options : DbContextOptionsBuilder
Multiple items
type DbContextOptionsBuilder =
  new : unit -> DbContextOptionsBuilder + 1 overload
  member AddInterceptors : interceptors:IEnumerable<IInterceptor> -> DbContextOptionsBuilder + 1 overload
  member ConfigureLoggingCacheTime : timeSpan:TimeSpan -> DbContextOptionsBuilder
  member ConfigureWarnings : warningsConfigurationBuilderAction:Action<WarningsConfigurationBuilder> -> DbContextOptionsBuilder
  member EnableDetailedErrors : ?detailedErrorsEnabled:bool -> DbContextOptionsBuilder
  member EnableSensitiveDataLogging : ?sensitiveDataLoggingEnabled:bool -> DbContextOptionsBuilder
  member EnableServiceProviderCaching : ?cacheServiceProvider:bool -> DbContextOptionsBuilder
  member EnableThreadSafetyChecks : ?enableChecks:bool -> DbContextOptionsBuilder
  member Equals : obj:obj -> bool
  member GetHashCode : unit -> int
  ...

--------------------
type DbContextOptionsBuilder<'TContext (requires 'TContext :> DbContext)> =
  inherit DbContextOptionsBuilder
  new : unit -> DbContextOptionsBuilder<'TContext> + 1 overload
  member AddInterceptors : interceptors:IEnumerable<IInterceptor> -> DbContextOptionsBuilder<'TContext> + 1 overload
  member ConfigureLoggingCacheTime : timeSpan:TimeSpan -> DbContextOptionsBuilder<'TContext>
  member ConfigureWarnings : warningsConfigurationBuilderAction:Action<WarningsConfigurationBuilder> -> DbContextOptionsBuilder<'TContext>
  member EnableDetailedErrors : ?detailedErrorsEnabled:bool -> DbContextOptionsBuilder<'TContext>
  member EnableSensitiveDataLogging : ?sensitiveDataLoggingEnabled:bool -> DbContextOptionsBuilder<'TContext>
  member EnableServiceProviderCaching : ?cacheServiceProvider:bool -> DbContextOptionsBuilder<'TContext>
  member EnableThreadSafetyChecks : ?checksEnabled:bool -> DbContextOptionsBuilder<'TContext>
  member LogTo : filter:Func<EventId, LogLevel, bool> * logger:Action<EventData> -> DbContextOptionsBuilder<'TContext> + 4 overloads
  member Options : DbContextOptions<'TContext>
  ...

--------------------
DbContextOptionsBuilder() : DbContextOptionsBuilder
DbContextOptionsBuilder(options: DbContextOptions) : DbContextOptionsBuilder

--------------------
DbContextOptionsBuilder() : DbContextOptionsBuilder<'TContext>
DbContextOptionsBuilder(options: DbContextOptions<'TContext>) : DbContextOptionsBuilder<'TContext>
type unit = Unit
val blog : Blog
val query : Linq.QueryBuilder
val ctx : MyContext
property MyContext.Blogs: DbSet<Blog> with get, set
custom operation: where (bool)

Calls Linq.QueryBuilder.Where
custom operation: select ('Result)

Calls Linq.QueryBuilder.Select
custom operation: headOrDefault

Calls Linq.QueryBuilder.HeadOrDefault
val blog' : Blog
(extension) Collections.Generic.IEnumerable.Where<'TSource>(predicate: Func<'TSource,bool>) : Collections.Generic.IEnumerable<'TSource>
(extension) Collections.Generic.IEnumerable.Where<'TSource>(predicate: Func<'TSource,int,bool>) : Collections.Generic.IEnumerable<'TSource>
(extension) IQueryable.Where<'TSource>(predicate: Expressions.Expression<Func<'TSource,bool>>) : IQueryable<'TSource>
(extension) IQueryable.Where<'TSource>(predicate: Expressions.Expression<Func<'TSource,int,bool>>) : IQueryable<'TSource>
val b : Blog
val blogQuery : Blog
val votes : int