Не только appsettings.json — практика конфигурации бизнес-приложений Windows (среды, секреты, куда писать)

· Обновлено: · · CSharp, .NET, appsettings.json, IConfiguration, IOptions, Generic Host, Управление конфигурацией, Разработка Windows, Техническая консультация

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619986)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). Не только appsettings.json — практика конфигурации бизнес-приложений Windows (среды, секреты, куда писать). KomuraSoft LLC. https://comcomponent.com/ru/blog/dotnet-configuration-management-guide/

DOI (зарегистрированный архив)
10.5281/zenodo.21619986
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21619987

«Нужно менять строку подключения по средам». «Хочу сохранять персональные настройки отображения». «API-ключ уже лежит в appsettings.json». Управление конфигурацией бизнес-приложений Windows почти всегда доходит до одного appsettings.json и чтения через IConfiguration. Дальше — настройки по средам, куда класть то, что приложение само записывает, как хранить секреты, как менять конфигурацию на ходу — часто уезжает в эксплуатацию так и не разобранным.

В статье идём по тем решениям, в которых на практике обычно путаются: от основы системы конфигурации .NET — IConfiguration и наложения поставщиков — к настройкам по средам, к типобезопасному получению через шаблон Options, к месту для записываемых настроек, к секретам, к реальным пределам изменения на ходу и к карте миграции с app.config/Settings.settings.

Глав восемь, текст длинный, поэтому сначала — как читать под задачу.

Ситуация читателя Как лучше читать
Проектируете конфигурацию с нуля Глава 1 (таблица решений) → главы 2–4 (основа, настройки по средам, шаблон Options). Главы 5–6 достаточно открыть, когда появятся запись настроек и секреты
Мигрируете с app.config существующего приложения Глава 8 (карта миграции) → глава 1 (таблица решений) → глава 2. Сначала поймите приёмник, затем закрывайте пункты по таблице соответствий — так быстрее
Разбираете случай «настройку сменил, а она не применилась» Глава 2 (приоритет поставщиков) и глава 7 (пределы reloadOnChange). Большинство причин — одно из двух
Нужно только куда класть секреты Глава 6. Сразу зафиксируйте, что в разработке и в production механизмы разные
Нужно только куда сохранять (на пользователя / на машину) Глава 5

1. Сначала выводы

Управление конфигурацией начинается с того, что для каждого вида настроек выбирают место. Сначала — общая таблица решений.

Вид настройки Пример Первое место, куда класть Почему
Значения приложения по умолчанию Уровень журнала по умолчанию, параметры UI appsettings.json Идёт в артефактах сборки; общие значения, не зависящие от среды
Настройки по средам Строка подключения и API-эндпоинт, которые отличаются между тестовой и production-средой appsettings.{Environment}.json + переменные окружения Можно использовать стандартный порядок переопределения как есть
Настройки пользователя Последняя открытая папка, положение окна, личные параметры отображения Собственный файл под %LOCALAPPDATA% (или %APPDATA%) Нужна область, куда каждый пользователь пишет отдельно
Настройки машины (общие для всех пользователей) Номер COM-порта устройства, адрес сервера лицензий Собственный файл под %ProgramData% Администратор задаёт один раз на компьютер, затем используют все
Секреты Пароль в строке подключения, API-ключи, токены Файл под защитой DPAPI (production), user-secrets (только разработка) Не храните открытым текстом. Механизм для разработки в production не переносите
Значения, которые меняются на ходу Feature flag, динамическая смена уровня журнала appsettings.json (reloadOnChange) + IOptionsMonitor Оставляйте только для тех значений, которые действительно хотите применить без перезапуска

