Перейти к содержанию

Каркас ASP.NET Core и соединение с PostgreSQL

В предыдущем уроке вы разделили пользователя и организацию. Теперь создадим технический каркас «Пространства», в котором эта модель будет развиваться. Небольшой API удобнее начать с понятных файлов, чем сразу прятать SQL за несколькими слоями. Каждый новый запрос будет показывать, откуда берётся организация и какое соединение выполняет операцию.

Полный исходник находится в архиве курса. Для текущего шага нужны Space.Api.csproj, Program.cs, Database.cs и SQL из каталога sql. Архив показывает завершённое состояние; пока изучайте регистрацию сервисов и открытие соединения. Приложение при подготовке материала не запускалось. Команды ниже предназначены читателю, а ожидаемые результаты не являются протоколом выполненной проверки.

Версия проекта и явная зависимость

Используем целевую платформу net10.0 и пакет Npgsql версии 10.0.0. Это зафиксированная линия учебного примера, а не обещание, что конкретный пакет всегда будет последним. Доступность Npgsql 10 подтверждается официальными заметками выпуска. Перед настоящим выпуском сервиса команда отдельно выбирает актуальное исправление безопасности и повторяет свою проверку.

Ниже приведён целый файл Space.Api.csproj. Для обработки HTTP хватает Web SDK; дополнительный MVC-пакет или отдельный пакет ASP.NET Core не нужен. SQL выполняется напрямую через Npgsql, чтобы условия по организации были видны в уроках.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Npgsql" Version="10.0.0" />
  </ItemGroup>
</Project>

Nullable заставляет отличать отсутствующую запись от существующего объекта. Это полезно уже при выборе организации: результат поиска членства может отсутствовать, и обработчик должен завершиться отказом. ImplicitUsings сокращает число обычных импортов, но не меняет модель доступа. Главная библиотека примера одна; читатель может сопоставить каждый вызов к базе с документацией драйвера.

Мы намеренно не смешиваем этот курс с изучением ORM. Использование Entity Framework Core вполне возможно, однако глобальные фильтры, время жизни контекста и обход фильтров потребовали бы отдельного разбора. Здесь сначала формируется контракт изоляции. Если позднее перенести его на ORM, организация должна остаться частью запросов, связей и условий изменения.

Источник данных живёт дольше запроса

NpgsqlDataSource представляет настройки доступа и пул соединений. Регистрируем его один раз на приложение, а открытое соединение получаем для конкретной операции. Отдельное соединение нельзя хранить в singleton-сервисе и одновременно использовать в двух запросах: в нём есть текущая транзакция и состояние сессии.

Следующий листинг — фрагмент регистрации из Program.cs. Имя Default используется во всех файлах архива. Строка подключения приходит из конфигурации, а не из текста статьи.

var connectionString = builder.Configuration.GetConnectionString("Default")
    ?? throw new InvalidOperationException("ConnectionStrings:Default is required");
builder.Services.AddSingleton(NpgsqlDataSource.Create(connectionString));
builder.Services.AddScoped<TenantContext>();
builder.Services.AddScoped<Database>();

Документация Npgsql Basic Usage описывает создание источника данных и асинхронное открытие соединений. В нашем проекте scoped-сервис Database хранит ссылку на общий источник, но не держит соединение между запросами. Метод операции открывает соединение, начинает транзакцию и освобождает оба объекта через await using.

Пул экономит стоимость установления соединения, но не превращает соединение в место для постоянного хранения текущей компании. Сегодня соединение обслуживает «Север», а позже может попасть запросу «Юга». В двенадцатом уроке мы зададим контекст PostgreSQL только внутри транзакции. Уже сейчас полезно отказаться от поля CurrentOrganization в singleton: такое поле стало бы общим для всех клиентов.

Конфигурация локальной среды

Пример использует отдельные роли базы: владелец схемы создаёт объекты, роль space_app обслуживает HTTP, а space_worker забирает задания. В SQL-файлах архива приведены команды подготовки прав. Не запускайте API с правами суперпользователя только потому, что так проще подключиться. Когда появится RLS, эти права изменят поведение защиты и скроют ошибку.

Для ручной работы читатель передаёт параметры через окружение. Пароли ниже являются локальными обозначениями и не должны становиться паролями какого-либо внешнего сервера. В архиве нет реальных реквизитов, ключей платежей или секретов входа.

export ASPNETCORE_ENVIRONMENT=Development
export ConnectionStrings__Default='Host=127.0.0.1;Database=space_lab;Username=space_app;Password=local-app'
export ConnectionStrings__Worker='Host=127.0.0.1;Database=space_lab;Username=space_worker;Password=local-worker'
dotnet run --project Space.Api

Разделитель __ соответствует вложенной конфигурации .NET. Имя Worker понадобится фоновому процессу; он не должен случайно взять соединение владельца схемы. Курс не требует контейнеров. Вы можете использовать уже установленный PostgreSQL 18 либо подготовить локальный экземпляр самостоятельно. Архив README описывает порядок SQL-файлов, а не утверждает, что база уже создана.

Если Default отсутствует, приложение должно остановиться с ясной ошибкой конфигурации. Такое поведение лучше скрытого перехода на другую базу: случайный запасной адрес способен отправить учебные изменения в чужую среду. Если база недоступна, ошибка подключения должна остаться ошибкой подключения, а не превращаться в пустой список проектов.

Первый наблюдаемый ответ

Маршрут /health сообщает только о жизни процесса. Он не читает проекты и не доказывает исправность всех зависимостей. Ниже — целое определение этого маршрута из каркаса; его можно разместить после создания app и до app.Run().

app.MapGet("/health", () => Results.Ok(new { status = "alive", execution = "local-lab" }));

Ожидаемый ответ содержит status: alive. Соединение с PostgreSQL проверяется отдельной операцией при работе с данными. Это разделение пригодится при разборе сбоя: живой HTTP-процесс может не иметь доступа к базе, а доступная база не подтверждает, что пользователь имеет членство в организации.

Обратите внимание и на завершённое состояние архива. Там зарегистрированы будущие маршруты, поэтому файл длиннее показанного каркаса. Это не приглашение скопировать кусок файла, выбросив нужные типы. Для воспроизводимого знакомства используйте весь проект, а статьи читайте как последовательное объяснение его частей. У каждой части будет определённый контракт и место изменения.

Окружение не выбирается случайно

Разделяйте адрес HTTP и адрес базы. Порт 5080 относится к API, а порт PostgreSQL задаётся в строке подключения. Смена одного не исправляет недоступность другого. Если на компьютере уже есть приложение с такой базой, создайте отдельную учебную базу и не применяйте seed к существующим клиентским таблицам. Пример рассчитан на новую схему; это ограничение помогает сохранить исходные UUID и делает результат сравнимым между уроками. При передаче проекта другому человеку отдайте README с именами параметров, а секретные значения он задаст самостоятельно. Наличие файла проекта не означает, что конфигурация уже правильно подготовлена.

После этого шага у вас есть платформа, источник данных и ясное место хранения конфигурации. Следующий урок вводит таблицы и исходные записи. Перед ним полезно различить три отказа: отсутствующий параметр останавливает старт, неверные реквизиты ломают доступ к базе, а недостаточное членство позднее отклонит конкретный запрос. Смешивание этих причин затрудняет диагностику и часто ведёт к ошибочному исправлению прав.

Оглавление серии