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

Конфигурация приложения

Конфигурация задаёт значения, которые приложению нужны при работе, но которые неудобно держать среди обработчиков. В учебном каталоге это подпись страницы и ограничение числа уроков в одном курсе. Позже появится строка подключения к отдельной базе. Рассмотрим, как ASP.NET Core объединяет несколько источников настроек и как получить из них осмысленный объект.

Продолжаем результат предыдущего урока: запуск, модель и endpoints уже разделены. Изменения находятся в lesson-03/after; четыре курса и их числа остаются прежними. Добавим GET /api/catalog-info, который показывает только два несекретных параметра. Он нужен для объяснения порядка чтения, а не для выдачи полной конфигурации процесса клиенту.

Один ключ в нескольких источниках

В appsettings.json разместим базовые значения. Файл является полным примером конфигурации текущего снимка:

{
  "Catalog": {
    "Caption": "Учебный каталог",
    "MaxLessons": 500
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  }
}

Иерархия JSON превращается в ключи вида Catalog:Caption и Catalog:MaxLessons. Вложенность помогает группировать связанные настройки, но не делает значения секретными. Файл хранится вместе с исходниками, поэтому сюда подходят подпись и обычные ограничения. Паролей, токенов и рабочих адресов базы здесь нет.

В appsettings.Development.json переопределим только подпись:

{
  "Catalog": {
    "Caption": "Каталог для разработчика"
  }
}

Остальные ключи не исчезают. При объединении конфигурации второй файл заменяет значение существующего ключа, а не весь объект Catalog целиком. Поэтому ожидаемая подпись в нашем Development-профиле — «Каталог для разработчика», а MaxLessons остаётся 500. Если позднее добавить другое несекретное поле в базовый JSON, оно также сохранится, пока другой источник его не переопределит.

Стандартный CreateBuilder уже подключает основные источники. Для обычной конфигурации приложения приоритет возрастает от appsettings.json к файлу окружения, затем к User Secrets в Development при настроенном идентификаторе, переменным окружения и аргументам командной строки. Этот порядок описан в документации Microsoft по конфигурации. Мы не очищаем коллекцию провайдеров и не создаём второй builder, поэтому используем стандартное объединение.

Переменная Catalog__Caption соответствует ключу Catalog:Caption: двойное подчёркивание позволяет выразить вложенность в переносимом имени переменной среды. Если читатель позднее задаст такую переменную, она перекроет JSON. Аргумент --Catalog:Caption=Учебная версия при запуске имеет ещё более высокий приоритет. Эти значения не исполняются как C#; они становятся строковыми настройками и затем преобразуются при привязке к типу.

Объект настроек и проверка при запуске

В Configuration/CatalogOptions.cs введём класс с двумя свойствами:

namespace CatalogApi;
public sealed class CatalogOptions
{
    public string Caption { get; set; } = "Учебный каталог";
    public int MaxLessons { get; set; } = 500;
}

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

В Program.cs после создания builder и проверки Development добавлено следующее выражение регистрации:

builder.Services.AddOptions<CatalogOptions>()
    .Bind(builder.Configuration.GetSection("Catalog"))
    .Validate(o => !string.IsNullOrWhiteSpace(o.Caption), "Caption is required.")
    .Validate(o => o.MaxLessons == 500, "The learning contract fixes MaxLessons at 500.")
    .ValidateOnStart();

Bind связывает свойства с одноимёнными ключами секции. Проверки описывают допустимое состояние полученного объекта. Подпись не должна состоять из пробелов, а предел в этой учебной модели закреплён ровно на 500. Мы пока не показываем гибкое изменение этого предела: последующая валидация и SQL-ограничение будут согласованы с тем же числом. Если разрешить произвольную настройку здесь и сохранить жёсткое ограничение в другом месте, клиент получит противоречивый контракт.

ValidateOnStart переносит проверку на начало работы host. Иначе неверная настройка может обнаружиться только при первом разрешении объекта options. Остановка запуска понятнее ситуации, когда приложение слушает порт и начинает отдавать ошибки лишь при обращении к конкретному endpoint. Привязку, время чтения и варианты options рассматривает описание Options pattern.

Получение значений в обработчике

После регистрации маршрутов добавлен GET /api/catalog-info:

app.MapGet("/api/catalog-info", (Microsoft.Extensions.Options.IOptions<CatalogOptions> options) =>
    new { caption = options.Value.Caption, maxLessons = options.Value.MaxLessons });

Зависимость IOptions<CatalogOptions> разрешается инфраструктурой приложения. Обработчик не открывает JSON сам и не читает переменные окружения по отдельности. Он получает результат установленного порядка провайдеров и правил привязки. В текущем Development-профиле ожидаемый ответ имеет форму:

{"caption":"Каталог для разработчика","maxLessons":500}

При внешнем переопределении подписи меняется только caption. Если установить пустую подпись, ожидается ошибка старта, а не JSON с пустым полем. Это различает два класса ситуации: отсутствующий HTTP-ресурс обрабатывается после запуска, неверная обязательная конфигурация мешает приложению начать работу.

IOptions в нашем примере даёт фиксированный для приложения объект настроек. Он не обещает, что каждое изменение файла немедленно появится в уже исполняющемся запросе. Для обновлений и границ запроса существуют другие интерфейсы options, но вводить их без задачи не требуется. Сначала важно понять, откуда пришло значение и когда оно было проверено.

Окружение приложения и конфигурация приложения связаны, но не тождественны. Название Development определяется достаточно рано, чтобы выбрать файл окружения. Не нужно пытаться переключить окружение обычным полем внутри уже выбранного appsettings.json. Учебный профиль задаёт его явно, и проверка в Program.cs сохраняет ограничение этого проекта.

User Secrets тоже не являются автоматическим универсальным хранилищем. В наших файлах ещё нет UserSecretsId, поэтому мы не утверждаем, что этот провайдер загрузил какие-то значения. Даже при подключении инструмент предназначен для разработки и не превращает локальный файл в защищённое хранилище production. Настоящие секреты следует передавать подходящим внешним способом, не возвращать клиенту и не писать в журнал.

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

Не возвращайте список всех провайдеров и полные значения настроек в публичном endpoint ради такого расследования. Операция /api/catalog-info сознательно выбирает два известных несекретных свойства. Позднее конфигурация расширится строкой базы, но этот endpoint не должен автоматически получить её. Явный ответ предотвращает случайное расширение диагностической выдачи вместе с настройками.

В результате endpoint знает форму нужных настроек и получает проверенный объект, а файл запуска определяет, как этот объект подготовить. В следующем уроке применим тот же принцип к источнику курсов: обработчик будет зависеть от поведения хранилища, а не от статического seed.