Выводы с опорой на эту таблицу.

  • Приоритет конфигурации подчиняется одному правилу: побеждает поставщик, которого добавили позже. По умолчанию источники читаются в порядке appsettings.json → appsettings.{Environment}.json → user secrets (только в development) → переменные окружения → аргументы командной строки. Если ключ есть в нескольких местах, действует значение, прочитанное позже. Одного этого порядка хватает, чтобы объяснить большую часть случаев «в JSON записал, а не применилось».1
  • Host.CreateApplicationBuilder собирает эту иерархию сам, вручную её складывать не нужно. Даже настольные приложения на Windows Forms/WPF получают те же значения по умолчанию, если берут Generic Host.23
  • Среду переключают через DOTNET_ENVIRONMENT (или ASPNETCORE_ENVIRONMENT). В приложениях на WebApplication приоритет у DOTNET_ENVIRONMENT; если не задано ни то, ни другое, по умолчанию будет Production. Для настольного приложения и службы Windows заранее решите, кто и как передаёт эту переменную процессу.4
  • Настройки принимайте не как голый DTO, а как тип через семейство IOptions<T>. Если достаточно вычислить значение один раз при запуске — IOptions<T>; если изменение должно применяться без перезапуска — IOptionsMonitor<T>. Если мешать их без понимания разницы, получаются обе аварии: меняется то, что меняться не должно, и не меняется то, что должно.5
  • Ошибку конфигурации ловите при запуске, а не во время работы. ValidateDataAnnotations() вместе с ValidateOnStart() превращает ошибку настроек в исключение сразу после старта — вместо NullReferenceException глубокой ночью в production.6
  • Секреты в appsettings.json открытым текстом не кладите. В разработке — user-secrets, в production — DPAPI (ProtectedData) или Диспетчер учётных данных. В официальной документации прямо сказано: user-secrets не шифруются и предназначены только для разработки.78

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 25, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Основа системы конфигурации .NET — IConfiguration и поставщики

Система конфигурации .NET выглядит как одно хранилище ключ–значение IConfiguration, а за ним накладываются несколько поставщиков конфигурации. Выгода в том, что источники разной природы — JSON, переменные окружения, аргументы командной строки, INI, XML, коллекции в памяти — сходятся за одним интерфейсом IConfiguration.9

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

Host.CreateApplicationBuilder(args) по умолчанию собирает поставщиков в таком порядке (чем больше номер, тем выше приоритет, то есть источник добавлен позже).2

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. Secret Manager (только в development)
  4. Переменные окружения
  5. Аргументы командной строки

Этот порядок одинаков для консольного приложения, Worker Service и Windows Forms. Минимальный пример:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// По умолчанию уже загружены appsettings.json / appsettings.{Environment}.json /
// переменные окружения / аргументы командной строки в указанном выше приоритете
string? connectionString = builder.Configuration.GetConnectionString("Main");

using IHost host = builder.Build();
await host.RunAsync();

Даже настольное приложение на Windows Forms может брать тот же HostApplicationBuilder, если добавить пакет Microsoft.Extensions.Hosting. Как написано в статье «Что такое Generic Host», Generic Host — основа, которая сразу закрывает конфигурацию, DI и логирование; настольному приложению нет причин отказываться от системы конфигурации. Как встраивать это в приложение с фоновой работой, см. «Generic Host + BackgroundService в настольном приложении».

У переменных окружения есть оговорка. Параметры хоста (корень содержимого, имя среды и т. п.) читаются из переменных с префиксом DOTNET_, но этот префикс не используется для параметров приложения — обычных значений, которые читают через IConfiguration. Переменные, которые должны войти как параметры приложения, без префикса уже стоят в стандартном порядке через AddEnvironmentVariables().10 Если нужен свой префикс, добавьте его явно: builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_").

using Microsoft.Extensions.Configuration;

// Добавляется после стандартных поставщиков, поэтому приоритет становится наивысшим
builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_");

3. Настройки по средам — appsettings.{Environment}.json

Если строка подключения или эндпоинт должны отличаться по средам, берите файлы вроде appsettings.Development.json / appsettings.Staging.json / appsettings.Production.json. Важно, что эти файлы — не полная копия, а переопределение базового appsettings.json. Переписывать все пункты не нужно: достаточно значений, которые меняются от среды к среде.1

