Перейти к содержанию
Educora
Продвинутый25 мин16 / 16

Web API на ASP.NET Core

Построй CRUD API поверх EF Core на ASP.NET Core: минимальные API и контроллеры, внедрение зависимостей, middleware, конфигурация, валидация и OpenAPI.

Проверь себя
В этом уроке ты узнаешь
  • Создавать проект Web API и объяснять устройство Program.cs (сервисы, middleware, эндпоинты)
  • Писать CRUD-эндпоинты с EF Core в стиле минимальных API и контроллеров
  • Выбирать время жизни сервисов в DI, читать конфигурацию и проверять API через curl и OpenAPI

В прошлом уроке ты научился хранить данные в базе с помощью EF Core. Теперь откроем их миру: мобильное приложение, сайт или другой сервер должны по HTTP запрашивать список учеников и добавлять новых. ASP.NET Core — быстрый, кроссплатформенный веб-фреймворк .NET с открытым исходным кодом. В этом уроке ты построишь на нём полноценный CRUD API поверх EF Core и разберёшься с внедрением зависимостей, middleware, конфигурацией и документом OpenAPI.

  1. 1
    Создай проект

    dotnet new webapi -n SchoolApi по умолчанию создаёт минимальный API; для контроллеров добавь --use-controllers.

  2. 2
    Подключи EF Core

    Добавь пакет Microsoft.EntityFrameworkCore.Sqlite и напиши классы модели и контекста.

  3. 3
    Конфигурация и база

    Запиши строку подключения в appsettings.json, затем, как в прошлом уроке, выполни dotnet ef migrations add и dotnet ef database update.

  4. 4
    Запусти и проверь

    dotnet run --urls http://localhost:5080 запускает сервер; отправляй запросы через curl, файл .http или Swagger UI.

Terminal
dotnet new webapi -n SchoolApi
cd SchoolApi
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet run --urls http://localhost:5080
Ожидаемый результат
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5080
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
Пример вывода (сокращён): встроенный сервер Kestrel слушает порт 5080

Program.cs: сервисы и middleware

В минимальном API всё приложение настраивается в Program.cs, и он состоит из трёх частей. Сначала в builder.Services регистрируются сервисы — это встроенный в .NET контейнер внедрения зависимостей (DI): AddDbContext добавляет SchoolContext, AddOpenApi — генератор документа, AddValidation — проверку на основе атрибутов. Затем builder.Build() создаёт приложение и настраивается конвейер middleware. В конце идут эндпоинты — методы, привязанные к URL.

C#
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// 1. services for dependency injection
builder.Services.AddDbContext<SchoolContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("School")));
builder.Services.AddOpenApi();
builder.Services.AddValidation();              // .NET 10: checks [Required], [Range]...

var app = builder.Build();

// 2. the middleware pipeline
if (app.Environment.IsDevelopment())
    app.MapOpenApi();                          // the document at /openapi/v1.json

app.Use(async (context, next) =>               // our own middleware: request timing
{
    var watch = System.Diagnostics.Stopwatch.StartNew();
    await next(context);
    app.Logger.LogInformation("{Method} {Path} -> {Status} in {Ms} ms", context.Request.Method,
        context.Request.Path, context.Response.StatusCode, watch.ElapsedMilliseconds);
});
Program.cs, часть 1: сервисы и middleware (нужны пакеты EF Core и OpenAPI)

Middleware — звено цепочки, через которую проходит каждый HTTP-запрос. Каждое звено может посмотреть на запрос, изменить его, само ответить или вызвать next и передать запрос следующему звену. Наш собственный middleware выше запускает секундомер, ждёт остальную цепочку через await next(context), а затем записывает в журнал статус и время. Порядок звеньев важен и обычно такой:

  1. UseExceptionHandler — ловит ошибки из всех звеньев ниже
  2. UseHttpsRedirection — перенаправляет HTTP-запросы на HTTPS
  3. UseCors — определяет, каким сайтам можно вызывать API
  4. UseAuthentication — «кто ты?»
  5. UseAuthorization — «что тебе разрешено?»
  6. Эндпоинты: MapGet, MapPost, MapControllers

