Optionsmuster in ASP.NET Core

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der ASP.NET Core 8.0-Version dieses Artikels.

Von Rick Anderson

Das Optionsmuster verwendet Klassen, um stark typisierten Zugriff auf zusammengehörige Einstellungsgruppen zu ermöglichen. Wenn Konfigurationseinstellungen nach Szenario in separate Klassen isoliert werden, entspricht die Anwendung zwei wichtigen Prinzipien der Softwareentwicklung:

  • Kapselung:
    • Klassen, die von Konfigurationseinstellungen abhängen, sind nur von den Konfigurationseinstellungen abhängig, die sie verwenden.
  • Separation of Concerns:
    • Einstellungen für verschiedene App-Komponenten sind nicht voneinander abhängig und nicht aneinander gekoppelt.

Optionen bieten auch einen Mechanismus, um Konfigurationsdaten zu validieren. Weitere Informationen finden Sie im Abschnitt Optionsvalidierung.

Dieser Artikel enthält Informationen zu den Optionsmustern in ASP.NET Core. Informationen zur Verwendung des Optionsmusters in Konsolen-Apps finden Sie unter Optionsmuster in .NET.

Binden von hierarchischen Konfigurationsdaten

Die bevorzugte Methode für das Lesen zugehöriger Konfigurationswerte ist die Verwendung des Optionsmusters. Um z. B. die folgenden Konfigurationswerte zu lesen:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Erstellen Sie die folgende neue PositionOptions-Klasse:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

Eine Optionsklasse:

  • Muss nicht abstrakt sein.
  • Hat öffentliche Schreib-Lese-Eigenschaften des Typs, die entsprechende Elemente in config gebunden sind.
  • Hat seine Lese-/Schreibeigenschaften, die an übereinstimmende Einträge in der Konfiguration gebunden sind.
  • Es sind keine Felder gebunden. Im vorangehenden Code ist Position nicht gebunden. Das Feld Position wird verwendet, sodass die Zeichenfolge "Position" nicht in der App hartcodiert werden muss, wenn die Klasse an einen Konfigurationsanbieter gebunden wird.

Der folgende Code

  • Ruft ConfigurationBinder.Bind auf, um die PositionOptions-Klasse an den Position-Abschnitt zu binden.
  • Zeigt die Position-Konfigurationsdaten an.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Im vorangehenden Code werden standardmäßig Änderungen an der JSON-Konfigurationsdatei gelesen, nachdem die App gestartet wurde.

ConfigurationBinder.Get<T> bindet den angegebenen Typ und gibt ihn zurück. ConfigurationBinder.Get<T> ist möglicherweise praktischer als die Verwendung von ConfigurationBinder.Bind. Der folgende Code zeigt die Verwendung von ConfigurationBinder.Get<T> mit der PositionOptions-Klasse:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Im vorangehenden Code werden standardmäßig Änderungen an der JSON-Konfigurationsdatei gelesen, nachdem die App gestartet wurde.

Bind ermöglicht auch die Verketten einer abstrakten Klasse. Beachten Sie den folgenden Code, der die abstrakte Klasse SomethingWithAName verwendet:

namespace ConfigSample.Options;

public abstract class SomethingWithAName
{
    public abstract string? Name { get; set; }
}

public class NameTitleOptions(int age) : SomethingWithAName
{
    public const string NameTitle = "NameTitle";

    public override string? Name { get; set; }
    public string Title { get; set; } = string.Empty;

    public int Age { get; set; } = age;
}

Der folgende Code zeigt die NameTitleOptions Konfigurationswerte an:

public class Test33Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test33Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var nameTitleOptions = new NameTitleOptions(22);
        Configuration.GetSection(NameTitleOptions.NameTitle).Bind(nameTitleOptions);

        return Content($"Title: {nameTitleOptions.Title} \n" +
                       $"Name: {nameTitleOptions.Name}  \n" +
                       $"Age: {nameTitleOptions.Age}"
                       );
    }
}

Anrufe an Bind sind weniger streng als Anrufe an Get<>:

  • Bind ermöglicht die Verständigung eines Abstraktums.
  • Get<> muss eine Instanz selbst erstellen.

Das Optionsmuster

Eine alternative Methode beim Verwenden des Optionsmusters besteht darin, den Abschnitt Position zu binden und zum Dienstcontainer für die Abhängigkeitsinjektion hinzuzufügen. Im folgenden Code wird PositionOptions mit Configure zum Dienstcontainer hinzugefügt und an die Konfiguration gebunden:

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

Mithilfe des vorangehenden Codes liest der folgende Code die Positionsoptionen:

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

Im vorangehenden Code werden Änderungen an der JSON-Konfigurationsdatei nach dem Start der App nicht gelesen. Verwenden Sie IOptionsSnapshot, um Änderungen lesen zu können, nachdem die App gestartet wurde.

Optionenschnittstellen

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Mit Postkonfigurationsszenarios können Sie Optionen festlegen oder ändern, nachdem alle IConfigureOptions<TOptions>-Konfigurationen durchgeführt wurden.

IOptionsFactory<TOptions> ist für das Erstellen neuer Optionsinstanzen zuständig. Es verfügt über eine einzelne Create-Methode. Die Standardimplementierung akzeptiert alle registrierten IConfigureOptions<TOptions> und IPostConfigureOptions<TOptions> und führt alle Konfigurationen zuerst und die Postkonfigurationen danach aus. Es wird zwischen IConfigureNamedOptions<TOptions> und IConfigureOptions<TOptions> unterschieden, und es werden nur die entsprechenden Schnittstellen aufgerufen.

IOptionsMonitorCache<TOptions> wird von IOptionsMonitor<TOptions> zum Zwischenspeichern der TOptions-Instanzen verwendet. IOptionsMonitorCache<TOptions> erklärt Optionsinstanzen im Monitor für ungültig, sodass der Wert neu berechnet wird (TryRemove). Werte können manuell mit TryAdd eingeführt werden. Die Clear-Methode wird verwendet, wenn alle benannten Instanzen bei Bedarf neu erstellt werden sollen.

