Logo Orsak

OpenAPI

ASP.NET Core's OpenAPI generator, Microsoft.AspNetCore.OpenApi, describes minimal API endpoints from the parameters and return type of their handler. Endpoints mapped with Orsak.AspNetCore's MapEffectEndpoints have a request delegate instead, so the generator leaves them out. Orsak.AspNetCore.OpenApi adds them, describing each from what can be read from it: its route, the function handling it, that function's XML docs, the type of its effect, and the responses its runner describes. Explicit metadata, such as WithName or Produces, comes first.

It needs .NET 10, and Microsoft.AspNetCore.OpenApi 10.0.14 or later.

This page is a script: it runs with dotnet fsi docs/openapi.fsx once Orsak is built, after dotnet fsi docs/.tools/aspnetcore-references.fsx, as for Web APIs, and the output shown is from such a run.

Setting up

Reference Orsak.AspNetCore.OpenApi, and add the effect endpoints to the document with AddEffectEndpoints. AddFSharpSchemas is optional, and describes F# types as System.Text.Json writes them:

builder.Services.AddOpenApi(fun options -> options.AddEffectEndpoints().AddFSharpSchemas() |> ignore)

app.MapOpenApi()

The endpoints are found through ASP.NET Core's routing, which knows of them once UseEndpoints has registered them, as a started application does.

An API to describe

The API from Web APIs looks orders up. Its handlers return the order itself, so the type of their effect tells what a successful response holds:

open System
open System.Text.Json.Nodes
open System.Threading.Tasks
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Mvc
open Microsoft.AspNetCore.OpenApi
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Logging
open Microsoft.OpenApi
open Orsak
open Orsak.AspNetCore
open Orsak.AspNetCore.OpenApi
open Orsak.AspNetCore.V2

type OrderLine = { Product: string; Quantity: int }

type Order = {
    Id: int
    Lines: OrderLine list
    Note: string option
}

type OrderError =
    | NotFound of id: int
    | Invalid of reason: string

let getOrder (id: int) : Effect<unit, Order, OrderError> = eff {
    if id = 1 then
        return { Id = 1; Lines = [ { Product = "mug"; Quantity = 2 } ]; Note = None }
    else
        return! Error(NotFound id)
}

let getOrderLine (id: int) (line: int) : Effect<unit, OrderLine, OrderError> = eff {
    let! order = getOrder id

    match List.tryItem line order.Lines with
    | Some l -> return l
    | None -> return! Error(NotFound id)
}

The runner's responses

Only the runner knows how an effect's result and errors become responses. A runner that implements IDescribeResponses says so, given the type of each endpoint's effect. This one writes the result as JSON, and the errors as problem details:

type Runner =
    | Runner

    static member inline ( *>> )(effect: Effect<unit, 'a, OrderError>, Runner) =
        RequestDelegate(fun ctx -> task {
            let! response =
                effect
                |> Effect.map Results.Ok
                |> Effect.run ()

            let response =
                match response with
                | Ok response -> response
                | Error(NotFound _) -> Results.Problem(statusCode = 404)
                | Error(Invalid reason) -> Results.Problem(reason, statusCode = 400)

            return! response.ExecuteAsync ctx
        })

    interface IDescribeResponses with
        member _.Responses effectType =
            match effectType.GetGenericArguments() with
            | [| _; result; _ |] -> [
                ResponseDescription.Json(200, result)
                ResponseDescription.Json(400, typeof<ProblemDetails>, "The request is invalid")
                ResponseDescription.Json(404, typeof<ProblemDetails>, "Not found")
              ]
            | _ -> []

Each endpoint gets the responses as metadata, a ResponseDescription each, which is also an IProducesResponseTypeMetadata for other tools that read ApiExplorer's metadata.

openApiOf maps endpoints, and returns the document the generator creates, as JSON:

let openApiOf (configure: OpenApiOptions -> OpenApiOptions) (endpoints: Orsak.AspNetCore.Endpoint list) =
    let builder = WebApplication.CreateSlimBuilder()
    builder.Logging.ClearProviders() |> ignore
    builder.Services.AddOpenApi(fun options -> configure options |> ignore) |> ignore
    let app = builder.Build()
    app.UseRouting().UseEndpoints(fun e -> e.MapEffectEndpoints endpoints) |> ignore

    let provider = app.Services.GetRequiredKeyedService<IOpenApiDocumentProvider>("v1")
    let document = provider.GetOpenApiDocumentAsync().Result
    document.SerializeAsJsonAsync(OpenApiSpecVersion.OpenApi3_1).Result |> JsonNode.Parse

let show (node: JsonNode) = printfn "%s" (node.ToJsonString(Text.Json.JsonSerializerOptions(WriteIndented = true)))

/// The node at the end of `keys`, such as the path item of a route.
let at (keys: string list) (node: JsonNode) = keys |> List.fold (fun (n: JsonNode) key -> n[key]) node

let endpoints = [
    Runner.RouteGet2("/orders/%i", getOrder)
    Runner.RouteGet2("/orders/%i/lines/%i", getOrderLine)
]

What is inferred

With nothing written for OpenAPI, each endpoint is an operation named after its handler, tagged with the handler's module, with its route values as path parameters, typed by their format specifiers, and the responses of its runner:

let document = openApiOf (fun o -> o.AddEffectEndpoints()) endpoints
document |> at [ "paths"; "/orders/{id}/lines/{line}" ] |> show
{
  "get": {
    "tags": [
      "FSI_0003"
    ],
    "operationId": "getOrderLine",
    "parameters": [
      {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "format": "int32"
        }
      },
      {
        "name": "line",
        "in": "path",
        "required": true,
        "schema": {
          "type": "integer",
          "format": "int32"
        }
      }
    ],
    "responses": {
      "200": {
        "description": "OK",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/OrderLine"
            }
          }
        }
      },
      "400": {
        "description": "The request is invalid",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ProblemDetails"
            }
          }
        }
      },
      "404": {
        "description": "Not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ProblemDetails"
            }
          }
        }
      }
    }
  }
}

The tag is the module of getOrderLine, which in a script is one F# Interactive names. The schemas are components of the document, named as the generator names its own, by OpenApiOptions.CreateSchemaReferenceId.

Without a runner that describes its responses, an operation has a 200 with the effect's result as its body, unless the result is unit or an IResult, which tell nothing of the body.

From the XML docs

The handler's XML docs describe the operation, when the project writes them, with GenerateDocumentationFile:

<PropertyGroup>
    <GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

They are read from the .xml file next to the handler's assembly. A script has none, so this part is from a project, the one of Orsak's tests:

/// <summary>Gets an order, with its lines.</summary>
/// <remarks>
/// The lines are in the order they were added. See <see cref="T:Orsak.Tests.OpenApiInferenceTests.Orders.OrderLine"/>.
/// </remarks>
/// <param name="orderId">The id of the order.</param>
/// <returns>The order.</returns>
/// <response code="404">No order has the id.</response>
let getOrder (orderId: int) : Effect<unit, Order, OrderError> = ...

The <summary> is the operation's summary, the <remarks> its description, each <param> describes the route value of the same name, <returns> the success response, and each <response code="..."> the response with that status code, which it adds if the runner doesn't describe one:

"summary": "Gets an order, with its lines.",
"description": "The lines are in the order they were added. See OrderLine.",
"operationId": "getOrder",
"parameters": [ { "name": "orderId", "in": "path", "description": "The id of the order.", ... } ],
"responses": {
  "200": { "description": "The order.", ... },
  "404": { "description": "No order has the id.", ... }
}

F# only reads a doc comment as XML when it starts with a tag. A comment of plain text is its summary, tags and all, so write <summary> when the comment has other tags.

Explicit metadata