CRUD-эндпоинты с EF Core

C#
public class Student
{
    public int Id { get; set; }
    public required string Name { get; set; }
    public int Score { get; set; }
}

public class SchoolContext(DbContextOptions<SchoolContext> options) : DbContext(options)
{
    public DbSet<Student> Students => Set<Student>();
}

public record StudentDto([Required] string Name, [Range(0, 100)] int Score);
Упрощённая модель, контекст, получающий параметры из DI, и DTO для входных данных
C#
// 3. endpoints
var students = app.MapGroup("/api/students");

students.MapGet("/", async (SchoolContext db) =>
    await db.Students.AsNoTracking().OrderBy(s => s.Id).ToListAsync());

students.MapGet("/{id:int}", async Task<Results<Ok<Student>, NotFound>> (int id, SchoolContext db) =>
    await db.Students.FindAsync(id) is Student s ? TypedResults.Ok(s) : TypedResults.NotFound());

students.MapPost("/", async (StudentDto dto, SchoolContext db) =>
{
    var student = new Student { Name = dto.Name, Score = dto.Score };
    db.Students.Add(student);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/api/students/{student.Id}", student);
});

students.MapPut("/{id:int}", async Task<Results<NoContent, NotFound>> (int id, StudentDto dto, SchoolContext db) =>
    await db.Students.Where(s => s.Id == id)
        .ExecuteUpdateAsync(set => set.SetProperty(s => s.Name, dto.Name).SetProperty(s => s.Score, dto.Score)) == 0
        ? TypedResults.NotFound() : TypedResults.NoContent());

students.MapDelete("/{id:int}", async Task<Results<NoContent, NotFound>> (int id, SchoolContext db) =>
    await db.Students.Where(s => s.Id == id).ExecuteDeleteAsync() == 0
        ? TypedResults.NotFound() : TypedResults.NoContent());

app.Run();
Program.cs, часть 2: пять эндпоинтов — чтение, создание, изменение, удаление

MapGroup позволяет один раз написать общий префикс /api/students. Параметры эндпоинта заполняются автоматически: id — из URL ({id:int} принимает только целые числа), StudentDto — из JSON-тела, SchoolContext — из DI-контейнера. TypedResults возвращают правильные статусы — Created даёт 201 и заголовок Location, NoContent — 204, NotFound — 404, а тип Results<...> документирует все возможные ответы, и OpenAPI тоже их видит. Имена в JSON автоматически становятся camelCase: Name → name.

