Документация

Koto.Api.FastEndpoints

Интеграция FastEndpoints для DDD-примитивов Koto.

Что внутри

ТипНазначение
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-found404 Not Found
*.already-*, *.conflict409 Conflict
*.unauthorized401 Unauthorized
*.forbidden403 Forbidden
general.*, validation.*, Error.Field != null400 Bad Request
всё остальное422 Unprocessable Entity (настраиваемый fallback; 500 зарезервирован для необработанных исключений)

Настройка через AddKotoApi(o => o.Map("subscription.payment-failed", 502)).