Skip to content

akhansari/AspFeat

Repository files navigation

AspFeat NuGet Status ASP.NET Core 6.0

A modular and low ceremony toolkit for ASP .Net and F#.

  • Modular injection of services and middlewares.
  • Set of low ceremony ready-to-use setups.
  • Functional helpers over ASP .Net and nothing else.
  • Focused on Web APIs.

You can find examples in the samples folder.

Startup

In order to setup a feature properly, it's necessary to first add the services to IServiceCollection and then use the middlewares with IApplicationBuilder. The downside is that they are mixed with other features and moreover the order of calls are important, which makes everything complicated.

To keep the startup clean, the idea is to package features into modules and then expose the setup of WebApplicationBuilder and WebApplication as a tuple.

The end result is that ASP .Net startup has never been so easy:

[<EntryPoint>]
let main args =
    let configure bld = uhttp bld Get "/" (write "hello world")
    WebApp.run args [ Endpoint.feat configure ]

Swagger sample:

module Swagger =
    open Microsoft.AspNetCore.Builder
    open Microsoft.Extensions.DependencyInjection

    let feat () : Feat =
        fun builder ->
            builder.Services
                .AddEndpointsApiExplorer()
                .AddSwaggerGen()
            |> ignore
        ,
        fun app ->
            app
                .UseSwagger()
                .UseSwaggerUI()
            |> ignore

[<EntryPoint>]
let main args =
    [ Endpoint.feat configureEndpoints
      Swagger.feat () ]
    |> WebApp.run args

Endpoint Routing

AspFeat comes with several helpers that ease the use of the functional programming paradigm.
We do not intend to completely change your way of using ASP .Net but rather to offer a more nice and more F#-idiomatic way of using ASP .Net.
So you are not limited to AspFeat and you can still use Vanilla ASP .Net, if you need to.

For further information please refer to Microsoft Docs

Example

Without AspFeat toolkit:

let configureEndpoints (bld: IEndpointRouteBuilder) =
    bld.MapGet("/", RequestDelegate getHandler) |> ignore

With AspFeat toolkit:

let configureEndpoints bld =
    uhttp bld Get "/" getHandler

With the DSL:

let configureEndpoints bld =
    endpoints bld {
        get "/" getHandler
    }

With the OpenApi/Swagger DSL:

let getHandler =
    writeAsJson "hello world"

type World =
    [<ProducesResponseType(StatusCodes.Status200OK)>]
    abstract member GetHandler: unit -> string

let configureEndpoints bld =
    endpointsMetadata<World> bld {
        get "/" getHandler (nameof getHandler)
    }

Route values and JSON content injection

Instead of manually fetching data through HttpContext, it is possible to inject them into the handler.

  • httpf / uhttpf injects route values.
    • A single value is injected as is.
    • Multiple values are injected in order as a tuple.
  • httpj / uhttpj injects the deserialized JSON content.
  • httpfj / uhttpfj combines both.
let hello firstname = write $"Hello {firstname}"
let createGift gift = write $"Create a {gift}"
let goodbye (firstname, lastname) gift =
    write $"Goodbye {firstname} {lastname} and here is your {gift}"

let configureEndpoints bld =
    uhttpf  bld Get  "/hello/{firstname}" hello
    uhttpj  bld Post "/gift" createGift
    uhttpfj bld Put  "/goodbye/{firstname}/{lastname}" goodbye

Http Handlers

Composition

It is possible to combine http handlers.

Those with input injection are railwayed with Result.

  • Ok type can be any value.
  • Error type is Map<string, string list> and the error response is a json of problem details with the status code 422 Unprocessable Entity.

Normal with =>

let enrich (ctx: HttpContext) =
    ctx.Response.GetTypedHeaders().Set("X-Powered-By", "AspFeat")
    Task.CompletedTask

let configureEndpoints bld =
    uhttp bld Get "/" (enrich => write "hello world")

Single value injection with =|

Ok type could be:

  • A route value
  • A tuple of route values
  • A deserialized JSON model
  • Or any mapped value
let validateGetEcho id ctx =
    if id > 0
    then Ok id
    else Map [ ("Id", [ "Is negative or zero" ]) ] |> Error
    |> Task.FromResult

let getEcho id =
    write $"Echo {id}"

let configureEndpoints bld =
    uhttpf bld Get  "/{id:int}" (validateGetEcho =| getEcho)

Double value injection with =||

Ok type is a two-value tuple that is then passed to the next function as two parameters.
It could be:

  1. Fist, route values
  2. Second, deserialized JSON model
let validateCreateEcho id name ctx =
    if not (String.IsNullOrWhiteSpace name)
    then Ok (id, {| Id = id; Name = name |})
    else Map [ ("Name", [ "Is empty" ]) ] |> Error
    |> Task.FromResult

let createEcho id model =
    //...
    createdWith $"/{id}" model

let configureEndpoints bld =
    uhttpfj bld Post "/{id:int}" (validateCreateEcho =|| createEcho)