Verwenden von IOptionsSnapshot zum Lesen aktualisierter Daten

Verwenden von IOptionsSnapshot<TOptions>:

  • Optionen werden einmal pro Anforderung berechnet. Dies geschieht, wenn auf sie zugegriffen wird und sie für die Dauer der Anforderung zwischengespeichert werden.
  • Kann einen erheblichen Leistungsnachteil mit sich, da es sich um einen bereichsbezogenen Dienst handelt und pro Anforderung eine neue Berechnung durchgeführt wird. Weitere Informationen finden Sie in diesem GitHub-Problem und unter Improve the performance of configuration binding (Verbessern der Leistung der Konfigurationsbindung).
  • Änderungen an der Konfiguration werden gelesen, nachdem die App gestartet wurde, wenn Konfigurationsanbieter verwendet werden, die das Lesen aktualisierter Konfigurationswerte unterstützen.

Der Unterschied zwischen IOptionsMonitor und IOptionsSnapshot ist folgender:

  • IOptionsMonitor ist ein Singleton-Dienst, der zu jeder Zeit aktuelle Optionswerte empfängt, was insbesondere bei Singleton-Abhängigkeiten nützlich ist.
  • IOptionsSnapshot ist ein bereichsbezogener Dienst und bietet eine Momentaufnahme der Optionen zu dem Zeitpunkt, an dem das IOptionsSnapshot<T>-Objekt konstruiert wird. Momentaufnahmen von Optionen sind für die Verwendung mit vorübergehenden und bereichsbezogenen Abhängigkeiten bestimmt.

Der folgende Code verwendet IOptionsSnapshot<TOptions>.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

Der folgende Code registriert eine Konfigurationsinstanz, mit der MyOptions eine Bindung herstellt:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

Im vorangehenden Code werden Änderungen an der JSKonfigurationsdateiON-Konfigurationsdatei nach dem Start der App gelesen.

IOptionsMonitor

Der folgende Code registriert eine Konfigurationsinstanz, mit der MyOptions eine Bindung herstellt.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

Im folgenden Beispiel wird IOptionsMonitor<TOptions> verwendet:

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

Im vorangehenden Code werden standardmäßig Änderungen an der JSON-Konfigurationsdatei gelesen, nachdem die App gestartet wurde.

Unterstützung für benannte Optionen mit IConfigureNamedOptions

Benannte Optionen...

  • sind nützlich, wenn mehrere Konfigurationsabschnitte an die gleichen Eigenschaften gebunden werden.
  • unterscheiden zwischen Groß-/Kleinschreibung.

Betrachten Sie die folgende appsettings.json -Datei:

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

Anstatt zwei Klassen für die Bindung von TopItem:Month und TopItem:Year zu erstellen, wird folgende Klasse für jeden Abschnitt verwendet:

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

Der folgende Code dient zum Konfigurieren der benannten Optionen:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

Der folgende Code zeigt die benannten Optionen an:

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

Alle Optionen sind benannte Instanzen. IConfigureOptions<TOptions>-Instanzen werden behandelt, als würden sie die Options.DefaultName-Instanz anzielen. Diese ist string.Empty. IConfigureNamedOptions<TOptions> implementiert auch IConfigureOptions<TOptions>. Die standardmäßige Implementierung von IOptionsFactory<TOptions> verfügt über eine Logik, um jede einzelne entsprechend zu verwenden. Die benannte Option null wird verwendet, um auf alle benannten Instanzen anstelle einer bestimmten benannten Instanz abzuzielen. ConfigureAll und PostConfigureAll verwenden diese Konvention.

OptionsBuilder-API

OptionsBuilder<TOptions> dient zum Konfigurieren von TOptions-Instanzen. OptionsBuilder optimiert die Erstellung von benannten Optionen, da es sich nur um einen einzelnen Parameter für den ersten AddOptions<TOptions>(string optionsName)-Aufruf handelt statt um alle nachfolgenden Aufrufe. Die Optionsvalidierung und die ConfigureOptions-Überladungen, die Dienstabhängigkeiten akzeptieren, sind nur über OptionsBuilder verfügbar.

OptionsBuilder wird im Abschnitt Überprüfung von Optionen verwendet.

Informationen zum Hinzufügen eines benutzerdefinierten Repositorys finden Sie unter Verwenden von AddOptions zum Konfigurieren eines benutzerdefinierten Repositorys.

Verwenden von DI-Diensten zum Konfigurieren von Optionen

Der Zugriff auf Dienste ist über die Dependency Injection möglich, während Optionen auf zwei Arten konfiguriert werden:

  • Übergeben Sie einen Konfigurationsdelegaten an Configure für OptionsBuilder<TOptions>. OptionsBuilder<TOptions> stellt Überladungen von Configure zur Verfügung, die es ermöglichen, bis zu fünf Dienste zu verwenden, um Optionen zu konfigurieren:

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • Erstellen Sie einen Typ, der IConfigureOptions<TOptions> oder IConfigureNamedOptions<TOptions> implementiert, und registrieren Sie den Typ als Dienst.

Es empfiehlt sich das Übergeben eines Konfigurationsdelegaten an Configure, da das Erstellen eines Diensts etwas komplexer ist. Das Erstellen eines Typs entspricht den Aktionen des Frameworks beim Aufruf von Configure. Beim Aufrufen von Configure wird eine vorübergehende generische IConfigureNamedOptions<TOptions> registriert, die über einen Konstruktor verfügt, der die angegebenen generischen Diensttypen akzeptiert.

Überprüfung von Optionen

Bei der Überprüfung der Optionen können Optionswerte überprüft werden.

Betrachten Sie die folgende appsettings.json -Datei:

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

Die folgende Klasse wird verwendet, um eine Bindung zum "MyConfig"-Konfigurationsabschnitt zu erstellen, und wendet einige DataAnnotations-Regeln an:

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

Der folgende Code

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

Die ValidateDataAnnotations-Erweiterungsmethode wird im NuGet-Paket Microsoft.Extensions.Options.DataAnnotations definiert. Für Web-Apps, die das Microsoft.NET.Sdk.Web SDK verwenden, wird auf dieses Paket implizit über das geteilte Framework verwiesen.

