Не только 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.json→appsettings.{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
appsettings.jsonappsettings.{Environment}.json- Secret Manager (только окружение development)
- Переменные окружения
- Аргументы командной строки
Этот порядок одинаков для 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 часто не дают лучшего ответа без просмотра реальных файлов настроек и среды развёртывания — если есть сомнения, свяжитесь с нами.
Связанные статьи
- Что такое Generic Host?
- Generic Host + BackgroundService в настольном приложении
- Задачи Task Scheduler не запускаются или завершаются 0x1
- Как создавать и эксплуатировать службы Windows
- Как выбрать место для данных Windows-приложения
- Как работают профили пользователей Windows
- Хранение чувствительных данных в Windows-приложениях: отказ от открытых настроек с DPAPI
- Практическое руководство по FileSystemWatcher
- Чек-лист перед миграцией .NET Framework → .NET
Связанные области консультаций
KomuraSoft LLC проводит технические консультации по проектированию управления конфигурацией Windows-бизнес-приложений, эксплуатационному проектированию настроек по окружениям и стратегии миграции с существующих ресурсов app.config.
Справочные ссылки
-
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 -
Microsoft Learn, .NET Generic Host - Host builder settings. О стандартном порядке host configuration (
DOTNET_-переменные и аргументы командной строки) и app configuration (appsettings.json, appsettings.{Environment}.json, Secret Manager, переменные окружения, аргументы), загружаемыхHost.CreateApplicationBuilder. ↩ ↩2 -
Microsoft Learn, Use the .NET Generic Host in a Windows Forms app. О шагах внедрения Generic Host в Windows Forms и использования DI, configuration и logging. ↩
-
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 -
Microsoft Learn, Options pattern in .NET - Options interfaces. О различиях lifetime, момента отражения изменений и поддерживаемых возможностей
IOptions<TOptions>,IOptionsSnapshot<TOptions>иIOptionsMonitor<TOptions>. ↩ ↩2 -
Microsoft Learn, Options pattern in .NET - Options validation. О настройке DataAnnotations-проверки через
ValidateDataAnnotations()и проверке при запуске черезValidateOnStart()либоAddOptionsWithValidateOnStart. ↩ ↩2 ↩3 -
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 -
Microsoft Learn, ProtectedData Class. О
ProtectedData.Protect/Unprotectкак обёртках DPAPI, различии scopeCurrentUser/LocalMachineи Windows-only поведении сPlatformNotSupportedExceptionна других платформах. ↩ ↩2 -
Microsoft Learn, Configuration providers in .NET. О механизме providers, объединяющем источники разной природы — JSON, переменные окружения, командную строку, INI, XML и другие — за
IConfiguration. ↩ -
Microsoft Learn, Configuration providers in .NET - Environment variable configuration provider. О том, как стандартная конфигурация загружает
DOTNET_-переменные и аргументы в host configuration, а обычные environment variables — в app configuration, и как добавить собственный префикс. ↩ -
Microsoft Learn, Environment.GetFolderPath Method. О получении путей special folder через
Environment.SpecialFolderиGetFolderPath. ↩ -
Microsoft Learn, Detect changes with change tokens in ASP.NET Core - Monitor for configuration changes. О параметре
reloadOnChangeAddJsonFileи использованииPhysicalFileProviderвнутриFileSystemWatcherдля наблюдения за файлом настроек. ↩ -
Microsoft Learn, Options pattern in .NET - IOptionsMonitor. Об ограничении уведомлений IOptionsMonitor filesystem-based provider, и о переходе на 4-секундный polling через
DOTNET_USE_POLLING_FILE_WATCHER, когда уведомления ненадёжны в Docker containers или network shares. ↩ -
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
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Печать и вывод PDF в Windows-бизнес-приложениях — выбор между System.Drawing.Printing, WPF и библиотеками отчётов
В виде таблицы выбора по требованиям собраны печать WinForms через PrintDocument, печать WPF через FlowDocument/FixedDocument и варианты ...
Как точно найти причину «медленной работы» с PerfView и dotnet-trace — практическое введение в анализ производительности .NET
Когда бизнес-приложение «работает медленно», «загружает ЦП под завязку» или «иногда зависает», какой инструмент выбрать и на что смотреть...
Введение в журнал событий Windows и ETW — как использовать стандартные механизмы ОС для логов бизнес-приложения
Ваше Windows-бизнес-приложение опирается только на файловые логи? Журнал событий и ETW — это записи другого уровня, видимые операционному...
Google Ads для BtoB с небольшим бюджетом — как получить результат при нескольких десятках тысяч иен в месяц
Практическое руководство для BtoB-компаний, которые начинают Google Ads с бюджетом в несколько десятков тысяч иен в месяц. Разбираются ср...
Как сделать сайт заметным по запросам с названием района — практическое руководство по локальному SEO для малого и среднего бизнеса
Для компаний, которые не появляются по запросу «название района + отрасль», собран порядок исправлений для локального SEO: подготовка Goo...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Generic Host и архитектура приложения
Generic Host, BackgroundService, DI, конфигурация, журналирование и жизненный цикл приложения.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Управление конфигурацией и проектирование файлов настроек относятся к практической консультации по разработке Windows-приложений.
Технические консультации и ревью дизайна
Выбор стратегии миграции с существующих ресурсов app.config — решение, которое относится к технической консультации с ревью дизайна.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Можно ли положить 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки