Nicht nur appsettings.json ── Praxisleitfaden für die Konfigurationsverwaltung in Windows-Geschäftsanwendungen (Umgebungen, Geheimnisse und Speicherorte)
· Go Komura · CSharp, .NET, appsettings.json, IConfiguration, IOptions, Generic Host, Konfigurationsverwaltung, Windows-Entwicklung, Technische Beratung
„Die Verbindungszeichenfolge soll je Umgebung anders sein“, „Anzeigeeinstellungen einzelner Benutzer sollen gespeichert werden“, „Der API-Schlüssel steht bereits in appsettings.json“. Bei der Konfigurationsverwaltung von Windows-Geschäftsanwendungen ist der erste Schritt – eine appsettings.json anzulegen und über IConfiguration zu lesen – schnell getan. Die folgenden Fragen bleiben jedoch oft ungeordnet im laufenden Betrieb: umgebungsspezifische Werte, der Speicherort veränderbarer Einstellungen, der Umgang mit Geheimnissen und Änderungen zur Laufzeit.
Dieser Artikel ordnet die Entscheidungen in der Reihenfolge, in der sie in der Praxis auftreten: von IConfiguration und der Schichtung von Anbietern als Grundlage des .NET-Konfigurationssystems über umgebungsspezifische Dateien und die typsichere Aufnahme mit dem IOptions-Muster bis zu Speicherorten für beschreibbare Einstellungen, Geheimnissen, den realistischen Grenzen von Änderungen zur Laufzeit und einer Migrationskarte für app.config/Settings.settings.
1. Zuerst das Fazit
Der Ausgangspunkt jeder Konfigurationsentscheidung ist, für jede Art von Einstellung einen passenden Speicherort festzulegen. Die folgende Tabelle gibt den Überblick.
| Art der Einstellung | Beispiel | Erster Speicherort | Grund |
|---|---|---|---|
| Standardwerte der Anwendung | Standard-Loglevel, UI-Standardparameter | appsettings.json |
Gemeinsame, umgebungsunabhängige Werte, die mit dem Build ausgeliefert werden |
| Umgebungsspezifische Einstellungen | Verbindungszeichenfolgen oder API-Endpunkte für Test und Produktion | appsettings.{Environment}.json + Umgebungsvariablen |
Die vorgegebene Überschreibungsreihenfolge kann direkt verwendet werden |
| Benutzerspezifische Einstellungen | Zuletzt geöffneter Ordner, Fensterposition, persönliche Anzeigeoptionen | Eigene Datei unter %LOCALAPPDATA% (oder %APPDATA%) |
Erfordert einen Bereich, in den pro Benutzer geschrieben werden darf |
| Rechnerbezogene Einstellungen für alle Benutzer | COM-Port eines Geräts, Adresse eines Lizenzservers | Eigene Datei unter %ProgramData% |
Ein Administrator verwaltet einen Wert pro Rechner, den alle Benutzer teilen |
| Geheimnisse | Passwort in einer Verbindungszeichenfolge, API-Schlüssel, Token | DPAPI-geschützte Datei (Produktion), user-secrets (nur Entwicklung) | Nicht im Klartext ablegen; Entwicklungsmechanismen nicht in die Produktion übernehmen |
| Werte, die sich zur Laufzeit ändern | Feature Flags, dynamische Änderung des Loglevels | appsettings.json (reloadOnChange) + IOptionsMonitor |
Nur für Werte einsetzen, die ohne Neustart wirksam werden sollen |
Aus dieser Tabelle ergeben sich die folgenden Grundregeln.
- Für die Priorität der Konfiguration gilt eine einzige Regel: Der zuletzt hinzugefügte Anbieter gewinnt. Standardmäßig werden
appsettings.json→appsettings.{Environment}.json→ (nur in der Entwicklung) user secrets → Umgebungsvariablen → Befehlszeilenargumente gelesen. Existiert derselbe Schlüssel mehrfach, überschreibt der später gelesene Wert die vorherigen. Allein diese Reihenfolge erklärt die meisten Fälle von „Ich habe den Wert in JSON geschrieben, aber er wirkt nicht“. 1 - Mit
Host.CreateApplicationBuildersteht diese Hierarchie standardmäßig bereit. Auch Desktop-Anwendungen wie Windows Forms oder WPF erhalten mit dem Generic Host dieselben sinnvollen Voreinstellungen.23 - Die Umgebung wird über
DOTNET_ENVIRONMENT(oderASPNETCORE_ENVIRONMENT) ausgewählt. BeiWebApplicationhatDOTNET_ENVIRONMENTVorrang. Fehlen beide Variablen, lautet der StandardProduction. Bei Desktop-Anwendungen und Windows-Diensten muss vorab geklärt werden, wer diese Variable dem Prozess wie übergibt.4 - Konfiguration sollte nicht als DTO, sondern über die
IOptions<T>-Familie typsicher entgegengenommen werden. Für Werte, die nur beim Start benötigt werden, genügtIOptions<T>; Änderungen ohne Neustart erfordernIOptionsMonitor<T>. Wer beide Typen ohne Verständnis ihres Unterschieds verwendet, riskiert zugleich Werte, die sich unerwartet ändern, und Werte, die sich nicht ändern, obwohl sie es sollten.5 - Fehlerhafte Konfiguration muss beim Start statt zur Laufzeit auffallen. Die Kombination aus
ValidateDataAnnotations()undValidateOnStart()sorgt dafür, dass ein Fehler direkt beim Start als Ausnahme sichtbar wird, statt nachts in Produktion alsNullReferenceExceptionentdeckt zu werden.6 - Geheimnisse gehören nicht im Klartext in
appsettings.json. In der Entwicklung sind user-secrets geeignet, in der Produktion DPAPI (ProtectedData) oder der Anmeldeinformations-Manager. Die offizielle Dokumentation weist ausdrücklich darauf hin, dass user-secrets nicht verschlüsselt und nur für die Entwicklung bestimmt sind.78
2. Die Grundlage des .NET-Konfigurationssystems ── IConfiguration und Anbieter
Das .NET-Konfigurationssystem verbindet mehrere Konfigurationsanbieter hinter der einheitlichen Sicht eines Schlüssel-Wert-Speichers namens IConfiguration. JSON, Umgebungsvariablen, Befehlszeilenargumente, INI, XML und Speicher-Kollektionen – Quellen mit unterschiedlichen Eigenschaften lassen sich so hinter derselben IConfiguration-Schnittstelle zusammenführen.9
Anbieter werden nach der einfachen Regel geschichtet, dass später hinzugefügte Anbieter Vorrang haben. Gibt es einen Schlüssel in mehreren Anbietern, ist der Wert des zuletzt hinzugefügten Anbieters wirksam.1
Mit Host.CreateApplicationBuilder(args) werden die Anbieter standardmäßig in folgender Reihenfolge angelegt; eine höhere Nummer bedeutet höhere Priorität und damit „später gewinnt“.2
appsettings.jsonappsettings.{Environment}.json- Secret Manager (nur in der Entwicklungsumgebung)
- Umgebungsvariablen
- Befehlszeilenargumente
Diese Standardreihenfolge ist bei Konsolenanwendungen, Worker Services und Windows-Forms-Anwendungen gleich. Das folgende Beispiel zeigt die kleinste Konfiguration.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Standardmäßig sind appsettings.json / appsettings.{Environment}.json /
// Umgebungsvariablen und Befehlszeilenargumente in dieser Priorität geladen.
string? connectionString = builder.Configuration.GetConnectionString("Main");
using IHost host = builder.Build();
await host.RunAsync();
Auch Desktop-Anwendungen wie Windows Forms können nach Installation des Pakets Microsoft.Extensions.Hosting denselben HostApplicationBuilder verwenden. Wie in unserem Artikel Was ist der Generic Host? beschrieben, bündelt der Generic Host Konfiguration, DI und Logging. Desktop-Anwendungen müssen auf die Vorteile des Konfigurationssystems nicht verzichten. Für ein konkretes Muster mit Hintergrundverarbeitung siehe Generic Host + BackgroundService in einer Desktop-Anwendung einsetzen.
Bei Umgebungsvariablen ist eine Unterscheidung wichtig: Host-Einstellungen wie Inhaltsstamm und Umgebungsname werden aus Umgebungsvariablen mit dem Präfix DOTNET_ gelesen; dieses Präfix gilt jedoch nicht für allgemeine Anwendungseinstellungen, die über IConfiguration gelesen werden. Umgebungsvariablen für Anwendungseinstellungen werden ohne Präfix über AddEnvironmentVariables() in die Standardreihenfolge aufgenommen.10 Für ein eigenes Präfix muss es explizit ergänzt werden, etwa mit builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_").
using Microsoft.Extensions.Configuration;
// Wird nach den Standardanbietern ergänzt und hat deshalb die höchste Priorität.
builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_");
3. Umgebungsspezifische Einstellungen ── appsettings.{Environment}.json
Sollen sich Verbindungszeichenfolgen oder Endpunkte je Umgebung unterscheiden, verwenden Sie Dateien wie appsettings.Development.json, appsettings.Staging.json und appsettings.Production.json. Entscheidend ist, dass diese Dateien als Differenzdateien behandelt werden, welche die Basisdatei appsettings.json überschreiben. Es müssen nicht alle Werte wiederholt werden; nur die Werte, die sich in der jeweiligen Umgebung ändern, gehören hinein.1
Der Umgebungsname wird durch die Umgebungsvariable DOTNET_ENVIRONMENT oder ASPNETCORE_ENVIRONMENT bestimmt. Bei Verwendung von WebApplication hat DOTNET_ENVIRONMENT gegenüber ASPNETCORE_ENVIRONMENT Vorrang. Fehlen beide, ist Production der Standard. Unter Windows unterscheiden Umgebungsvariablen nicht zwischen Groß- und Kleinschreibung, unter Linux dagegen schon. Wer Containerbetrieb einplant, sollte die Schreibweise daher konsequent vereinheitlichen.4
Bei Desktop-Anwendungen und Windows-Diensten entsteht dabei eine eigene Aufgabe: Schon die Frage, wie die Umgebungsvariable übergeben wird, muss geplant werden. Entwicklungshilfen wie launchSettings.json aus ASP.NET Core gelten nur lokal. Für die Produktion braucht es daher einen separaten Weg zur Umgebungswahl. Übliche Varianten sind:
- Als rechnerweite Umgebungsvariable setzen. Wird sie mit
setx DOTNET_ENVIRONMENT Production /Mdauerhaft hinterlegt, erben alle Prozesse auf diesem Rechner den Wert. Das passt jedoch nicht, wenn auf einem Rechner Anwendungen für mehrere Umgebungen parallel laufen sollen. - Die Umgebungsvariable dem Startprozess eines Windows-Diensts übergeben. Ein Dienst läuft getrennt von der interaktiven Benutzersitzung; benutzerspezifische Variablen werden deshalb nicht übernommen. Einzelheiten zu Dienstkonto und Sitzungsisolation beschreibt Windows-Dienste entwickeln und betreiben. Praktikabel sind eine rechnerweite Umgebungsvariable oder eine schlanke Batch-Datei als Dienststarter, die zunächst
SETausführt und danach das eigentliche Programm startet. - Beim Start über die Aufgabenplanung sind Befehlszeilenargumente verlässlicher. Die Oberfläche „Aktion“ der Aufgabenplanung erlaubt keine direkte Definition von Umgebungsvariablen. Ein Argument wie
--environment Production, das überAddCommandLine(args)eingelesen wird, verursacht daher weniger Überraschungen. Hinweise zu Ausführungskonten und Anmeldearten der Aufgabenplanung finden sich in Aufgaben der Windows-Aufgabenplanung starten nicht oder enden mit 0x1.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
var options = new HostApplicationBuilderSettings
{
Args = args,
// Für Startwege ohne Umgebungsvariable (etwa die Aufgabenplanung)
// ist eine Umschaltung über Befehlszeilenargumente ebenfalls zulässig.
};
HostApplicationBuilder builder = Host.CreateApplicationBuilder(options);
Console.WriteLine($"Aktuelle Umgebung: {builder.Environment.EnvironmentName}");
4. Das Options-Muster ── Konfiguration typsicher entgegennehmen
Das Lesen über Zeichenfolgenschlüssel wie IConfiguration["Key:SubKey"] hat Schwächen: Tippfehler bleiben unbemerkt, und tiefe Verschachtelungen sind schwer nachzuvollziehen. In der Praxis wird Konfiguration deshalb an POCOs gebunden und mit dem Options-Muster typsicher entgegengenommen.
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;
}
Registrierung und Bindung sehen so aus:
using Microsoft.Extensions.DependencyInjection;
builder.Services
.AddOptions<ExternalApiOptions>()
.Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName));
Für den Zugriff gibt es drei Schnittstellen mit unterschiedlichen Eigenschaften.5
| Schnittstelle | Registrierungslebensdauer | Wirkung von Konfigurationsänderungen | Hauptzweck |
|---|---|---|---|
IOptions<T> |
Singleton | Keine; Wert wird nur einmal beim Start ermittelt | Einstellungen, die unverändert bleiben; die einfachste Variante |
IOptionsSnapshot<T> |
Scoped | Wird bei jedem Neuaufbau eines Scopes neu berechnet | Kontexte mit klaren Scopes, etwa Web-Anfragen |
IOptionsMonitor<T> |
Singleton | Aktueller Wert jederzeit verfügbar, inklusive Benachrichtigung über OnChange |
Dauerhafte Dienste, die Änderungen direkt erkennen sollen |
In Desktop-Anwendungen und Windows-Diensten ohne HTTP-Anfrage-Scopes verhält sich IOptionsSnapshot<T> praktisch wie IOptions<T>, sofern nicht selbst ein Scope angelegt wird. Die Entscheidung wird einfacher, wenn für Änderungsbenachrichtigungen direkt IOptionsMonitor<T> verwendet wird.
Werte wie BaseUrl, ApiKey und Timeout werden bei jedem Aufruf aus IOptionsMonitor gelesen; das HttpClient-Objekt selbst kommt jedoch aus IHttpClientFactory und wird wiederverwendet. Würde HttpClient bei jedem Aufruf mit new erzeugt und anschließend freigegeben, müssten interne Sockets und Verbindungspools ständig neu aufgebaut werden. Bei häufigem Polling oder Batch-Verarbeitung kann das zu erschöpften ephemeren Ports führen. Werte dürfen sich ändern, die Wiederverwendung von Verbindungen sollte jedoch IHttpClientFactory überlassen bleiben.
using Microsoft.Extensions.Options;
public sealed class ExternalApiClient(
IHttpClientFactory httpClientFactory,
IOptionsMonitor<ExternalApiOptions> optionsMonitor)
{
public async Task<string> FetchAsync(CancellationToken cancellationToken)
{
// Bei jedem Aufruf den aktuellen Wert holen. Änderungen an der
// Konfigurationsdatei werden dadurch berücksichtigt.
ExternalApiOptions current = optionsMonitor.CurrentValue;
// HttpClient über die Factory beziehen und den internen Verbindungspool
// wiederverwenden.
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);
}
}
Aufrufende Komponenten registrieren beispielsweise mit builder.Services.AddHttpClient(nameof(ExternalApiClient)); einen benannten Client. Werte wie BaseUrl und ApiKey, die sich durch Konfigurationsänderungen ändern können, werden pro Anfrage in HttpRequestMessage gesetzt. Die Kosten für Erzeugung und Freigabe der HttpClient-Instanz verbleiben dagegen bei der Factory. Das ist die klare Aufgabenverteilung.
Konfigurationsfehler, die erst zur Laufzeit als Ausnahme erscheinen, verlängern die Fehlersuche. Mit Validierung durch DataAnnotations und ValidateOnStart() lässt sich festlegen: Eine Anwendung mit kaputter Konfiguration startet gar nicht erst.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(); // Validierung beim Hoststart (StartAsync/RunAsync), vor dem Start der Hostdienste
Ohne ValidateOnStart() wird die Validierung erst ausgeführt, wenn tatsächlich erstmals auf diese Options zugegriffen wird. Sie erfolgt außerdem beim Start des Hosts, also bei StartAsync/RunAsync, nicht direkt nach Build(). Testcode, der nur Build() ausführt und RunAsync() noch nicht aufruft, hat die Validierung daher noch nicht ausgelöst. Um den sporadischen Fehler zu verhindern, dass die Anwendung startet und erst beim Öffnen eines Bildschirms mit der betreffenden Einstellung abstürzt, sollten Einstellungen in Geschäftsanwendungen grundsätzlich mit ValidateOnStart() versehen werden.6
5. Wo gehören Einstellungen hin, die geschrieben werden können?
appsettings.json ist der Ort für schreibgeschützte Standardwerte, nicht für Einstellungen, die die Anwendung selbst verändert. Viele Geschäftsanwendungen werden unter Program Files installiert. Dort haben Standardbenutzer keine Schreibrechte; sonst treten zur Laufzeit Ausnahmen auf oder die Dateisystemvirtualisierung von Windows führt dazu, dass verschiedene Benutzer unterschiedliche Dateiinhalte sehen.
Einstellungen, die geschrieben werden müssen, werden nach ihrer Eigenschaft getrennt. Als Grundlage eignen sich die speziellen Ordner, die sich über Environment.GetFolderPath ermitteln lassen.11
| Speicherort | Ermittlung | Verwendung |
|---|---|---|
%LOCALAPPDATA%\Firmenname\Appname |
Environment.SpecialFolder.LocalApplicationData |
Standard für benutzerspezifische Einstellungen und Daten |
%APPDATA%\Firmenname\Appname (Roaming) |
Environment.SpecialFolder.ApplicationData |
Nur Einstellungen, die in Umgebungen mit roamingfähigen Profilen dem Benutzer folgen sollen |
%ProgramData%\Firmenname\Appname |
Environment.SpecialFolder.CommonApplicationData |
Rechnerbezogene Einstellungen für alle Benutzer; ACL-Konzept erforderlich |
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 });
// Mehrfaches Schreiben innerhalb desselben Prozesses wird vom Aufrufer serialisiert.
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;
}
Wer benutzer- und rechnerbezogene Einstellungen in einer Datei mischt, riskiert auf gemeinsam genutzten Rechnern, dass die Einstellung von Person A Person B beeinträchtigt. Für die Trennung von Benutzer- und Rechnerwerten sowie die Struktur von Windows-Profilen siehe So funktionieren Windows-Benutzerprofile. Eine Entscheidungstabelle für Speicherorte allgemein bietet Speicherorte für Daten von Windows-Anwendungen richtig auswählen.
6. Geheimnisse ── Verbindungszeichenfolgen und API-Schlüssel
Passwörter aus Verbindungszeichenfolgen oder API-Schlüssel direkt in appsettings.json zu schreiben, ist auch dann ein praktisches Risiko, wenn die Datei in .gitignore steht. Klartext kann über Verteilung in gemeinsamen Ordnern, eingesendete Dateien bei Supportfällen oder alte Konfigurationsdateien in Backups nach außen gelangen.
In der Entwicklung verwenden Sie den Secret Manager (dotnet user-secrets). Das ist jedoch ein Mechanismus für die Entwicklungserfahrung und nicht verschlüsselt. Die Werte werden im JSON-Klartext unter %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json abgelegt. Die offizielle Dokumentation bezeichnet ihn ausdrücklich als nur für die Entwicklung geeignet und nicht als vertrauenswürdigen Speicher. Produktivgeheimnisse per user-secrets auszuliefern, ist daher falsch.7
dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "Wert nur für die Entwicklung"
In der Produktion verschlüsselt DPAPI (Data Protection API) von Windows Geheimnisse gebunden an Benutzer- oder Rechneranmeldeinformationen. System.Security.Cryptography.ProtectedData stellt dazu Protect/Unprotect bereit. Mit DataProtectionScope.CurrentUser kann der Wert nur auf demselben Rechner und nur vom selben Benutzerkonto entschlüsselt werden. DPAPI ist eine Windows-spezifische Funktion und löst auf anderen Plattformen PlatformNotSupportedException aus. Das muss in die Architekturentscheidung einfließen.8
using System.Security.Cryptography;
using System.Text;
public static class SecretProtector
{
// Zusätzliche Entropie mit dem Verwendungszweck verhindert, dass die Daten
// leicht mit für andere Zwecke geschützten Daten verwechselt werden.
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);
}
}
Ausführliche Implementierungsdetails – Speicherort der durch DPAPI geschützten Werte, Wahl zwischen CurrentUser und LocalMachine sowie Fallstricke bei Geschäftsanwendungen mit mehreren Benutzern auf einem Rechner – finden Sie in Geheimnisse in Windows-Anwendungen speichern: Klartextkonfiguration mit DPAPI vermeiden.
Die Grenze zwischen Werten, die im Klartext bleiben dürfen, und solchen, die geschützt werden müssen, ist einfach: Fragen Sie, ob ein Leak Passwort- oder API-Schlüssel-Neuausstellung, eine Untersuchung unbefugten Zugriffs oder eine Meldung an eine Aufsichtsbehörde auslösen würde. Hostname und Port einer Verbindungszeichenfolge richten meist wenig Schaden an, darin enthaltene Passwörter und Authentifizierungstoken sind dagegen immer schutzbedürftig. Wenn die Verbindungszeichenfolge in Serverinformationen und Anmeldeinformationen getrennt aufgebaut wird und nur Letztere durch DPAPI geschützt werden, ist die Trennung auch im Code klar erkennbar.
7. Konfigurationsänderungen zur Laufzeit ── Die Realität von reloadOnChange
Der Standardaufruf von AddJsonFile, den Host.CreateApplicationBuilder intern verwendet, setzt reloadOnChange: true. Änderungen an appsettings.json und appsettings.{Environment}.json werden daher erkannt und die Dateien automatisch neu geladen. Intern überwacht PhysicalFileProvider die Änderungen mit FileSystemWatcher.12
Diese Funktion hat in der Praxis mehrere Grenzen:
- Nur Werte, die über
IOptionsMonitor<T>oderIOptionsSnapshot<T>gelesen werden, werden aktualisiert.IOptions<T>behält den Startwert bei; das Ändern einer Datei wirkt dann nicht. Viele Meldungen „Ich habe die Konfigurationsdatei direkt bearbeitet, aber nichts passiert“ beruhen auf der Verwechslung dieser beiden Varianten. FileSystemWatcherliefert auf Dateisystemen wie Docker-Containern oder Netzwerkfreigaben unter Umständen keine verlässlichen Änderungsereignisse. In solchen Umgebungen schaltet die UmgebungsvariableDOTNET_USE_POLLING_FILE_WATCHERmit dem Werttrueauf eine Überwachung per Polling im Abstand von vier Sekunden um; das Intervall lässt sich nicht verändern.13 Allgemeine Eigenheiten wie ausgelassene Ereignisse, Pufferüberlauf und doppelte Ereignisse behandelt Praxisleitfaden zu FileSystemWatcher.- Eine Dateiveränderung kann mehrere Änderungsbenachrichtigungen auslösen. Wenn die Anwendung bei einer Änderung schwere Arbeit erneut ausführt, sind Schutzmaßnahmen nötig: kurz aufeinanderfolgende Signale entprellen oder den Inhaltshash vergleichen und nur tatsächliche Änderungen verarbeiten.
Eine Änderung ohne Neustart muss nicht immer das Ziel sein. Leichte Werte wie Loglevel oder Feature Flags können mit IOptionsMonitor sofort wirken. Werte wie DB-Verbindungszeichenfolgen oder die Größe eines Threadpools können dagegen zu Widersprüchen mit bereits laufenden Ressourcen führen; für sie ist „wirksam nach Neustart“ als klare Spezifikation sicherer. Wichtig ist, dass Betriebsteams unmissverständlich wissen, welche Einstellungen nach dem Speichern sofort gelten und welche einen Neustart benötigen – wichtiger als die Frage, ob reloadOnChange verwendet wird.
8. Migrationskarte von app.config / Settings.settings
Bei der Migration einer Anwendung aus der .NET-Framework-Zeit zu .NET muss auch das Konfigurationssystem neu aufgebaut werden. Die folgende Tabelle ordnet die Entsprechungen zu.14
| .NET Framework | .NET | Hinweis |
|---|---|---|
<appSettings> in App.config / Web.config |
appsettings.json + IConfiguration |
Hierarchische Struktur lässt sich natürlich durch JSON-Verschachtelung ausdrücken |
ConfigurationManager.AppSettings["Key"] |
builder.Configuration["Key"] oder IOptions<T> |
Von Zeichenfolgenzugriff zu typsicherem Zugriff |
ConfigurationManager.ConnectionStrings |
builder.Configuration.GetConnectionString("Name") |
Die Konvention des Abschnitts ConnectionStrings bleibt erhalten |
Settings.settings (Benutzerscope) |
Eigene JSON-Datei unter %LOCALAPPDATA% |
Kein automatisch erzeugter Mechanismus wie ApplicationSettingsBase; selbst serialisieren und speichern |
Settings.settings (Anwendungsscope) |
appsettings.json |
Als schreibgeschützter Standardwert behandeln |
Verschlüsselung von <connectionStrings> (z. B. aspnet_regiis) |
DPAPI (ProtectedData) |
Der Verschlüsselungsmechanismus ändert sich; bei der Migration neu aufbauen |
Bei der Migration gibt es zwei häufige Fallen.
Die erste: Mit dem NuGet-Paket System.Configuration.ConfigurationManager lassen sich Lesezugriffe auf App.config zunächst unverändert weiterführen. Das ist ein sinnvoller erster Schritt, aber die Migration nach appsettings.json sollte trotzdem geplant werden. Auch umgebende Bibliotheken wie Logging-Anbieter gehen zunehmend von appsettings.json aus; allein zum Lesen von App.config eine ältere Struktur beizubehalten, wird damit immer weniger begründbar.14
Die zweite: Benutzerspezifische Einstellungen in Settings.settings. Im .NET Framework reichte Properties.Settings.Default.Save(), um Werte pro Benutzer automatisch zu speichern. .NET bietet standardmäßig keinen entsprechenden automatisch generierten Mechanismus. Bei der Migration wird daher eine eigene Speicherklasse wie die in Kapitel 5 benötigt.
Neben der Konfigurationsverwaltung müssen bei einer Migration auch die Kompatibilität von Abhängigkeiten, COM-Integration und die Verteilungsform geprüft werden. Eine Übersicht bietet die Checkliste vor der Migration von .NET Framework zu .NET; sie sollte früh im Migrationsprojekt einmal durchgegangen werden.
Zusammenfassung
Die Konfigurationsverwaltung in .NET entsteht aus dem Zusammenspiel dreier Mechanismen: der Schichtung von Anbietern über IConfiguration, der typsicheren Aufnahme über die IOptions-Familie und der Umgebungswahl durch Umgebungsvariablen. Bis hierher ähneln sich viele Anwendungen. Die besonderen Fragen von Windows-Geschäftsanwendungen liegen danach: Speicherorte für beschreibbare Einstellungen, Schutz von Geheimnissen, die Grenzen von Änderungen zur Laufzeit und der Umgang mit Bestand aus der app.config-Generation.
Der nächste Schritt nach „alles steht in appsettings.json“ lautet, Speicherort und Handhabung je nach Art der Einstellung zu trennen. Die Entscheidungstabelle dieses Artikels soll dafür einen Einstieg bieten. Bei der Bestandsaufnahme der Konfigurationsverwaltung oder bei der Migrationsstrategie von app.config hängt die beste Lösung oft vom konkreten Dateibestand und der Zielumgebung ab. Wenn Sie unsicher sind, sprechen Sie uns gern an.
Verwandte Artikel
- Was ist der Generic Host?
- Generic Host + BackgroundService in einer Desktop-Anwendung einsetzen
- Aufgaben der Windows-Aufgabenplanung starten nicht oder enden mit 0x1
- Windows-Dienste entwickeln und betreiben
- Speicherorte für Daten von Windows-Anwendungen richtig auswählen
- So funktionieren Windows-Benutzerprofile
- Geheimnisse in Windows-Anwendungen speichern: Klartextkonfiguration mit DPAPI vermeiden
- Praxisleitfaden zu FileSystemWatcher
- Checkliste vor der Migration von .NET Framework zu .NET
Zugehörige Beratungsfelder
KomuraSoft unterstützt bei der Konzeption der Konfigurationsverwaltung für Windows-Geschäftsanwendungen, beim Betrieb umgebungsspezifischer Einstellungen und bei der technischen Beratung zur Migrationsstrategie für vorhandenes app.config.
Referenzen
-
Microsoft Learn, Konfiguration in .NET – alternativer Hostingansatz. Zur Priorität der Konfigurationsanbieter, die
Host.CreateApplicationBuilderstandardmäßig anlegt: Befehlszeilenargumente → Umgebungsvariablen → user secrets in der Entwicklung → appsettings.{Environment}.json → appsettings.json. ↩ ↩2 ↩3 -
Microsoft Learn, .NET Generic Host – Einstellungen des Hostbuilders. Zur Standardreihenfolge der Hostkonfiguration (
DOTNET_-Umgebungsvariablen und Befehlszeilenargumente) und Anwendungskonfiguration (appsettings.json, appsettings.{Environment}.json, Secret Manager, Umgebungsvariablen und Befehlszeilenargumente), dieHost.CreateApplicationBuildereinliest. ↩ ↩2 -
Microsoft Learn, Den .NET Generic Host in einer Windows-Forms-Anwendung verwenden. Zur Einbindung des Generic Host in Windows Forms und zur Nutzung von DI, Konfiguration und Logging. ↩
-
Microsoft Learn, ASP.NET-Core-Laufzeitumgebungen – Umgebungsvariablen zur Bestimmung der Laufzeitumgebung. Zur Beziehung zwischen
DOTNET_ENVIRONMENTundASPNETCORE_ENVIRONMENT, zum Vorrang vonDOTNET_ENVIRONMENTbeiWebApplication, zum StandardwertProductionund zur Groß-/Kleinschreibung bei Umgebungsvariablen unter Windows und Linux. ↩ ↩2 -
Microsoft Learn, Options-Muster in .NET – Options-Schnittstellen. Zu Lebensdauer, Zeitpunkt der Anwendung von Konfigurationsänderungen und Funktionsunterschieden zwischen
IOptions<TOptions>,IOptionsSnapshot<TOptions>undIOptionsMonitor<TOptions>. ↩ ↩2 -
Microsoft Learn, Options-Muster in .NET – Options-Validierung. Zur Validierung mit
ValidateDataAnnotations()sowie zur Startvalidierung mitValidateOnStart()oderAddOptionsWithValidateOnStart. ↩ ↩2 ↩3 -
Microsoft Learn, Sichere Speicherung von Anwendungsgeheimnissen während der Entwicklung in ASP.NET Core – Secret-Manager-Tool verwenden. Dazu, dass Secret Manager Werte unverschlüsselt im Klartext unter
%APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.jsonspeichert und nur für die Entwicklung, nicht als vertrauenswürdiger Speicher, gedacht ist. ↩ ↩2 -
Microsoft Learn, Klasse ProtectedData. Zu
ProtectedData.Protect/Unprotectals DPAPI-Wrapper, zum Unterschied zwischenDataProtectionScope.CurrentUserundLocalMachinesowie zur Beschränkung auf Windows mitPlatformNotSupportedExceptionauf anderen Plattformen. ↩ ↩2 -
Microsoft Learn, Konfigurationsanbieter in .NET. Zum Mechanismus der Konfigurationsanbieter, die Quellen wie JSON, Umgebungsvariablen, Befehlszeilenargumente, INI und XML hinter
IConfigurationzusammenführen. ↩ -
Microsoft Learn, Konfigurationsanbieter in .NET – Umgebungsvariablenanbieter. Dazu, dass die Standardkonfiguration Umgebungsvariablen mit
DOTNET_-Präfix und Befehlszeilenargumente für Host- und Anwendungskonfiguration einliest, dies aber nicht für Benutzerkonfiguration gilt, sowie zur Ergänzung eines eigenen Präfixes. ↩ -
Microsoft Learn, Methode Environment.GetFolderPath. Zur Ermittlung der Pfade spezieller Ordner über die Enumeration
Environment.SpecialFolderundGetFolderPath. ↩ -
Microsoft Learn, Änderungen mit Change Tokens erkennen in ASP.NET Core – Konfigurationsänderungen überwachen. Zum Parameter
reloadOnChangevonAddJsonFileund dazu, dassPhysicalFileProviderinternFileSystemWatcherzur Überwachung von Konfigurationsdateien verwendet. ↩ -
Microsoft Learn, Options-Muster in .NET – IOptionsMonitor. Dazu, dass Änderungsbenachrichtigungen von
IOptionsMonitorauf dateisystembasierte Konfigurationsanbieter beschränkt sind und in Docker-Containern oder Netzwerkfreigaben bei unzuverlässigen Ereignissen überDOTNET_USE_POLLING_FILE_WATCHERauf Polling im Vier-Sekunden-Abstand umgestellt werden können. ↩ -
Microsoft Learn, Nach dem Upgrade von .NET Framework zu .NET modernisieren – App.config. Zur Migration von
App.confignachappsettings.json, zur Aufrechterhaltung der Kompatibilität mit dem NuGet-PaketSystem.Configuration.ConfigurationManagerund zur Verwendung vonMicrosoft.Extensions.Configuration.Json. ↩ ↩2
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Mit PerfView und dotnet-trace die Ursache von „langsam“ finden — Praxis-Einstieg in die .NET-Performanceanalyse
Wenn eine Geschäftsanwendung langsam ist, die CPU auslastet oder gelegentlich einfriert: Welches Werkzeug zeigt was? Dieser Beitrag ordne...
Drucken und PDF-Ausgabe in Windows-Geschäftsanwendungen — System.Drawing.Printing, WPF und Berichtsbibliotheken richtig einsetzen
Dieser Beitrag ordnet WinForms-Druck mit PrintDocument, WPF-Druck mit FlowDocument und FixedDocument sowie Möglichkeiten zur PDF-Ausgabe ...
Einführung in Windows-Ereignisprotokoll und ETW ── Protokolle von Geschäftsanwendungen in die Standardmechanismen des Betriebssystems einordnen
Verlassen Sie sich bei den Protokollen einer Windows-Geschäftsanwendung nur auf Dateien? Ereignisprotokoll und ETW bilden eine eigene Ebe...
Google Ads mit kleinem Budget im B2B ── Planung und wöchentliche Routine für Ergebnisse mit einigen Zehntausend Yen im Monat
Ein Praxisleitfaden für B2B-Unternehmen, die Google Ads mit einigen Zehntausend Yen monatlich beginnen möchten. Er behandelt durchschnitt...
Bei Suchen nach Ortsnamen sichtbar werden — Praxisleitfaden für lokales SEO für kleine und mittlere Unternehmen (Gebietsseiten und Google Unternehmensprofil)
Ihre Firma erscheint nicht bei einer Suche nach „Ortsname + Branche“? Dieser Beitrag ordnet für kleine und mittlere Unternehmen die sinnv...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Generic Host und Anwendungsarchitektur
Generic Host, BackgroundService, DI, Konfiguration, Protokollierung und Anwendungslebenszyklus.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Die Konzeption der Konfigurationsverwaltung und von Einstellungsdateien gehört zu den praktischen Fragen der Windows-Anwendungsentwicklung.
Technische Beratung und Design-Review
Die Entscheidung über eine Migrationsstrategie von bestehendem app.config erfordert ein Design-Review und gehört daher zu einer technischen Beratung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Darf appsettings.json im selben Ordner wie die EXE liegen?
- Für schreibgeschützte Standardwerte ist das unproblematisch. Wird der Ordner jedoch unter Program Files installiert, kann die Anwendung appsettings.json nicht selbst ändern. Standardbenutzer haben dort keine Schreibrechte; andernfalls entstehen zur Laufzeit Ausnahmen oder die Dateisystemvirtualisierung von Windows führt dazu, dass einzelne Benutzer unterschiedliche Inhalte sehen. Einstellungen, die geschrieben werden müssen, sollten in eine getrennte Datei unter %LOCALAPPDATA% oder %ProgramData% ausgelagert werden.
- Sollten Umgebungsvariablen oder appsettings.json Vorrang haben?
- Grundsätzlich sollte die vorgesehene Reihenfolge unverändert bleiben: appsettings.json → appsettings.{Environment}.json → Umgebungsvariablen → Befehlszeilenargumente. Entscheidend ist der Charakter des Werts: Standardwerte, die mit dem Build ausgeliefert werden dürfen, gehören in appsettings.json; Werte, die je Zielumgebung oder Container anders sind, in Umgebungsvariablen. Diese Aufteilung bleibt auch später verständlich.
- Wie sollten Konfigurationsklassen aufgeteilt werden?
- Options-Klassen sollten nach Funktionen und Verantwortlichkeiten getrennt werden. Für Verbindungszeichenfolgen etwa ConnectionOptions, für externe API-Integration ExternalApiOptions. Unzusammenhängende Werte gehören nicht in eine einzige Klasse. Die Trennung gliedert zugleich die Einheiten für Validierung und Neuladen mit IOptionsSnapshot/IOptionsMonitor; DataAnnotations bleiben pro Klasse überschaubar.
- Sollte von INI-Dateien oder der Registry migriert werden?
- Wenn die Konfigurationsverwaltung neu aufgebaut wird, empfiehlt sich eine Vereinheitlichung auf appsettings.json und IConfiguration. .NET stellt auch einen INI-Konfigurationsanbieter bereit, sodass bestehende INI-Dateien zunächst weitergelesen und schrittweise migriert werden können. Die Registry gehört nicht zum Standardbereich der .NET-Konfigurationsanbieter. Bei vorhandenen Registry-Abhängigkeiten ist eine schreibgeschützte Brücke sinnvoll, die Werte nach und nach in appsettings.json überführt.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.
Öffentliche Links