Der folgende Code zeigt die Konfigurationswerte oder die Überprüfungsfehler an:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

Der folgende Code wendet mithilfe eines Delegaten eine komplexere Überprüfungsregel an:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> und IValidatableObject

Die folgende Klasse implementiert IValidateOptions<TOptions>:

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions ermöglicht das Verschieben des Überprüfungscodes aus Program.cs und in eine Klasse.

Mit dem vorangehenden Code wird die Überprüfung in Program.cs aktiviert:

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

Die Optionsüberprüfung unterstützt auch IValidatableObject. So führen Sie die Überprüfung einer Klasse auf Klassenebene innerhalb der Klasse selbst aus:

  • Implementieren Sie die IValidatableObject-Schnittstelle und die Validate-Methode innerhalb der Klasse.
  • Rufen Sie Program.cs in ValidateDataAnnotations auf.

ValidateOnStart

Die Optionsüberprüfung wird zum ersten Mal ausgeführt, wenn eine IOptions<TOptions>-, IOptionsSnapshot<TOptions>- oder IOptionsMonitor<TOptions>-Implementierung erstellt wird. Um die Optionsüberprüfung beim Start der App sofort auszuführen, rufen Sie ValidateOnStart in Program.cs auf:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

Optionen für die Postkonfiguration

Legen Sie die Postkonfiguration mit IPostConfigureOptions<TOptions> fest. Die Postkonfiguration erfolgt, nachdem die gesamte IConfigureOptions<TOptions>-Konfiguration abgeschlossen ist:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure ist nach dem Konfigurieren der benannten Optionen verfügbar:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

Verwenden Sie PostConfigureAll zur Postkonfiguration aller Konfigurationsinstanzen:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Zugriffsoptionen in Program.cs

Um auf IOptions<TOptions> oder IOptionsMonitor<TOptions> in Program.cs zuzugreifen, rufen Sie GetRequiredService für WebApplication.Services auf:

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

Zusätzliche Ressourcen

Von Kirk Larkin und Rick Anderson

Das Optionsmuster verwendet Klassen, um stark typisierten Zugriff auf zusammengehörige Einstellungsgruppen zu ermöglichen. Wenn Konfigurationseinstellungen nach Szenario in separate Klassen isoliert werden, entspricht die Anwendung zwei wichtigen Prinzipien der Softwareentwicklung:

  • Kapselung:
    • Klassen, die von Konfigurationseinstellungen abhängen, sind nur von den Konfigurationseinstellungen abhängig, die sie verwenden.
  • Separation of Concerns:
    • Einstellungen für verschiedene App-Komponenten sind nicht voneinander abhängig und nicht aneinander gekoppelt.

Optionen bieten auch einen Mechanismus, um Konfigurationsdaten zu validieren. Weitere Informationen finden Sie im Abschnitt Optionsvalidierung.

Dieser Artikel enthält Informationen zu den Optionsmustern in ASP.NET Core. Informationen zur Verwendung des Optionsmusters in Konsolen-Apps finden Sie unter Optionsmuster in .NET.

Binden von hierarchischen Konfigurationsdaten

Die bevorzugte Methode für das Lesen zugehöriger Konfigurationswerte ist die Verwendung des Optionsmusters. Um z. B. die folgenden Konfigurationswerte zu lesen:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Erstellen Sie die folgende neue PositionOptions-Klasse:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; } = String.Empty;
    public string Name { get; set; } = String.Empty;
}

Eine Optionsklasse:

  • Eine Optionsklasse muss nicht abstrakt sein und über einen öffentlichen parameterlosen Konstruktor verfügen.
  • Alle öffentlichen Lese-/Schreibeigenschaften des Typs sind gebunden.
  • Felder sind nicht gebunden. Im vorangehenden Code ist Position nicht gebunden. Das Feld Position wird verwendet, sodass die Zeichenfolge "Position" nicht in der App hartcodiert werden muss, wenn die Klasse an einen Konfigurationsanbieter gebunden wird.

Der folgende Code

  • Ruft ConfigurationBinder.Bind auf, um die PositionOptions-Klasse an den Position-Abschnitt zu binden.
  • Zeigt die Position-Konfigurationsdaten an.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Im vorangehenden Code werden standardmäßig Änderungen an der JSON-Konfigurationsdatei gelesen, nachdem die App gestartet wurde.

ConfigurationBinder.Get<T> bindet den angegebenen Typ und gibt ihn zurück. ConfigurationBinder.Get<T> ist möglicherweise praktischer als die Verwendung von ConfigurationBinder.Bind. Der folgende Code zeigt die Verwendung von ConfigurationBinder.Get<T> mit der PositionOptions-Klasse:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions? positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Im vorangehenden Code werden standardmäßig Änderungen an der JSON-Konfigurationsdatei gelesen, nachdem die App gestartet wurde.

Eine alternative Methode beim Verwenden des Optionsmusters besteht darin, den Abschnitt Position zu binden und zum Dienstcontainer für die Abhängigkeitsinjektion hinzuzufügen. Im folgenden Code wird PositionOptions mit Configure zum Dienstcontainer hinzugefügt und an die Konfiguration gebunden:

using ConfigSample.Options;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<PositionOptions>(
    builder.Configuration.GetSection(PositionOptions.Position));

var app = builder.Build();

Mithilfe des vorangehenden Codes liest der folgende Code die Positionsoptionen:

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

Im vorangehenden Code werden Änderungen an der JSON-Konfigurationsdatei nach dem Start der App nicht gelesen. Verwenden Sie IOptionsSnapshot, um Änderungen lesen zu können, nachdem die App gestartet wurde.

Optionenschnittstellen

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Mit Postkonfigurationsszenarios können Sie Optionen festlegen oder ändern, nachdem alle IConfigureOptions<TOptions>-Konfigurationen durchgeführt wurden.

