This page takes a project from nothing to a Problem Details response in four steps.
dotnet add package Offside
Add the ASP.NET Core integration in the web host only:
dotnet add package Offside.AspNetCore
Offside targets netstandard2.0, net8.0, and net10.0. Offside.AspNetCore targets net8.0 and net10.0.
FluentValidation or FastEndpoints hosts can add:
dotnet add package Offside.FluentValidation
dotnet add package Offside.FastEndpoint
See FluentValidation and FastEndpoints.
Optionally install the CLI to scaffold catalogs and agent skills — see the CLI page:
dotnet tool install -g Offside.Tool
offside init
Offside never hard-codes message text. Create errors/errors.json with a template per error code:
{
"not_found": "{resource} '{id}' was not found.",
"gone": "{resource} '{id}' is gone.",
"conflict": "Conflict on {resource}.",
"validation": "{field} is invalid.",
"bad_request": "Bad request.",
"unauthorized": "Unauthorized.",
"forbidden": "Forbidden.",
"precondition_failed": "Precondition failed.",
"unprocessable": "Unable to process the request.",
"too_many_requests": "Too many requests.",
"unexpected": "An unexpected error occurred.",
"service_unavailable": "The service is temporarily unavailable.",
"timeout": "The request timed out."
}
Make sure the file reaches the output directory:
<ItemGroup>
<None Update="errors\*.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
using System.Globalization;
using Offside;
using Offside.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddOffside(options =>
{
options.AddJsonFile(CultureInfo.InvariantCulture, "errors/errors.json");
var ptBr = Path.Combine(AppContext.BaseDirectory, "errors/errors.pt-BR.json");
if (File.Exists(ptBr))
options.AddJsonFile(new CultureInfo("pt-BR"), ptBr);
});
builder.Services.AddOffsideAspNetCore();
Two things trip people up here:
AddJsonFile reads the file. Relative paths resolve against AppContext.BaseDirectory. A missing file fails at startup and names the path. AddJson still takes catalog content when you already have a string; AddJsonFromAssembly loads an embedded resource.AddOffside throws an InvalidOperationException at startup — deliberately, so a missing catalog is a boot failure rather than a surprise at 3 a.m.AddOffsideAspNetCore registers OffsideAspNetCoreOptions. When an IHostEnvironment is present, ExposeExceptionDetails defaults to IsDevelopment().
The domain returns a Result<T> and knows nothing about HTTP:
using Offside;
public sealed class OrderService(IOrderRepository orders)
{
public Result<Order> Get(string id)
{
var order = orders.Find(id);
return order is null
? Result<Order>.Failure(Error.NotFound("order", id))
: Result<Order>.Success(order);
}
}
The endpoint converts it in one call:
app.MapGet("/orders/{id}", (string id, OrderService orders, HttpContext http) =>
orders.Get(id).ToHttpResult(http));
A hit returns 200 OK with the order. A miss returns:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://httpstatuses.io/404",
"title": "NotFound",
"status": 404,
"detail": "order '42' was not found.",
"errorCode": "NOT_FOUND",
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"errors": [
{ "code": "not_found", "errorCode": "NOT_FOUND", "kind": "NotFound", "detail": "order '42' was not found.", "field": null }
]
}
Domain and application projects reference Offside only. Offside.AspNetCore belongs to the web host, which is the single place that knows about status codes and Problem Details. That boundary is what lets the same domain code back an HTTP API, a worker, and a CLI without change.
Domain / Application ──► Offside
Web host ──► Offside + Offside.AspNetCore
ErrorUseOffside and SendOffsideAsync