Что внутри
| Тип | Назначение |
|---|---|
CommandEndpoint<TCommand> | Отправляет void-команду → 204 при успехе, Problem Details при ошибке |
CommandEndpoint<TCommand, TResult> | Отправляет команду с результатом → 200 при успехе |
QueryEndpoint<TQuery, TResult> | Отправляет запрос → 200 / 404 / 400 |
MappedCommandEndpoint<TRequest, TCommand[, TResult]> | Превращает HTTP-DTO в команду через ToCommand — серверные поля не утекают в wire-контракт |
MappedQueryEndpoint<TRequest, TQuery, TResult> | Превращает HTTP-DTO в запрос через ToQuery |
ClaimsPrincipalExtensions.GetUserId() | Достаёт id пользователя из claim NameIdentifier (TryGetUserId — вариант без исключений) |
CorrelationIdMiddleware | Читает или генерирует X-Correlation-ID и возвращает его в ответе |
ICorrelationIdAccessor | Доступ к текущему correlation ID из любого места в scope запроса |
GlobalExceptionHandler | Ловит необработанные исключения → 500 Problem Details |
Неуспешные результаты формирует KotoProblemDetails из Koto.Api.AspNetCore: уходят
все Result.Errors (несколько ошибок собираются в RFC 7807 validation problem details
с группировкой по Error.Field), плюс расширения errorCode/errorCodes и correlationId.
Настройка
// Program.cs
builder.Services.AddKotoApi(); // включает AddKotoAspNetCore()
// Или переопределите реестр Error.Code → HTTP-статус:
builder.Services.AddKotoApi(o => o.Map("payments.gateway-failed", 502));
builder.Services.AddFastEndpoints();
// ... остальные сервисы
var app = builder.Build();
app.UseKotoApi(); // middleware CorrelationId + обработчик исключений
app.UseFastEndpoints();
app.Run();
Реализация эндпоинта
// Void-команда (204 при успехе)
public class DeleteOrderEndpoint : CommandEndpoint<DeleteOrderCommand>
{
public override void Configure()
{
Delete("/orders/{id}");
AllowAnonymous();
}
public override async Task HandleAsync(DeleteOrderCommand req, CancellationToken ct)
=> await SendCommandAsync(req, ct);
}
// Команда с результатом (200 при успехе)
public class PlaceOrderEndpoint : CommandEndpoint<PlaceOrderCommand, OrderId>
{
public override void Configure()
{
Post("/orders");
AllowAnonymous();
}
public override async Task HandleAsync(PlaceOrderCommand req, CancellationToken ct)
=> await SendCommandAsync(req, ct);
}
// Запрос (GET, 200/404)
public class GetOrderEndpoint : QueryEndpoint<GetOrderQuery, OrderDto>
{
public override void Configure()
{
Get("/orders/{id}");
AllowAnonymous();
}
public override async Task HandleAsync(GetOrderQuery req, CancellationToken ct)
=> await SendQueryAsync(req, ct);
}
Серверные поля (claims, роут, tenant)
Когда команда или запрос несёт поля, которые обязаны приходить с сервера (id пользователя, tenant,
correlation id вызывающего) и которые нельзя биндить из тела запроса, берите mapped-эндпоинты.
В DTO запроса этих полей нет; ToCommand/ToQuery собирает команду из запроса и контекста
эндпоинта (User, Route<T>(), заголовки):
public sealed record SubmitJudgmentRequest(Guid SubmissionId, int GoeScore); // JudgeId не уходит в wire-контракт
public sealed record SubmitJudgmentCommand(Guid JudgeId, Guid SubmissionId, int GoeScore) : ICommand<Judgment>;
public sealed class SubmitJudgmentEndpoint
: MappedCommandEndpoint<SubmitJudgmentRequest, SubmitJudgmentCommand, Judgment>
{
public override void Configure() { Post("/api/v1/judgments"); Policies("IsJudge"); }
protected override SubmitJudgmentCommand ToCommand(SubmitJudgmentRequest r) =>
new(JudgeId: User.GetUserId(), r.SubmissionId, r.GoeScore); // JudgeId из claims, не из тела
}
Если запрос и есть команда (серверных полей нет) — берите обычные CommandEndpoint/QueryEndpoint.
У mapped-вариантов HandleAsync запечатан: вы пишете только Configure и ToCommand/ToQuery.
Маппинг кода ошибки → HTTP-статус
Статусы берутся из KotoHttpErrorOptions (Koto.Api.AspNetCore) — это расширяемый
реестр (точный код → пользовательские правила → суффикс → префикс → fallback). По умолчанию:
| Паттерн кода ошибки | Статус |
|---|---|
*.not-found | 404 Not Found |
*.already-*, *.conflict | 409 Conflict |
*.unauthorized | 401 Unauthorized |
*.forbidden | 403 Forbidden |
general.*, validation.*, Error.Field != null | 400 Bad Request |
| всё остальное | 422 Unprocessable Entity (fallback настраивается; 500 зарезервирован под необработанные исключения) |
Переопределяется через AddKotoApi(o => o.Map("subscription.payment-failed", 502)).