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

Minimal API и OpenAPI

Разработчику
Загрузка симулятора первой программы…

Minimal API и OpenAPI

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

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

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

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


Словарь

ТерминПростыми словами
ЭндпоинтОдин маршрут + метод (например 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

Шаблон 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 — компактный тип для JSON; свойства задаются в конструкторе.

Program.cs:

using MinimalNotes.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new() { Title = "Minimal Notes API", Version = "v1" });
});

var notes = new List<Note>();
var nextId = 1;

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

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

api.MapGet("/", () => Results.Ok(notes.OrderByDescending(n => n.Created)));

api.MapPost("/", (CreateNoteRequest req) =>
{
if (string.IsNullOrWhiteSpace(req.Text))
return Results.ValidationProblem(new Dictionary<string, string[]>
{
["Text"] = ["Текст обязателен"]
});

var note = new Note(nextId++, req.Text.Trim(), DateTime.UtcNow);
notes.Add(note);
return Results.Created($"/api/notes/{note.Id}", note);
});

api.MapGet("/{id:int}", (int id) =>
notes.FirstOrDefault(n => n.Id == id) is { } note
? Results.Ok(note)
: Results.NotFound());

api.MapDelete("/{id:int}", (int id) =>
{
var idx = notes.FindIndex(n => n.Id == id);
if (idx < 0) return Results.NotFound();
notes.RemoveAt(idx);
return Results.NoContent();
});

app.Run();

public partial class 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.


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);

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


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

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

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

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


Endpoint filters

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);
});

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

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

См. 4515.


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

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

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

Встроенный 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"));
}

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


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

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

В .csproj:

<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
builder.Services.AddSwaggerGen(o =>
{
var xml = Path.Combine(AppContext.BaseDirectory, "MinimalNotes.xml");
if (File.Exists(xml)) o.IncludeXmlComments(xml);
});

Версионирование API (обзор)

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

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


Валидация

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

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

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


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

СимптомПричина
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 интеграционными тестами.

Дальше


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).