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, фильтры и документация.
Подход особенно удобен для микросервисов, внутренних 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 |
| OpenAPI | Produces, 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]. Варианты:
- Ручная проверка →
Results.ValidationProblem(как в примере). - FluentValidation в endpoint filter.
- Data Annotations + пакеты вроде MiniValidation.
Для форм с десятком полей иногда проще контроллер с [FromBody] и [ApiController].
Мини-сценарий production-потока
- Клиент вызывает
POST /api/notes. - Endpoint filter логирует запрос и проверяет контекст (например, tenant/user).
- Валидатор проверяет DTO и при ошибке возвращает
ValidationProblem. - Handler/сервис сохраняет данные.
- Ответ оформляется через
TypedResults.Created(...). - OpenAPI-спецификация автоматически отражает контракт для клиентов.
Такой шаблон легко переносится на реальные сущности — заказы, платежи, задачи, события.
Частые ошибки
| Симптом | Причина |
|---|---|
| Swagger пустой | Нет AddEndpointsApiExplorer() |
| 415 | POST без Content-Type: application/json |
| Схема "object" | Handler возвращает object вместо Note / TypedResults |
| Дубли маршрутов | Два MapGet на один шаблон |
404 при {id} | Передан не int — constraint {id:int} отсекает |
Что попробовать
WithOpenApi()на маршруте (если доступно в вашей версии SDK).- Сгенерировать клиент: Kiota или NSwag по
/openapi/v1.json. - Покрыть API интеграционными тестами.
Дальше
- Web API — первая программа · ASP.NET — обзор
- Интеграционные тесты · Identity и JWT
- Справочник ASP.NET
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.