Implementieren eines benutzerdefinierten Konfigurationsanbieters in .NET

Es stehen zahlreiche Konfigurationsanbieter für allgemeine Konfigurationsquellen zur Verfügung, z. B. JSON-, XML- und INI-Dateien. Sie sollten einen benutzerdefinierten Konfigurationsanbieter implementieren, wenn keiner der verfügbaren Anbieter die Anforderungen Ihrer Anwendung erfüllt. In diesem Artikel erfahren Sie, wie Sie einen benutzerdefinierten Konfigurationsanbieter implementieren, der eine Datenbank als Konfigurationsquelle verwendet.

Benutzerdefinierter Konfigurationsanbieter

Die Beispiel-App veranschaulicht, wie ein Standardkonfigurationsanbieter erstellt wird, der Schlüssel-Wert-Paare für die Konfiguration über Entity Framework (EF) Core aus einer Datenbank liest.

Der Anbieter weist die folgenden Merkmale auf:

  • Die EF-In-Memory-Datenbank wird zu Demonstrationszwecken verwendet.
    • Um eine Datenbank zu verwenden, die eine Verbindungszeichenfolge erfordert, rufen Sie eine Verbindungszeichenfolge aus einer Zwischenkonfiguration ab.
  • Der Anbieter liest eine Datenbanktabelle beim Start in die Konfiguration. Der Anbieter fragt die Datenbank nicht pro Schlüssel ab.
  • Das erneute Laden bei Änderung ist nicht implementiert. Das heißt, das Aktualisieren der Datenbank nach dem App-Start hat keine Auswirkungen auf die App-Konfiguration.

Definieren Sie eine Entität des Settings-Datensatztyps, um Konfigurationswerte in der Datenbank zu speichern. Beispielsweise können Sie im Ordner Models eine Datei Settings.cs hinzufügen:

namespace CustomProvider.Example.Models;

public record Settings(string Id, string? Value);

Informationen zu Datensatztypen finden Sie unter Datensatztypen in C# 9.

Fügen Sie EntityConfigurationContext hinzu, um die konfigurierten Werte zu speichern und auf diese zugreifen.

Providers/EntityConfigurationContext.cs:

using CustomProvider.Example.Models;
using Microsoft.EntityFrameworkCore;

namespace CustomProvider.Example.Providers;

public sealed class EntityConfigurationContext(string? connectionString) : DbContext
{
    public DbSet<Settings> Settings => Set<Settings>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        _ = connectionString switch
        {
            { Length: > 0 } => optionsBuilder.UseSqlServer(connectionString),
            _ => optionsBuilder.UseInMemoryDatabase("InMemoryDatabase")
        };
    }
}

Durch Außerkraftsetzen von OnConfiguring(DbContextOptionsBuilder) können Sie die entsprechende Datenbankverbindung verwenden. Wenn beispielsweise eine Verbindungszeichenfolge bereitgestellt wurde, können Sie eine Verbindung mit SQL Server herstellen. Andernfalls können Sie auch eine In-Memory-Datenbank nutzen.

Erstellen Sie eine Klasse, die das IConfigurationSource implementiert.

Providers/EntityConfigurationSource.cs:

using Microsoft.Extensions.Configuration;

namespace CustomProvider.Example.Providers;

public sealed class EntityConfigurationSource(
    string? connectionString) : IConfigurationSource
{
    public IConfigurationProvider Build(IConfigurationBuilder builder) =>
        new EntityConfigurationProvider(connectionString);
}

Erstellen Sie den benutzerdefinierten Konfigurationsanbieter durch Vererbung von ConfigurationProvider. Der Konfigurationsanbieter initialisiert die Datenbank, wenn diese leer ist. Da für Konfigurationsschlüssel die Groß-/Kleinschreibung nicht relevant ist, wird das zum Initialisieren der Datenbank verwendete Wörterbuch mit der Vergleichsfunktion ohne Beachtung der Groß-/Kleinschreibung erstellt (StringComparer.OrdinalIgnoreCase).

Providers/EntityConfigurationProvider.cs:

using CustomProvider.Example.Models;
using Microsoft.Extensions.Configuration;

namespace CustomProvider.Example.Providers;

