Lassen Sie uns für eine Weile auf DDD und Reflexion verzichten. Ich schlage vor, über das Einfache, über die Organisation von Anwendungseinstellungen zu sprechen.
Nachdem meine Kollegen und ich beschlossen hatten, zu .NET Core zu wechseln, stellte sich die Frage, wie Konfigurationsdateien organisiert, Transformationen usw. in der neuen Umgebung durchgeführt werden. Der folgende Code ist in vielen Beispielen enthalten und wurde von vielen erfolgreich verwendet.
public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Aber lassen Sie uns sehen, wie die Konfiguration funktioniert und in welchen Fällen dieser Ansatz verwendet werden muss und in welchen Fällen die Entwickler von .NET Core vertrauen können. Ich frage nach Katze.
Wie zuvor
Wie jede Geschichte hat dieser Artikel einen Anfang. Eines der ersten Probleme nach dem Wechsel zu ASP.NET Core war die Umwandlung von Konfigurationsdateien.
Erinnern Sie sich daran, wie es vorher mit web.config war
Die Konfiguration bestand aus mehreren Dateien. Die Hauptdatei war web.config , und je nach Konfiguration der Assembly wurden bereits Transformationen ( web.Development.config usw.) darauf angewendet. Gleichzeitig wurden XML- Attribute aktiv verwendet, um den Abschnitt des XML- Dokuments zu durchsuchen und zu transformieren.
Wie wir jedoch in ASP.NET Core wissen, wurde die Datei web.config durch appsettings.json ersetzt, und es gibt nicht mehr den üblichen Transformationsmechanismus.
Was sagt uns Google?Das Suchergebnis für "Transformieren in ASP.NET Core" bei Google ergab den folgenden Code:
public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Im Konstruktor der Startup- Klasse erstellen wir mit ConfigurationBuilder ein Konfigurationsobjekt. In diesem Fall geben wir explizit an, welche Konfigurationsquellen wir verwenden möchten.
Und so:
public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Abhängig von der Umgebungsvariablen wird die eine oder andere Konfigurationsquelle ausgewählt.
Diese Antworten finden sich häufig in SO und anderen weniger beliebten Ressourcen. Aber das Gefühl ließ nicht nach. dass wir falsch liegen. Was ist, wenn ich Umgebungsvariablen oder Befehlszeilenargumente in der Konfiguration verwenden möchte? Warum muss ich diesen Code in jedem Projekt schreiben?
Auf der Suche nach der Wahrheit musste ich mich eingehend mit der Dokumentation und dem Quellcode befassen. Und ich möchte das in diesem Artikel gewonnene Wissen teilen.
Mal sehen, wie die Konfiguration in .NET Core funktioniert.
Konfiguration
Die Konfiguration in .NET Core wird durch ein IConfiguration- Schnittstellenobjekt dargestellt.
public interface IConfiguration { string this[string key] { get; set; } IConfigurationSection GetSection(string key); IEnumerable<IConfigurationSection> GetChildren(); IChangeToken GetReloadToken(); }
- Indexer [Zeichenfolgenschlüssel] , mit dem der Wert des Konfigurationsparameters per Schlüssel abgerufen werden kann
- GetSection (Zeichenfolgenschlüssel) gibt den Konfigurationsabschnitt zurück, der dem Schlüsselschlüssel entspricht
- GetChildren () gibt einen Unterabschnittssatz des aktuellen Konfigurationsabschnitts zurück
- GetReloadToken () gibt eine Instanz von IChangeToken zurück , mit der Benachrichtigungen empfangen werden können, wenn sich die Konfiguration ändert
Eine Konfiguration ist eine Sammlung von Schlüssel-Wert-Paaren. Beim Lesen aus einer Konfigurationsquelle (Datei, Umgebungsvariablen) werden hierarchische Daten auf eine flache Struktur reduziert. Zum Beispiel ist json ein Objekt des Formulars
{ "Settings": { "Key": "I am options" } }
wird auf eine flache Ansicht reduziert:
Settings:Key = I am options
Hier ist der Schlüssel Einstellungen: Schlüssel , und der Wert ist Ich bin Optionen .
Die Konfigurationsanbieter werden zum Auffüllen der Konfiguration verwendet.
Konfigurationsanbieter
Ein Schnittstellenobjekt ist für das Lesen von Daten aus der Konfigurationsquelle verantwortlich.
IConfigurationProvider :
public interface IConfigurationProvider { bool TryGet(string key, out string value); void Set(string key, string value); IChangeToken GetReloadToken(); void Load(); IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath); }
- Mit TryGet (String-Schlüssel, Out-String-Wert) kann der Wert des Konfigurationsparameters nach Schlüssel abgerufen werden
- Mit Set (String-Schlüssel, String-Wert) wird der Wert des Konfigurationsparameters festgelegt
- GetReloadToken () gibt eine Instanz von IChangeToken zurück , mit der Benachrichtigungen empfangen werden können, wenn sich eine Konfigurationsquelle ändert
- Load () -Methode, die für das Lesen der Konfigurationsquelle verantwortlich ist
- Mit GetChildKeys (IEnumerable <string> previousKeys, string parentPath) können Sie eine Liste aller von diesem Konfigurationsanbieter bereitgestellten Schlüssel abrufen
Die folgenden Anbieter sind in der Box verfügbar:
- Json
- Ini
- Xml
- Umgebungsvariablen
- Inmemory
- Azure
- Benutzerdefinierter Konfigurationsanbieter
Die folgenden Konventionen zur Verwendung von Konfigurationsanbietern werden akzeptiert.
- Konfigurationsquellen werden in der Reihenfolge gelesen, in der sie angegeben wurden.
- Wenn dieselben Schlüssel in verschiedenen Konfigurationsquellen vorhanden sind (der Vergleich unterscheidet nicht zwischen Groß- und Kleinschreibung), wird der zuletzt hinzugefügte Wert verwendet.
Wenn wir mit CreateDefaultBuilder eine Instanz eines Webservers erstellen , sind standardmäßig die folgenden Konfigurationsanbieter verbunden:

- ChainedConfigurationProvider Über diesen Anbieter können Sie Werte und Konfigurationsschlüssel abrufen , die von anderen Konfigurationsanbietern hinzugefügt wurden
- JsonConfigurationProvider verwendet JSON- Dateien als Konfigurationsquelle. Wie Sie sehen, werden drei Anbieter dieses Typs zur Liste der Anbieter hinzugefügt. Der erste verwendet appsettings.json als Quelle, der zweite verwendet appsettings. {Environment} .json . Der dritte liest Daten aus secret.json . Wenn Sie die Anwendung in der Release- Konfiguration erstellen, wird der dritte Anbieter nicht verbunden, da nicht empfohlen wird, Geheimnisse in der Produktionsumgebung zu verwenden
- EnvironmentVariablesConfigurationProvider ruft Konfigurationsparameter aus Umgebungsvariablen ab
- CommandLineConfigurationProvider ermöglicht das Hinzufügen von Befehlszeilenargumenten zur Konfiguration
Da die Konfiguration als Wörterbuch gespeichert ist, muss die Eindeutigkeit der Schlüssel sichergestellt werden. Standardmäßig funktioniert dies so.
Wenn der CommandLineConfigurationProvider- Anbieter ein Element mit dem Schlüsselschlüssel und der JsonConfigurationProvider- Anbieter ein Element mit dem Schlüssel hat, wird das Element von JsonConfigurationProvider durch das Element von CommandLineConfigurationProvider ersetzt, da es zuletzt registriert wurde und eine höhere Priorität hat.
Erinnern Sie sich an ein Beispiel vom Anfang des Artikels public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Wir müssen IConfiguration nicht selbst erstellen, um die Konfigurationsdateien zu transformieren, da dies standardmäßig aktiviert ist. Dieser Ansatz ist erforderlich, wenn wir die Anzahl der Konfigurationsquellen begrenzen möchten.
Benutzerdefinierter Konfigurationsanbieter
Um Ihren Konfigurationsanbieter zu schreiben, müssen Sie die Schnittstellen IConfigurationProvider und IConfigurationSource implementieren . IConfigurationSource ist eine neue Schnittstelle, die wir in diesem Artikel noch nicht berücksichtigt haben.
public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); }
Die Schnittstelle besteht aus einer einzelnen Build- Methode, die IConfigurationBuilder als Parameter verwendet und eine neue Instanz von IConfigurationProvider zurückgibt .
Zur Implementierung unserer Konfigurationsanbieter stehen uns die abstrakten Klassen ConfigurationProvider und FileConfigurationProvider zur Verfügung. In diesen Klassen ist die Logik der Methoden TryGet , Set , GetReloadToken und GetChildKeys bereits implementiert und es bleibt nur die Load- Methode zu implementieren.
Schauen wir uns ein Beispiel an. Es ist erforderlich, das Lesen der Konfiguration aus der yaml- Datei zu implementieren, und es ist auch erforderlich, dass wir die Konfiguration ändern können, ohne unsere Anwendung neu zu starten.
Erstellen Sie die YamlConfigurationProvider- Klasse und machen Sie sie zum Erben des FileConfigurationProvider .
public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { throw new NotImplementedException(); } }
Im obigen Code-Snippet können Sie einige Funktionen der FileConfigurationProvider- Klasse erkennen. Der Konstruktor akzeptiert eine Instanz von FileConfigurationSource , die den IFileProvider enthält. IFileProvider wird zum Lesen einer Datei und zum Abonnieren eines Dateiänderungsereignisses verwendet. Sie können auch feststellen, dass die Load- Methode einen Stream akzeptiert, in dem die Konfigurationsdatei zum Lesen geöffnet ist. Dies ist eine Methode der FileConfigurationProvider- Klasse und befindet sich nicht in der IConfigurationProvider- Schnittstelle.
Fügen Sie eine einfache Implementierung hinzu, mit der wir die yaml- Datei lesen können . Zum Lesen der Datei verwende ich das YamlDotNet- Paket.
YamlConfigurationProvider-Implementierung public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { if (stream.CanSeek) { stream.Seek(0L, SeekOrigin.Begin); using (StreamReader streamReader = new StreamReader(stream)) { var fileContent = streamReader.ReadToEnd(); var yamlObject = new DeserializerBuilder() .Build() .Deserialize(new StringReader(fileContent)) as IDictionary<object, object>; Data = new Dictionary<string, string>(); foreach (var pair in yamlObject) { FillData(String.Empty, pair); } } } } private void FillData(string prefix, KeyValuePair<object, object> pair) { var key = String.IsNullOrEmpty(prefix) ? pair.Key.ToString() : $"{prefix}:{pair.Key}"; switch (pair.Value) { case string value: Data.Add(key, value); break; case IDictionary<object, object> section: { foreach (var sectionPair in section) FillData(pair.Key.ToString(), sectionPair); break; } } } }
Um eine Instanz unseres Konfigurationsanbieters zu erstellen, müssen Sie FileConfigurationSource implementieren.
YamlConfigurationSource-Implementierung public class YamlConfigurationSource : FileConfigurationSource { public YamlConfigurationSource(string fileName) { Path = fileName; ReloadOnChange = true; } public override IConfigurationProvider Build(IConfigurationBuilder builder) { this.EnsureDefaults(builder); return new YamlConfigurationProvider(this); } }
Hierbei ist zu beachten, dass Sie zum Initialisieren der Eigenschaften der Basisklasse die Methode this.EnsureDefaults (Builder) aufrufen müssen.
Um einen benutzerdefinierten Konfigurationsanbieter in der Anwendung zu registrieren, müssen Sie die Anbieterinstanz zu IConfigurationBuilder hinzufügen . Sie können die Add- Methode von IConfigurationBuilder aus aufrufen , aber ich werde sofort die YamlConfigurationProvider- Initialisierungslogik in der Erweiterungsmethode ausgeben .
Implementieren Sie YamlConfigurationExtensions public static class YamlConfigurationExtensions { public static IConfigurationBuilder AddYaml( this IConfigurationBuilder builder, string filePath) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); return builder .Add(new YamlConfigurationSource(filePath)); } }
Rufen Sie die AddYaml-Methode auf public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, builder) => { builder.AddYaml("appsettings.yaml"); }) .UseStartup<Startup>(); }
Tracking ändern
In der neuen API- Konfiguration wurde es möglich, die Konfigurationsquelle erneut zu lesen, wenn sie geändert wird. Gleichzeitig wird die Anwendung nicht neu gestartet.
Wie es funktioniert:
- Der Konfigurationsanbieter überwacht Änderungen der Konfigurationsquelle
- Wenn eine Konfigurationsänderung auftritt, wird ein neues IChangeToken erstellt .
- Wenn IChangeToken geändert wird , wird ein erneutes Laden der Konfiguration aufgerufen
Lassen Sie uns sehen, wie die Änderungsverfolgung in FileConfigurationProvider implementiert wird .
ChangeToken.OnChange(
Zwei Parameter werden an die OnChange- Methode der statischen ChangeToken- Klasse übergeben. Der erste Parameter ist eine Funktion, die ein neues IChangeToken zurückgibt, wenn sich die Konfigurationsquelle (in diesem Fall die Datei) ändert. Dies ist der sogenannte Produzent . Der zweite Parameter ist die Rückruffunktion (oder Consumer- Funktion), die aufgerufen wird, wenn die Konfigurationsquelle geändert wird.
Erfahren Sie mehr über die ChangeToken- Klasse.
Nicht alle Konfigurationsanbieter implementieren die Änderungsverfolgung. Dieser Mechanismus steht Nachkommen von FileConfigurationProvider und AzureKeyVaultConfigurationProvider zur Verfügung .
Fazit
In .NET Core verfügen wir über einen einfachen und bequemen Mechanismus zum Verwalten von Anwendungseinstellungen. Viele Add-Ons sind sofort verfügbar, viele Dinge werden standardmäßig verwendet.
Natürlich entscheidet jede Person, wie sie es benutzt, aber ich bin dafür, dass die Leute ihre Werkzeuge kennen.
Dieser Artikel behandelt nur die Grundlagen. Zusätzlich zu den Grundlagen stehen uns IOptions, Skripte nach der Konfiguration, Überprüfung der Einstellungen und vieles mehr zur Verfügung. Aber das ist eine andere Geschichte.
Sie finden das Anwendungsprojekt mit Beispielen aus diesem Artikel im Repository auf Github .
Teilen Sie in den Kommentaren mit, wer welche Konfigurationsmanagement-Ansätze verwendet?
Vielen Dank für Ihre Aufmerksamkeit.
upd .: Wie AdAbsurdum richtig vorgeschlagen hat, werden bei der Arbeit mit Arrays Elemente nicht immer ersetzt, wenn eine Konfiguration aus zwei Quellen zusammengeführt wird.
Betrachten Sie ein Beispiel. Beim Lesen eines Arrays aus appsettings.json erhalten wir diese flache Ansicht:
array:0=valueA
Beim Lesen aus appsettings.Development.json :
array:0=valueB array:1=value
Infolgedessen lautet die Konfiguration wie folgt:
array:0=valueB array:1=value
Alle Elemente mit eindeutigen Indizes ( Array: 1 im Beispiel) werden dem resultierenden Array hinzugefügt. Elemente aus verschiedenen Konfigurationsquellen, die jedoch denselben Index haben ( Array: 0 im Beispiel), werden zusammengeführt, und das zuletzt hinzugefügte Element wird verwendet.