Имя среды задают переменные DOTNET_ENVIRONMENT или ASPNETCORE_ENVIRONMENT. Если используется WebApplication, значение DOTNET_ENVIRONMENT важнее ASPNETCORE_ENVIRONMENT; если не задано ни то, ни другое, по умолчанию будет Production. В Windows имена переменных окружения не зависят от регистра, в Linux зависят, поэтому если смотрите в сторону контейнеров, безопаснее держать написание одинаковым.4

Здесь и всплывает проблема: для настольного приложения и службы Windows само «как передать переменную окружения» — отдельная задача. Механизмы вроде launchSettings.json в ASP.NET Core работают только в локальной разработке, поэтому для production переключение среды нужно готовить отдельно. Типичных способов три.

  • Задать переменную на всю машину. Если сохранить её как setx DOTNET_ENVIRONMENT Production /M, её унаследует каждый процесс на этом компьютере. На одной машине с приложениями разных сред это не подходит.
  • Передать переменную процессу запуска службы Windows. Служба стартует в контексте, отличном от интерактивного сеанса пользователя, поэтому пользовательские переменные окружения не наследуются. Об учётной записи запуска и изоляции сеансов см. «Как создавать службы Windows и как их сопровождать». На практике берут переменную уровня машины либо тонкий bat-файл-обёртку, который делает SET и затем запускает сам exe (содержимое bat ниже).
  • Если запускаете через Планировщик заданий, надёжнее передать аргумент командной строки. Во вкладке «Действие» нет UI, чтобы задать переменные окружения напрямую, поэтому аргумент вроде --environment Production и чтение через AddCommandLine(args) даёт меньше сбоев. Особенности учётной записи запуска и типа входа у Планировщика заданий разобраны в «Задача Планировщика не запускается или завершается с кодом 0x1».

Второму способу хватает трёх строк в bat-обёртке.

@echo off
set DOTNET_ENVIRONMENT=Production
"%~dp0MyApp.Service.exe" %*

%~dp0 — каталог, где лежит сам bat (со слешем \ в конце), поэтому exe рядом запускается независимо от текущего рабочего каталога. set действует только на этот процесс и его дочерние, поэтому приложения других сред на той же машине друг другу не мешают. Но сам этот bat нельзя зарегистрировать службой через sc create. Службой может быть только exe, который отвечает на команды управления службами; если указать bat, при запуске будет таймаут (ошибка 1053). Этот приём работает, когда процесс стартует через обёртку службы или через Планировщик заданий, о котором ниже. Если хотите зарегистрировать саму службу как есть, берите переменную уровня машины или аргументы командной строки.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

var options = new HostApplicationBuilderSettings
{
    Args = args,
    // Для путей запуска, где переменная окружения не передаётся (Планировщик заданий и т. п.),
    // допускаем переключение среды и через аргументы командной строки
};
HostApplicationBuilder builder = Host.CreateApplicationBuilder(options);

Console.WriteLine($"Текущая среда: {builder.Environment.EnvironmentName}");

4. Шаблон Options — принимать настройки типом

Чтение по строковому ключу вроде IConfiguration["Key:SubKey"] слабо тем, что опечатку не поймаешь и при глубокой вложенности трудно следить за ключами. На практике базовый путь — шаблон Options: связать настройки с POCO и принимать их как тип.

public sealed class ExternalApiOptions
{
    public const string SectionName = "ExternalApi";

    public required string BaseUrl { get; set; }
    public required string ApiKey { get; set; }
    public int TimeoutSeconds { get; set; } = 30;
}

Регистрация и привязка выглядят так:

using Microsoft.Extensions.DependencyInjection;

builder.Services
    .AddOptions<ExternalApiOptions>()
    .Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName));

Для получения значений есть три интерфейса с разным поведением.5

Интерфейс Время жизни регистрации Как применяется смена настроек Основное применение
IOptions<T> Singleton Не применяется (считается один раз при запуске) Настройки, которые по задумке не меняются. Самый простой вариант
IOptionsSnapshot<T> Scoped Пересчитывается каждый раз, когда scope собирается заново Контексты с явным scope, например scope HTTP-запроса
IOptionsMonitor<T> Singleton Всегда отдаёт актуальное значение и уведомляет об изменении (OnChange) Постоянно работающие службы и другие случаи, когда изменение нужно заметить сразу