Metadata on the endpoint comes before what is inferred, and before the docs. The standard conventions apply, through AddConvention, and a ResponseDescription adds a response, or replaces the runner's for its status code:

let described =
    Runner
        .RouteGet2("/orders/%i", getOrder)
        .WithName("GetOrder")
        .WithMetadata(ResponseDescription.Empty(404, "No order has the id"))
        .AddConvention(fun b -> b.WithSummary("Gets an order").WithTags("Orders"))

let explicitDocument = openApiOf (fun o -> o.AddEffectEndpoints()) [ described ]
let operation = explicitDocument |> at [ "paths"; "/orders/{id}"; "get" ]

printfn "%s, %s, %O" (operation["operationId"].ToString()) (operation["summary"].ToString()) (operation["tags"].ToJsonString())

for response in operation["responses"].AsObject() do
    printfn "%s: %s" response.Key (response.Value["description"].ToString())
GetOrder, Gets an order, ["Orders"]
200: OK
400: The request is invalid
404: No order has the id

The precedence, for each part of an operation:

Part

First

Then

Then

Operation id

WithName

the handler's name

Tags

WithTags

the handler's module

Summary

WithSummary

<summary>

Description

WithDescription

<remarks>

Path parameters

<param> describes them

typed by their route constraints

Responses

ResponseDescription, Produces

<response code="...">

the runner's, or a 200 of the effect's result

Response descriptions

the endpoint's metadata

the docs

the runner's

A handler's name is only an operation id once, as for a function handling two routes, since operation ids are unique. An endpoint is in the documents of its group name, set with WithGroupName, or in every document without one, and ExcludeFromDescription leaves it out.

F# types

The generator describes types as System.Text.Json writes them, and knows .NET's types, not F#'s. An option is written as its value, or null for none, but the generator describes it as a component of its own, and requires it in a record, though a missing option field is read as None. An F# list is never null, but is described as one that can be:

document |> at [ "components"; "schemas"; "Order" ] |> show
{
  "required": [
    "id",
    "lines",
    "note"
  ],
  "type": "object",
  "properties": {
    "id": {
      "pattern": "^-?(?:0|[1-9]\\d
let fsharpDocument = openApiOf (fun o -> o.AddEffectEndpoints().AddFSharpSchemas()) endpoints
fsharpDocument |> at [ "components"; "schemas"; "Order" ] |> show
{
  "required": [
    "id",
    "lines"
  ],
  "type": "object",
  "properties": {
    "id": {
      "pattern": "^-?(?:0|[1-9]\\d
val ignore: value: 'T -> unit
namespace System
namespace System.Text
namespace System.Text.Json
namespace System.Text.Json.Nodes
namespace System.Threading
namespace System.Threading.Tasks
namespace Microsoft
namespace Microsoft.AspNetCore
namespace Microsoft.AspNetCore.Builder
namespace Microsoft.AspNetCore.Http
namespace Microsoft.AspNetCore.Mvc
namespace Microsoft.AspNetCore.OpenApi
namespace Microsoft.Extensions
namespace Microsoft.Extensions.DependencyInjection
namespace Microsoft.Extensions.Logging
namespace Microsoft.OpenApi
namespace Orsak
namespace Orsak.AspNetCore
namespace Orsak.AspNetCore.OpenApi
namespace Orsak.AspNetCore.V2
type OrderLine = { Product: string Quantity: int }
Multiple items
val string: value: 'T -> string

--------------------
type string = String
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
type Order = { Id: int Lines: OrderLine list Note: string option }
type 'T list = List<'T>
type 'T option = Option<'T>
type OrderError = | NotFound of id: int | Invalid of reason: string
val id: x: 'T -> 'T
val getOrder: id: int -> Effect<unit,Order,OrderError>
val id: int
Multiple items
union case Effect.Effect: EffectDelegate<'r,'a,'e> -> Effect<'r,'a,'e>

--------------------
module Effect from Orsak
<summary> Functions for running, combining, recovering and repeating effects. </summary>

--------------------
type Effect = static member Create: f: ('a -> Task<Result<'b,'e>>) -> Effect<'a,'b,'e> + 2 overloads static member Error: error: 'e -> Effect<'a,'b,'e>
<summary> Creates effects. <c>Effect.Create</c> turns a function of an environment into an effect, and is how effects are made from the interfaces that describe side effects. The function may return a plain value, a <c>Result</c>, a <c>Task</c>, a <c>ValueTask</c> or an <c>Async</c>, of a value or of a <c>Result</c>; returning a <c>Result</c> lets the effect fail. </summary>
<example> The usual pattern: an interface describing the side effect, a provider interface exposing it, and a function creating the effect, which requires any environment that implements the provider (<c>#IConsoleProvider</c>). <code lang="fsharp"> type IConsole = abstract ReadLine: unit -&gt; string abstract WriteLine: string -&gt; unit type IConsoleProvider = abstract Console: IConsole module Console = let readLine () = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.ReadLine()) let writeLine line = Effect.Create(fun (p: #IConsoleProvider) -&gt; p.Console.WriteLine line) </code> The Orsak.Myriad generators can write the functions of such a module from the interface. </example>


--------------------
type Effect<'r,'a,'e> = | Effect of EffectDelegate<'r,'a,'e> member Run: r: 'r -> AsyncResult<'a,'e> member RunOrFail: r: 'r -> Task<'a> static member (<*>)<'env,'arg,'res,'a (requires member (+))> : f: Effect<'env,('arg -> 'res),'a0> * e: Effect<'env,'arg,'a0> -> Effect<'env,'res,'a0> (requires member (+)) static member (>>=) : h: Effect<'r,'a,'e> * f: ('a -> Effect<'r,'b,'e>) -> Effect<'r,'b,'e> static member Map: h: Effect<'r,'a,'e> * f: ('a -> 'b) -> Effect<'r,'b,'e> static member Return<'a,'b,'c> : a: 'a0 -> Effect<'b,'a0,'c> static member ap<'r,'a,'b,'e> : applicative: Effect<'r,('b -> 'a),'e> -> e: Effect<'r,'b,'e> -> Effect<'r,'a,'e>
<summary> Describes an effect that can either succeed with <typeparamref name="'a" />, or fail with <typeparamref name="'e" />. The effect is 'cold', and only starts when run with an <typeparamref name="'r" />. </summary>
<remarks> Effects are usually written with the <c>eff</c> computation expression, and created from interfaces that describe side effects with <c>Effect.Create</c>. The environment <typeparamref name="'r" /> is inferred from what the effect uses, and supplied when the effect is run. </remarks>
<example><code lang="fsharp"> let greet () = eff { let! name = Console.readLine () do! Console.writeLine $"Hello {name}" } // at the composition root, with an environment providing the console effects: let! result = greet () |&gt; Effect.run env </code></example>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
type unit = Unit
val eff: EffBuilder
<summary> The computation expression for writing effects. Binding (<c>let!</c>, <c>do!</c>, <c>return!</c>) works with other effects, and with <c>Task</c>, <c>ValueTask</c>, <c>Async</c> and <c>Result</c>, so .NET APIs can be used directly. It supports <c>for</c>, <c>while</c>, <c>use</c>, <c>try/with</c> and <c>try/finally</c>, and <c>and!</c> runs effects concurrently. A failed effect stops the rest of the expression, with its error. </summary>
<example><code lang="fsharp"> let placeOrder (order: Order) = eff { let! id = GuidGenerator.genGuid () let! now = Time.utcNow () do! Orders.save { order with Id = id; Placed = now } let! customer = Customers.load order.CustomerId and! stock = Stock.reserve order.Lines return id } </code></example>
union case Option.None: Option<'T>
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
union case OrderError.NotFound: id: int -> OrderError
val getOrderLine: id: int -> line: int -> Effect<unit,OrderLine,OrderError>
val line: int
val order: Order
Multiple items
module List from Microsoft.FSharp.Collections

--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
val tryItem: index: int -> list: 'T list -> 'T option
Order.Lines: OrderLine list
union case Option.Some: Value: 'T -> Option<'T>
val l: OrderLine
type Runner = | Runner interface IDescribeResponses static member ( *>> ) : effect: Effect<unit,'a,OrderError> * Runner: Runner -> RequestDelegate
val effect: Effect<unit,'a,OrderError>
'a
Multiple items
union case Runner.Runner: Runner

--------------------
type Runner = | Runner interface IDescribeResponses static member ( *>> ) : effect: Effect<unit,'a,OrderError> * Runner: Runner -> RequestDelegate
type RequestDelegate = new: object: obj * method: nativeint -> unit member BeginInvoke: context: HttpContext * callback: AsyncCallback * object: obj -> IAsyncResult member EndInvoke: result: IAsyncResult -> Task member Invoke: context: HttpContext -> Task
val ctx: HttpContext
val task: TaskBuilder
val response: Result<IResult,OrderError>
val map: f: ('a -> 'b) -> e: Effect<'r,'a,'e> -> Effect<'r,'b,'e>
<summary> map of the effect. </summary>
<param name="f">The continuation function to run in case <paramref name="e" /> succeeds.</param>
<param name="e">The effect</param>
type Results = static member Accepted: ?uri: string * ?value: obj -> IResult + 1 overload static member AcceptedAtRoute: ?routeName: string * ?routeValues: obj * ?value: obj -> IResult + 3 overloads static member BadRequest: ?error: obj -> IResult + 1 overload static member Bytes: contents: byte array * ?contentType: string * ?fileDownloadName: string * ?enableRangeProcessing: bool * ?lastModified: Nullable<DateTimeOffset> * ?entityTag: EntityTagHeaderValue -> IResult + 1 overload static member Challenge: ?properties: AuthenticationProperties * ?authenticationSchemes: IList<string> -> IResult static member Conflict: ?error: obj -> IResult + 1 overload static member Content: content: string * contentType: string * contentEncoding: Encoding -> IResult + 2 overloads static member Created: unit -> IResult + 4 overloads static member CreatedAtRoute: ?routeName: string * ?routeValues: obj * ?value: obj -> IResult + 3 overloads static member File: fileContents: byte array * ?contentType: string * ?fileDownloadName: string * ?enableRangeProcessing: bool * ?lastModified: Nullable<DateTimeOffset> * ?entityTag: EntityTagHeaderValue -> IResult + 2 overloads ...
Results.Ok<'TValue>(value: 'TValue) : IResult
Results.Ok(?value: obj) : IResult
val run: env: 'r -> e: Effect<'r,'a,'e> -> AsyncResult<'a,'e>
<summary> Starts the effect. </summary>
<typeparam name="'r"> The environment required to run the effect </typeparam>
<typeparam name="'a"> The resulting type when the effect runs successfully </typeparam>
<typeparam name="'e"> The resulting type when the effect fails</typeparam>
<param name="env">The environment needed to start the effect</param>
<param name="e">The effect to run</param>
<example><code lang="fsharp"> task { match! placeOrder order |&gt; Effect.run env with | Ok id -&gt; printfn $"Placed {id}" | Error err -&gt; printfn $"Failed: {err}" } </code></example>
val response: IResult
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
Results.Problem(problemDetails: ProblemDetails) : IResult
Results.Problem(?detail: string, ?instance: string, ?statusCode: Nullable<int>, ?title: string, ?``type`` : string, ?extensions: Collections.Generic.KeyValuePair<string,obj> seq) : IResult
Results.Problem(detail: string, instance: string, statusCode: Nullable<int>, title: string, ``type`` : string, extensions: Collections.Generic.IDictionary<string,obj>) : IResult
union case OrderError.Invalid: reason: string -> OrderError
val reason: string
type IDescribeResponses = abstract Responses: effectType: Type -> ResponseDescription list
<summary> A runner that describes the responses it gives, for OpenAPI: only the runner knows how an effect's result and errors become responses. When the runner of an endpoint implements it, the endpoint gets the responses as metadata. </summary>
<example><code lang="fsharp"> type Runner = | RunWith of (HttpContext -&gt; AppEnv) static member ( *&gt;&gt; )(effect: Effect&lt;AppEnv, 'a, AppError&gt;, RunWith createEnv) = ... interface IDescribeResponses with member _.Responses effectType = match effectType.GetGenericArguments() with | [| _; result; _ |] -&gt; [ ResponseDescription.Json(200, result) ResponseDescription.Json(404, typeof&lt;ProblemDetails&gt;, "Not found") ] | _ -&gt; [] </code></example>
val effectType: Type
Type.GetGenericArguments() : Type array
val result: Type
type ResponseDescription = { StatusCode: int BodyType: Type option ContentTypes: string list Description: string option } interface IProducesResponseTypeMetadata static member Empty: statusCode: int * ?description: string -> ResponseDescription static member Json: statusCode: int * bodyType: Type * ?description: string -> ResponseDescription
<summary> A response an endpoint can give: its status code, the type of its body, if it has one, and what it means. As endpoint metadata, it is an <see cref="T:Microsoft.AspNetCore.Http.Metadata.IProducesResponseTypeMetadata" />, which ApiExplorer and OpenAPI generators read. </summary>
static member ResponseDescription.Json: statusCode: int * bodyType: Type * ?description: string -> ResponseDescription
val typeof<'T> : Type
Multiple items
type ProblemDetails = new: unit -> unit member Detail: string member Extensions: IDictionary<string,obj> member Instance: string member Status: Nullable<int> member Title: string member Type: string

--------------------
ProblemDetails() : ProblemDetails
val openApiOf: configure: (OpenApiOptions -> OpenApiOptions) -> endpoints: Endpoint list -> JsonNode
val configure: (OpenApiOptions -> OpenApiOptions)
Multiple items
type OpenApiOptions = new: unit -> unit member AddDocumentTransformer<'TTransformerType (requires 'TTransformerType :> IOpenApiDocumentTransformer)> : unit -> OpenApiOptions + 2 overloads member AddOperationTransformer<'TTransformerType (requires 'TTransformerType :> IOpenApiOperationTransformer)> : unit -> OpenApiOptions + 2 overloads member AddSchemaTransformer<'TTransformerType (requires 'TTransformerType :> IOpenApiSchemaTransformer)> : unit -> OpenApiOptions + 2 overloads static member CreateDefaultSchemaReferenceId: jsonTypeInfo: JsonTypeInfo -> string member CreateSchemaReferenceId: Func<JsonTypeInfo,string> member DocumentName: string member OpenApiVersion: OpenApiSpecVersion member ShouldInclude: Func<ApiDescription,bool>
<summary> Options to support the construction of OpenAPI documents. </summary>

--------------------
OpenApiOptions() : OpenApiOptions
val endpoints: Endpoint list
Multiple items
union case Endpoint.Endpoint: {| conventions: (IEndpointConventionBuilder -> IEndpointConventionBuilder); path: string; requestDelegate: RequestDelegate; verb: string |} -> Endpoint

--------------------
type Endpoint = | Endpoint of {| conventions: (IEndpointConventionBuilder -> IEndpointConventionBuilder); path: string; requestDelegate: RequestDelegate; verb: string |} member Add: f: Action<EndpointBuilder> -> Endpoint member AddConvention: f: (IEndpointConventionBuilder -> IEndpointConventionBuilder) -> Endpoint member AllowAnonymous: unit -> Endpoint member RequireCors: name: string -> Endpoint + 1 overload member RequiresAuthorization: unit -> Endpoint member WithDisplayName: name: string -> Endpoint + 1 overload member WithGroupName: name: string -> Endpoint member WithMetadata: items: obj array -> Endpoint member WithName: name: string -> Endpoint
<summary> An endpoint created by one of the <c>Route*</c> methods, such as <c>RouteGet</c>, to be mapped with <c>MapEffectEndpoints</c>. Its methods add conventions, as on ASP.NET Core's endpoint convention builders. </summary>
<example><code lang="fsharp"> runner.RouteGet("/users/%i", getUser) .RequiresAuthorization() .WithName("GetUser") </code></example>
val builder: WebApplicationBuilder
type WebApplication = interface IHost interface IDisposable interface IApplicationBuilder interface IEndpointRouteBuilder interface IAsyncDisposable member DisposeAsync: unit -> ValueTask member Run: ?url: string -> unit member RunAsync: ?url: string -> Task member StartAsync: ?cancellationToken: CancellationToken -> Task member StopAsync: ?cancellationToken: CancellationToken -> Task ...
WebApplication.CreateSlimBuilder() : WebApplicationBuilder
WebApplication.CreateSlimBuilder(options: WebApplicationOptions) : WebApplicationBuilder
WebApplication.CreateSlimBuilder(args: string array) : WebApplicationBuilder
property WebApplicationBuilder.Logging: ILoggingBuilder with get
(extension) ILoggingBuilder.ClearProviders() : ILoggingBuilder
property WebApplicationBuilder.Services: IServiceCollection with get
(extension) IServiceCollection.AddOpenApi() : IServiceCollection
(extension) IServiceCollection.AddOpenApi(documentName: string) : IServiceCollection
(extension) IServiceCollection.AddOpenApi(configureOptions: Action<OpenApiOptions>) : IServiceCollection
(extension) IServiceCollection.AddOpenApi(documentName: string, configureOptions: Action<OpenApiOptions>) : IServiceCollection
val options: OpenApiOptions
val app: WebApplication
WebApplicationBuilder.Build() : WebApplication
(extension) IApplicationBuilder.UseRouting() : IApplicationBuilder
val e: AspNetCore.Routing.IEndpointRouteBuilder
static member EffectRunnerExtensions.MapEffectEndpoints: builder: AspNetCore.Routing.IEndpointRouteBuilder * endpoints: Endpoint list -> unit
val provider: IOpenApiDocumentProvider
property WebApplication.Services: IServiceProvider with get
(extension) IServiceProvider.GetRequiredKeyedService<'T>(serviceKey: obj) : 'T
(extension) IServiceProvider.GetRequiredKeyedService(serviceType: Type, serviceKey: obj) : obj
type IOpenApiDocumentProvider = override GetOpenApiDocumentAsync: ?cancellationToken: CancellationToken -> Task<OpenApiDocument>
<summary> Represents a provider for OpenAPI documents that can be used by consumers to retrieve generated OpenAPI documents at runtime. </summary>
val document: OpenApiDocument
IOpenApiDocumentProvider.GetOpenApiDocumentAsync(?cancellationToken: Threading.CancellationToken) : Task<OpenApiDocument>
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
(extension) OpenApiDocument.SerializeAsJsonAsync<'T (requires 'T :> IOpenApiSerializable)>(specVersion: OpenApiSpecVersion, ?cancellationToken: Threading.CancellationToken) : Task<string>
(extension) OpenApiDocument.SerializeAsJsonAsync<'T (requires 'T :> IOpenApiSerializable)>(stream: IO.Stream, specVersion: OpenApiSpecVersion, ?cancellationToken: Threading.CancellationToken) : Task
type OpenApiSpecVersion = | OpenApi2_0 = 0 | OpenApi3_0 = 1 | OpenApi3_1 = 2
<summary> Represents versions of OpenAPI specification. </summary>
field OpenApiSpecVersion.OpenApi3_1: OpenApiSpecVersion = 2
type JsonNode = member AsArray: unit -> JsonArray member AsObject: unit -> JsonObject member AsValue: unit -> JsonValue member DeepClone: unit -> JsonNode member GetElementIndex: unit -> int member GetPath: unit -> string member GetPropertyName: unit -> string member GetValue<'T> : unit -> 'T member GetValueKind: unit -> JsonValueKind member ReplaceWith<'T> : value: 'T -> unit ...
<summary>The base class that represents a single node within a mutable JSON document.</summary>
JsonNode.Parse(reader: byref<Text.Json.Utf8JsonReader>, ?nodeOptions: Nullable<JsonNodeOptions>) : JsonNode
JsonNode.Parse( json: string, ?nodeOptions: Nullable<JsonNodeOptions>, ?documentOptions: Text.Json.JsonDocumentOptions) : JsonNode
JsonNode.Parse(utf8Json: ReadOnlySpan<byte>, ?nodeOptions: Nullable<JsonNodeOptions>, ?documentOptions: Text.Json.JsonDocumentOptions) : JsonNode
JsonNode.Parse(utf8Json: IO.Stream, ?nodeOptions: Nullable<JsonNodeOptions>, ?documentOptions: Text.Json.JsonDocumentOptions) : JsonNode
val show: node: JsonNode -> unit
val node: JsonNode
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
JsonNode.ToJsonString(?options: Text.Json.JsonSerializerOptions) : string
Multiple items
type JsonSerializerOptions = new: unit -> unit + 2 overloads member AddContext<'TContext (requires default constructor and 'TContext :> JsonSerializerContext)> : unit -> unit member GetConverter: typeToConvert: Type -> JsonConverter member GetTypeInfo: ``type`` : Type -> JsonTypeInfo member MakeReadOnly: unit -> unit + 1 overload member TryGetTypeInfo: ``type`` : Type * typeInfo: byref<JsonTypeInfo> -> bool member AllowDuplicateProperties: bool member AllowOutOfOrderMetadataProperties: bool member AllowTrailingCommas: bool member Converters: IList<JsonConverter> ...
<summary>Provides options to be used with <see cref="T:System.Text.Json.JsonSerializer" />.</summary>

--------------------
Text.Json.JsonSerializerOptions() : Text.Json.JsonSerializerOptions
Text.Json.JsonSerializerOptions(defaults: Text.Json.JsonSerializerDefaults) : Text.Json.JsonSerializerOptions
Text.Json.JsonSerializerOptions(options: Text.Json.JsonSerializerOptions) : Text.Json.JsonSerializerOptions
val at: keys: string list -> node: JsonNode -> JsonNode
 The node at the end of `keys`, such as the path item of a route.
val keys: string list
val fold<'T,'State> : folder: ('State -> 'T -> 'State) -> state: 'State -> list: 'T list -> 'State
val n: JsonNode
val key: string
val document: JsonNode
val o: OpenApiOptions
static member OpenApiOptionsExtensions.AddEffectEndpoints: options: OpenApiOptions -> OpenApiOptions
val described: Endpoint
static member ResponseDescription.Empty: statusCode: int * ?description: string -> ResponseDescription
val b: IEndpointConventionBuilder
(extension) IEndpointConventionBuilder.WithSummary<'TBuilder (requires 'TBuilder :> IEndpointConventionBuilder)>(summary: string) : 'TBuilder
val explicitDocument: JsonNode
val operation: JsonNode
val response: Collections.Generic.KeyValuePair<string,JsonNode>
property Collections.Generic.KeyValuePair.Key: string with get
<summary>Gets the key in the key/value pair.</summary>
<returns>A <typeparamref name="TKey" /> that is the key of the <see cref="T:System.Collections.Generic.KeyValuePair`2" />.</returns>
property Collections.Generic.KeyValuePair.Value: JsonNode with get
<summary>Gets the value in the key/value pair.</summary>
<returns>A <typeparamref name="TValue" /> that is the value of the <see cref="T:System.Collections.Generic.KeyValuePair`2" />.</returns>
val fsharpDocument: JsonNode

Type something to start searching.