Created
January 27, 2020 06:21
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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