← Back to list

The Oxpecker 2

There is such a time of year when people temporarily stop their routine tasks and endeavors, find a warm and cozy place, and start waiting…

Vladimir Shchur · 2025-12-01 01:29 · 10 claps · 4.8 min read
#fsharp #oxpeckers #dotnet #aspnetcore #advent
Open on Medium ↗

The Oxpecker 2

Oxpecker is ready for new adventures

Oxpecker is ready for new adventures

There is such a time of year when people temporarily stop their routine tasks and endeavors, find a warm and cozy place, and start waiting for something good with awe and hope. 🎄

As you might have correctly guessed, I’m talking about F# developers and the traditional December Advent of articles about their beloved tool, and I’m very excited to open it this year with (hopefully) good news 🎉.

Oxpecker 2 has been released!

If you are unfamiliar with Oxpecker and its history, you can start with the previous materials: introduction, Blazor comparison, ViewEngine performance and the video about Oxpecker’s performance.

Quick recap

If you are not actively following Oxpecker’s development, let me quickly mention features added after the first introduction article:

GetLogger extension method allowing a categoryName

let myLogger = ctx.GetLogger("MyModule")

IResult native support

return! ctx.Write(TypedResults.Ok "Hello world")

Html streaming

return! ctx.WriteHtmlChunked(taskSeq { raw "1"; raw "2" })

Aria attributes support in ViewEngine

div(ariaErrorMessage = "error")

Htmx support

button(hxDelete= $"/contacts/{contactId}", hxPushUrl="true")

OpenApi support

routef "/text/{%s}" text |> addOpenApiSimple<unit, string>

configureEndpoint function

route "/text" (text "Hi") |> configureEndpoint _.WithName("GetText")

Model Validation

let! validationResult = ctx.BindAndValidateForm<MyDTO>()

Nullable reference types

All Oxpecker APIs have been annotated with NRT.

Performance

Various performance improvements were made for ViewEngine rendering as well as model binding.

Documentation site

[embed]*Oxpecker documentation *Backend and Frontend F# frameworks lanayx.github.io

Examples

Multiple example projects were added: traditional Weather app, CRUD with DI, HTMX Contact app, MCP integration and empty templates.

Fast forward to December 1st, 2025 and the new release of server-side packages. What is included?

Security improvements

Oxpecker 2 comes with CSRF protection enabled by default, if you add the built-in Antiforgery Middleware to the pipeline:

let configureApp (appBuilder: WebApplication) =
  appBuilder
    .UseRouting()
    .UseAntiforgery() // add between UseRouting and UseOxpecker
    .UseOxpecker(endpoints) |> ignore

let configureServices (services: IServiceCollection) =
  services
    .AddRouting()
    .AddAntiforgery() // don't forget about the DI
    .AddOxpecker()
  |> ignore

Default protection means that your POST, PUT and PATCH requests will be validated using AntiForgery Middleware, but the validation result will only be checked when bindForm or .BindForm is executed. This behavior matches the behavior of Minimal APIs framework, but you can still do manual validation using antiforgery.ValidateRequestAsync by writing custom EndpointMiddleware even before model binding or for other HTTP methods.

To place antiforgery token in a form as a hidden input, you should use the GetAntiforgeryInput extension method:

let myForm (ctx: HttpContext) =
  form() {
    ctx.GetAntiforgeryInput()
    input(type'="text", name="Message", value="Hello")
    button(type'="submit") { "Submit" }
  }

If you don’t use Oxpecker.ViewEngine or prefer header to a form field, you can leverage GetAntiforgeryTokens extension method instead.

One more security improvement is that ViewEngine no longer uses the shared ArrayPool for rendering.

OpenAPI improvements

In the new release Oxpecker’s API has been updated to be compatible with the new version of the Microsoft.AspNetCore.OpenApi package. While addOpenApiSimple function hasn’t changed, the configuration object passed to addOpenApi actually has, now the configureOperation parameter requires three arguments:

type OpenApiConfig
  (
    ?requestBody: RequestBody,
    ?responseBodies: ResponseBody seq,
    ?configureOperation: OpenApiOperation -> OpenApiOperationTransformerContext -> CancellationToken -> Task
  )

This also fixes the issue that required you to use Swashbuckle.AspNetCore for OpenAPI schema generation in .NET 9, so in .NET 10 you have both options available.

Another added feature is better handling of F# options (and value options) in the generated schema. While there is still an open issue to add it by default, it looks like the ASP.NET Core team doesn’t prioritize F# tickets, so Oxpecker.OpenApi now provides a transformer to deal with it.

Here is how the schema looks for the following simple type:

[<CLIMutable>]
type MyType = {
  Field: int option
}

Note: without [<CLIMutable>] attribute, the type’s fields will be marked as required (this is a System.Text.JSON specific behavior).

Before:

"FSharpOptionOfint": {
  "pattern": "^-?(?:0|[1-9]\\d*)$",
  "type": [
    "integer",
    "string"
  ]
},
"MyType": {
  "type": "object",
  "properties": {
    "field": {
      "oneOf": [
        {
          "type": "null"
        },
        {
          "$ref": "#/components/schemas/FSharpOptionOfint"
        }
      ]
    }
  }
}

After:

"MyType": {
  "type": "object",
  "properties": {
    "field": {
      "pattern": "^-?(?:0|[1-9]\\d*)$",
      "type": [
        "null",
        "integer",
        "string"
      ],
      "format": "int32"
    }
  }
}

You can learn more about ASP.NET Core OpenAPI updates and configuration in the corresponding documentation.

Default handler and middleware

Over these years I’ve found myself copying two pieces of code from one project to another — Exception middleware and NotFound handler. So, I finally thought: why not include them in Oxpecker so others can save some time as well?

Default.exceptionMiddleware

  • Logs all exceptions using Oxpecker.Default.ExceptionMiddleware category
  • Returns HTTP status code 400 for ModelBindException and RouteParseException
  • Returns HTTP status code 403 for AntiforgeryValidationException
  • Returns HTTP status code 500 for other unhandled exceptions

Default.notFoundHandler

  • Logs all requests using Oxpecker.Default.NotFoundHandler category
  • Returns HTTP status code 404

Since I think those are useful additions to have by default, I’ve updated the Empty template project:

let builder = WebApplication.CreateBuilder(args)
builder.Services.AddRouting().AddOxpecker() |> ignore
let app = builder.Build()
app.UseRouting()
  .Use(Default.exceptionMiddleware)
  .UseOxpecker(endpoints)
  .Run(Default.notFoundHandler)
app.Run()

Feel free to replace those default implementations with anything that is more suitable for your needs.

Routing improvements

There are two routing updates in Oxpecker 2:

  • routef performance has slightly improved due to the reduced allocations
  • route group (created by GET [] or subRoute "" [] ) configuration performed by using configureEndpoint function (yes, you can apply it to both individual endpoints and groups) is now directly mapped as the ASP.NET Core routing group configuration

ViewEngine improvements

There are minor performance tweaks in ViewEngine, but also one usability improvement — now you can place your int numbers into the markup without first converting them to string:

div(){
    "You number is: "
    123 
}
// <div>You number is: 123</div>

Breaking changes

  • Oxpecker 2 and sibling libraries are now .NET 10 based. For Oxpecker.OpenApi it was a requirement; for others it was not, but since you won’t be able to use the new Oxpecker.OpenApi on .NET 8 or .NET 9 anyway, and taking into account other changes, I decided to do this change sooner rather than later.
  • CSRF protection by default is also a breaking change. If you had .UseAntiforgery enabled in your project, but didn’t validate requests, they will start failing on form binding without a proper AntiforgeryToken included in request.
  • As mentioned above, the parameters of configureOperation in the Oxpecker.OpenApi package have changed, so if you used it before — you’ll need to update the code to conform to the new type signature.

Non-breaking changes

  • TheapplyBefore helper function has been deprecated in favor of the new addFilter function. I think the new name reads better in most common use cases and matches .AddEndpointFilter from Minimal APIs
  • The applyAfter helper function has been deprecated and will be just removed in the next release. This handler was initially ported from Giraffe as is, but later I realized that it doesn’t fit Oxpecker’s model well.
  • The htmlString handler and the .WriteHtmlString extension method have been deprecated. Those functions don’t do HTML encoding, but rather are identical to returning text, just with different header. As soon as they are neither performant nor safe, I decided to drop them.

That concludes the Oxpecker 2 overview, now you are welcome to try it and give feedback! I’d like to thank all the contributors who participated in creating issues and pull requests and helped to push the project forward, as well as Sergei Tihon for his continuous effort in organizing the event every year. And looking forward to other F# Advent stories!☃️


메타데이터
post_id
dbeef82249f5
slug
the-oxpecker-2-dbeef82249f5
url
https://medium.com/@lanayx/the-oxpecker-2-dbeef82249f5
canonical_url
https://medium.com/@lanayx/the-oxpecker-2-dbeef82249f5
author_url
https://medium.com/@lanayx
status
ok
fetched_at
2026-07-11 12:05:32