В настольном приложении и службе Windows нет HTTP request scope, поэтому IOptionsSnapshot<T> ведёт себя по сути как IOptions<T>, пока вы сами не создаёте scope. Если нужно замечать изменения, берите IOptionsMonitor<T> — такое правило снимает большую часть сомнений.

Значения настроек (BaseUrl, ApiKey, тайм-аут) берите из IOptionsMonitor на каждый вызов, а сам HttpClient получайте через IHttpClientFactory и используйте повторно. Если на каждый вызов делать new и Dispose для HttpClient, внутренний пул сокетов и соединений каждый раз уничтожается и собирается заново; при частом опросе или пакетной обработке это ведёт к исчерпанию эфемерных портов. Поэтому значения могут меняться, а повторное использование соединений оставляют IHttpClientFactory.

using Microsoft.Extensions.Options;

public sealed class ExternalApiClient(
    IHttpClientFactory httpClientFactory,
    IOptionsMonitor<ExternalApiOptions> optionsMonitor)
{
    public async Task<string> FetchAsync(CancellationToken cancellationToken)
    {
        // На каждый вызов берём актуальное значение. Если файл настроек обновили — подхватится
        ExternalApiOptions current = optionsMonitor.CurrentValue;

        // Сам HttpClient берём через фабрику и переиспользуем внутренний пул соединений
        HttpClient client = httpClientFactory.CreateClient(nameof(ExternalApiClient));

        using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(new Uri(current.BaseUrl), "status"));
        request.Headers.Add("X-Api-Key", current.ApiKey);

        using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(current.TimeoutSeconds));
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

        using HttpResponseMessage response = await client.SendAsync(request, linkedCts.Token);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync(cancellationToken);
    }
}

На вызывающей стороне зарегистрируйте именованный клиент: builder.Services.AddHttpClient(nameof(ExternalApiClient));. Значения вроде BaseUrl и ApiKey, которые могут смениться через конфигурацию, кладите в HttpRequestMessage на каждый запрос, а стоимость создания и освобождения экземпляра HttpClient оставляйте фабрике. В этом и состоит разделение ролей.

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

using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;

public sealed class ExternalApiOptions
{
    public const string SectionName = "ExternalApi";

    [Required, Url]
    public required string BaseUrl { get; set; }

    [Required, MinLength(16)]
    public required string ApiKey { get; set; }

    [Range(1, 300)]
    public int TimeoutSeconds { get; set; } = 30;
}

builder.Services
    .AddOptions<ExternalApiOptions>()
    .Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart(); // Проверка идёт при запуске хоста (StartAsync/RunAsync), до старта hosted services

Без ValidateOnStart() проверка откладывается до первого фактического обращения к этим параметрам. Сама проверка выполняется не сразу после Build(), а при запуске хоста (StartAsync/RunAsync), поэтому в тестах, которые вызывают только Build() и ещё не вызывают RunAsync(), она в этот момент ещё не бежала. Чтобы не получить плавающий сбой «приложение стартовало, а упало в момент открытия экрана с этой настройкой», для настроек бизнес-приложения по умолчанию ставьте ValidateOnStart().6

5. Куда класть настройки, которые приложение само записывает

appsettings.json — место для «значений по умолчанию только для чтения», а не место для настроек, которые приложение перезаписывает само. Многие бизнес-приложения ставятся в Program Files, куда у обычного пользователя нет права записи, и тогда наступают на одну из двух проблем: исключение во время работы либо виртуализация файловой системы Windows, из-за которой видимое содержимое разъезжается между пользователями.

То, что нужно писать, разделяйте по характеру. Безопаснее опираться на специальные папки, которые возвращает Environment.GetFolderPath.11

Куда класть Как получить Для чего
%LOCALAPPDATA%\ИмяКомпании\ИмяПриложения Environment.SpecialFolder.LocalApplicationData Значения и данные по умолчанию на пользователя
%APPDATA%\ИмяКомпании\ИмяПриложения (Roaming) Environment.SpecialFolder.ApplicationData Только настройки, которые должны следовать за пользователем в среде перемещаемого профиля
%ProgramData%\ИмяКомпании\ИмяПриложения Environment.SpecialFolder.CommonApplicationData Настройки на машину, общие для всех пользователей. Нужно проектировать ACL
using System;
using System.IO;
using System.Text.Json;

