appsettings.jsonだけじゃない ── Windows業務アプリの構成管理実務(環境別設定・秘密情報・書き込み先)

· · CSharp, .NET, appsettings.json, IConfiguration, IOptions, Generic Host, 構成管理, Windows開発, 技術相談

「接続文字列を環境ごとに変えたい」「ユーザーごとの表示設定を保存したい」「APIキーをappsettings.jsonに書いてしまっている」。Windows業務アプリの構成管理は、appsettings.json を1つ置いて IConfiguration から読むところまでは誰でもたどり着きますが、そこから先──環境別設定、書き込み可能な設定の置き場所、秘密情報の扱い、実行中の設定変更──は、意外と整理されないまま運用に乗ってしまうことが多い領域です。

この記事では、.NETの構成システムの土台であるIConfigurationとプロバイダーの重ね合わせから、環境別設定、IOptionsパターンによる型安全な受け取り方、書き込み可能な設定の置き場所、秘密情報の扱い、実行中の設定変更の現実的な限界、そしてapp.config/Settings.settingsからの移行マップまで、実務で判断に迷う順に整理します。

1. まず結論

構成管理の判断は、「設定の種類」ごとに「置き場所」を決めるところから始まります。まず全体像を判断表にまとめます。

設定の種類 具体例 置き場所の第一候補 理由
アプリ既定値 ログレベルの既定、UIの既定パラメータ appsettings.json ビルド成果物に同梱する、環境に依存しない共通値
環境別設定 検証環境/本番環境で変える接続文字列、APIエンドポイント appsettings.{Environment}.json + 環境変数 既定の上書き順序をそのまま利用できる
ユーザーごとの設定 前回開いたフォルダー、ウィンドウ位置、個人の表示設定 %LOCALAPPDATA%(または%APPDATA%)配下の自前ファイル ユーザー単位で書き込める領域が必要
マシンごとの設定(全ユーザー共有) 装置のCOMポート番号、ライセンスサーバーのアドレス %ProgramData%配下の自前ファイル 管理者が1台につき1つ設定し、全ユーザーで共有する
秘密情報 接続文字列のパスワード、APIキー、トークン DPAPI保護ファイル(本番)、user-secrets(開発時のみ) 平文で置かない。開発用の仕組みを本番に流用しない
実行中に変わる値 機能フラグ、ログレベルの動的変更 appsettings.json(reloadOnChange) + IOptionsMonitor 再起動なしで反映したい値だけに限定して使う

この表を踏まえたうえでの結論を先に書きます。

  • 構成の優先順位は「後から追加したプロバイダーが勝つ」という1本のルールに従います。 既定では appsettings.jsonappsettings.{Environment}.json → (開発環境のみ) user secrets → 環境変数 → コマンドライン引数の順に読み込まれ、同じキーがあれば後から読んだ値で上書きされます。この順序を覚えておくだけで、「json に書いたのに反映されない」の大半は説明がつきます。1
  • Host.CreateApplicationBuilder を使えば、この階層は自分で組み立てなくても既定で用意されます。 Windows Forms/WPFのようなデスクトップアプリでも Generic Host を使えば同じ既定値の恩恵を受けられます。23
  • 環境の切り替えは DOTNET_ENVIRONMENT(または ASPNETCORE_ENVIRONMENT)で行います。 WebApplication 系では DOTNET_ENVIRONMENT が優先され、どちらも未設定なら既定は Production です。デスクトップアプリやWindowsサービスでは、この環境変数を「誰がどうやってプロセスに渡すか」を先に設計しておく必要があります。4
  • 設定はDTOで受けず、IOptions<T> ファミリーで型として受け取ります。 起動時に1回だけでよいなら IOptions<T>、再起動なしで変更を反映したいなら IOptionsMonitor<T> を使います。この2つの違いを理解しないまま両方を雑に使うと、「変わってほしくない値が変わる」「変わってほしい値が変わらない」の両方の事故が起きます。5
  • 設定の不備は実行時ではなく起動時に落とします。 ValidateDataAnnotations()ValidateOnStart() を組み合わせれば、設定ミスは「起動直後に例外で分かる」状態になり、「本番の深夜にNullReferenceExceptionで気づく」事故を防げます。6
  • 秘密情報は平文で appsettings.json に置きません。 開発時は user-secrets、本番は DPAPI(ProtectedData)や資格情報マネージャーを使います。user-secrets は暗号化されておらず、開発専用の仕組みであることが公式ドキュメントで明記されています。78

2. .NETの構成システムの土台 ── IConfigurationとプロバイダー

.NETの構成システムは、IConfiguration という1つのキー・バリューストアの見た目の裏側に、複数の構成プロバイダーを重ねて読み込む仕組みでできています。JSON、環境変数、コマンドライン引数、INI、XML、メモリ上のコレクションなど、性質の異なるソースを同じ IConfiguration インターフェイスの背後に統合できるのがこの仕組みの利点です。9

プロバイダーは「後から追加した方が勝つ」という単純なルールで積み重なります。同じキーが複数のプロバイダーに存在する場合、最後に追加されたプロバイダーの値が有効になります。1

Host.CreateApplicationBuilder(args) を使うと、次の順序でプロバイダーが既定で組み立てられます(番号が大きいほど優先度が高い、つまり後勝ち)。2

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. Secret Manager(開発環境のときのみ)
  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のようなデスクトップアプリでも、Microsoft.Extensions.Hosting パッケージを追加すれば同じ HostApplicationBuilder をそのまま使えます。当ブログの「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サービスでは「環境変数をどう渡すか」自体が一仕事になることです。ASP.NET Coreの launchSettings.json のような開発時の仕組みはローカル開発でしか使われないため、本番運用で環境を切り替える手段を別途用意する必要があります。代表的な渡し方は次の3つです。

  • マシン単位の環境変数として設定する。 setx DOTNET_ENVIRONMENT Production /M のように永続化すれば、そのマシン上で動くすべてのプロセスに引き継がれます。ただし1台のマシンで複数の環境のアプリを同居させたい場合には向きません。
  • Windowsサービスの起動プロセスに環境変数を渡す。 サービスとして常駐させる場合、対話ユーザーのセッションとは別の文脈で起動するため、ユーザー環境変数は引き継がれません。実行アカウントとセッション分離の詳細は「Windowsサービスの作り方と運用」を参照してください。マシン単位の環境変数、あるいはサービス実行ファイルを薄いラッパーバッチにしてSETしてから本体を起動する構成が現実的です。
  • タスクスケジューラ経由で起動する場合は、コマンドライン引数で渡す方が確実です。 タスクスケジューラの「操作」で環境変数を直接設定するUIはないため、--environment Production のようなコマンドライン引数として渡し、AddCommandLine(args) で読ませる方が事故が少なくなります。タスクスケジューラ特有の実行アカウント・ログオン種別の癖は「タスクスケジューラのタスクが実行されない・0x1で終わる」にまとめています。
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. オプションパターン ── 設定を型で受ける

IConfiguration["Key:SubKey"] のような文字列キーでの読み取りは、タイプミスに気づけない・入れ子が深くなると追いにくいという弱点があります。実務では、設定を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));

受け取り方には3つのインターフェイスがあり、それぞれ性質が異なります。5

インターフェイス 登録の生存期間 設定変更の反映 主な用途
IOptions<T> シングルトン 反映されない(起動時に1回だけ計算) 変わらない前提の設定。最もシンプル
IOptionsSnapshot<T> スコープ スコープが再構築されるたびに再計算 Webのリクエストスコープなどスコープが明確な文脈
IOptionsMonitor<T> シングルトン 変更通知(OnChange)付きでいつでも最新値を取得 常駐サービスなど、変更をその場で検知したい文脈

デスクトップアプリやWindowsサービスのようにHTTPリクエストスコープを持たないアプリでは、IOptionsSnapshot<T> を使ってもスコープを自分で作らない限り実質的に IOptions<T> と同じ挙動になります。変更を検知したいなら素直に IOptionsMonitor<T> を使う、という選び方で迷いが減ります。

設定値(BaseUrl・ApiKey・タイムアウト)は呼び出しのたびにIOptionsMonitorから取得しますが、HttpClient自体はIHttpClientFactoryから取得して使い回します。HttpClientを呼び出しごとにnewしてDisposeすると、内部のソケット・接続プールを毎回破棄・再構築することになり、高頻度のポーリングやバッチ処理ではエフェメラルポートの枯渇につながるため、値は変わっても接続の使い回しは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時)、ホストサービス開始前に検証が走る

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, "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 });
        // 同一プロセス内での多重書き込みは呼び出し側で直列化する前提
        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;
}

ユーザーごとの設定とマシンごとの設定を混ぜて1つのファイルに入れてしまうと、複数ユーザーが同じマシンを使う環境で「Aさんの設定でBさんが困る」事故になります。ユーザーごと・マシンごとの区分けと、Windowsのプロファイル構造そのものの理解は「Windowsユーザープロファイルの仕組み」を、保存先の選び方全般の判断表は「Windowsアプリのデータ保存先の選び方」も参照してください。

6. 秘密情報 ── 接続文字列・APIキー

