Announcing EFCore.FSharp

Today marks the first alpha release of F# support for Entity Framework Core, allowing for direct usage of EF Core from your F# projects with support for EF Core 5.0.3
It contains support for migrations and reverse-engineering, allowing you to map tables to F# records and take advantage of F# idioms such as option to represent nullable columns. At the moment though, this does not work for primary or foreign keys, just plain columns (GitHub issue).

To take it for a test drive, grab the NuGet package from https://www.nuget.org/packages/EntityFrameworkCore.FSharp
Bear in mind that it is an alpha release so please log any issues on the GitHub repo.

Currently it has support for relatively simple schemas, it does not support many-types-per-table relationships yet but that is on the to-do list.

Although it does support option columns it does not yet support types such as discriminated unions.

Installation

All the usual ways of grabbing a NuGet package...

Install-Package EntityFramworkCore.FSharp
dotnet add package EntityFramworkCore.FSharp
paket install EntityFramworkCore.FSharp

Usage

When it is installed, you will need to register it for dotnet ef to pick it up. Simply add the following code to your project. It will be detected at compile time, no need to call it from a Startup class

module DesignTimeServices

open Microsoft.Extensions.DependencyInjection
open Microsoft.EntityFrameworkCore.Design
open EntityFrameworkCore.FSharp

type DesignTimeServices() =
    interface IDesignTimeServices with 
        member __.ConfigureDesignTimeServices(serviceCollection: IServiceCollection) = 
            let fSharpServices = EFCoreFSharpServices.Default
            fSharpServices.ConfigureDesignTimeServices serviceCollection
            ()


Migrations

Code first databases are supported, a working example of creating a basic MVC application in F# with user authentication is available at https://github.com/simon-reynolds/EFCore.FSharp.MvcAuth

One thing to remember is that record types have to have the [<CliMutable>] attribute specified so that they can be created correctly by Entity Framework.

After running dotnet ef migration add you will need to add the created files to your your project. The migration files are generated sequentially so you can add a single glob reference to your fsproj file to add them all at once

<Compile Include="Migrations/*.fs" />

There is an issue to track this to try find an automated solution. As always, any help or suggestions are welcome.

Scaffolding

When scaffolding a model from an existing database, we can specify how we want the generated code to be created in our DesignTimeServices type above.

We can create types as either record types or classes similar to how they behave in C#

For instance, given a blog post type with an Id, Title and Content, it can generated as either example below.


// Record type
type BlogPost = {
    Id : int
    Title: string
    Content: string
}

// Class type
type BlogPost() =

    [<DefaultValue>] val mutable private _Id : int
    member this.Id with get() = this._Id and set v = this._Id <- v

    [<DefaultValue>] val mutable private _Title : string
    member this.Title with get() = this._Title and set v = this._Title <- v

    [<DefaultValue>] val mutable private _Content : string
    member this.Content with get() = this._Content and set v = this._Content <- v

Similarly, optional columns can either be rendered as Nullable<'a> or as 'a option. The default configuration will create record types with nullable columns specified as option types.

These are done by defining our scaffold options and passing them into the DesignTimeServices, e.g.

module DesignTimeServices =

    open Microsoft.Extensions.DependencyInjection
    open Microsoft.EntityFrameworkCore.Design
    open EntityFrameworkCore.FSharp

    type DesignTimeServices() =
        interface IDesignTimeServices with
            member __.ConfigureDesignTimeServices(serviceCollection: IServiceCollection) =
                
                // The default behaviour can be specified by calling
                let fSharpServices = EFCoreFSharpServices.Default

                // Or we can define a ScaffoldOptions use that instead
                let scaffoldOptions =
                    ScaffoldOptions (
                        ScaffoldTypesAs = ScaffoldTypesAs.ClassType,
                        ScaffoldNullableColumnsAs = ScaffoldNullableColumnsAs.NullableTypes)

                let fSharpServices = EFCoreFSharpServices.WithScaffoldOptions scaffoldOptions

                fSharpServices.ConfigureDesignTimeServices serviceCollection
                ()

More details can be found in our project documentation here.

Option types

It has basic support for option types, including an OptionConverter for mapping nullable columns to options. When building a code first database simply include the following at the end of OnModelCreating

type MyContext (options) = 
    inherit DbContext (options)

    override this.OnModelCreating mb =
        (* Define entities here *)
        
        modelBuilder.RegisterOptionTypes()

Other issues

I'm sure there are. Please report any issues you discover and help us improve F# support for Entity Framework Core!