DEV Community

Justin Hewlett
Justin Hewlett

Posted on • Updated on

Dependency Injection in F# Web APIs

I've spent a lot of time thinking about the best way to manage dependencies in F# web APIs built on top of ASP.NET Core. As you might expect from a .NET library, it makes heavy use of object-oriented patterns. For example, everything seems to be based around the built-in Inversion of Control (IoC) container.

In many ways, IoC containers appear to be antithetical to the F# ethos. For one, they are a bit magical, which runs counter to the F# philosophy of being explicit. You also give up a lot of compile-time checking and have to rely on runtime failures for your dependency wire-up. Further, usually IoC containers are used with classes and interfaces, while many F# programmers favor modules, functions, and algebraic types.

On the other hand, containers do give you a good way to manage resource lifetimes. Which things should be a singleton for the duration of the server, and which things should be created once per request?

And really, since the container is built-in to ASP.NET Core, it's hard to avoid it completely. If you use the built-in configuration or logging, you'll likely need to use it. It's actually impossible to create a HttpClientFactory without using the container!

A compromise

I've come up with a few strategies that allow me to be a good .NET citizen and get some of the benefits of containers, such as lifetime management, without giving up my desire to wire things up manually using partial application.

Manual wire-up with partial application

When registering classes, it's common to register all the parameters to a class constructor in the IoC container, then let the container construct the class automatically via reflection.

The problem with this approach is that the construction of your class is now implicit. If you add a parameter to your class constructor and forget to register it, you won't get a compile error — it will just fail at runtime. I think this is one of the biggest drawbacks of containers.

Instead, I use partial application to explicitly wire up my dependencies. Then I only register the top-level functions that will be injected into the controller.1

Let's look at an example:

module Database =
    let readData (connectionString : string) (query : string) : Async<'a list> = ...

