- Создавать проект 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Создай проект
dotnet new webapi -n SchoolApiпо умолчанию создаёт минимальный API; для контроллеров добавь--use-controllers. - 2Подключи EF Core
Добавь пакет
Microsoft.EntityFrameworkCore.Sqliteи напиши классы модели и контекста. - 3Конфигурация и база
Запиши строку подключения в
appsettings.json, затем, как в прошлом уроке, выполниdotnet ef migrations addиdotnet ef database update. - 4Запусти и проверь
dotnet run --urls http://localhost:5080запускает сервер; отправляй запросы через curl, файл.httpили Swagger UI.
dotnet new webapi -n SchoolApi
cd SchoolApi
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet run --urls http://localhost:5080info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5080
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.Program.cs: сервисы и middleware
В минимальном API всё приложение настраивается в Program.cs, и он состоит из трёх частей. Сначала в builder.Services регистрируются сервисы — это встроенный в .NET контейнер внедрения зависимостей (DI): AddDbContext добавляет SchoolContext, AddOpenApi — генератор документа, AddValidation — проверку на основе атрибутов. Затем builder.Build() создаёт приложение и настраивается конвейер middleware. В конце идут эндпоинты — методы, привязанные к URL.
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), а затем записывает в журнал статус и время. Порядок звеньев важен и обычно такой:
UseExceptionHandler— ловит ошибки из всех звеньев нижеUseHttpsRedirection— перенаправляет HTTP-запросы на HTTPSUseCors— определяет, каким сайтам можно вызывать APIUseAuthentication— «кто ты?»UseAuthorization— «что тебе разрешено?»- Эндпоинты:
MapGet,MapPost,MapControllers
CRUD-эндпоинты с EF Core
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);// 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.
{
"ConnectionStrings": {
"School": "Data Source=school.db"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}appsettings.json: GetConnectionString("School") читает это значение; appsettings.Development.json и переменные окружения могут его переопределитьПроверка через curl
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."]}}Контроллеры, время жизни в DI и OpenAPI
Минимальные API удобны для небольших и средних сервисов. В больших проектах часто используют контроллеры: эндпоинты группируются в классах, унаследованных от ControllerBase, а маршруты задаются атрибутами. dotnet new webapi --use-controllers создаёт такой проект. Оба стиля используют одни и те же DI, middleware и EF Core, и их можно сочетать в одном проекте.
[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();SchoolContext приходит через первичный конструктор, а [ApiController] автоматически возвращает 400 при ошибках валидации| Метод | Время жизни | Для чего |
|---|---|---|
AddSingleton | один экземпляр на всё приложение | настройки, кэш в памяти |
AddScoped | один экземпляр на HTTP-запрос | DbContext и сервисы, которые его используют |
AddTransient | новый экземпляр при каждом запросе к DI | лёгкие вспомогательные классы без состояния |
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 dependencyDbContext делили бы все запросы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.
AddDbContext регистрирует DbContext?