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

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

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

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

1. Итог сначала

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

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

На основе этой таблицы — основные выводы.

  • Приоритет конфигурации подчиняется одному правилу: побеждает provider, добавленный позже. По умолчанию providers загружаются в порядке appsettings.jsonappsettings.{Environment}.json → user secrets (только development) → переменные окружения → аргументы командной строки. Если ключ есть в нескольких источниках, позднее значение перезаписывает раннее. Одно запоминание порядка объясняет большинство случаев «я записал в JSON, но значение не действует». 1
  • Используйте Host.CreateApplicationBuilder, и всё это настраивается по умолчанию — собирать слои вручную не нужно. Даже настольные WinForms/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 открытым текстом. В development используйте user-secrets, а в production DPAPI (ProtectedData) или Credential Manager. Официальная документация прямо указывает, что user-secrets не шифруются и предназначены только для разработки. 78

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

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

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

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

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

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

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// appsettings.json / appsettings.{Environment}.json / environment variables /
// command-line arguments are already loaded by default, in the priority order shown above
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 в настольном приложении».

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

using Microsoft.Extensions.Configuration;

// Added after the default providers, so it has the highest priority
builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_");

3. Настройки по окружению: appsettings.{Environment}.json

Когда строки соединения или endpoints отличаются по окружениям, используйте файлы appsettings.Development.json, appsettings.Staging.json, appsettings.Production.json и т. п. Важно, что эти файлы рассматриваются как diff-файлы, которые переопределяют базовый 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». Практичен вариант с переменной уровня машины либо тонким wrapper batch-файлом, который делает SET перед запуском настоящего exe.
  • При запуске Task Scheduler надёжнее передавать аргумент командной строки. Вкладка Action планировщика не имеет UI для прямой установки environment variables, поэтому аргумент вроде --environment Production с чтением через AddCommandLine(args) даёт меньше ошибок. Особенности Task Scheduler, учётных записей и типов входа разобраны в статье о задачах, которые не запускаются или завершаются 0x1.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

var options = new HostApplicationBuilderSettings
{
    Args = args,
    // For execution paths where the environment variable isn't passed through (Task Scheduler, etc.),
    // also allow switching the environment via command-line arguments
};
HostApplicationBuilder builder = Host.CreateApplicationBuilder(options);

Console.WriteLine($"Current environment: {builder.Environment.EnvironmentName}");

4. Options pattern: получать настройки типами

Чтение через строковые ключи вроде IConfiguration["Key:SubKey"] имеет недостатки: опечатку нельзя поймать, а при глубокой вложенности трудно проследить, что происходит. На практике базовый путь — options pattern: связать настройки с 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;
}

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

using Microsoft.Extensions.DependencyInjection;

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

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

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

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

Значения настроек — BaseUrl, ApiKey и timeout — берите из IOptionsMonitor при каждом вызове, а сам HttpClient получайте через IHttpClientFactory и переиспользуйте. Если при каждом вызове делать new и Dispose для HttpClient, внутренний пул сокетов и соединений каждый раз уничтожается и строится заново, что при частом polling или пакетной обработке ведёт к исчерпанию ephemeral ports. Стандартная практика поэтому такова: значения могут меняться, а повторное использование соединений остаётся за IHttpClientFactory.

using Microsoft.Extensions.Options;

public sealed class ExternalApiClient(
    IHttpClientFactory httpClientFactory,
    IOptionsMonitor<ExternalApiOptions> optionsMonitor)
{
    public async Task<string> FetchAsync(CancellationToken cancellationToken)
    {
        // Get the latest value on every call. Reflects updates if the settings file has changed
        ExternalApiOptions current = optionsMonitor.CurrentValue;

        // Get HttpClient itself via the factory, reusing the internal connection pool
        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 оставляется factory. Это и есть разделение ответственности.

Если ошибка конфигурации проявляется как runtime exception, расследование затягивается. Сочетание проверки 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(); // Validation runs at host startup (during StartAsync/RunAsync), before hosted services start

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

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

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

Разделяйте записываемые настройки по характеру. Безопаснее всего привязывать их к special folders, которые возвращает Environment.GetFolderPath. 11

Место Как получить Использование
%LOCALAPPDATA%\CompanyName\AppName Environment.SpecialFolder.LocalApplicationData Стандартное место для персональных настроек и данных
%APPDATA%\CompanyName\AppName (Roaming) Environment.SpecialFolder.ApplicationData Только для настроек, которые должны следовать за пользователем в среде roaming profile
%ProgramData%\CompanyName\AppName Environment.SpecialFolder.CommonApplicationData Настройки на компьютер, общие всем пользователям. Нужен ACL design
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, "KomuraSoft", "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 });
        // Assumes the caller serializes concurrent writes within the same process
        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 profile см. «Как работают профили пользователей Windows»; общая таблица выбора хранилища приведена в «Как выбрать место для данных Windows-приложения».

6. Секреты: строки соединения и API keys

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

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

dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "dev-only-value"

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

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

public static class SecretProtector
{
    // Mixing in entropy that indicates the purpose makes it harder to confuse with data protected for other purposes
    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);
    }
}

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

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