module Person =
    let getPerson (readData : string -> Async<'a list>) (personId : string) : Async<Person option> = ...
Enter fullscreen mode Exit fullscreen mode

We have a Database.readData function that takes in a connection string and query and returns a list of results.

Then we have a Person.getPerson function that takes in a function that knows how to read data from the database and a personId, executes a query, and returns the result.

Notice that the readData parameter in getPerson is just Database.readData with the connection string already baked into it via partial application.

Here's what our container registration would like for this:

type GetPerson = string -> Async<Person option>

type Startup (configuration : IConfiguration) =
    member _.ConfigureServices(services : IServiceCollection) : unit =
        let readData = Database.readData configuration.["dbConnection"]    //partially apply the connection string
        let getPerson = Person.getPerson readData    //partially apply the readData function

        services
            .AddSingleton<GetPerson>(getPerson)
            .AddControllers()
        |> ignore
Enter fullscreen mode Exit fullscreen mode

Yes, you can register plain functions in the container!

GetPerson is just a type alias to a function signature; we could have just as easily inlined it:

.AddSingleton<string -> Async<Person option>>(getPerson)
Enter fullscreen mode Exit fullscreen mode

You'll note that we're doing all of the wiring manually via partial application2. Then, we only register the top-level function, getPerson. The function readData becomes a supporting function for getPerson, but we have no need to register it because we won't inject it into our controller.

Injecting records and discriminated unions

Injecting bare functions is simple and lightweight, but you do risk having functions with the same signature collide. For that reason, it's probably best to wrap the function in a record or single-case discriminated union:

//Option 1: Record
type PersonControllerDependencies = {
    GetPerson : string -> Async<Person option>
}

//Option 2: Single-case DU
type GetPerson = GetPerson of (string -> Async<Person option>)
Enter fullscreen mode Exit fullscreen mode

In the controller, you can inject the type into the constructor just like you would a class or interface:

[<ApiController>]
type PersonController(deps : PersonControllerDependencies) =
    inherit ControllerBase()

    [<HttpGet>]
    [<Route("people/{personId}")>]
    member _.Get(personId : string) =
        async {
            let! person = deps.GetPerson personId
            return OkObjectResult(person) :> IActionResult
        }
Enter fullscreen mode Exit fullscreen mode

You may find it helpful to use destructuring:

let { GetPerson = getPerson } = deps
let! person = getPerson personId
Enter fullscreen mode Exit fullscreen mode

This is especially true if you go the single-case union route because you can't just "dot" into it:

let (GetPerson getPerson) = GetPerson
let! person = getPerson personId
Enter fullscreen mode Exit fullscreen mode

What about Giraffe?

Giraffe is a functional web framework built on top of ASP.NET Core. Interestingly, Giraffe doesn't do anything to try to hide or work around the IoC container. In your handlers, you use:

let service = ctx.GetService<T>()
Enter fullscreen mode Exit fullscreen mode

This is a service locator pattern. In some ways, this is worse than using stock MVC because at least with MVC you have a constructor to inject your dependencies into. For this reason, I like to add in Giraffe.GoodRead, which provides a slick way of injecting the dependencies into your request handler functions.

For contrast, here is what the request handler would look like with Giraffe:

type GetPersonDependencies = {
    GetPerson : string -> Async<Person option>
}

let getPersonHandler { GetPerson = getPerson } personId =
    fun next ctx ->
        task {
            let! person = getPerson personId |> Async.StartAsTask

            return! json person next ctx
        }

let webApp =
    choose [
        routef "/people/%s" (fun personId ->
            //inject the dependency into getPersonHandler
            Require.services<GetPersonDependencies> (fun getPersonDeps ->
                getPersonHandler getPersonDeps personId
            )
        )
    ]
Enter fullscreen mode Exit fullscreen mode

Final thoughts

I mentioned a few different approaches here. Let me give you a more concrete recommendation if you're still not sure where to start.

I recommend starting with stock MVC Web API first, especially for a simple app. Use partial application in ConfigureServices and inject a record of top-level functions. Records provide a nice grouping of related functions3 and provide an easy way to access the functions inside.

As your app grows and you become more comfortable, or if you anticipate needing request handler pipelines that compose easily, Giraffe with GoodRead is a good next step.

This post is part of the F# Advent Calendar 2019


  1. It occurs to me that this strategy gives you less flexibility in managing lifetimes since you're not registering all the intermediate functions. I acknowledge this and am not sure how much of a problem this is in practice. It's also worth keeping in mind that you can manage lifetimes yourself outside of an IoC container. For example, you could have a singleton factory function that, when called, created a new resource every time. 

  2. Well, most of it. The framework is still constructing the controllers themselves for us. If you really want to, you can create the controllers manually, too. Here's a good example of how to do this in C#. I haven't played around with this much, so I'm not sure whether it's worthwhile. 

  3. Yes, a record of functions is basically equivalent to an interface. Those will work fine too, especially if you pair them with object expressions to implement the interface, but I prefer records because it's easy to remember the syntax to declare and create them. 

Top comments (3)

Collapse
 
macfma01 profile image
Matthew MacFarland

Love this!!! I'm trying the record of functions style and I prefer that. I was really stuck on this and this article was a great help. In C# we hardly even ask questions about stuff like this. F# leaves a lot more room for creative solutions.

Collapse
 
amieres profile image
amieres • Edited

Thanks for your post, I find it very interesting and helpful as I still need to learn much of ASP.NET and IoC. Currently for my projects I am using this instead:

github.com/amieres/FsDepend

It is (I believe) a novel approach, and I would very much like to know your opinion, whether you find it useful or not, logical or absurd, pros/cons limitations, etc.

Thanks in advance.

Collapse
 
amieres profile image
amieres • Edited

To mimic your example, this is more or less how it would go:

module Database =
    type Response = Response of string

    let connectionString_  = "connectionString not injected"
    let connectionStringD = Depend.depend0 <@ connectionString_ @>

    let readDataD = Depend.depend {
        let! connectionString = connectionStringD

        return fun (query:string) -> async {
            return [ Response (connectionString + " : " + query) ]
        }
    } 

module Person =
    open Database

    type Person = Person of string

    let getPersonD = Depend.depend {
        let! readData = readDataD

        return fun (personId : string) -> async {
            let query = sprintf "SELECT person FROM Table WHERE id = %s" personId
            match! readData query with
            | [ Response resp ] -> return Some  (Person resp)
            | _-> return None
        }
    } 

module Startup =
    open Database

    let getPerson = 
        Person.getPersonD 
        |> Depend.resolver [ Depend.replace0 <@ connectionString_ @> "dbConnection" ]

    getPerson "111"
    |> Async.RunSynchronously
    |> printfn "%A"

which prints: Some (Person "dbConnection : SELECT person FROM Table WHERE id = 111")