Что внутри
| Тип | Назначение |
|---|---|
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 не на проводе
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)).