Getting Started with F# and Entity Framework

Date: 2023-11-24 | create | tech | fsharp | dotnet | entity-framework |

Entity Framework is the most popular ORM in the dotnet community. It's been optimized and battle-hardened through years of enterprise use and thousands of development years.

However this popularity is mostly concentrated in the C# community. The F# community typically shies away from it as it's seen as a bit too abstracted from the underlying DB technologies (usually SQL though sometimes a different query language).

For a time I was in this boat, utilizing raw Dapper for handling my SQL queries in all my projects built with CloudSeed (Dapper.Fsharp is also popular but I like to minimize my use of niche DSLs as much as possible). But over time I've found this approach to be tedious and risky - requiring lots of manual repo boilerplate and non-type-safe (raw string) querying.

In this post we're going to explore Entity Framework as an alternative ORM for F#:

Q: How can we use Entity Framework for simple CRUD operations in F#?

Answer

In this post we'll explore an example project using Entity Framework as its ORM.

  • App: F# / Giraffe Web API
  • Data Layer: DBUp for Data Migrations, Entity Framework for ORM
  • Database: Postgres
  • Host: Containerized with Docker / Docker-Compose

Note: This example project started as a clone of CloudSeed - my F# project boilerplate - which includes Web API, data integration, and containerization out-of-the-box.

A lot of the components of this app are unnecessary for this tutorial so we won't touch on them in great detail. However I believe it's important to build "tracer bullet" examples that approximate real world use cases to prove that it's not so contrived as to not be useful. So here we are.

In the rest of this post, we'll be zooming in on the App domain and how we configure / use EF for our CRUD data operations.

  • App - Models and Commands / Queries
  • Data: Entity Framework and Setup

All project source code is available:

  • Entity Framework <> F# code: available in this post
  • Full project source code is available to HAMINIONS members

Entity Framework Installation

Entity Framework is a full-service ORM officially supported by Microsoft. It allows you to define your models and configure how they map to the underlying data store in code. This allows you to do all data accesses with your standard list operations (like LINQ) without needing to write any SQL / query syntax.

If you're coming from Javascript land, I consider it to be pretty similar to Prisma.

Install Entity Framework DataProvider

The first thing we need to do is actually install Entity Framework. EF is a translation layer which allows us to model our data in code and translate it into the data store paradigm of our choosing.

This means you need to find and install the appropriate "Provider" for your data store of choice so that it knows how to do that translation. Here's the list of recognized Entity Framework data providers - find the one matching what you're building.

In my case, I'm building with Postgres and the listed data provider for that is Npgsql.EntityFrameworkCore.PostgreSQL.

To install from nuget, I can run:

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL

Install Data Store Package

This is optional but usually a good idea. The Entity Framework Data Provider we installed above allows EF to integrate with our data store.

But often we'll want to do a few other things with our store - like building a type-safe, properly formatted connection string. Most reasonably popular data stores have a package like this so I'd recommend installing it as well even if you don't use it yet (it's usually a direct dependency of your EF DataProvider as well).

For Postgres, this is the Npgsql package.

dotnet add package Npgsql

Modeling the Domain

With our dependencies installed, we can start modeling the domain.

Personally I like my Domain to be data store agnostic. This means it shouldn't know where it's being stored which IME allows me to model the domain more authentically, w/o unnecessary complexity from implementation details.

To this end we're going to be modeling a very simple type that just holds some strings. Note that I am letting the data store leak a little here as I want to show how we can utilize Postgres' JSONB columns.

  • Sentinel - Our type
  • SentinelData - An inner type that will map to a JSONB column

SentinelDomain

module SentinelDomain = 

    [<CLIMutable>]
    type SentinelData = {
        name: string
    }

    // Mutable for Dapper
    [<CLIMutable>]
    type Sentinel = {
        id: string
        data: SentinelData
    }

Next we'll create the SQL table that will hold this data. My project uses DBUP to handle data migrations but you could run this manually as well.

  • id - Holds Sentinel.id
  • data - Will hold the json of SentinelData

SentinelDataMigration

CREATE TABLE sentinels(
    id VARCHAR PRIMARY KEY,
    data JSONB
);

Now that we have both the Domain and SQLTable defined, we can configure Entity Framework to map the two. We do this by creating a DataContext (EF calls this a DBContext) which we then configure to tell it what all the data is and how it maps to the underlying data store (like how to connect, types to use, the names of columns, etc).

SentinelDataContext

open Microsoft.EntityFrameworkCore
open SentinelDomain