7. Смена конфигурации во время работы: реальность reloadOnChange

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

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

  • Обновляются только значения, читаемые через IOptionsMonitor<T>IOptionsSnapshot<T>). IOptions<T> держит значение старта, поэтому остаётся устаревшим даже после переписывания файла. Большинство случаев «я отредактировал файл настроек, но ничего не действует» сводится к путанице этих двух.
  • FileSystemWatcher не всегда надёжно доставляет уведомления об изменениях на файловых системах Docker containers и сетевых shares. В таких средах DOTNET_USE_POLLING_FILE_WATCHER=true переключает мониторинг на polling раз в 4 секунды; интервал не настраивается. 13 О типичных особенностях самого FileSystemWatcher — пропущенных событиях, переполнении буфера и дубликатах — см. «Практическое руководство по FileSystemWatcher».
  • Одно изменение файла настроек способно вызвать уведомление несколько раз. Если приложение при каждом изменении повторяет дорогую операцию, учитывайте это: делайте debounce частых близких срабатываний или сравнивайте hash содержимого файла, обрабатывая только существенные изменения.

Не всегда нужно стремиться к «применению конфигурации без перезапуска». Лёгкие значения, например уровень лога или feature flag, можно безопасно применять сразу через IOptionsMonitor, но строку соединения БД или размер thread pool, изменение которых мгновенно конфликтует с уже работающими ресурсами, безопаснее явно специфицировать как «применяется после перезапуска». Важнее самого 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 (user scope) Собственный JSON-файл под %LOCALAPPDATA% Нет автосгенерированного механизма вроде ApplicationSettingsBase; сериализация/сохранение выполняются самостоятельно
Settings.settings (application scope) appsettings.json Рассматривайте как неизменяемые значения по умолчанию
Шифрование <connectionStrings> (aspnet_regiis и т. п.) DPAPI (ProtectedData) Механизм шифрования меняется полностью; его нужно перестроить при миграции

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

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

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

У миграции есть и другие стороны: совместимость зависимых библиотек, наличие COM interop, переосмысление способа распространения и общая картина проверок вне конфигурации. Такая инвентарная перспектива собрана в чек-листе перед миграцией .NET Framework → .NET; рекомендуем прочитать его в начале проекта миграции.

Итоги

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

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

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

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

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

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

  1. Microsoft Learn, Configuration in .NET - Alternative hosting approach. О порядке приоритета providers, который Host.CreateApplicationBuilder собирает по умолчанию: command-line arguments → environment variables → user secrets в development → appsettings.{Environment}.json → appsettings.json.  2 3

  2. Microsoft Learn, .NET Generic Host - Host builder settings. О стандартном порядке host configuration (DOTNET_-переменные и аргументы командной строки) и app configuration (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, configuration и logging. 

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

  5. Microsoft Learn, Options pattern in .NET - Options interfaces. О различиях lifetime, момента отражения изменений и поддерживаемых возможностей 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 хранит секреты как открытый JSON в %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json, не шифрует их и предназначен только для development.  2

  8. Microsoft Learn, ProtectedData Class. О ProtectedData.Protect/Unprotect как обёртках DPAPI, различии scope CurrentUser/LocalMachine и Windows-only поведении с PlatformNotSupportedException на других платформах.  2

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

  10. Microsoft Learn, Configuration providers in .NET - Environment variable configuration provider. О том, как стандартная конфигурация загружает DOTNET_-переменные и аргументы в host configuration, а обычные environment variables — в app configuration, и как добавить собственный префикс. 

  11. Microsoft Learn, Environment.GetFolderPath Method. О получении путей special folder через 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 filesystem-based provider, и о переходе на 4-секундный polling через DOTNET_USE_POLLING_FILE_WATCHER, когда уведомления ненадёжны в Docker containers или network shares. 

  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

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

Как сделать сайт заметным по запросам с названием района — практическое руководство по локальному SEO для малого и среднего бизнеса

Для компаний, которые не появляются по запросу «название района + отрасль», собран порядок исправлений для локального SEO: подготовка Goo...

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

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

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

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

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

Об авторе

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

Го Комура

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

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

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

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