.NET • .NET Core • Microsoft Extensions

Options (3) – Dynamisch berechnet

29. November 2022

Für klassische ASP.NET WebForms oder ASP.NET Core Controller sind dynamisch berechnete Options die erste Wahl.

Mit deren Beschreibung endet die Serie zu Microsoft Options. Der erste Beitrag hat die häufigsten Anwendungsfälle beschrieben, der zweite Beitrag Options mit mehreren Instanzen.

autor Alexander Jung

Alexander Jung

Chief eXpert Alexander.Jung@sdx-ag.de

Dynamisch berechnete Options

Im WCF-Beispiel des letzten Beitrags sind die Services während der Registrierung bekannt und man kann die Options entsprechend registrieren.

Es gibt jedoch auch Fälle, in denen die Klassen während der Registrierung nicht bekannt sind (oder es ist unüblich, sie einzeln anzumelden). Klassisches ASP.NET WebForms ist so ein Beispiel, ebenso ASP.NET Core Controller.

Möchte ich etwa zu Controllern Konfigurationswerte als Options hinterlegen, dann spricht natürlich nichts gegen explizite Aufrufe. Ein (etwas konstruiertes) Beispiel: Ich möchte Versions-Header einstellen:

services.Configure<ControllerOptions>(null, o => o.VersionHeader = "1.1"); );
services.Configure<ControllerOptions>(typeof(WeatherForecastController).FullName, o => o.VersionHeader = "2.0");

Der Weg über ein Lambda funktioniert, weil es damit eine entsprechende Registrierung der Options-Klassen gibt.

Wenn ich den Eintrag aber lediglich in der Konfiguration festlegen will, sieht das anders aus:

"controller": {
    "default": {
        "VersionHeader": "1.1"
    },
    "SDX.Flurfunk.AspNetCore.Controllers.WeatherForecastController": {
        "VersionHeader": "2.0"
    }
}

Wenn jetzt keine entsprechenden Registrierungsaufrufe kommen, dann weiß die Anwendung nichts davon, dass es Konfigurationseinträge für bestimmte Controller gibt. (Allenfalls der Standardwert ist einfach zu registrieren.) Und jeden Controller einzeln anzumelden, ist kaum im Sinne des Erfinders.

In diesem Fall könnte man die Controller per Reflection suchen oder die Konfiguration auslesen und daraufhin die entsprechenden Options registrieren. Aber diese Wege sind spezifisch für den jeweiligen Fall, evtl. mit Aufwand verbunden oder nicht immer wasserdicht.

Es gibt jedoch einen Weg, die Werte von Options zur Laufzeit zu berechnen.

IConfigureNamedOptions

Die Lösung besteht darin, eine passende Implementierung von IConfigureNamedOptions<> bereitzustellen. Diese kann neben den anderen Configure<>()-Aufrufen registriert werden und klinkt sich dadurch in den schon bekannten Erzeugungsprozess für Options ein.

Der folgende Code zeigt eine Implementierung, die dies nutzt, um die Options nachträglich aus der Konfiguration zu befüllen:

internal class ConfigureControllerOptions : IConfigureNamedOptions<ControllerOptions>
{
    private readonly IConfiguration _configuration;

    public ConfigureControllerOptions(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public void Configure(string name, ControllerOptions options)
    {
        // 1
        _configuration
            .GetSection(ControllerOptions.ParentSection)
            .GetSection(name)
            .Bind(options);
    }

    public void Configure(ControllerOptions options) { }
}

Die Implementierung hier nutzt die Methode Bind(), um die Werte der Konfiguration in die Options zu übertragen (1) – das ist der gleiche Mechanismus, der unter der Haube auch von den bisher angesprochenen Wegen genutzt wird.

Nicht unbedingt offensichtlich ist die Wahl der Schnittstelle und die Registrierung: IConfigureNamedOptions<> leitet von IConfigureOptions<> ab. Implementiert man IConfigureNamedOptions<> wird nur die erste Configure()-Methode aufgerufen (ggf. mit string.Empty als name), die zweite Methode wird hingegen nie aufgerufen. Man kann auch IConfigureOptions<> implementieren (hier kommt die zweite Methode zum Zug), damit deckt man aber nur den einfachen Fall von unbenannten Options ab.

Die Registrierung muss aber unabhängig von der implementierten Schnittstelle für IConfigureOptions<> erfolgen.

Hintergrund ist, dass beim Erzeugungsprozess die IConfigureOptions<> über Dependency Injection aufgelöst werden, es findet keine Unterscheidung der beiden Fälle statt. Das ist gut, denn damit bleibt die Reihenfolge der Registrierung erhalten. Erst beim Aufruf findet anhand des tatsächlichen Typs eine Fallunterscheidung statt. (link)

Alles zusammengenommen haben wir damit folgende Registrierungsmethode, die uns Options für alle Controller zur Verfügung stellt:

public static IServiceCollection AddControllerOptions(this IServiceCollection services, IConfiguration configuration)
{
    // 1
    var parentSection = configuration.GetSection(ControllerOptions.ParentSection);
    services.Configure<ControllerOptions>(null, parentSection.GetSection(ControllerOptions.DefaultSection));

    // 2
    services.AddSingleton<IConfigureOptions<ControllerOptions>, ConfigureControllerOptions>();
    return services;
}

Zusätzlich würde man noch Erweiterungsmethoden für Lambdas bereitstellen, sowohl für einzelne Controller, als auch für die Standardwerte.

Mit (1) werden die Standardwerte aus der Konfiguration gesetzt – kein Grund hier einen anderen Weg zu suchen – und mit (2) wird unsere eigene Klasse bereitgestellt.

Verwendung

Für die Verwendung würde man im konkreten Beispiel sicher über ASP.NET Core Filter oder ähnliches arbeiten; hier nur die vereinfachte Variante, um den Umgang mit den Options zu zeigen:

public HomeController(ILogger<HomeController> logger, IOptionsMonitor<ControllerOptions> optionsMonitor)
{
    _logger = logger;
    _optionsMonitor = optionsMonitor;
}

[HttpGet("Ping")]
public string Ping()
{
    AddVersionHeader();
    return "Ping " + DateTime.Now;
}

private void AddVersionHeader()
{
    var name = GetType().FullName;
    // 1
    var o = _optionsMonitor.Get(name);
    Response.Headers.Add("x-sdx-version", o.VersionHeader);
}

(1) zeigt den Aufruf über IOptionsMonitor<>. Keine Überraschung, die Nutzung unterscheidet sich nicht von den bisher betrachteten Fällen. Lediglich der Ablauf hinter den Kulissen unterschiedet sich dadurch, dass während des Aufbaus der Werte unsere ConfigureControllerOptions-Klasse aufgerufen wird. Und damit werden erst zu diesem Zeitpunkt – wenn der Name bekannt ist und als Section verwendet werden kann – die Werte aus der Konfiguration gelesen und auf die Options abgebildet. Ziel erreicht.

Fazit zu Microsoft Extensions Options

Das Angebot der Microsoft Extensions Options ist breit:

  • Einfache typisierte Sections, benannte Instanzen oder dynamisch berechnete Werte.
  • Überlagerung der Werte aus unterschiedlichen Quellen.
  • Konfigurationsdateien in der Anwendung oder programmatisch gesetzte Werte in UnitTests.

Options gehen dabei in allen Belangen deutlich über System.Configuration und die dort vorhandene Bindung an genau eine .config-Datei hinaus. Und sie sind dabei durch die Verwendung einfacher POCOs auch noch einfacher zu verwenden.

Insgesamt zeigt das, wie durchdacht, flexibel und mächtig Microsoft Extensions Options ist.

Alles was man dafür tun muss: Sich etwas mit den Konzepten auseinandersetzen und ein paar Konventionen einhalten.