不只是 appsettings.json ── Windows 商用應用程式的組態管理實務(環境別設定・機密資訊・寫入位置)
· 小村 豪 · CSharp, .NET, appsettings.json, IConfiguration, IOptions, Generic Host, 組態管理, Windows 開發, 技術諮詢
「想依環境切換連線字串」「想儲存每個使用者的顯示設定」「把 API 金鑰寫進了 appsettings.json」。Windows 商用應用程式的組態管理,大家多半都能做到放一個 appsettings.json、用 IConfiguration 讀取,但再往下──環境別設定、可寫入設定的存放位置、機密資訊的處理、執行期設定變更──卻常常在沒有整理清楚的情況下就這樣上線運作。
本文從 .NET 組態系統的基礎 IConfiguration 與提供者的疊加開始,依序整理環境別設定、透過 IOptions 模式以型別安全的方式接收設定、可寫入設定的存放位置、機密資訊的處理、執行期設定變更的現實限制,以及從 app.config/Settings.settings 遷移的對應表,按實務上容易猶豫的順序來說明。
1. 先下結論
組態管理的判斷,要從依「設定的種類」決定「存放位置」開始。先用一張判斷表整理全貌。
| 設定的種類 | 具體範例 | 存放位置首選 | 理由 |
|---|---|---|---|
| 應用程式預設值 | 記錄層級的預設值、UI 的預設參數 | appsettings.json |
隨建置成品發佈、不依環境而異的共用值 |
| 環境別設定 | 驗證環境/正式環境會不同的連線字串、API 端點 | appsettings.{Environment}.json + 環境變數 |
可直接沿用預設的覆寫順序 |
| 使用者別設定 | 上次開啟的資料夾、視窗位置、個人化顯示設定 | %LOCALAPPDATA%(或 %APPDATA%)底下的自訂檔案 |
需要以使用者為單位可寫入的區域 |
| 機器別設定(全體使用者共用) | 裝置的 COM 埠號、授權伺服器位址 | %ProgramData% 底下的自訂檔案 |
由管理員針對每台機器設定一次,所有使用者共用 |
| 機密資訊 | 連線字串中的密碼、API 金鑰、token | DPAPI 保護的檔案(正式環境)、user-secrets(僅限開發時) | 不以純文字存放。不要把開發用機制挪用到正式環境 |
| 執行期會變動的值 | 功能旗標、記錄層級的動態變更 | appsettings.json(reloadOnChange) + IOptionsMonitor |
僅限於希望不重新啟動也能反映的值 |
以這張表為基礎,先寫出結論。
- 組態的優先順序遵循「後加入的提供者優先」這一條規則。 預設會依
appsettings.json→appsettings.{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>家族以型別接收。 只需要在啟動時取得一次就用IOptions<T>,希望不重新啟動也能反映變更就用IOptionsMonitor<T>。如果不理解兩者的差異就隨意混用,「不該變的值變了」「該變的值卻沒變」這兩種事故都可能發生。5 - 設定錯誤要在啟動時就讓程式失敗,而不是在執行時才出錯。 搭配
ValidateDataAnnotations()與ValidateOnStart(),設定錯誤就會變成「啟動後立即以例外方式被發現」,避免「正式環境半夜才因為 NullReferenceException 而察覺」的事故。6 - 機密資訊不要以純文字放進
appsettings.json。 開發時使用 user-secrets,正式環境則使用 DPAPI(ProtectedData)或憑證管理員。官方文件明確指出 user-secrets 沒有加密,是開發專用的機制。78
2. .NET 組態系統的基礎 ── IConfiguration 與提供者
.NET 的組態系統,是在 IConfiguration 這個單一鍵值儲存的外觀之下,疊加多個組態提供者來讀取資料的機制。這種機制的優點是,可以把 JSON、環境變數、命令列引數、INI、XML、記憶體中的集合等性質不同的來源,統一整合到同一個 IConfiguration 介面之後。9
提供者的疊加方式很簡單,就是「後加入的優先」。如果同一個鍵存在於多個提供者中,會以最後加入的提供者的值為準。1
使用 Host.CreateApplicationBuilder(args) 時,會依下列順序預設組成提供者(數字越大優先度越高,也就是後者優先)。2
appsettings.jsonappsettings.{Environment}.json- Secret Manager(僅開發環境時)
- 環境變數
- 命令列引數
無論是主控台應用程式、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 如果想加上自訂前綴,可以像下面這樣明確加入。
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這樣永久設定,就會傳遞給該機器上執行的所有行程。但如果想在一台機器上同時運作多個環境的應用程式,這種方式並不合適。 - 在 Windows 服務的啟動流程中傳遞環境變數。 以服務常駐執行時,是在跟互動式使用者工作階段不同的環境下啟動的,因此不會傳遞使用者環境變數。關於執行帳戶與工作階段分離的細節,請參閱「Windows 服務的建立方式與運作」。比較現實的做法是使用機器層級的環境變數,或是把服務執行檔做成一個薄的包裝批次檔,先
SET再啟動本體。 - 透過工作排程器啟動時,用命令列引數傳遞比較可靠。 工作排程器的「動作」沒有直接設定環境變數的介面,因此改用像
--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"] 這種透過字串鍵讀取的方式,有個弱點:打錯字不容易察覺,而且巢狀層級一深就很難追蹤。實務上的基本做法是使用選項模式(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;
}
註冊與綁定的寫法如下。
using Microsoft.Extensions.DependencyInjection;
builder.Services
.AddOptions<ExternalApiOptions>()
.Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName));
接收方式有 3 種介面,各自性質不同。5
| 介面 | 註冊的生命週期 | 設定變更的反映 | 主要用途 |
|---|---|---|---|
IOptions<T> |
單一實例(Singleton) | 不會反映(僅在啟動時計算一次) | 假設不會變動的設定。最單純 |
IOptionsSnapshot<T> |
範圍(Scoped) | 每次範圍重新建立時重新計算 | Web 的請求範圍等,範圍明確的情境 |
IOptionsMonitor<T> |
單一實例(Singleton) | 附帶變更通知(OnChange),隨時可取得最新值 |
常駐服務等想即時偵測變更的情境 |
在像桌面應用程式或 Windows 服務這種沒有 HTTP 請求範圍的應用程式中,即使使用 IOptionsSnapshot<T>,只要不自行建立範圍,實質上的行為就會跟 IOptions<T> 相同。如果想偵測變更,就直截了當地使用 IOptionsMonitor<T>,用這個原則來選擇會減少猶豫。
設定值(BaseUrl・ApiKey・逾時)每次呼叫都從 IOptionsMonitor 取得,但 HttpClient 本身則從 IHttpClientFactory 取得並重複使用。如果每次呼叫都 new 一個 HttpClient 再 Dispose,就會每次都廢棄、重建內部的通訊端與連線池,在高頻率輪詢或批次處理中容易導致臨時埠(ephemeral port)耗盡,因此即使值會變動,連線的重複使用仍應交給 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;
}
如果把使用者別設定和機器別設定混在同一個檔案裡,在多個使用者共用同一台機器的環境中,就會發生「因為 A 的設定而讓 B 困擾」的事故。關於使用者別・機器別的區分,以及 Windows 設定檔結構本身的理解,請參閱「Windows 使用者設定檔的結構」;至於儲存位置選擇的整體判斷表,也可以參考「Windows 應用程式資料儲存位置的選擇方式」。
6. 機密資訊 ── 連線字串・API 金鑰
把連線字串中的密碼或 API 金鑰直接寫進 appsettings.json,即使已經加進 .gitignore,實務上仍然有風險。透過共用資料夾發佈、支援對應時要求對方傳送檔案、殭屍化的舊設定檔留在備份中──都可能讓純文字外流。
開發時使用 Secret Manager(dotnet user-secrets)。但這只是為了改善開發體驗的機制,並沒有加密。 這些值會以 JSON 純文字儲存在 %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json,官方文件也明確指出「不應把它當作可信任的儲存區,僅供開發使用」。把正式環境的機密資訊放進 user-secrets 再發佈,是一種錯誤的用法。7
dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "開發用的值"
正式環境運作時,使用 Windows 提供的 DPAPI(Data Protection API),將資料綁定到使用者或機器的憑證上進行加密。System.Security.Cryptography.ProtectedData 的 Protect/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 保護的值該存在哪裡、CurrentUser 和 LocalMachine 該選哪個範圍、多個使用者共用同一台機器的商用應用程式會遇到哪些陷阱,這類實作層級的細節寫在「Windows 應用程式的機密資訊儲存 - 用 DPAPI 避免純文字設定」中,實作時請參閱該文。
該以純文字保留、還是不該保留的界線很單純。判斷準則是「這個值一旦外流,是否需要重新發行密碼或 API 金鑰、進行未授權存取調查、向主管機關報告」。 連線字串中的主機名稱或埠號,外流時實際損害通常較小,但其中內嵌的密碼或驗證 token,則永遠是保護對象。如果把連線字串拆成「伺服器資訊」與「憑證」兩部分來組合,只把後者列為 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環境變數設為true,切換成每 4 秒一次的輪詢監視(間隔無法變更)。13 關於FileSystemWatcher本身遺漏事件・緩衝區溢位・事件重複等常見特性,整理在「FileSystemWatcher 實務指南」中。- 設定檔的變更通知,有可能針對一次檔案變更觸發多次。 如果應用程式端要做「偵測到變更就重新執行耗時處理」這類實作,需要考慮對短時間內連續觸發做防抖(debounce),或比較檔案內容的雜湊值,只處理實質上的變化。
不必永遠以「不重新啟動也能反映設定變更」為目標。記錄層級或功能旗標這類輕量的值,可以用 IOptionsMonitor 立即反映;但像資料庫連線字串或執行緒集區大小這類,一旦變更就可能跟正在運作的資源產生矛盾的值,明確規定「需要重新啟動才會生效」反而更安全。 能明確告知維運人員「這項設定存檔就立即生效」「這項設定需要重新啟動」的設計,比是否使用 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 個。
第一個是,只要加入 System.Configuration.ConfigurationManager NuGet 套件,讀取 App.config 的程式碼就能直接沿用。這作為遷移的第一步是有效的手段,但不應就此放著不管,而應該把遷移到 appsettings.json 納入計劃。像記錄提供者這類周邊函式庫,也大多已經以 appsettings.json 為前提遷移,只為了讀取 App.config 而保留舊機制的理由會越來越薄弱。14
第二個是 Settings.settings 的使用者範圍設定。在 .NET Framework 中,只要呼叫 Properties.Settings.Default.Save(),就有自動儲存使用者別設定的機制,但 .NET 並沒有對應的標準自動產生機制。遷移時需要準備像第 5 章那樣的自訂儲存類別。
遷移的另一個面向──相依函式庫的相容性、是否有 COM 整合、發佈方式的重新檢視等,組態管理以外需要確認的項目全貌,整理在「.NET Framework→.NET 遷移前檢查清單」中,建議在遷移專案初期先瀏覽一次。
結語
.NET 的組態管理,是由 IConfiguration 的提供者疊加、IOptions 家族的型別安全接收、環境變數的環境切換這 3 種機制組合而成。到這裡為止,多數應用程式都能用相同的方式使用,但 Windows 商用應用程式特有的難題,集中在再往下的部分──可寫入設定的存放位置、機密資訊的保護、執行期設定變更該允許到什麼程度,以及該如何收拾 app.config 世代的資產。
從「appsettings.json 裡什麼都寫」的狀態再往前一步,依設定種類分開存放位置與處理方式。如果本文的判斷表能成為這個整理過程的起點,那就是我們的榮幸。既有應用程式的組態管理盤點,或是從 app.config 遷移的方針諮詢,往往需要實際看過設定檔和部署環境才能找到最佳解,如果猶豫不決,歡迎與我們洽談。
相關文章
- Generic Host 是什麼
- 在桌面應用程式中使用 Generic Host + BackgroundService
- 工作排程器的工作沒有執行、以 0x1 結束
- Windows 服務的建立方式與運作
- Windows 應用程式資料儲存位置的選擇方式
- Windows 使用者設定檔的結構
- Windows 應用程式的機密資訊儲存 - 用 DPAPI 避免純文字設定
- FileSystemWatcher 實務指南
- .NET Framework→.NET 遷移前檢查清單
相關諮詢領域
合同會社小村軟體處理 Windows 商用應用程式的組態管理設計、環境別設定的運作設計,以及既有 app.config 資產遷移方針的技術諮詢。
參考連結
-
Microsoft Learn, Configuration in .NET - Alternative hosting approach. 說明
Host.CreateApplicationBuilder預設組成的組態提供者優先順序(命令列引數 → 環境變數 → 開發環境的 user secrets → appsettings.{Environment}.json → appsettings.json)。 ↩ ↩2 ↩3 -
Microsoft Learn, .NET Generic Host - Host builder settings. 說明
Host.CreateApplicationBuilder讀取的主機組態(帶DOTNET_前綴的環境變數、命令列引數)與應用程式組態(appsettings.json、appsettings.{Environment}.json、Secret Manager、環境變數、命令列引數)的預設順序。 ↩ ↩2 -
Microsoft Learn, Use the .NET Generic Host in a Windows Forms app. 說明在 Windows Forms 應用程式中整合 Generic Host、使用 DI・組態・記錄的步驟。 ↩
-
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 -
Microsoft Learn, Options pattern in .NET - Options interfaces. 說明
IOptions<TOptions>・IOptionsSnapshot<TOptions>・IOptionsMonitor<TOptions>在生命週期、設定變更反映時機、對應功能上的差異。 ↩ ↩2 -
Microsoft Learn, Options pattern in .NET - Options validation. 說明透過
ValidateDataAnnotations()進行 DataAnnotations 驗證,以及透過ValidateOnStart()(或AddOptionsWithValidateOnStart)進行啟動時驗證的設定方式。 ↩ ↩2 ↩3 -
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 -
Microsoft Learn, ProtectedData Class. 說明包裝 DPAPI(Data Protection API)的
ProtectedData.Protect/Unprotect方法、DataProtectionScope.CurrentUser/LocalMachine範圍的差異,以及該功能僅限 Windows、在其他平台會丟出PlatformNotSupportedException。 ↩ ↩2 -
Microsoft Learn, Configuration providers in .NET. 說明將 JSON・環境變數・命令列・INI・XML 等性質不同的組態來源整合到
IConfiguration之後的組態提供者機制。 ↩ -
Microsoft Learn, Configuration providers in .NET - Environment variable configuration provider. 說明預設組態會將帶
DOTNET_前綴的環境變數・命令列引數讀入主機組態・應用程式組態,但不會用於使用者組態,以及新增自訂前綴的方法。 ↩ -
Microsoft Learn, Environment.GetFolderPath Method. 說明
Environment.SpecialFolder列舉與GetFolderPath方法取得特殊資料夾路徑的方式。 ↩ -
Microsoft Learn, Detect changes with change tokens in ASP.NET Core - Monitor for configuration changes. 說明
AddJsonFile的reloadOnChange參數,以及PhysicalFileProvider內部使用FileSystemWatcher監視設定檔變更的機制。 ↩ -
Microsoft Learn, Options pattern in .NET - IOptionsMonitor. 說明
IOptionsMonitor的變更通知僅限於以檔案系統為基礎的組態提供者,在 Docker 容器或網路共用等變更通知不可靠的環境中,可透過DOTNET_USE_POLLING_FILE_WATCHER環境變數切換成每 4 秒一次的輪詢監視。 ↩ -
Microsoft Learn, Modernize after upgrading to .NET from .NET Framework - App.config. 說明從
App.config遷移到appsettings.json的步驟、透過System.Configuration.ConfigurationManagerNuGet 套件維持相容性,以及使用Microsoft.Extensions.Configuration.Json套件的方式。 ↩ ↩2
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
用 PerfView 與 dotnet-trace 找出「變慢」的原因 ── .NET 效能調查實務入門
當商用應用程式「變慢」「CPU 卡住不動」「偶爾當掉」時,該用哪個工具看什麼?本文整理 PerfView 與 dotnet-trace 的角色分工、CPU 取樣的讀法(inclusive/exclusive)、用 ThreadTime 調查阻塞時間,以及與 EventSou...
Windows 業務應用程式的列印與 PDF 輸出 ── System.Drawing.Printing / WPF / 報表函式庫的取捨
本文以要件別判斷表整理 PrintDocument 的 WinForms 列印、WPF 的 FlowDocument/FixedDocument 列印,以及 PDF 輸出的各種選項。並從實務角度說明分頁控制、DPI 落差等實作陷阱,以及從 Windows 服務列印、依賴預設...
Windows 事件記錄・ETW 入門 ── 讓業務應用程式的日誌搭上 OS 標準機制
Windows 業務應用程式的日誌,是不是只靠檔案日誌解決?事件記錄與 ETW,是維運人員與 OS 標準工具能看到的另一個層級的記錄。本文說明三種手段的分工、.NET 的寫入方式、透過 EventSource 進行 ETW 儀器化、收集與調查的實務,以及來源未註冊、日誌肥大...
用 WinDbg + SOS 解讀當機傾印檔 ── 收集之後的實務分析入門
本文說明如何用 WinDbg 與 SOS 擴充功能實際讀取已收集的 Windows 當機傾印檔。內容涵蓋符號路徑設定、以 !clrstack・!pe・!dumpheap -stat・!gcroot 追蹤例外與記憶體洩漏的方法、原生當機的 !analyze -v,以及與 do...
防止 Windows 應用程式重複啟動 ── 具名 Mutex 與重複執行時的視窗前置
整理業務用 Windows 應用程式的常見需求「不讓同一個應用程式啟動兩次」,如何用具名 Mutex 實作。涵蓋 Global\ 與 Local\ 命名空間差異在 RDP 環境中的陷阱、擁有執行緒限制與 AbandonedMutexException、考量 SetForeg...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
Generic Host & 應用程式架構
整理 Generic Host、BackgroundService、DI、組態、日誌以及應用程式生命週期設計的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
常見問題
整理諮詢這個主題時常見的問題。
- appsettings.json 可以放在跟 exe 一樣的資料夾嗎?
- 如果只是唯讀的預設值,這樣做沒有問題。但如果該資料夾安裝在 Program Files 底下,就不能把應用程式設計成會自行改寫 appsettings.json。標準使用者沒有 Program Files 底下的寫入權限,執行時不是丟例外,就是因為 Windows 的虛擬化功能,導致不同使用者看到的內容不一致。需要寫入的設定,應該分離到 %LOCALAPPDATA% 或 %ProgramData% 底下的另一個檔案。
- 環境變數和 appsettings.json,哪一個該優先?
- 不要改變預設的優先順序,直接沿用 appsettings.json → appsettings.{Environment}.json → 環境變數 → 命令列引數這種依序覆寫的機制即可。難以判斷時的準則是「值的性質」:可以隨建置成品一起發佈的預設值放 appsettings.json,依部署對象或容器而異的值放環境變數,這樣分工之後日後回頭看也不會混淆。
- 設定類別該怎麼拆分?
- 基本原則是按功能或職責來拆分選項類別。例如連線字串用 ConnectionOptions、外部 API 整合用 ExternalApiOptions,避免把不相關的設定塞進同一個類別。拆分之後,IOptionsSnapshot/IOptionsMonitor 的驗證與重新載入單位也會自然分開,DataAnnotations 的驗證也能以類別為單位獨立完成。
- 需要從 INI 檔或登錄檔遷移嗎?
- 如果要重新打造組態管理,建議統一改用 appsettings.json + IConfiguration。.NET 也提供 INI 組態提供者,因此可以邊直接讀取現有的 INI 檔,邊逐步遷移。登錄檔並不在 .NET 組態提供者的標準支援範圍內,如果有依賴登錄檔的既有資產,比較現實的做法是寫一段唯讀的橋接程式碼,逐步把設定搬到 appsettings.json。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。