module SentinelPersistence = 

    type SentinelDataContext(
        connectionString : string) 
        =
        inherit DbContext()

        [<DefaultValue>]
        val mutable sentinels : DbSet<Sentinel>

        member public this.Sentinels
            with get() = this.sentinels 
            and set s = this.sentinels <- s

        override __.OnConfiguring(optionsBuilder : DbContextOptionsBuilder) = 
            optionsBuilder.UseNpgsql(connectionString)
            |> ignore

        override __.OnModelCreating(modelBuilder : ModelBuilder) = 

            // Sentinels

            modelBuilder.Entity<Sentinel>()
                .ToTable("sentinels")
                |> ignore

            modelBuilder.Entity<Sentinel>()
                .HasKey("id")
                |> ignore

            modelBuilder.Entity<Sentinel>()
                .Property(fun s -> s.id)
                .HasColumnName("id")
                |> ignore

            modelBuilder.Entity<Sentinel>()
                .Property(fun s -> s.data)
                .HasColumnName("data") 
                .HasColumnType("jsonb")
                |> ignore

CRUD Operations with F# and Entity Framework

Now that we've got our ORM configured, let's go over how to actually perform these CRUD operations from our F# app.

If you're new to F#, this code may look weird. Get more familiar w Formatting F# functions the right way

The first thing we've got to do is actually set up our DataContext for use by the rest of our app. I usually like to do this at the composition root (for a web api this is typically where I am building dependencies and mapping routes to handlers).

With a basic, manual Dependency Injection approach, we can simply create our connection string, configure how to create the DataContext with it, and pass it down to any services that may want it.

SentinelServiceTree = {
    DbContext = fun () -> new SentinelDataContext(connectionString)
}

In the above code I'm creating what I call a ServiceTree which is essentially a type-safe register of any IO / dependencies a domain needs. Here we give it a function it can call to create the configured DataContext.

Create

To Create a Sentinel, we can simply create our Sentinel object, add it to the DataContext, and save the changes.

let createSentinelCommandAsync 
    (serviceTree : SentinelServiceTree) 
    : Async<Result<Sentinel, CreateSentinelCommandErrors>> 
    = 
    async {
        use db = serviceTree.DbContext()

        let newSentinel : Sentinel = {
            id = Guid.NewGuid().ToString()
            data = {
                name = Guid.NewGuid().ToString()
            }
        }

        db.Sentinels.Add(newSentinel)
            |> ignore

        let! linesChanged =
            db.SaveChangesAsync()
            |> Async.AwaitTask

        return 
            match linesChanged with
            | x when x = 1 -> Ok newSentinel 
            | _ -> Error CreateSentinelCommandErrors.CouldNotCreateSentinel

    } 
  • Create an Async (cold) function
  • Use the DbContext (use allows us to dispose when done which is good for not wasting resources)
  • Create sentinel and add to our DbContext - note we must convert from C#'s Task to F#'s Async, though this is pretty easy
  • We get the linesChanged as a proxy for whether our thing worked or not
  • We return a Result so callers know if it worked or not

Read

Next we'll move onto how to Read from our data. This one has a bit more conversion from C# Task / Linq to F# Async but for all the conversions it's actually still quite readable.

Here we take an Event with an id attribute and we try to find it. If we find it, we return the Result.Ok Sentinel otherwise we return Result.Error

let getSentinelQueryAsync 
    (serviceTree : SentinelServiceTree) 
    (event : GetSentinelQuery) 
    : Async<Result<Sentinel, GetSentinelQueryErrors>> 
    = 
    async {
        use db = serviceTree.DbContext()

        let! sentinel = 
            db
                .Sentinels
                .Where(fun s -> s.id = event.id)
                .Select(fun s -> Some s)
                .DefaultIfEmpty(None)
                .SingleOrDefaultAsync()
            |> Async.AwaitTask

        return 
            match sentinel with
            | Some x -> Ok x 
            | None -> Error GetSentinelQueryErrors.NoSentinelFound
    } 
  • Use DBContext for auto dispose of resources when done
  • Access our datacontext using asynchronous LINQ functions, then convert from Task to Async
  • Match on the resulting Option to get a Result back

Update

Basically the same as the above except:

  • Read out your desired model
  • Update its values in memory
  • Then save your DBContext

Delete

Sames as Create except instead of Add, you Remove:

  • Read out model to delete
  • Remove it from the Context - DbContext.Remove(YOUR_OBJECT)

Next

Personally I've found EF with F# to be much more ergonomic than Dapper. There are a few bits of ceremony required for the C# <> F# translation but ultimately they're small and not too bad.

The Full project source code is available to HAMINIONS members.

If you liked this post, you might also like:

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.