Header menu logo FSharp.AspNetCore.WebAppBuilder

FSharp.AspNetCore.WebAppBuilder

The webApp computation expression lets you define ASP.NET Core web applications in a succinct and declarative way, minimizing code, maximizing readability, and providing a simple but thorough set of escape hatches, so you can always drop down to the raw ASP.NET Core APIs when you need to.

Installation

Get it on NuGet: FSharp.AspNetCore.WebAppBuilder.

dotnet add package FSharp.AspNetCore.WebAppBuilder

API documentation

See the API documentation here.

Examples

open FSharp.AspNetCore.Builder

Hello, world.

Program.fs

let app =
    webApp {
        get "/hello" (fun () -> "🌎")
        post "/chars" (fun char -> printf $"%c{char}")
        delete "/chars" (fun count -> printf $"""{String.replicate count "\b \b"}""")
    }

app.Run ()

Progressively enhance your minimal API endpoints with additional metadata and configuration while keeping it simple.

Program.fs

let app =
    webApp {
        get "/hello" [
            Status200OK, typeof<string>
            Status404NotFound, null
        ] (fun () ->
            if DateTime.Today.DayOfWeek = DayOfWeek.Monday then Results.NotFound ()
            else Results.Ok "🌎"
        ) (fun routeHandler ->
            routeHandler.WithOpenApi (fun op -> op.Description <- "Hello, 🌎 — unless it's Monday."; op)
            routeHandler.AllowAnonymous ()
        )
    }

app.Run ()

Wire up your dependencies in a succinct and declarative way.

Program.fs

let app =
    webApp {
        connectionString "SqlDb" SqlConnectionString
        configurationValue "AppSettings:SqlCommandTimeout" SqlCommandTimeout
        singleton Id.NewId
        singleton typeof<IDataAccess> typeof<DataAccess>

        get "/xs" (fun (db : IDataAccess) -> Results.Ok (db.GetAll ()))

        get "/xs/{id}" (fun (db : IDataAccess) id ->
            db.TryGet id
            |> Option.map Results.Ok
            |> Option.defaultWith Results.NotFound)

        post "/xs" (fun (db : IDataAccess) newId x ->
            let x = db.Create (newId (), x)
            Results.Created ($"/xs/{x.Id}", x))

        delete "/xs/{id}" (fun (db : IDataAccess) id -> Results.NoContent (db.Delete id))
    }

app.Run ()

Easily evolve your code to use Swagger UI, heavyweight controllers, and enable dead simple integration testing, keeping your app's definition clean and simple without giving up the freedom to use arbitrary first- or third-party APIs on the underlying Microsoft.AspNetCore.Builder constructs.

Program.fs

let app configureBuilder =
    webApp {
        connectionString "SqlDb" SqlConnectionString
        configurationValue "AppSettings:SqlCommandTimeout" SqlCommandTimeout
        singleton Id.NewId
        singleton typeof<IDataAccess> typeof<DataAccess>
        logging (fun logging -> logging.AddConsole ())

        services (fun services ->
            services.AddEndpointsApiExplorer ()
            services.AddSwaggerGen ()
            services.AddControllers ())

        buildWith configureBuilder

        webApp (fun app ->
            app.UseSwagger ()
            app.UseSwaggerUI ()
            app.MapControllers ())
    }

(app ignore).Run ()

Tests.fs

[<Property>]
let ``POST /clowns returns 400 Bad Request if the shoe size is too small`` (Bad clown) =
    task {
        use app = Program.app (fun builder -> builder.WebHost.UseTestServer ())
        do! app.StartAsync ()
        use client = app.GetTestClient ()
        use! badRequest = client.PostAsJsonAsync ("clowns", clown)
        Assert.Equal (Status400BadRequest, badRequest.StatusCode)
        let! problem = badRequest.Content.ReadFromJsonAsync<ValidationProblemDetails> ()
        match Assert.Contains ("shoeSize", problem.Errors) with
        | [|_|] -> ()
        | unexpected -> failwith $"Expected a single error message but got: %A{unexpected}"
    }
    |> Async.AwaitTask
    |> Async.RunSynchronously

See Examples for some more realistic (and runnable) examples.

The webApp computation expression

The library includes custom operations for each of the top-level properties of Microsoft.AspNetCore.Builder.WebApplicationBuilder, one for the builder itself, and one for the Microsoft.AspNetCore.Builder.WebApplication once it's been built:

A few more specialized custom operations are provided to make certain common scenarios more concise, including:

See the API documentation for the full set of supported operations, with examples.

The number of other special cases that could be added is practically infinite, corresponding to the countless first- and third-party .Add* extension methods and their even-more-countless overloads—scoped, transient, serverSideBlazor… A few more may be added, but it is unlikely to be many.

You can always call any extension method on IServiceCollection you like inside the services operation, for example:

services (fun services ->
    services.AddScoped<MyScopedService> ()
    services.AddHsts ()
    services.AddOptions ()
    // Etc., etc.
)

The same is true for any of the other top-level properties of Microsoft.AspNetCore.Builder.WebApplicationBuilder, or the built Microsoft.AspNetCore.Builder.WebApplication.

Minimal APIs endpoint support

Add endpoints to your web app using custom operations corresponding to the MapGet, MapPost, etc., extension methods. Currently supported: get, post, put, delete, and patch.

