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…
The Oxpecker 2

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:
GetLoggerextension method allowing acategoryName
let myLogger = ctx.GetLogger("MyModule")
IResultnative support
return! ctx.Write(TypedResults.Ok "Hello world")
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>
configureEndpointfunction
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:
routefperformance has slightly improved due to the reduced allocations- route group (created by
GET []orsubRoute "" []) configuration performed by usingconfigureEndpointfunction (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
.UseAntiforgeryenabled 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
configureOperationin 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
- The
applyBeforehelper function has been deprecated in favor of the newaddFilterfunction. I think the new name reads better in most common use cases and matches.AddEndpointFilterfrom Minimal APIs - The
applyAfterhelper 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
htmlStringhandler and the.WriteHtmlStringextension 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