public sealed class EntityConfigurationProvider(
    string? connectionString)
    : ConfigurationProvider
{
    public override void Load()
    {
        using var dbContext = new EntityConfigurationContext(connectionString);

        dbContext.Database.EnsureCreated();

        Data = dbContext.Settings.Any()
            ? dbContext.Settings.ToDictionary(
                static c => c.Id,
                static c => c.Value, StringComparer.OrdinalIgnoreCase)
            : CreateAndSaveDefaultValues(dbContext);
    }

    static Dictionary<string, string?> CreateAndSaveDefaultValues(
        EntityConfigurationContext context)
    {
        var settings = new Dictionary<string, string?>(
            StringComparer.OrdinalIgnoreCase)
        {
            ["WidgetOptions:EndpointId"] = "b3da3c4c-9c4e-4411-bc4d-609e2dcc5c67",
            ["WidgetOptions:DisplayLabel"] = "Widgets Incorporated, LLC.",
            ["WidgetOptions:WidgetRoute"] = "api/widgets"
        };

        context.Settings.AddRange(
            [.. settings.Select(static kvp => new Settings(kvp.Key, kvp.Value))]);

        context.SaveChanges();

        return settings;
    }
}

Mit einer AddEntityConfiguration-Erweiterungsmethode kann die Konfigurationsquelle der zugrundeliegenden ConfigurationManager-Instanz hinzugefügt werden.

Extensions/ConfigurationManagerExtensions.cs:

using CustomProvider.Example.Providers;

namespace Microsoft.Extensions.Configuration;

public static class ConfigurationManagerExtensions
{
    public static ConfigurationManager AddEntityConfiguration(
        this ConfigurationManager manager)
    {
        var connectionString = manager.GetConnectionString("WidgetConnectionString");

        IConfigurationBuilder configBuilder = manager;
        configBuilder.Add(new EntityConfigurationSource(connectionString));

        return manager;
    }
}

Da der ConfigurationManager eine Implementierung vom IConfigurationBuilder sowie IConfigurationRoot ist, kann die Erweiterungsmethode auf die Konfiguration der Verbindungszeichenfolgen zugreifen und die EntityConfigurationSource hinzufügen.

Der folgende Code veranschaulicht die Verwendung des benutzerdefinierten Anbieters EntityConfigurationProvider in Program.cs:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using CustomProvider.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Configuration.AddEntityConfiguration();

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

using IHost host = builder.Build();

WidgetOptions options = host.Services.GetRequiredService<IOptions<WidgetOptions>>().Value;
Console.WriteLine($"DisplayLabel={options.DisplayLabel}");
Console.WriteLine($"EndpointId={options.EndpointId}");
Console.WriteLine($"WidgetRoute={options.WidgetRoute}");

await host.RunAsync();
// Sample output:
//    WidgetRoute=api/widgets
//    EndpointId=b3da3c4c-9c4e-4411-bc4d-609e2dcc5c67
//    DisplayLabel=Widgets Incorporated, LLC.

Nutzen des Anbieters

Um den benutzerdefinierten Konfigurationsanbieter zu nutzen, können Sie das Optionsmuster verwenden. Definieren Sie bei vorhandener Beispiel-App ein Optionsobjekt, das die Widgeteinstellungen darstellt.

namespace CustomProvider.Example;

public class WidgetOptions
{
    public required Guid EndpointId { get; set; }

    public required string DisplayLabel { get; set; } = null!;

    public required string WidgetRoute { get; set; } = null!;
}

Ein Aufruf von Configure registriert eine Konfigurationsinstanz, an die TOptions bindet.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using CustomProvider.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Configuration.AddEntityConfiguration();

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

using IHost host = builder.Build();

WidgetOptions options = host.Services.GetRequiredService<IOptions<WidgetOptions>>().Value;
Console.WriteLine($"DisplayLabel={options.DisplayLabel}");
Console.WriteLine($"EndpointId={options.EndpointId}");
Console.WriteLine($"WidgetRoute={options.WidgetRoute}");

await host.RunAsync();
// Sample output:
//    WidgetRoute=api/widgets
//    EndpointId=b3da3c4c-9c4e-4411-bc4d-609e2dcc5c67
//    DisplayLabel=Widgets Incorporated, LLC.

Der vorangehende Code konfiguriert das WidgetOptions-Objekt aus dem "WidgetOptions"-Abschnitt der Konfiguration. Dies ermöglicht das Optionsmuster und macht eine „Dependency Injection“-fähige IOptions<WidgetOptions>-Darstellung der EF-Einstellungen verfügbar. Die Optionen werden letztendlich vom benutzerdefinierten Konfigurationsanbieter bereitgestellt.

Siehe auch