Docs

Koto.Api.AspNetCore

Transport-agnostic ASP.NET Core integration — Minimal APIs, MVC, or any host.

What you get

  • ToHttpResult() / ToHttpResultAsync()Result<T>IResult (Minimal API)
  • ToActionResult() / ToActionResultAsync()Result<T>ActionResult<T> (MVC)
  • KotoProblemDetails — RFC 7807 Problem Details from one or many Errors (multiple errors become validation problem details with an errors dictionary per field)
  • KotoHttpErrorOptions — extensible Error.Code → HTTP status registry

Quick start

builder.Services.AddKotoAspNetCore(); // optional: o => o.Map("payments.gateway-failed", 502)

Minimal API:

app.MapPost("/orders", (CreateOrderCommand cmd, ICqrsDispatcher dispatcher, HttpContext ctx, CancellationToken ct)
    => dispatcher.SendAsync(cmd, ct).ToHttpResultAsync(ctx));

MVC controller:

[HttpPost]
public async Task<ActionResult<OrderDto>> Create(CreateOrderCommand cmd, CancellationToken ct)
    => (await _dispatcher.SendAsync(cmd, ct)).ToActionResult(this);

Status code mapping

Resolution order: exact code → custom rules → suffix → prefix → field-error default → fallback.

RuleStatus
*.not-found404
*.already-*, *.conflict409
*.unauthorized401
*.forbidden403
general.*, validation.*, Error.Field != null400
everything else422 (fallback, configurable)

Unmapped business errors are 422, never 500 — a failed Result is a rule violation the client can act on; 500 is reserved for unhandled exceptions.

Customize:

builder.Services.AddKotoAspNetCore(o => o
    .Map("subscription.payment-failed", StatusCodes.Status502BadGateway) // exact, highest priority
    .MapSuffix(".expired", StatusCodes.Status410Gone)
    .MapPrefix("quota.", StatusCodes.Status429TooManyRequests)
    .Map(e => e.Code.Contains(".locked-") ? StatusCodes.Status423Locked : null));