Первый HTTP API на ASP.NET Core
Серверный HTTP API получает запрос, выбирает обработчик и формирует ответ для другой программы. Вместо готовой HTML-страницы он может вернуть данные каталога: названия курсов, темы и количество уроков. Браузерная страница затем сама решает, как их показать. В этом уроке создадим такое приложение на ASP.NET Core и разберём связь между процессом сервера, адресом запроса и JSON-ответом.
Возьмём четыре знакомых учебных курса: JavaScript, HTML и CSS, производительность и Markdown. Их числа остаются фиксированными: 20, 16, 12 и 12 уроков, всего 60. Это данные самостоятельной песочницы, а не запрос к работающему ProfessorWeb. Нужны основные конструкции C#: переменные, массивы, методы и анонимные объекты. Исходники серии подготовлены чтением; компилятор, приложение, HTTP-запросы и тесты при подготовке не запускались. Все ответы ниже описывают ожидаемое поведение.
Проект и процесс сервера
В примерах закреплены .NET SDK 10.0.401, ASP.NET Core runtime 10.0.12 и C# 14. Эти опубликованные версии указаны на странице Microsoft .NET 10. Выбор фиксирует среду курса; он не означает, что любой будущий выпуск даст совершенно одинаковый диагностический текст. SDK нужен для подготовки приложения, runtime — для выполнения подготовленного приложения. Установка только runtime не добавляет инструменты разработки SDK.
Архив исходников содержит самостоятельные снимки lesson-01..16: start означает состояние перед уроком, after — после него. У первого урока start пуст, поскольку проекта ещё нет. Создайте отдельный каталог CatalogApi, перенесите в него файлы lesson-01/after и работайте именно в нём. Не соединяйте start и after: два файла с одинаковыми определениями создадут другую программу. Не переносите этот проект в папку сайта или его конфигурацию хостинга.
Файл проекта сообщает инструментам, какую модель приложения и целевую платформу использовать. Полный CatalogApi.csproj первого снимка выглядит так:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
Microsoft.NET.Sdk.Web включает обычную инфраструктуру веб-приложения и ссылку на разделяемый framework ASP.NET Core. net10.0 задаёт целевую платформу. Включённые неявные using позволяют не повторять стандартные пространства имён в маленьком примере. Nullable-анализ помогает увидеть места, где значение может отсутствовать, но сам по себе не проверяет данные сетевого запроса.
Соседний global.json закрепляет SDK и запрещает автоматический выбор другого выпуска. Если указанного SDK нет, читателю потребуется установить его самостоятельно. Файл не скачивает инструменты и не создаёт сервер. В архиве нет результатов сборки, папок bin и obj или выдуманного lockfile: они появляются после настоящего выполнения соответствующих действий.
Маршрут и данные каталога
Веб-приложение начинает работу с подготовки host: он объединяет настройки, сервисы, сервер и время жизни процесса. В нашем полном Program.cs затем создаётся приложение и регистрируется один маршрут:
var builder = WebApplication.CreateBuilder(args);
if (!builder.Environment.IsDevelopment())
throw new InvalidOperationException("This learning project is Development-only.");
var app = builder.Build();
var courses = new[]
{
new { Id="javascript", Title="Современный JavaScript", Topic="frontend", Lessons=20 },
new { Id="html-css", Title="HTML и CSS", Topic="frontend", Lessons=16 },
new { Id="performance", Title="Производительность сайта", Topic="frontend", Lessons=12 },
new { Id="markdown", Title="Статический сайт из Markdown", Topic="publishing", Lessons=12 }
};
app.MapGet("/api/courses", () => courses);
app.Run();
CreateBuilder подготавливает конфигурацию и инфраструктуру. Build создаёт приложение с выбранными сервисами. MapGet связывает HTTP-метод GET и путь /api/courses с функцией, которая возвращает массив. Run запускает обслуживание запросов и удерживает процесс до завершения. Регистрация маршрута ещё не является запросом: функция будет вызвана, когда сервер получит подходящее обращение.
Мы сознательно используем Minimal API. Название означает компактный способ описывать обработчики, а не ограничение API одной строкой. Для первой операции не нужны контроллер, представление Razor или HTML-шаблон. При росте проекта обработчики можно перенести в отдельные методы и файлы; способ получения HTTP-запроса от этого не меняется.
Массив создаётся один раз при запуске и читается каждым обращением. Поля Id и Title описывают курс, Topic относится к одной из двух тем, Lessons является целым числом. Все записи имеют одну структуру, поэтому C# выводит общий тип анонимных объектов. Это удобное начало; на следующем шаге дадим модели явное имя, чтобы она могла переходить между файлами.
Адрес сервера и ожидаемый ответ
Файл Properties/launchSettings.json задаёт учебный профиль: HTTP только на 127.0.0.1:5080, окружение Development. При самостоятельном прохождении читатель запускает проект из его папки командой:
dotnet run --launch-profile catalog-api
Эта команда относится к работе читателя; при подготовке статьи она не выполнялась. На той же машине ожидаемый адрес операции — http://127.0.0.1:5080/api/courses. Число 5080 обозначает порт приложения, /api/courses — маршрут внутри него. Адрес локального ProfessorWeb имеет другой порт и обслуживается другим процессом. Совпадение слова «каталог» не связывает два приложения автоматически.
При GET ожидается статус 200 и JSON-массив. Начало представления имеет следующую форму:
[
{"id":"javascript","title":"Современный JavaScript","topic":"frontend","lessons":20},
{"id":"html-css","title":"HTML и CSS","topic":"frontend","lessons":16},
{"id":"performance","title":"Производительность сайта","topic":"frontend","lessons":12},
{"id":"markdown","title":"Статический сайт из Markdown","topic":"publishing","lessons":12}
]
Порядок пробелов в JSON не является частью задачи. Важны четыре объекта, имена свойств и значения. Сумма lessons равна 60, но API пока не вычисляет её отдельным полем: это наблюдение над ответом. GET к другому пути должен дать 404; POST к тому же пути не создаёт курс, поскольку обработчик для POST не зарегистрирован. Знание адреса не добавляет серверу отсутствующую операцию.
В начале программы есть проверка Development. Она намеренно запрещает запуск этого учебного варианта в другом окружении. Это дополнительный барьер от случайного использования снимка, но название окружения не является аутентификацией. Профиль можно переопределить внешними настройками, поэтому не открывайте песочницу в сеть и не считайте loopback полноценной моделью безопасности рабочего API. Вход пользователей, политики доступа и защита изменяющих запросов будут самостоятельными темами продолжения.
Путь запроса следует отделять от пути файла на диске. /api/courses не означает, что в проекте обязательно должен существовать файл api/courses. Мы зарегистрировали программный обработчик, и сервер вызывает его по описанию endpoint. Именно поэтому HTTP API может отдавать данные из памяти, а позднее из базы, сохраняя одинаковый адрес. Если вместо этого добавить файл с похожим именем, он не получит поведение handler без отдельной настройки выдачи файлов.
Кроме того, успешная сериализация не подтверждает актуальность человеческого названия. В первом шаге значения задаёт сам автор seed. Если в нём опечатка, API честно вернёт опечатку. Разработчик должен отличать исправность протокола от правильности исходных данных, иначе техническое свидетельство начнёт подменять редакционную проверку.
При последующем выполнении полезно сопоставлять три свидетельства: процесс действительно слушает нужный порт, GET попадает в нужный маршрут, а тело содержит ожидаемые данные. Один открывшийся браузер не подтверждает все три. Теперь серверная граница появилась, и в следующем уроке разделим запуск, модель каталога и регистрацию обработчиков, сохранив этот ответ.