IOptionsFactory<TOptions> ist für das Erstellen neuer Optionsinstanzen zuständig. Es verfügt über eine einzelne Create-Methode. Die Standardimplementierung akzeptiert alle registrierten IConfigureOptions<TOptions> und IPostConfigureOptions<TOptions> und führt alle Konfigurationen zuerst und die Postkonfigurationen danach aus. Es wird zwischen IConfigureNamedOptions<TOptions> und IConfigureOptions<TOptions> unterschieden, und es werden nur die entsprechenden Schnittstellen aufgerufen.

IOptionsMonitorCache<TOptions> wird von IOptionsMonitor<TOptions> zum Zwischenspeichern der TOptions-Instanzen verwendet. IOptionsMonitorCache<TOptions> erklärt Optionsinstanzen im Monitor für ungültig, sodass der Wert neu berechnet wird (TryRemove). Werte können manuell mit TryAdd eingeführt werden. Die Clear-Methode wird verwendet, wenn alle benannten Instanzen bei Bedarf neu erstellt werden sollen.

Verwenden von IOptionsSnapshot zum Lesen aktualisierter Daten

Verwenden von IOptionsSnapshot<TOptions>:

  • Optionen werden einmal pro Anforderung berechnet. Dies geschieht, wenn auf sie zugegriffen wird und sie für die Dauer der Anforderung zwischengespeichert werden.
  • Kann einen erheblichen Leistungsnachteil mit sich, da es sich um einen bereichsbezogenen Dienst handelt und pro Anforderung eine neue Berechnung durchgeführt wird. Weitere Informationen finden Sie in diesem GitHub-Problem und unter Improve the performance of configuration binding (Verbessern der Leistung der Konfigurationsbindung).
  • Änderungen an der Konfiguration werden gelesen, nachdem die App gestartet wurde, wenn Konfigurationsanbieter verwendet werden, die das Lesen aktualisierter Konfigurationswerte unterstützen.

Der Unterschied zwischen IOptionsMonitor und IOptionsSnapshot ist folgender:

  • IOptionsMonitor ist ein Singleton-Dienst, der zu jeder Zeit aktuelle Optionswerte empfängt, was insbesondere bei Singleton-Abhängigkeiten nützlich ist.
  • IOptionsSnapshot ist ein bereichsbezogener Dienst und bietet eine Momentaufnahme der Optionen zu dem Zeitpunkt, an dem das IOptionsSnapshot<T>-Objekt konstruiert wird. Momentaufnahmen von Optionen sind für die Verwendung mit vorübergehenden und bereichsbezogenen Abhängigkeiten bestimmt.

Der folgende Code verwendet IOptionsSnapshot<TOptions>.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

Der folgende Code registriert eine Konfigurationsinstanz, mit der MyOptions eine Bindung herstellt:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

Im vorangehenden Code werden Änderungen an der JSKonfigurationsdateiON-Konfigurationsdatei nach dem Start der App gelesen.

IOptionsMonitor

Der folgende Code registriert eine Konfigurationsinstanz, mit der MyOptions eine Bindung herstellt.

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<MyOptions>(
    builder.Configuration.GetSection("MyOptions"));

var app = builder.Build();

Im folgenden Beispiel wird IOptionsMonitor<TOptions> verwendet:

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

Im vorangehenden Code werden standardmäßig Änderungen an der JSON-Konfigurationsdatei gelesen, nachdem die App gestartet wurde.

Unterstützung für benannte Optionen mit IConfigureNamedOptions

Benannte Optionen...

  • sind nützlich, wenn mehrere Konfigurationsabschnitte an die gleichen Eigenschaften gebunden werden.
  • unterscheiden zwischen Groß-/Kleinschreibung.

Betrachten Sie die folgende appsettings.json -Datei:

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

Anstatt zwei Klassen für die Bindung von TopItem:Month und TopItem:Year zu erstellen, wird folgende Klasse für jeden Abschnitt verwendet:

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
}

Der folgende Code dient zum Konfigurieren der benannten Optionen:

using SampleApp.Models;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

var app = builder.Build();

Der folgende Code zeigt die benannten Optionen an:

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

Alle Optionen sind benannte Instanzen. IConfigureOptions<TOptions>-Instanzen werden behandelt, als würden sie die Options.DefaultName-Instanz anzielen. Diese ist string.Empty. IConfigureNamedOptions<TOptions> implementiert auch IConfigureOptions<TOptions>. Die standardmäßige Implementierung von IOptionsFactory<TOptions> verfügt über eine Logik, um jede einzelne entsprechend zu verwenden. Die benannte Option null wird verwendet, um auf alle benannten Instanzen anstelle einer bestimmten benannten Instanz abzuzielen. ConfigureAll und PostConfigureAll verwenden diese Konvention.

OptionsBuilder-API

OptionsBuilder<TOptions> dient zum Konfigurieren von TOptions-Instanzen. OptionsBuilder optimiert die Erstellung von benannten Optionen, da es sich nur um einen einzelnen Parameter für den ersten AddOptions<TOptions>(string optionsName)-Aufruf handelt statt um alle nachfolgenden Aufrufe. Die Optionsvalidierung und die ConfigureOptions-Überladungen, die Dienstabhängigkeiten akzeptieren, sind nur über OptionsBuilder verfügbar.

OptionsBuilder wird im Abschnitt Überprüfung von Optionen verwendet.

Informationen zum Hinzufügen eines benutzerdefinierten Repositorys finden Sie unter Verwenden von AddOptions zum Konfigurieren eines benutzerdefinierten Repositorys.

Verwenden von DI-Diensten zum Konfigurieren von Optionen

Der Zugriff auf Dienste ist über die Dependency Injection möglich, während Optionen auf zwei Arten konfiguriert werden:

  • Übergeben Sie einen Konfigurationsdelegaten an Configure für OptionsBuilder<TOptions>. OptionsBuilder<TOptions> stellt Überladungen von Configure zur Verfügung, die es ermöglichen, bis zu fünf Dienste zu verwenden, um Optionen zu konfigurieren:

    builder.Services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • Erstellen Sie einen Typ, der IConfigureOptions<TOptions> oder IConfigureNamedOptions<TOptions> implementiert, und registrieren Sie den Typ als Dienst.