JSON
{
  "ConnectionStrings": {
    "School": "Data Source=school.db"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
appsettings.json: GetConnectionString("School") читает это значение; appsettings.Development.json и переменные окружения могут его переопределить

Проверка через curl

Terminal
curl -X POST localhost:5080/api/students -H 'Content-Type: application/json' -d '{"name":"Aysel","score":95}'
curl -X POST localhost:5080/api/students -H 'Content-Type: application/json' -d '{"name":"Murad","score":78}'
curl localhost:5080/api/students
curl -s -o /dev/null -w '%{http_code}\n' -X PUT localhost:5080/api/students/2 \
     -H 'Content-Type: application/json' -d '{"name":"Murad","score":84}'
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE localhost:5080/api/students/2
curl -s -o /dev/null -w '%{http_code}\n' localhost:5080/api/students/2
curl -X POST localhost:5080/api/students -H 'Content-Type: application/json' -d '{"name":"Elvin","score":120}'
Ожидаемый результат
{"id":1,"name":"Aysel","score":95}
{"id":2,"name":"Murad","score":78}
[{"id":1,"name":"Aysel","score":95},{"id":2,"name":"Murad","score":78}]
204
204
404
{"title":"One or more validation errors occurred.","errors":{"Score":["The field Score must be between 0 and 100."]}}
Вывод проверен на .NET 10 с версией того же API, хранящей данные в памяти; с пустой базой SQLite ответы те же. Последний запрос не прошёл валидацию и получил 400

Контроллеры, время жизни в DI и OpenAPI

Минимальные API удобны для небольших и средних сервисов. В больших проектах часто используют контроллеры: эндпоинты группируются в классах, унаследованных от ControllerBase, а маршруты задаются атрибутами. dotnet new webapi --use-controllers создаёт такой проект. Оба стиля используют одни и те же DI, middleware и EF Core, и их можно сочетать в одном проекте.

C#
[ApiController]
[Route("api/[controller]")]                      // -> /api/students
public class StudentsController(SchoolContext db) : ControllerBase
{
    [HttpGet]
    public async Task<List<Student>> GetAll() =>
        await db.Students.AsNoTracking().OrderBy(s => s.Id).ToListAsync();

    [HttpGet("{id:int}")]
    public async Task<ActionResult<Student>> Get(int id) =>
        await db.Students.FindAsync(id) is Student s ? s : NotFound();

    [HttpPost]
    public async Task<ActionResult<Student>> Create(StudentDto dto)
    {
        var student = new Student { Name = dto.Name, Score = dto.Score };
        db.Students.Add(student);
        await db.SaveChangesAsync();
        return CreatedAtAction(nameof(Get), new { id = student.Id }, student);
    }
}
// Program.cs: builder.Services.AddControllers(); ... app.MapControllers();
Тот же API на контроллере: SchoolContext приходит через первичный конструктор, а [ApiController] автоматически возвращает 400 при ошибках валидации
МетодВремя жизниДля чего
AddSingletonодин экземпляр на всё приложениенастройки, кэш в памяти
AddScopedодин экземпляр на HTTP-запросDbContext и сервисы, которые его используют
AddTransientновый экземпляр при каждом запросе к DIлёгкие вспомогательные классы без состояния
Неверно: singleton захватывает scoped-сервис
builder.Services.AddDbContext<SchoolContext>(...);   // scoped: one per request
builder.Services.AddSingleton<ReportService>();      // ReportService needs SchoolContext
// error at startup (Development):
// Cannot consume scoped service '...SchoolContext' from singleton '...ReportService'.
Верно: согласованное время жизни
builder.Services.AddDbContext<SchoolContext>(...);   // scoped: one per request
builder.Services.AddScoped<ReportService>();         // the same lifetime as its dependency
В режиме Development ASP.NET Core ловит эту ошибку уже при запуске; иначе один DbContext делили бы все запросы

OpenAPI (прежнее название — Swagger) — машиночитаемое описание API: какие есть URL, какой JSON они ждут и какие статусы возвращают. Начиная с .NET 9 шаблон использует AddOpenApi() и MapOpenApi(), а документ доступен по адресу /openapi/v1.json. Для интерактивной страницы в браузере добавь пакет Swashbuckle.AspNetCore.SwaggerUI и напиши app.UseSwaggerUI(o => o.SwaggerEndpoint("/openapi/v1.json", "School API")): на странице /swagger каждый эндпоинт можно попробовать кнопкой. Из этого же документа можно автоматически сгенерировать клиентский код для мобильных приложений.

Главное

  • Program.cs: сервисы (DI) → Build() → middleware → эндпоинты → Run().
  • Минимальные API: MapGroup, MapGet/MapPost/MapPut/MapDelete; параметры приходят из URL, тела и DI, а TypedResults возвращают 200/201/204/404.
  • Время жизни в DI: Singleton, Scoped (DbContext), Transient; не внедряй scoped-сервис в singleton.
  • Конфигурация берётся из appsettings.json, файлов для разных сред и переменных окружения; строку подключения читает GetConnectionString.
  • На входе — DTO и AddValidation; документ OpenAPI находится по адресу /openapi/v1.json, а Swagger UI добавляется пакетом.

Проверь себя

Вопросов: 10. Каждый правильный ответ приносит XP.

1 / 10
С каким временем жизни AddDbContext регистрирует DbContext?