Nicht nur appsettings.json ── Praxisleitfaden für die Konfigurationsverwaltung in Windows-Geschäftsanwendungen (Umgebungen, Geheimnisse und Speicherorte)

· · 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.jsonappsettings.{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.CreateApplicationBuilder steht 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 (oder ASPNETCORE_ENVIRONMENT) ausgewählt. Bei WebApplication hat DOTNET_ENVIRONMENT Vorrang. Fehlen beide Variablen, lautet der Standard Production. 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ügt IOptions<T>; Änderungen ohne Neustart erfordern IOptionsMonitor<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() und ValidateOnStart() sorgt dafür, dass ein Fehler direkt beim Start als Ausnahme sichtbar wird, statt nachts in Produktion als NullReferenceException entdeckt 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

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. Secret Manager (nur in der Entwicklungsumgebung)
  4. Umgebungsvariablen
  5. 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 /M dauerhaft 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 SET ausfü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 über AddCommandLine(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> oder IOptionsSnapshot<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.
  • FileSystemWatcher liefert auf Dateisystemen wie Docker-Containern oder Netzwerkfreigaben unter Umständen keine verlässlichen Änderungsereignisse. In solchen Umgebungen schaltet die Umgebungsvariable DOTNET_USE_POLLING_FILE_WATCHER mit dem Wert true auf 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

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

  1. Microsoft Learn, Konfiguration in .NET – alternativer Hostingansatz. Zur Priorität der Konfigurationsanbieter, die Host.CreateApplicationBuilder standardmäßig anlegt: Befehlszeilenargumente → Umgebungsvariablen → user secrets in der Entwicklung → appsettings.{Environment}.json → appsettings.json.  2 3

  2. 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), die Host.CreateApplicationBuilder einliest.  2

  3. 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. 

  4. Microsoft Learn, ASP.NET-Core-Laufzeitumgebungen – Umgebungsvariablen zur Bestimmung der Laufzeitumgebung. Zur Beziehung zwischen DOTNET_ENVIRONMENT und ASPNETCORE_ENVIRONMENT, zum Vorrang von DOTNET_ENVIRONMENT bei WebApplication, zum Standardwert Production und zur Groß-/Kleinschreibung bei Umgebungsvariablen unter Windows und Linux.  2

  5. Microsoft Learn, Options-Muster in .NET – Options-Schnittstellen. Zu Lebensdauer, Zeitpunkt der Anwendung von Konfigurationsänderungen und Funktionsunterschieden zwischen IOptions<TOptions>, IOptionsSnapshot<TOptions> und IOptionsMonitor<TOptions> 2

  6. Microsoft Learn, Options-Muster in .NET – Options-Validierung. Zur Validierung mit ValidateDataAnnotations() sowie zur Startvalidierung mit ValidateOnStart() oder AddOptionsWithValidateOnStart 2 3

  7. 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.json speichert und nur für die Entwicklung, nicht als vertrauenswürdiger Speicher, gedacht ist.  2

  8. Microsoft Learn, Klasse ProtectedData. Zu ProtectedData.Protect/Unprotect als DPAPI-Wrapper, zum Unterschied zwischen DataProtectionScope.CurrentUser und LocalMachine sowie zur Beschränkung auf Windows mit PlatformNotSupportedException auf anderen Plattformen.  2

  9. Microsoft Learn, Konfigurationsanbieter in .NET. Zum Mechanismus der Konfigurationsanbieter, die Quellen wie JSON, Umgebungsvariablen, Befehlszeilenargumente, INI und XML hinter IConfiguration zusammenführen. 

  10. 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. 

  11. Microsoft Learn, Methode Environment.GetFolderPath. Zur Ermittlung der Pfade spezieller Ordner über die Enumeration Environment.SpecialFolder und GetFolderPath

  12. Microsoft Learn, Änderungen mit Change Tokens erkennen in ASP.NET Core – Konfigurationsänderungen überwachen. Zum Parameter reloadOnChange von AddJsonFile und dazu, dass PhysicalFileProvider intern FileSystemWatcher zur Überwachung von Konfigurationsdateien verwendet. 

  13. Microsoft Learn, Options-Muster in .NET – IOptionsMonitor. Dazu, dass Änderungsbenachrichtigungen von IOptionsMonitor auf dateisystembasierte Konfigurationsanbieter beschränkt sind und in Docker-Containern oder Netzwerkfreigaben bei unzuverlässigen Ereignissen über DOTNET_USE_POLLING_FILE_WATCHER auf Polling im Vier-Sekunden-Abstand umgestellt werden können. 

  14. Microsoft Learn, Nach dem Upgrade von .NET Framework zu .NET modernisieren – App.config. Zur Migration von App.config nach appsettings.json, zur Aufrechterhaltung der Kompatibilität mit dem NuGet-Paket System.Configuration.ConfigurationManager und zur Verwendung von Microsoft.Extensions.Configuration.Json 2

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

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

Zurück zum Blog