Skip to content

Instantly share code, notes, and snippets.

@jhewlett
Created January 27, 2020 06:21
Show Gist options
  • Save jhewlett/8cfcfd5b50f97655724e365725e4a915 to your computer and use it in GitHub Desktop.
Save jhewlett/8cfcfd5b50f97655724e365725e4a915 to your computer and use it in GitHub Desktop.
Pure DI in F# ASP.NET Core Web API, with examples of how to handle singleton and per-request disposables
namespace PureDI
open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Microsoft.AspNetCore.Mvc
open Microsoft.AspNetCore.Mvc.Controllers
open Microsoft.Extensions.Logging
open Microsoft.AspNetCore
open Microsoft.AspNetCore.Hosting
open System
open Microsoft.Extensions.Hosting
type Person = {
Id : string
Name : string
}
type PersonDb = class end
module PersonDb =
let people = [{ Id = "1"; Name = "Person 1" }; { Id = "2"; Name = "Person 2" }]
let getPersonById connectionPool redisConnection (logger : ILogger) id =
logger.LogInformation(sprintf "Fetching person with id %s" id)
async {
return people |> List.tryFind (fun p -> p.Id = id)
}
let getAllPeople connectionPool redisConnection (logger : ILogger) =
logger.LogInformation("Fetching all people")
async {
return people
}
type GetPersonById = string -> Async<Person option>
type GetAllPeople = unit -> Async<Person list>
[<ApiController>]
type PersonController(getPersonById : GetPersonById, getAllPeople : GetAllPeople) =
inherit ControllerBase()
[<HttpGet>]
[<Route("people/{personId}")>]
member _.GetPersonById(personId : string) : Async<IActionResult> =
async {
let! person = getPersonById personId
return
match person with
| Some p -> OkObjectResult(p) :> IActionResult
| None -> NotFoundResult() :> IActionResult
}
[<HttpGet>]
[<Route("people")>]
member _.GetAllPeople() : Async<IActionResult> =
async {
let! people = getAllPeople ()
return OkObjectResult(people) :> IActionResult
}
type DbConnection() = class end
//singleton resource
type DbConnectionPool(connectionString, logger : ILogger<DbConnectionPool>) =
member _.GetConnection() = DbConnection()
interface IDisposable with
member _.Dispose() =
logger.LogInformation("Disposing DbConnectionPool")
//per-request resource
type RedisConnection(redisHost, logger : ILogger<RedisConnection>) =
interface IDisposable with
member _.Dispose() =
logger.LogInformation("Disposing RedisConnection")
type CompositionRoot(dbConnectionString, redisHost) =
//Singletons
let loggerFactory = LoggerFactory.Create(fun loggingBuilder -> loggingBuilder.AddConsole() |> ignore)
let personDbLogger = loggerFactory.CreateLogger<PersonDb>()
let redisLogger = loggerFactory.CreateLogger<RedisConnection>()
let dbConnectionPool = new DbConnectionPool(dbConnectionString, loggerFactory.CreateLogger<DbConnectionPool>())
interface IControllerActivator with
member _.Create(context : ControllerContext) : obj =
let controller = context.ActionDescriptor.ControllerTypeInfo.AsType()
//Per-request deps
let redisConnection = new RedisConnection(redisHost, redisLogger)
context.HttpContext.Response.RegisterForDispose(redisConnection) //dispose of per-request deps at end of request
if controller = typeof<PersonController> then
let getPersonById = PersonDb.getPersonById dbConnectionPool redisConnection personDbLogger
let getAllPeople () = PersonDb.getAllPeople dbConnectionPool redisConnection personDbLogger
PersonController(getPersonById, getAllPeople) :> obj
else
failwith ("Unknown controller " + controller.Name)
member _.Release(context : ControllerContext, controller : obj) : unit =
()
interface IDisposable with
member _.Dispose() =
(dbConnectionPool :> IDisposable).Dispose() //dispose of singletons
type Startup (config : IConfiguration) =
let compositionRoot = new CompositionRoot(config.["dbConnectionString"], config.["redisHost"])
member _.ConfigureServices(services : IServiceCollection) : unit =
services
.AddSingleton<IControllerActivator>(compositionRoot)
.AddControllers()
|> ignore
member _.Configure(app : IApplicationBuilder, appLifetime : IHostApplicationLifetime) : unit =
//Dispose of composition root (which will dispose of singletons)
appLifetime.ApplicationStopping.Register(
fun () -> (compositionRoot :> IDisposable).Dispose()
) |> ignore
app.UseRouting () |> ignore
app.UseEndpoints (fun endpoints ->
endpoints.MapControllers () |> ignore
) |> ignore
module Program =
let createHostBuilder args =
WebHost
.CreateDefaultBuilder(args)
.UseStartup<Startup>()
[<EntryPoint>]
let main args =
createHostBuilder(args).Build().Run()
0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment