Перейти к основному содержимому

Minimal API и OpenAPI

Разработчику

Play ITЗагрузка интерактивного демо…


Minimal API и OpenAPI

Если контроллеры кажутся тяжёлыми для первых сервисов, Minimal API даёт быстрый вход с меньшим количеством бойлерплейта и понятной структурой маршрутов. При этом архитектурные принципы остаются теми же, что и в "большом" ASP.NET Core.


Minimal API — маршруты без контроллеров

В Первая программа на ASP.NET Core вы видели app.MapGet("/health", ...). Это и есть Minimal API: HTTP-обработчик объявляется рядом с настройкой приложения, без класса XxxController.

OpenAPI — стандарт описания API (пути, методы, схемы JSON). Из него строят Swagger UI — страницу в браузере, где можно нажать "Try it out" и отправить запрос.

Здесь — структура проекта, группы маршрутов, типизированные ответы, DI, фильтры и документация.

Когда использовать Minimal API

Подход особенно удобен для микросервисов, внутренних API и BFF-слоя. Для очень больших монолитных API с десятками контроллеров и сложной политикой версионирования команды часто выбирают классическую controller-based структуру.


Словарь

ТерминПростыми словами
ЭндпоинтОдин маршрут + метод (например GET /api/notes).
MapGroupОбщий префикс и настройки для нескольких маршрутов.
Results / TypedResultsФабрики ответов: 200, 201, 404, 400 с телом.
Problem DetailsСтандартный JSON ошибки (RFC 7807).
Endpoint filterКод до/после handler (лог, auth) — аналог action filter.
SwashbuckleПопулярный пакет Swagger для ASP.NET Core.

Minimal API или контроллеры?

Minimal APIКонтроллеры
Объём кодаМеньше файловПапка Controllers/, атрибуты
Сложная логикаСервисы + тонкие handlersПривычная структура MVC
OpenAPIProduces, WithSummary, TypedResultsЧасто богаче из коробки
КомандаМикросервисы, BFFКрупные API-проекты

В одном приложении: MapControllers() + MapGroup("/api/v1").


Стартовый проект

dotnet new web -n MinimalNotes -o MinimalNotes
cd MinimalNotes
dotnet add package Swashbuckle.AspNetCore

Разбор:

  • dotnet new web создаёт минимальный ASP.NET Core-хост без контроллерного шаблона.
  • Параметры -n и -o задают имя приложения и отдельную рабочую папку проекта.
  • cd MinimalNotes переводит терминал в корень проекта для дальнейшей настройки.
  • Пакет Swashbuckle.AspNetCore добавляет генерацию Swagger/OpenAPI и UI-документацию.

Шаблон web — чистый Minimal API без контроллеров по умолчанию.


Эндпоинты и DTO

Models/NoteDto.cs:

namespace MinimalNotes.Models;

public record Note(int Id, string Text, DateTime Created);

public record CreateNoteRequest(string Text);

Разбор:

  • record описывает компактные DTO/модели с value-semantics и удобной сериализацией.
  • Note хранит структуру ответа API: идентификатор, текст и время создания.
  • CreateNoteRequest задаёт контракт входящего POST-запроса.
  • Разделение request/response-моделей упрощает эволюцию API и валидацию.

record — компактный тип для JSON; свойства задаются в конструкторе.

Program.cs:

Код ITЗагрузка примера кода…

Разбор:

  • AddEndpointsApiExplorer и AddSwaggerGen включают генерацию OpenAPI для endpoint-метаданных.
  • Локальные notes и nextId реализуют простое in-memory хранилище без БД.
  • MapGroup("/api/notes") объединяет маршруты под общий префикс и общие настройки.
  • MapGet/MapPost/MapDelete описывают полный CRUD-контур через функциональные обработчики.
  • ValidationProblem возвращает структурированную ошибку 400 при пустом Text.
  • Results.Created формирует 201 и Location, а partial Program нужен для интеграционных тестов.

Разбор

ФрагментСмысл
MapGroup("/api/notes")Все маршруты ниже с префиксом /api/notes.
WithTags("Notes")Группа в Swagger UI.
MapGet("/", ...)Полный путь: GET /api/notes/.
{id:int}Параметр маршрута только целое число.
ValidationProblemОтвет 400 с полем errors по стандарту.
Results.Created(...)201 + тело + заголовок Location.
Results.NoContent()204 без тела (успешное удаление).
public partial class ProgramДля интеграционных тестов.
dotnet run
/swagger

Разбор:

  • dotnet run запускает API и активирует Swagger в Development.
  • Путь /swagger открывает интерактивный UI со всеми endpoint'ами группы Notes.
  • Через UI удобно сразу проверить коды ответов и JSON-контракты без внешнего клиента.

TypedResults (явные типы ответов)

OpenAPI точнее, если указать типы:

api.MapGet("/{id:int}", (int id) =>
{
var note = notes.FirstOrDefault(n => n.Id == id);
return note is null ? TypedResults.NotFound() : TypedResults.Ok(note);
})
.Produces<Note>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);

Разбор:

  • TypedResults задаёт строго типизированные варианты ответа вместо обобщённого IResult.
  • Тернарный оператор возвращает либо 200 OK с объектом Note, либо 404 NotFound.
  • .Produces<Note>(200) явно описывает схему успешного ответа в OpenAPI.
  • .Produces(404) фиксирует возможный ошибочный код, чтобы клиенты видели полный контракт.

.Produces<T> — метаданные для генератора схемы.


Внедрение зависимостей

builder.Services.AddSingleton<INoteRepository, InMemoryNoteRepository>();

api.MapGet("/", (INoteRepository repo) => repo.GetAll());

Разбор:

  • AddSingleton регистрирует реализацию репозитория в контейнере DI.
  • Интерфейс INoteRepository отделяет бизнес-код endpoint'а от конкретного хранилища.
  • Параметр repo в обработчике автоматически резолвится из DI без ручного GetService.
  • Подход облегчает замену памяти на БД и упрощает unit/integration тестирование.

Параметры handler резолвятся из DI так же, как параметры конструктора контроллера.


Endpoint filters

Endpoint filter — код после routing, вокруг конкретного handler — валидация входа маршрута, feature flag на группу, аудит POST. Это не middleware (оно для всех URL) и не цепочка доменных регуляций (её оформляют как policy pipeline в Application — см. ASP.NET - фреймворк для веб-приложений).

api.MapPost("/", Handler)
.AddEndpointFilter(async (context, next) =>
{
var logger = context.HttpContext.RequestServices
.GetRequiredService<ILoggerFactory>()
.CreateLogger("Notes");
logger.LogInformation("POST /api/notes");
return await next(context);
});

Разбор:

  • AddEndpointFilter добавляет обёртку вокруг handler и выполняется на каждом POST.
  • Через RequestServices фильтр получает зависимости из DI, здесь — фабрику логгеров.
  • LogInformation фиксирует вход запроса и помогает наблюдаемости API.
  • await next(context) передаёт управление следующему фильтру или конечному обработчику.
  • Такой механизм подходит для сквозной валидации, аудита, метрик и tenant-check логики.

Авторизация на группу:

var api = app.MapGroup("/api/notes").RequireAuthorization();

Разбор:

  • RequireAuthorization() навешивает требование авторизации на все endpoint'ы группы.
  • Это сокращает дублирование атрибутов и снижает риск забыть защиту на новом маршруте.
  • Проверка выполняется middleware до вызова бизнес-handler'а.
  • Для детальных правил можно добавить политики или role-check на уровне маршрутов.

См. Identity — JWT, cookie и MVC.


OpenAPI — Swashbuckle и встроенный AddOpenApi

Swashbuckle (.NET 8 и привычный Swagger UI)

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(/* ... */);

Разбор:

  • Эти две строки включают классическую связку Swashbuckle для Minimal API в .NET 8.
  • AddEndpointsApiExplorer собирает описание endpoint'ов из route builder metadata.
  • AddSwaggerGen генерирует OpenAPI-документ и передаёт его в Swagger UI.
  • Конфигурация остаётся самой совместимой для большинства действующих проектов.

Встроенный OpenAPI (.NET 9+)

builder.Services.AddOpenApi();

var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.UseSwaggerUI(options =>
options.SwaggerEndpoint("/openapi/v1.json", "v1"));
}

Разбор:

  • AddOpenApi() включает встроенную генерацию OpenAPI, доступную в новых версиях .NET.
  • MapOpenApi() публикует JSON-спеку по маршруту вроде /openapi/v1.json.
  • UseSwaggerUI можно оставить как UI-слой поверх встроенного документа.
  • Такой вариант уменьшает зависимость от внешних пакетов при актуальном SDK.

Выбор зависит от версии SDK в проекте.


Аннотации и XML-комментарии

api.MapGet("/{id:int}", GetById)
.WithName("GetNoteById")
.WithSummary("Получить заметку по Id")
.WithDescription("Возвращает 404, если заметка не найдена");

Разбор:

  • WithName даёт endpoint'у стабильный идентификатор для ссылок и генераторов клиентов.
  • WithSummary формирует короткое описание операции в Swagger UI.
  • WithDescription добавляет подробный текст поведения и условий ошибки.
  • Эти метаданные повышают читаемость API для разработчиков и интеграторов.

В .csproj:

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

Разбор:

  • Свойство заставляет компилятор генерировать XML-документацию из комментариев к коду.
  • Файл с комментариями используется Swagger-генератором для расширенного описания API.
  • Это уменьшает дублирование текстов между кодом и внешней документацией.
  • Обычно включается на уровне основного web-проекта.
builder.Services.AddSwaggerGen(o =>
{
var xml = Path.Combine(AppContext.BaseDirectory, "MinimalNotes.xml");
if (File.Exists(xml)) o.IncludeXmlComments(xml);
});

Разбор:

  • Конфигурация вычисляет путь к XML-файлу документации в каталоге сборки.
  • Проверка File.Exists предотвращает ошибку при отсутствии файла в отдельных окружениях.
  • IncludeXmlComments добавляет summary/remarks из кода в Swagger/OpenAPI.
  • Результат — более информативные описания endpoint'ов прямо в UI.

Версионирование API

var v1 = app.MapGroup("/api/v1").WithTags("v1");
var v2 = app.MapGroup("/api/v2").WithTags("v2");

Разбор:

  • Код разделяет API по URL-версии, что упрощает безопасные несовместимые изменения.
  • Каждая группа получает собственный набор маршрутов и может развиваться независимо.
  • WithTags визуально разделяет версии в Swagger UI.
  • Такой подход удобен для поэтапной миграции клиентов с v1 на v2.

В Swagger — отдельные SwaggerDoc("v1", …) и SwaggerDoc("v2", …).


Валидация

У Minimal API нет автоматического ModelState, как у [ApiController]. Варианты:

  1. Ручная проверка → Results.ValidationProblem (как в примере).
  2. FluentValidation в endpoint filter.
  3. Data Annotations + пакеты вроде MiniValidation.

Для форм с десятком полей иногда проще контроллер с [FromBody] и [ApiController].


Мини-сценарий production-потока

  1. Клиент вызывает POST /api/notes.
  2. Endpoint filter логирует запрос и проверяет контекст (например, tenant/user).
  3. Валидатор проверяет DTO и при ошибке возвращает ValidationProblem.
  4. Handler/сервис сохраняет данные.
  5. Ответ оформляется через TypedResults.Created(...).
  6. OpenAPI-спецификация автоматически отражает контракт для клиентов.

Такой шаблон легко переносится на реальные сущности — заказы, платежи, задачи, события.


Частые ошибки

СимптомПричина
Swagger пустойНет AddEndpointsApiExplorer()
415POST без Content-Type: application/json
Схема "object"Handler возвращает object вместо Note / TypedResults
Дубли маршрутовДва MapGet на один шаблон
404 при {id}Передан не int — constraint {id:int} отсекает

Что попробовать

  1. WithOpenApi() на маршруте (если доступно в вашей версии SDK).
  2. Сгенерировать клиент: Kiota или NSwag по /openapi/v1.json.
  3. Покрыть API интеграционными тестами.

Дальше


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.