Es empfiehlt sich das Übergeben eines Konfigurationsdelegaten an Configure, da das Erstellen eines Diensts etwas komplexer ist. Das Erstellen eines Typs entspricht den Aktionen des Frameworks beim Aufruf von Configure. Beim Aufrufen von Configure wird eine vorübergehende generische IConfigureNamedOptions<TOptions> registriert, die über einen Konstruktor verfügt, der die angegebenen generischen Diensttypen akzeptiert.

Überprüfung von Optionen

Bei der Überprüfung der Optionen können Optionswerte überprüft werden.

Betrachten Sie die folgende appsettings.json -Datei:

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

Die folgende Klasse wird verwendet, um eine Bindung zum "MyConfig"-Konfigurationsabschnitt zu erstellen, und wendet einige DataAnnotations-Regeln an:

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

Der folgende Code

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

var app = builder.Build();

Die ValidateDataAnnotations-Erweiterungsmethode wird im NuGet-Paket Microsoft.Extensions.Options.DataAnnotations definiert. Für Web-Apps, die das Microsoft.NET.Sdk.Web SDK verwenden, wird auf dieses Paket implizit über das geteilte Framework verwiesen.

Der folgende Code zeigt die Konfigurationswerte oder die Überprüfungsfehler an:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
            msg = $"Key1: {_config.Value.Key1} \n" +
                  $"Key2: {_config.Value.Key2} \n" +
                  $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

Der folgende Code wendet mithilfe eines Delegaten eine komplexere Überprüfungsregel an:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
            .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

var app = builder.Build();

IValidateOptions<TOptions> und IValidatableObject

Die folgende Klasse implementiert IValidateOptions<TOptions>:

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string? vor = null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1!);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions ermöglicht das Verschieben des Überprüfungscodes aus Program.cs und in eine Klasse.

Mit dem vorangehenden Code wird die Überprüfung in Program.cs aktiviert:

using Microsoft.Extensions.Options;
using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.Configure<MyConfigOptions>(builder.Configuration.GetSection(
                                        MyConfigOptions.MyConfig));

builder.Services.AddSingleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>();

var app = builder.Build();

Die Optionsüberprüfung unterstützt auch IValidatableObject. So führen Sie die Überprüfung einer Klasse auf Klassenebene innerhalb der Klasse selbst aus:

  • Implementieren Sie die IValidatableObject-Schnittstelle und die Validate-Methode innerhalb der Klasse.
  • Rufen Sie Program.cs in ValidateDataAnnotations auf.

ValidateOnStart

Die Optionsüberprüfung wird zum ersten Mal ausgeführt, wenn eine IOptions<TOptions>-, IOptionsSnapshot<TOptions>- oder IOptionsMonitor<TOptions>-Implementierung erstellt wird. Um die Optionsüberprüfung beim Start der App sofort auszuführen, rufen Sie ValidateOnStart in Program.cs auf:

builder.Services.AddOptions<MyConfigOptions>()
    .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig))
    .ValidateDataAnnotations()
    .ValidateOnStart();

Optionen für die Postkonfiguration

Legen Sie die Postkonfiguration mit IPostConfigureOptions<TOptions> fest. Die Postkonfiguration erfolgt, nachdem die gesamte IConfigureOptions<TOptions>-Konfiguration abgeschlossen ist:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigure<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

PostConfigure ist nach dem Konfigurieren der benannten Optionen verfügbar:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.Configure<TopItemSettings>(TopItemSettings.Month,
    builder.Configuration.GetSection("TopItem:Month"));
builder.Services.Configure<TopItemSettings>(TopItemSettings.Year,
    builder.Configuration.GetSection("TopItem:Year"));

builder.Services.PostConfigure<TopItemSettings>("Month", myOptions =>
{
    myOptions.Name = "post_configured_name_value";
    myOptions.Model = "post_configured_model_value";
});

var app = builder.Build();

Verwenden Sie PostConfigureAll zur Postkonfiguration aller Konfigurationsinstanzen:

using OptionsValidationSample.Configuration;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

builder.Services.AddOptions<MyConfigOptions>()
                .Bind(builder.Configuration.GetSection(MyConfigOptions.MyConfig));

builder.Services.PostConfigureAll<MyConfigOptions>(myOptions =>
{
    myOptions.Key1 = "post_configured_key1_value";
});

Zugriffsoptionen in Program.cs

Um auf IOptions<TOptions> oder IOptionsMonitor<TOptions> in Program.cs zuzugreifen, rufen Sie GetRequiredService für WebApplication.Services auf:

var app = builder.Build();

var option1 = app.Services.GetRequiredService<IOptionsMonitor<MyOptions>>()
    .CurrentValue.Option1;

Zusätzliche Ressourcen

Von Kirk Larkin und Rick Anderson

Das Optionsmuster verwendet Klassen, um stark typisierten Zugriff auf zusammengehörige Einstellungsgruppen zu ermöglichen. Wenn Konfigurationseinstellungen nach Szenario in separate Klassen isoliert werden, entspricht die Anwendung zwei wichtigen Prinzipien der Softwareentwicklung:

  • Kapselung:
    • Klassen, die von Konfigurationseinstellungen abhängen, sind nur von den Konfigurationseinstellungen abhängig, die sie verwenden.
  • Separation of Concerns:
    • Einstellungen für verschiedene App-Komponenten sind nicht voneinander abhängig und nicht aneinander gekoppelt.

Optionen bieten auch einen Mechanismus, um Konfigurationsdaten zu validieren. Weitere Informationen finden Sie im Abschnitt Optionsvalidierung.

Dieses Thema enthält Informationen zu den Optionsmustern in ASP.NET Core. Informationen zur Verwendung des Optionsmusters in Konsolen-Apps finden Sie unter Optionsmuster in .NET.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Binden von hierarchischen Konfigurationsdaten

Die bevorzugte Methode für das Lesen zugehöriger Konfigurationswerte ist die Verwendung des Optionsmusters. Um z. B. die folgenden Konfigurationswerte zu lesen:

  "Position": {
    "Title": "Editor",
    "Name": "Joe Smith"
  }

Erstellen Sie die folgende neue PositionOptions-Klasse:

public class PositionOptions
{
    public const string Position = "Position";

    public string Title { get; set; }
    public string Name { get; set; }
}

Eine Optionsklasse:

  • Eine Optionsklasse muss nicht abstrakt sein und über einen öffentlichen parameterlosen Konstruktor verfügen.
  • Alle öffentlichen Lese-/Schreibeigenschaften des Typs sind gebunden.
  • Felder sind nicht gebunden. Im vorangehenden Code ist Position nicht gebunden. Die Position-Eigenschaft wird verwendet, sodass die Zeichenfolge "Position" nicht in der App hartcodiert werden muss, wenn die Klasse an einen Konfigurationsanbieter gebunden wird.

Der folgende Code

  • Ruft ConfigurationBinder.Bind auf, um die PositionOptions-Klasse an den Position-Abschnitt zu binden.
  • Zeigt die Position-Konfigurationsdaten an.
public class Test22Model : PageModel
{
    private readonly IConfiguration Configuration;

    public Test22Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {
        var positionOptions = new PositionOptions();
        Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Im vorangehenden Code werden standardmäßig Änderungen an der JSON-Konfigurationsdatei gelesen, nachdem die App gestartet wurde.

ConfigurationBinder.Get<T> bindet den angegebenen Typ und gibt ihn zurück. ConfigurationBinder.Get<T> ist möglicherweise praktischer als die Verwendung von ConfigurationBinder.Bind. Der folgende Code zeigt die Verwendung von ConfigurationBinder.Get<T> mit der PositionOptions-Klasse:

public class Test21Model : PageModel
{
    private readonly IConfiguration Configuration;
    public PositionOptions positionOptions { get; private set; }

    public Test21Model(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public ContentResult OnGet()
    {            
        positionOptions = Configuration.GetSection(PositionOptions.Position)
                                                     .Get<PositionOptions>();

        return Content($"Title: {positionOptions.Title} \n" +
                       $"Name: {positionOptions.Name}");
    }
}

Im vorangehenden Code werden standardmäßig Änderungen an der JSON-Konfigurationsdatei gelesen, nachdem die App gestartet wurde.

Eine alternative Methode beim Verwenden des Optionsmusters besteht darin, den Abschnitt Position zu binden und zum Dienstcontainer für die Abhängigkeitsinjektion hinzuzufügen. Im folgenden Code wird PositionOptions mit Configure zum Dienstcontainer hinzugefügt und an die Konfiguration gebunden:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(Configuration.GetSection(
                                        PositionOptions.Position));
    services.AddRazorPages();
}

Mithilfe des vorangehenden Codes liest der folgende Code die Positionsoptionen:

public class Test2Model : PageModel
{
    private readonly PositionOptions _options;

    public Test2Model(IOptions<PositionOptions> options)
    {
        _options = options.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Title: {_options.Title} \n" +
                       $"Name: {_options.Name}");
    }
}

Im vorangehenden Code werden Änderungen an der JSON-Konfigurationsdatei nach dem Start der App nicht gelesen. Verwenden Sie IOptionsSnapshot, um Änderungen lesen zu können, nachdem die App gestartet wurde.

Optionenschnittstellen

IOptions<TOptions>:

IOptionsSnapshot<TOptions>:

IOptionsMonitor<TOptions>:

Mit Postkonfigurationsszenarios können Sie Optionen festlegen oder ändern, nachdem alle IConfigureOptions<TOptions>-Konfigurationen durchgeführt wurden.

IOptionsFactory<TOptions> ist für das Erstellen neuer Optionsinstanzen zuständig. Es verfügt über eine einzelne Create-Methode. Die Standardimplementierung akzeptiert alle registrierten IConfigureOptions<TOptions> und IPostConfigureOptions<TOptions> und führt alle Konfigurationen zuerst und die Postkonfigurationen danach aus. Es wird zwischen IConfigureNamedOptions<TOptions> und IConfigureOptions<TOptions> unterschieden, und es werden nur die entsprechenden Schnittstellen aufgerufen.

IOptionsMonitorCache<TOptions> wird von IOptionsMonitor<TOptions> zum Zwischenspeichern der TOptions-Instanzen verwendet. IOptionsMonitorCache<TOptions> erklärt Optionsinstanzen im Monitor für ungültig, sodass der Wert neu berechnet wird (TryRemove). Werte können manuell mit TryAdd eingeführt werden. Die Clear-Methode wird verwendet, wenn alle benannten Instanzen bei Bedarf neu erstellt werden sollen.

Verwenden von IOptionsSnapshot zum Lesen aktualisierter Daten

Mithilfe von IOptionsSnapshot<TOptions> werden Optionen einmal pro Anforderung berechnet. Dies geschieht, wenn auf sie zugegriffen wird und sie für die Dauer der Anforderung zwischengespeichert werden. Änderungen an der Konfiguration werden gelesen, nachdem die App gestartet wurde, wenn Konfigurationsanbieter verwendet werden, die das Lesen aktualisierter Konfigurationswerte unterstützen.

Der Unterschied zwischen IOptionsMonitor und IOptionsSnapshot ist folgender:

  • IOptionsMonitor ist ein Singleton-Dienst, der zu jeder Zeit aktuelle Optionswerte empfängt, was insbesondere bei Singleton-Abhängigkeiten nützlich ist.
  • IOptionsSnapshot ist ein bereichsbezogener Dienst und bietet eine Momentaufnahme der Optionen zu dem Zeitpunkt, an dem das IOptionsSnapshot<T>-Objekt konstruiert wird. Momentaufnahmen von Optionen sind für die Verwendung mit vorübergehenden und bereichsbezogenen Abhängigkeiten bestimmt.

Der folgende Code verwendet IOptionsSnapshot<TOptions>.

public class TestSnapModel : PageModel
{
    private readonly MyOptions _snapshotOptions;

    public TestSnapModel(IOptionsSnapshot<MyOptions> snapshotOptionsAccessor)
    {
        _snapshotOptions = snapshotOptionsAccessor.Value;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_snapshotOptions.Option1} \n" +
                       $"Option2: {_snapshotOptions.Option2}");
    }
}

Der folgende Code registriert eine Konfigurationsinstanz, mit der MyOptions eine Bindung herstellt:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

Im vorangehenden Code werden Änderungen an der JSKonfigurationsdateiON-Konfigurationsdatei nach dem Start der App gelesen.

IOptionsMonitor

Der folgende Code registriert eine Konfigurationsinstanz, mit der MyOptions eine Bindung herstellt.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyOptions>(Configuration.GetSection("MyOptions"));

    services.AddRazorPages();
}

Im folgenden Beispiel wird IOptionsMonitor<TOptions> verwendet:

public class TestMonitorModel : PageModel
{
    private readonly IOptionsMonitor<MyOptions> _optionsDelegate;

    public TestMonitorModel(IOptionsMonitor<MyOptions> optionsDelegate )
    {
        _optionsDelegate = optionsDelegate;
    }

    public ContentResult OnGet()
    {
        return Content($"Option1: {_optionsDelegate.CurrentValue.Option1} \n" +
                       $"Option2: {_optionsDelegate.CurrentValue.Option2}");
    }
}

Im vorangehenden Code werden standardmäßig Änderungen an der JSON-Konfigurationsdatei gelesen, nachdem die App gestartet wurde.

Unterstützung für benannte Optionen mit IConfigureNamedOptions

Benannte Optionen...

  • sind nützlich, wenn mehrere Konfigurationsabschnitte an die gleichen Eigenschaften gebunden werden.
  • unterscheiden zwischen Groß-/Kleinschreibung.

Betrachten Sie die folgende appsettings.json -Datei:

{
  "TopItem": {
    "Month": {
      "Name": "Green Widget",
      "Model": "GW46"
    },
    "Year": {
      "Name": "Orange Gadget",
      "Model": "OG35"
    }
  }
}

Anstatt zwei Klassen für die Bindung von TopItem:Month und TopItem:Year zu erstellen, wird folgende Klasse für jeden Abschnitt verwendet:

public class TopItemSettings
{
    public const string Month = "Month";
    public const string Year = "Year";

    public string Name { get; set; }
    public string Model { get; set; }
}

Der folgende Code dient zum Konfigurieren der benannten Optionen:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<TopItemSettings>(TopItemSettings.Month,
                                       Configuration.GetSection("TopItem:Month"));
    services.Configure<TopItemSettings>(TopItemSettings.Year,
                                        Configuration.GetSection("TopItem:Year"));

    services.AddRazorPages();
}

Der folgende Code zeigt die benannten Optionen an:

public class TestNOModel : PageModel
{
    private readonly TopItemSettings _monthTopItem;
    private readonly TopItemSettings _yearTopItem;

    public TestNOModel(IOptionsSnapshot<TopItemSettings> namedOptionsAccessor)
    {
        _monthTopItem = namedOptionsAccessor.Get(TopItemSettings.Month);
        _yearTopItem = namedOptionsAccessor.Get(TopItemSettings.Year);
    }

    public ContentResult OnGet()
    {
        return Content($"Month:Name {_monthTopItem.Name} \n" +
                       $"Month:Model {_monthTopItem.Model} \n\n" +
                       $"Year:Name {_yearTopItem.Name} \n" +
                       $"Year:Model {_yearTopItem.Model} \n"   );
    }
}

Alle Optionen sind benannte Instanzen. IConfigureOptions<TOptions>-Instanzen werden behandelt, als würden sie die Options.DefaultName-Instanz anzielen. Diese ist string.Empty. IConfigureNamedOptions<TOptions> implementiert auch IConfigureOptions<TOptions>. Die standardmäßige Implementierung von IOptionsFactory<TOptions> verfügt über eine Logik, um jede einzelne entsprechend zu verwenden. Die benannte Option null wird verwendet, um auf alle benannten Instanzen anstelle einer bestimmten benannten Instanz abzuzielen. ConfigureAll und PostConfigureAll verwenden diese Konvention.

OptionsBuilder-API

OptionsBuilder<TOptions> dient zum Konfigurieren von TOptions-Instanzen. OptionsBuilder optimiert die Erstellung von benannten Optionen, da es sich nur um einen einzelnen Parameter für den ersten AddOptions<TOptions>(string optionsName)-Aufruf handelt statt um alle nachfolgenden Aufrufe. Die Optionsvalidierung und die ConfigureOptions-Überladungen, die Dienstabhängigkeiten akzeptieren, sind nur über OptionsBuilder verfügbar.

OptionsBuilder wird im Abschnitt Überprüfung von Optionen verwendet.

Informationen zum Hinzufügen eines benutzerdefinierten Repositorys finden Sie unter Verwenden von AddOptions zum Konfigurieren eines benutzerdefinierten Repositorys.

Verwenden von DI-Diensten zum Konfigurieren von Optionen

Der Zugriff auf Dienste ist über die Dependency Injection möglich, während Optionen auf zwei Arten konfiguriert werden:

  • Übergeben Sie einen Konfigurationsdelegaten an Configure für OptionsBuilder<TOptions>. OptionsBuilder<TOptions> stellt Überladungen von Configure zur Verfügung, die es ermöglichen, bis zu fünf Dienste zu verwenden, um Optionen zu konfigurieren:

    services.AddOptions<MyOptions>("optionalName")
        .Configure<Service1, Service2, Service3, Service4, Service5>(
            (o, s, s2, s3, s4, s5) => 
                o.Property = DoSomethingWith(s, s2, s3, s4, s5));
    
  • Erstellen Sie einen Typ, der IConfigureOptions<TOptions> oder IConfigureNamedOptions<TOptions> implementiert, und registrieren Sie den Typ als Dienst.

Es empfiehlt sich das Übergeben eines Konfigurationsdelegaten an Configure, da das Erstellen eines Diensts etwas komplexer ist. Das Erstellen eines Typs entspricht den Aktionen des Frameworks beim Aufruf von Configure. Beim Aufrufen von Configure wird eine vorübergehende generische IConfigureNamedOptions<TOptions> registriert, die über einen Konstruktor verfügt, der die angegebenen generischen Diensttypen akzeptiert.

Überprüfung von Optionen

Bei der Überprüfung der Optionen können Optionswerte überprüft werden.

Betrachten Sie die folgende appsettings.json -Datei:

{
  "MyConfig": {
    "Key1": "My Key One",
    "Key2": 10,
    "Key3": 32
  }
}

Die folgende Klasse erstellt eine Bindung zum "MyConfig"-Konfigurationsabschnitt und wendet einige DataAnnotations-Regeln an:

public class MyConfigOptions
{
    public const string MyConfig = "MyConfig";

    [RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
    public string Key1 { get; set; }
    [Range(0, 1000,
        ErrorMessage = "Value for {0} must be between {1} and {2}.")]
    public int Key2 { get; set; }
    public int Key3 { get; set; }
}

Der folgende Code

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddOptions<MyConfigOptions>()
            .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
            .ValidateDataAnnotations();

        services.AddControllersWithViews();
    }

Die ValidateDataAnnotations-Erweiterungsmethode wird im NuGet-Paket Microsoft.Extensions.Options.DataAnnotations definiert. Für Web-Apps, die das Microsoft.NET.Sdk.Web SDK verwenden, wird auf dieses Paket implizit über das geteilte Framework verwiesen.

Der folgende Code zeigt die Konfigurationswerte oder die Überprüfungsfehler an:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly IOptions<MyConfigOptions> _config;

    public HomeController(IOptions<MyConfigOptions> config,
                          ILogger<HomeController> logger)
    {
        _config = config;
        _logger = logger;

        try
        {
            var configValue = _config.Value;

        }
        catch (OptionsValidationException ex)
        {
            foreach (var failure in ex.Failures)
            {
                _logger.LogError(failure);
            }
        }
    }

    public ContentResult Index()
    {
        string msg;
        try
        {
             msg = $"Key1: {_config.Value.Key1} \n" +
                   $"Key2: {_config.Value.Key2} \n" +
                   $"Key3: {_config.Value.Key3}";
        }
        catch (OptionsValidationException optValEx)
        {
            return Content(optValEx.Message);
        }
        return Content(msg);
    }

Der folgende Code wendet mithilfe eines Delegaten eine komplexere Überprüfungsregel an:

public void ConfigureServices(IServiceCollection services)
{
    services.AddOptions<MyConfigOptions>()
        .Bind(Configuration.GetSection(MyConfigOptions.MyConfig))
        .ValidateDataAnnotations()
        .Validate(config =>
        {
            if (config.Key2 != 0)
            {
                return config.Key3 > config.Key2;
            }

            return true;
        }, "Key3 must be > than Key2.");   // Failure message.

    services.AddControllersWithViews();
}

IValidateOptions für die komplexe Überprüfung

Die folgende Klasse implementiert IValidateOptions<TOptions>:

public class MyConfigValidation : IValidateOptions<MyConfigOptions>
{
    public MyConfigOptions _config { get; private set; }

    public  MyConfigValidation(IConfiguration config)
    {
        _config = config.GetSection(MyConfigOptions.MyConfig)
            .Get<MyConfigOptions>();
    }

    public ValidateOptionsResult Validate(string name, MyConfigOptions options)
    {
        string vor=null;
        var rx = new Regex(@"^[a-zA-Z''-'\s]{1,40}$");
        var match = rx.Match(options.Key1);

        if (string.IsNullOrEmpty(match.Value))
        {
            vor = $"{options.Key1} doesn't match RegEx \n";
        }

        if ( options.Key2 < 0 || options.Key2 > 1000)
        {
            vor = $"{options.Key2} doesn't match Range 0 - 1000 \n";
        }

        if (_config.Key2 != default)
        {
            if(_config.Key3 <= _config.Key2)
            {
                vor +=  "Key3 must be > than Key2.";
            }
        }

        if (vor != null)
        {
            return ValidateOptionsResult.Fail(vor);
        }

        return ValidateOptionsResult.Success;
    }
}

IValidateOptions ermöglicht das Verschieben des Überprüfungscodes aus StartUp und in eine Klasse.

Mit dem vorangehenden Code wird die Überprüfung in Startup.ConfigureServices aktiviert:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<MyConfigOptions>(Configuration.GetSection(
                                        MyConfigOptions.MyConfig));
    services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions
                              <MyConfigOptions>, MyConfigValidation>());
    services.AddControllersWithViews();
}

Optionen für die Postkonfiguration

Legen Sie die Postkonfiguration mit IPostConfigureOptions<TOptions> fest. Die Postkonfiguration erfolgt, nachdem die gesamte IConfigureOptions<TOptions>-Konfiguration abgeschlossen ist:

services.PostConfigure<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

PostConfigure ist nach dem Konfigurieren der benannten Optionen verfügbar:

services.PostConfigure<MyOptions>("named_options_1", myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

Verwenden Sie PostConfigureAll zur Postkonfiguration aller Konfigurationsinstanzen:

services.PostConfigureAll<MyOptions>(myOptions =>
{
    myOptions.Option1 = "post_configured_option1_value";
});

Zugreifen auf Optionen während des Starts

IOptions<TOptions> und IOptionsMonitor<TOptions> können in Startup.Configure verwendet werden, da Dienste erstellt werden, bevor die Configure-Methode ausgeführt wird.

public void Configure(IApplicationBuilder app, 
    IOptionsMonitor<MyOptions> optionsAccessor)
{
    var option1 = optionsAccessor.CurrentValue.Option1;
}

Verwenden Sie IOptions<TOptions> oder IOptionsMonitor<TOptions> nicht in Startup.ConfigureServices. Es kann einen inkonsistenten Optionszustand geben. Dies liegt an der Reihenfolge der Dienstregistrierungen.

NuGet-Paket Options.ConfigurationExtensions

Auf das Paket Microsoft.Extensions.Options.ConfigurationExtensions wird implizit in ASP.NET Core-Apps verwiesen.