接続文字列のパスワードやAPIキーを appsettings.json にそのまま書くのは、.gitignore に入れていたとしても実務的にはリスクです。共有フォルダーで配布する、サポート対応でファイルを送ってもらう、ゾンビ化した古い設定ファイルがバックアップに残る、といった経路で平文が漏れます。

開発時は Secret Manager(dotnet user-secrets)を使います。ただし、これは開発体験のための仕組みであり、暗号化はされていません。値は%APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.jsonにJSONの平文で保存され、公式ドキュメントも「信頼できるストアとして扱ってはいけない、開発専用」と明記しています。本番の秘密情報をuser-secretsに入れて配布する、という使い方は誤りです。7

dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "開発用の値"

本番運用では、Windowsが提供するDPAPI(Data Protection API)を使ってユーザーまたはマシンの資格情報に紐づけて暗号化します。System.Security.Cryptography.ProtectedDataProtect/Unprotect がラッパーで、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);
    }
}

DPAPIで保護した値をどこに保存するか、CurrentUserLocalMachineのどちらのスコープを選ぶか、複数ユーザーが同じマシンで使う業務アプリでの落とし穴といった実装レベルの詳細は「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> は起動時の値を保持し続けるため、ファイルを書き換えても古い値のままです。「設定ファイルを直接編集したのに反映されない」という問い合わせの多くは、この2つの取り違えが原因です。
  • FileSystemWatcher はDockerコンテナやネットワーク共有のようなファイルシステムでは変更通知を確実に送れないことがあります。 そのような環境では DOTNET_USE_POLLING_FILE_WATCHER 環境変数を true にすると、4秒間隔のポーリング監視に切り替わります(間隔は変更できません)。13 FileSystemWatcher そのものの取りこぼし・バッファーあふれ・イベントの重複といった一般的な癖については「FileSystemWatcher実務ガイド」にまとめています。
  • 設定ファイルの変更通知は1回のファイル変更に対して複数回発火することがあります。 アプリ側で「変更を検知したら重い処理をやり直す」ような実装をする場合は、短時間の連続発火をデバウンスする、ファイルの内容ハッシュを比較して実質的な変化だけを処理する、といった配慮が必要です。

「設定変更を再起動なしで反映する」ことを常に目指す必要はありません。ログレベルや機能フラグのような軽量な値は IOptionsMonitor で即時反映してよい一方、DB接続文字列やスレッドプールのサイズのような、変更した瞬間に動いているリソースと矛盾を起こしうる値は、「再起動で反映する」を仕様として明言してしまう方が安全です。運用担当者に「この設定は保存すればすぐ効く」「この設定は再起動が必要」と明確に伝えられる設計にすることが、reloadOnChangeを使うかどうかより重要です。

8. app.config / Settings.settingsからの移行マップ

.NET Framework時代のアプリを.NETに移行する際、構成の仕組みも作り直しが必要になります。対応関係を整理します。14

.NET Framework .NET 備考
App.config / Web.config<appSettings> appsettings.json + IConfiguration 階層構造をJSONのネストで自然に表現できる
ConfigurationManager.AppSettings["Key"] builder.Configuration["Key"] または IOptions<T> 文字列アクセスから型付きアクセスへ
ConfigurationManager.ConnectionStrings builder.Configuration.GetConnectionString("Name") ConnectionStringsセクションの規約は維持されている
Settings.settings(ユーザースコープ) 自前の %LOCALAPPDATA% JSONファイル ApplicationSettingsBaseのような自動生成の仕組みはない。自分でシリアライズ/保存する
Settings.settings(アプリケーションスコープ) appsettings.json 読み取り専用の既定値として扱う
<connectionStrings> の暗号化(aspnet_regiis 等) DPAPI(ProtectedData) 暗号化の仕組みそのものが変わる。移行時に作り直しが必要

移行時によく踏む罠が2つあります。

1つ目は、System.Configuration.ConfigurationManager NuGetパッケージを追加すれば App.config の読み込みコードはそのまま動く、という点です。これは移行の第一段階として有効な手段ですが、そのまま放置せず、appsettings.jsonへの移行を計画に入れるべきです。ロギングプロバイダーのような周辺ライブラリも軒並みappsettings.json前提に移行しているため、App.config読み込みのためだけに古い仕組みを残す理由は薄れていきます。14

2つ目は、Settings.settings のユーザースコープ設定です。.NET Frameworkでは Properties.Settings.Default.Save() を呼ぶだけでユーザーごとの設定が自動的に保存される仕組みがありましたが、.NETにはこれに相当する自動生成の仕組みが標準では用意されていません。移行時には、第5章で示したような自前の保存クラスを用意する必要があります。