public sealed class UserSettingsStore
{
    private readonly string _filePath;

    public UserSettingsStore()
    {
        string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        string dir = Path.Combine(root, "YourCompany", "MyApp");
        Directory.CreateDirectory(dir);
        _filePath = Path.Combine(dir, "user-settings.json");
    }

    public UserSettings Load()
    {
        if (!File.Exists(_filePath))
        {
            return new UserSettings();
        }
        string json = File.ReadAllText(_filePath);
        return JsonSerializer.Deserialize<UserSettings>(json) ?? new UserSettings();
    }

    public void Save(UserSettings settings)
    {
        string json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
        // Предполагается, что параллельную запись в рамках одного процесса сериализует вызывающий код
        File.WriteAllText(_filePath, json);
    }
}

public sealed class UserSettings
{
    public string? LastOpenedFolder { get; set; }
    public int WindowWidth { get; set; } = 1024;
    public int WindowHeight { get; set; } = 768;
}

Если смешать настройки пользователя и настройки машины в одном файле, на общей машине получите аварию вида «настройка пользователя A мешает пользователю B». О разделении «на пользователя / на машину» и о самой структуре профиля Windows см. «Как устроен профиль пользователя Windows»; общая таблица выбора места хранения — в «Как выбрать место хранения данных Windows-приложения».

6. Секреты — строки подключения и API-ключи

Писать пароль из строки подключения или API-ключ прямо в appsettings.json — практический риск, даже если файл в .gitignore. Открытый текст утекает через общую папку при распространении, через пересылку файла в поддержку, через забытый старый файл настроек, который остался в резервной копии.

В разработке берите Secret Manager (dotnet user-secrets). Но это механизм удобства разработки, и он не шифруется. Значения лежат открытым JSON в %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json; официальная документация прямо говорит: «не считайте это доверенным хранилищем, только для разработки». Класть туда production-секреты и распространять их — ошибка.7

dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "значение для разработки"

В production шифруйте значение через DPAPI (Data Protection API) Windows, привязав его к учётным данным пользователя или машины. Protect/Unprotect в System.Security.Cryptography.ProtectedData — обёртки над ним; с DataProtectionScope.CurrentUser расшифровать можно только под той же учётной записью и только на той же машине. Проектируйте с учётом того, что DPAPI — функция только для Windows: на других платформах будет PlatformNotSupportedException.8

using System.Security.Cryptography;
using System.Text;

public static class SecretProtector
{
    // Дополнительная энтропия по назначению снижает шанс перепутать данные, защищённые для другой цели
    private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("MyApp.ExternalApi.ApiKey");

    public static string Protect(string plainText)
    {
        byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
        byte[] protectedBytes = ProtectedData.Protect(plainBytes, Entropy, DataProtectionScope.CurrentUser);
        return Convert.ToBase64String(protectedBytes);
    }

    public static string Unprotect(string protectedBase64)
    {
        byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
        byte[] plainBytes = ProtectedData.Unprotect(protectedBytes, Entropy, DataProtectionScope.CurrentUser);
        return Encoding.UTF8.GetString(plainBytes);
    }
}

Коротко о роли энтропии (optionalEntropy) в коде выше. Область CurrentUser у DPAPI означает «расшифровать может тот же пользователь на той же машине», поэтому без энтропии расшифровать смогут и другие приложения, запущенные с правами того же пользователя. Если защищать с энтропией, при расшифровке нужно передать то же значение — это прямо сказано в официальной документации.8 То есть энтропия — пароль самого приложения: она мешает расшифровать данные другим программам того же пользователя на той же машине. Но этот пароль вшит в exe, поэтому защита такого уровня, который снимается разбором файла. Это не способ запретить расшифровку самому пользователю.

Где хранить значение после защиты DPAPI, какую область выбрать между CurrentUser и LocalMachine, какие ловушки ждут бизнес-приложение с несколькими пользователями на одной машине — это уже уровень реализации, подробно разобранный в «Хранение секретов в Windows-приложениях: как не оставлять настройки открытым текстом с DPAPI». При реализации смотрите туда.

Граница «можно открытым текстом / нельзя» простая. Смотрите, потребуется ли при утечке перевыпустить пароль или API-ключ, расследовать несанкционированный доступ или отчитываться перед надзорным органом. Имя узла или порт в строке подключения при утечке чаще дают небольшой реальный вред; встроенный пароль или токен аутентификации всегда нужно защищать. Если собирать строку подключения из «информации о сервере» и «учётных данных» и отдавать под DPAPI только второе, на уровне кода отделяется то, что в открытом виде относительно безвредно, от того, что нужно защищать.

7. Смена конфигурации на ходу — реальность reloadOnChange

Стандартный вызов AddJsonFile — тот, что внутри делает Host.CreateApplicationBuilder — задаёт reloadOnChange: true, поэтому appsettings.json / appsettings.{Environment}.json сами перечитываются, когда файл изменился. Внутри PhysicalFileProvider следит за изменениями через FileSystemWatcher.12

У механизма есть несколько практических пределов.

  • Обновляются только значения, прочитанные через IOptionsMonitor<T> (и IOptionsSnapshot<T>). IOptions<T> держит значение старта, поэтому после правки файла остаётся старым. Большинство обращений «файл настроек правил напрямую, а не применилось» сводятся к путанице этих двух.
  • FileSystemWatcher не всегда надёжно доставляет уведомления об изменении на таких файловых системах, как контейнер Docker или сетевая папка. В этих средах переменная окружения DOTNET_USE_POLLING_FILE_WATCHER со значением 1 или true переключает наблюдение на опрос раз в 4 секунды (интервал не меняется).13 Формулировку «4 секунды, интервал не настраивается» на момент написания (июль 2026) мы сверяли со страницей Microsoft Learn «Options pattern in .NET» (обновлена в октябре 2025). У документа нет привязки к версии, поэтому если кладёте это в долгоживущую архитектуру, сверьтесь с актуальной редакцией той же страницы. Типичные особенности самого FileSystemWatcher — пропуски, переполнение буфера, дубли событий — собраны в «Практическое руководство по FileSystemWatcher».
  • На одно изменение файла настроек уведомление может прийти несколько раз. Если приложение при каждом срабатывании заново запускает тяжёлую работу, учитывайте это: делайте debounce частых срабатываний подряд или сравнивайте хеш содержимого файла и обрабатывайте только реальные изменения.

Не обязательно всегда целиться в «применить без перезапуска». Лёгкие значения вроде уровня журнала или feature flag можно сразу подхватывать через IOptionsMonitor. Строку подключения к БД или размер пула потоков — значения, которые в момент смены могут разъехаться с уже живыми ресурсами, — безопаснее явно описать в спецификации как «применяется после перезапуска». Важнее самого reloadOnChange спроектировать так, чтобы эксплуатация ясно слышала: «эта настройка действует сразу после сохранения», «эта требует перезапуска».

8. Карта миграции с app.config / Settings.settings

Когда приложение эпохи .NET Framework переносят на .NET, механизм конфигурации тоже нужно собрать заново. Сводим соответствия.14

.NET Framework .NET Примечание
<appSettings> в App.config / Web.config appsettings.json + IConfiguration Иерархию естественно выражает вложенность JSON
ConfigurationManager.AppSettings["Key"] builder.Configuration["Key"] или IOptions<T> От строковых ключей к типизированному доступу
ConfigurationManager.ConnectionStrings builder.Configuration.GetConnectionString("Name") Соглашение о секции ConnectionStrings сохраняется
Settings.settings (область пользователя) Собственный JSON-файл под %LOCALAPPDATA% Автогенерации вроде ApplicationSettingsBase нет. Сериализацию и сохранение пишете сами (пример класса сохранения — в главе 5)
Settings.settings (область приложения) appsettings.json Считайте значениями по умолчанию только для чтения
Шифрование <connectionStrings> (aspnet_regiis и т. п.) DPAPI (ProtectedData) Меняется сам механизм шифрования. При миграции его нужно сделать заново

На миграции часто наступают на две ловушки.

Первая: если добавить NuGet-пакет System.Configuration.ConfigurationManager, код чтения App.config продолжит работать без изменений. Как первый этап миграции это годится, но на нём нельзя останавливаться: перенос на appsettings.json нужно заложить в план. Сопутствующие библиотеки вроде logging provider почти везде уже ждут appsettings.json, поэтому держать старый механизм только ради чтения App.config становится всё менее оправданно.14

Вторая — настройки Settings.settings с областью пользователя. В .NET Framework вызов Properties.Settings.Default.Save() сам сохранял параметры на пользователя; в .NET аналогичной автогенерации из коробки нет. При миграции подготовьте свой класс сохранения, как в главе 5.

У миграции есть и другая сторона — совместимость зависимых библиотек, есть ли COM, пересмотр способа распространения и прочие проверки вне конфигурации. Такую инвентаризацию мы собрали в «Чек-лист перед миграцией с .NET Framework на .NET»; имеет смысл просмотреть его на ранней стадии проекта миграции.

Итоги

Управление конфигурацией .NET складывается из трёх механизмов: наложение поставщиков через IConfiguration, типобезопасное получение значений через семейство IOptions и переключение среды переменными окружения. До этого места большинство приложений пользуются системой одинаково. Специфика бизнес-приложений Windows начинается дальше: куда класть то, что приложение само записывает, как защищать секреты, насколько разрешать смену настроек на ходу и как свернуть наследие поколения app.config.

Сделайте шаг дальше состояния «всё записано в appsettings.json» и разделите место и способ обработки для каждого вида настроек. Если таблица решений в статье станет отправной точкой такой раскладки — хорошо. Инвентаризация конфигурации существующего приложения и выбор, как уходить с app.config, часто не дают лучшего ответа без реальных файлов настроек и среды развёртывания — если застряли, напишите нам.

Связанные статьи

Связанные области консультаций

KomuraSoft LLC консультирует по проектированию конфигурации бизнес-приложений Windows, по сопровождению настроек по средам и по стратегии миграции с существующего app.config.

Справочные ссылки

  1. Microsoft Learn, Configuration in .NET - Alternative hosting approach. О приоритете поставщиков конфигурации, который Host.CreateApplicationBuilder собирает по умолчанию (аргументы командной строки → переменные окружения → user secrets в development → appsettings.{Environment}.json → appsettings.json). ↩ ↩2 ↩3

  2. Microsoft Learn, .NET Generic Host - Host builder settings. О стандартном порядке конфигурации хоста (переменные с префиксом DOTNET_, аргументы командной строки) и конфигурации приложения (appsettings.json, appsettings.{Environment}.json, Secret Manager, переменные окружения, аргументы командной строки), которые загружает Host.CreateApplicationBuilder. ↩ ↩2

  3. Microsoft Learn, Use the .NET Generic Host in a Windows Forms app. О том, как встроить Generic Host в приложение Windows Forms и пользоваться DI, конфигурацией и логированием. ↩

  4. Microsoft Learn, ASP.NET Core runtime environments - Environment variables that determine the runtime environment. О связи DOTNET_ENVIRONMENT и ASPNETCORE_ENVIRONMENT, о том, что при WebApplication приоритет у DOTNET_ENVIRONMENT, что при отсутствии обеих по умолчанию будет Production, и о том, что в Windows имена переменных не зависят от регистра, а в Linux зависят. ↩ ↩2

  5. Microsoft Learn, Options pattern in .NET - Options interfaces. О различиях IOptions<TOptions>, IOptionsSnapshot<TOptions> и IOptionsMonitor<TOptions> по времени жизни, моменту применения смены настроек и поддерживаемым возможностям. ↩ ↩2

  6. Microsoft Learn, Options pattern in .NET - Options validation. О проверке DataAnnotations через ValidateDataAnnotations() и о проверке при запуске через ValidateOnStart() (или AddOptionsWithValidateOnStart). ↩ ↩2 ↩3

  7. Microsoft Learn, Safe storage of app secrets in development in ASP.NET Core - Use the Secret Manager tool. О том, что Secret Manager не шифрует секреты и сохраняет их открытым текстом в %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json, предназначен только для разработки и не должен считаться доверенным хранилищем. ↩ ↩2

  8. Microsoft Learn, ProtectedData Class. О методах ProtectedData.Protect/Unprotect как обёртках над DPAPI (Data Protection API), о различии областей DataProtectionScope.CurrentUser/LocalMachine и о том, что это функция только для Windows: на других платформах будет PlatformNotSupportedException. ↩ ↩2 ↩3

  9. Microsoft Learn, Configuration providers in .NET. О поставщиках конфигурации, которые сводят за IConfiguration источники разной природы: JSON, переменные окружения, командную строку, INI, XML и другие. ↩

  10. Microsoft Learn, Configuration providers in .NET - Environment variable configuration provider. О том, что стандартная конфигурация читает переменные с префиксом DOTNET_ и аргументы командной строки в конфигурацию хоста и приложения, но это не идёт в пользовательскую конфигурацию, и о том, как добавить свой префикс. ↩

  11. Microsoft Learn, Environment.GetFolderPath Method. О получении путей специальных папок через перечисление Environment.SpecialFolder и метод GetFolderPath. ↩

  12. Microsoft Learn, Detect changes with change tokens in ASP.NET Core - Monitor for configuration changes. О параметре reloadOnChange у AddJsonFile и о том, что PhysicalFileProvider внутри следит за файлом настроек через FileSystemWatcher. ↩

  13. Microsoft Learn, Options pattern in .NET - IOptionsMonitor. О том, что уведомления IOptionsMonitor ограничены поставщиками на основе файловой системы, и о переключении на опрос раз в 4 секунды через DOTNET_USE_POLLING_FILE_WATCHER, когда уведомления ненадёжны в контейнере Docker или на сетевой папке. ↩

  14. Microsoft Learn, Modernize after upgrading to .NET from .NET Framework - App.config. О переносе App.config на appsettings.json, о совместимости через NuGet-пакет System.Configuration.ConfigurationManager и о пакете Microsoft.Extensions.Configuration.Json. ↩ ↩2

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Можно ли класть appsettings.json в ту же папку, что и exe?
Если это значения по умолчанию только для чтения — да. Но если приложение ставится в Program Files, его нельзя проектировать так, чтобы оно само перезаписывало appsettings.json. У обычного пользователя нет права записи в Program Files: во время работы получите исключение либо виртуализация файловой системы Windows покажет разным пользователям разное содержимое. Всё, что нужно писать, вынесите в отдельный файл под %LOCALAPPDATA% или %ProgramData%.
Что должно побеждать: переменные окружения или appsettings.json?
Стандартный приоритет не меняйте: используйте встроенный порядок переопределения appsettings.json → appsettings.{Environment}.json → переменные окружения → аргументы командной строки. Если сомневаетесь, смотрите на характер значения. Значения по умолчанию, которые можно класть в артефакты сборки, — в appsettings.json; то, что меняется на целевом стенде или в контейнере, — в переменные окружения. Такое разделение потом не путает.
Как делить классы настроек?
Делите options-классы по функции или зоне ответственности. Строка подключения — ConnectionOptions, внешний API — ExternalApiOptions: не складывайте в один класс несвязанные настройки. Тогда и единица проверки и перечитывания для IOptionsSnapshot/IOptionsMonitor получается естественной, и проверка DataAnnotations замыкается на класс.
Стоит ли уходить с INI-файлов или реестра?
Если конфигурацию поднимаете заново, лучше свести её к appsettings.json + IConfiguration. В .NET есть и INI configuration provider, поэтому существующие INI можно продолжать читать и переносить постепенно. Реестр в стандартный набор поставщиков конфигурации .NET не входит. Если от реестра зависят старые активы, напишите мост только для чтения и постепенно переносите значения в appsettings.json.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог