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

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 не уходит в 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-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)).