All endpoint operations have overloads that take a (int * Type) list for specifying which status codes and types the endpoint produces, as well as overloads accepting a function that can be used to further configure the route handler.

open Microsoft.AspNetCore.Http.StatusCodes

let app =
    webApp {
        get "/hello" (fun () -> "🌎")

        get "/clowns"
            [Status200OK, typeof<seq<Dtos.Get.Clown>>]
            (fun (db : IDataAccess) -> Results.Ok (db.GetAll ()))

        post "/clowns" [
            Status201Created,             typeof<Dtos.Get.Clown>
            Status400BadRequest,          typeof<ValidationProblemDetails>
            Status409Conflict,            typeof<string>
            Status500InternalServerError, typeof<ProblemDetails>
        ] (fun (logger : ILogger<Program>) mkId (db : IDataAccess) clown ->
            // ...
            Results.Created ($"/clowns/{id}", clown)
        ) (fun routeHandler ->
            routeHandler.Accepts (typeof<Dtos.Create.Clown>, MediaTypeNames.Application.Json)
            routeHandler.AddFilter<MyEndpointFilter> ()
        )
    }

Adding your own custom operations to the webApp builder

If you like, you can always add a custom operation corresponding to any first- or third-party API you choose by extending the WebAppBuilder yourself:

type WebAppBuilder with
    [<CustomOperation("scoped")>]
    member _.Scoped (builder : WebApplicationBuilder, serviceType : Type, implementationType : Type) =
        ignore <| builder.Services.AddScoped (serviceType, implementationType)
        builder
let app =
    webApp {
        scoped typeof<IScopedService> typeof<ScopedService>
    }

Rationale

❔: Why use a computation expression? Why not write an F# wrapper using "functions and data"? I don't see anything monadic or applicative happening here.

Functions and data are absolutely more straightforward, robust, and maintainable than computation expressions with ad hoc custom operations—most of the time. But we're not designing a whole new web framework here: we're just trying to make the extensive functionality of the ASP.NET Core ecosystem a bit nicer to use in F#. There are two main things at play:

  1. Method overloads.

    The existing set of ASP.NET Core APIs, including the minimal APIs functionality, leans heavily on pervasive method overloading. Trying to wrap every overload in the ever-growing set of first- and third-party APIs using distinctly-named module functions, or else trying to plaster them over using some other kind of data structure, no matter how clever, would be a losing battle.

    Custom operations in computation expressions support method overloading while papering over some of the rough edges you run into consuming mutable builder-style APIs in F#.

  2. The builder pattern.

    Again, APIs using the mutable builder pattern are just not much fun to consume in F#. In the language they were originally designed for, you don't need to explicitly discard the final return value, but in F# you do. Repeatedly. Sometimes 20 times in a single Program.fs file.

    A small amount of cleverness with custom operations (and a #nowarn "20") really does work magic:

    services (fun s -> s.AddScoped<MyScopedService> ())
    

    The function that the services operation (or most of the others) takes can have any return type; it gets magically swallowed, because it doesn't matter. It shouldn't be so satisfying, but it is. Just try it the old way, piping it into |> ignore every time, and then come back.

Multiple items
namespace Microsoft.FSharp

--------------------
namespace FSharp
val app: obj
Multiple items
val char: value: 'T -> char (requires member op_Explicit)

--------------------
type char = System.Char
val printf: format: Printf.TextWriterFormat<'T> -> 'T
module String from Microsoft.FSharp.Core
val replicate: count: int -> str: string -> string
val typeof<'T> : System.Type
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
val id: x: 'T -> 'T
module Option from Microsoft.FSharp.Core
val map: mapping: ('T -> 'U) -> option: 'T option -> 'U option
val defaultWith: defThunk: (unit -> 'T) -> option: 'T option -> 'T
val app: configureBuilder: 'a -> 'b
val configureBuilder: 'a
val ignore: value: 'T -> unit
val clown: obj
val task: TaskBuilder
val app: System.IAsyncDisposable
val client: System.IAsyncDisposable
val badRequest: System.IAsyncDisposable
val problem: obj
val unexpected: obj array
val failwith: message: string -> 'T
Multiple items
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * obj -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...

--------------------
type Async<'T>
static member Async.AwaitTask: task: System.Threading.Tasks.Task -> Async<unit>
static member Async.AwaitTask: task: System.Threading.Tasks.Task<'T> -> Async<'T>
static member Async.RunSynchronously: computation: Async<'T> * ?timeout: int * ?cancellationToken: System.Threading.CancellationToken -> 'T
namespace Microsoft
Multiple items
val seq: sequence: 'T seq -> 'T seq

--------------------
type 'T seq = System.Collections.Generic.IEnumerable<'T>
Multiple items
type CustomOperationAttribute = inherit Attribute new: name: string -> CustomOperationAttribute member AllowIntoPattern: bool with get, set member IsLikeGroupJoin: bool with get, set member IsLikeJoin: bool with get, set member IsLikeZip: bool with get, set member JoinConditionWord: string with get, set member MaintainsVariableSpace: bool with get, set member MaintainsVariableSpaceUsingBind: bool with get, set member Name: string

--------------------
new: name: string -> CustomOperationAttribute

Type something to start searching.