移行のもう1つの側面──依存ライブラリの互換性、COM連携の有無、配布方式の見直しなど、構成管理以外に確認すべき項目の全体像は「.NET Framework→.NET移行前チェックリスト」で棚卸しの観点をまとめているので、移行プロジェクトの初期段階で一度目を通しておくことをおすすめします。

まとめ

.NETの構成管理は、IConfiguration によるプロバイダーの重ね合わせ、IOptions ファミリーによる型安全な受け取り、環境変数による環境切り替え、という3つの仕組みの組み合わせでできています。ここまでは多くのアプリで同じように使えますが、Windows業務アプリ固有の悩みは、その先──書き込み可能な設定の置き場所、秘密情報の保護、実行中の設定変更をどこまで許すか、そしてapp.config世代の資産をどう畳むか──に集中します。

「appsettings.jsonに全部書いてある」状態から一歩進めて、設定の種類ごとに置き場所と扱い方を分ける。この記事の判断表がその整理の出発点になれば幸いです。既存アプリの構成管理の棚卸しや、app.configからの移行方針の相談は、実際の設定ファイルとデプロイ環境を見ながらでないと最適解が見えてこないことが多いので、迷ったらご相談ください。

関連記事

関連する相談領域

合同会社小村ソフトでは、Windows業務アプリの構成管理設計、環境別設定の運用設計、既存app.config資産からの移行方針の技術相談を扱っています。

参考リンク

  1. Microsoft Learn, Configuration in .NET - Alternative hosting approach. Host.CreateApplicationBuilderが既定で組み立てる構成プロバイダーの優先順位(コマンドライン引数→環境変数→開発環境のuser secrets→appsettings.{Environment}.json→appsettings.json)について。  2 3

  2. Microsoft Learn, .NET Generic Host - Host builder settings. Host.CreateApplicationBuilderが読み込むホスト構成(DOTNET_プレフィックスの環境変数、コマンドライン引数)とアプリ構成(appsettings.json、appsettings.{Environment}.json、Secret Manager、環境変数、コマンドライン引数)の既定の順序について。  2

  3. Microsoft Learn, Use the .NET Generic Host in a Windows Forms app. Windows FormsアプリでGeneric Hostを組み込み、DI・構成・ロギングを利用する手順について。 

  4. Microsoft Learn, ASP.NET Core runtime environments - Environment variables that determine the runtime environment. DOTNET_ENVIRONMENTASPNETCORE_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. ValidateDataAnnotations()によるDataAnnotations検証と、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. DPAPI(Data Protection API)をラップするProtectedData.Protect/Unprotectメソッド、DataProtectionScope.CurrentUser/LocalMachineによるスコープの違い、Windows専用でありそれ以外のプラットフォームではPlatformNotSupportedExceptionになることについて。  2

  9. Microsoft Learn, Configuration providers in .NET. JSON・環境変数・コマンドライン・INI・XMLなど、性質の異なる構成ソースをIConfigurationの背後に統合する構成プロバイダーの仕組みについて。 

  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. AddJsonFilereloadOnChangeパラメーターと、PhysicalFileProviderが内部でFileSystemWatcherを使って設定ファイルの変更を監視する仕組みについて。 

  13. Microsoft Learn, Options pattern in .NET - IOptionsMonitor. IOptionsMonitorの変更通知がファイルシステムベースの構成プロバイダーに限定されること、Dockerコンテナやネットワーク共有で変更通知が確実でない場合にDOTNET_USE_POLLING_FILE_WATCHER環境変数で4秒間隔のポーリング監視に切り替えられることについて。 

  14. Microsoft Learn, Modernize after upgrading to .NET from .NET Framework - App.config. App.configからappsettings.jsonへの移行手順、System.Configuration.ConfigurationManagerNuGetパッケージによる互換維持、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、デプロイ先やコンテナごとに変える値は環境変数、という役割分担にすると、後から見て迷いません。
設定クラスはどう分割すればよいですか?
機能や責務の単位でオプションクラスを分けるのが基本です。接続文字列ならConnectionOptions、外部API連携ならExternalApiOptions のように、1つのクラスに無関係な設定を詰め込まないようにします。分割しておくと、IOptionsSnapshot/IOptionsMonitorでの検証・再読み込みの単位も自然に分かれ、DataAnnotationsによる検証もクラスごとに完結します。
INIファイルやレジストリから移行すべきですか?
新規に構成管理を作り直すなら、appsettings.json + IConfigurationへの統一をおすすめします。.NETはINI構成プロバイダーも提供しているため、既存のINIファイルをそのまま読みつつ段階的に移行することも可能です。レジストリは.NETの構成プロバイダーの標準守備範囲ではないため、レジストリに依存した既存資産がある場合は、読み取り専用の橋渡しコードを書いて段階的にappsettings.json側へ寄せていくのが現実的です。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る