Implementare un provider di configurazione personalizzato in .NET

Sono disponibili molti provider di configurazione per origini di configurazione comuni, ad esempio file JSON, XML e INI. Potrebbe essere necessario implementare un provider di configurazione personalizzato quando uno dei provider disponibili non soddisfa le esigenze dell'applicazione. In questo articolo si apprenderà come implementare un provider di configurazione personalizzato che si basa su un database come origine di configurazione.

Provider di configurazione personalizzato

L'app campione dimostra come creare un provider di configurazione di base che legge le coppie chiave-valore di configurazione da un database usando Entity Framework (EF) Core.

Il provider ha le caratteristiche seguenti:

  • Il database in memoria di Entity Framework viene usato a scopo dimostrativo.
    • Per usare un database che richiede una stringa di connessione, ottenere una stringa di connessione da una configurazione provvisoria.
  • Il provider legge una tabella di database in una configurazione all'avvio. Il provider non esegue una query sul database per ogni chiave.
  • Il ricaricamento in caso di modifica non è implementato, quindi l'aggiornamento del database dopo l'avvio dell'app non interesserà la configurazione dell'app.

Definire un'entità di tipo record Settings per l'archiviazione dei valori di configurazione nel database. Ad esempio, è possibile aggiungere un file Settings.cs nella cartella Models:

namespace CustomProvider.Example.Models;

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

Per informazioni sui tipi di record, vedere Tipi di record in C#.

Aggiungere EntityConfigurationContext per archiviare i valori configurati e accedervi.

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")
        };
    }
}

Eseguendo l'override di OnConfiguring(DbContextOptionsBuilder) è possibile usare la connessione dati appropriata. Ad esempio, se è stata fornita una stringa di connessione, è possibile connettersi a SQL Server; in caso contrario, è possibile basarsi su un database in memoria.

Creare una classe che implementi IConfigurationSource.

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);
}

Creare il provider di configurazione personalizzato ereditando da ConfigurationProvider. Il provider di configurazione inizializza il database quando è vuoto. Poiché nelle chiavi di configurazione non viene fatta distinzione tra maiuscole e minuscole, il dizionario usato per inizializzare il database viene creato con l'operatore di confronto senza distinzione tra maiuscole e minuscole (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;
    }
}

Un metodo di estensione AddEntityConfiguration consente di aggiungere l'origine di configurazione a un'istanza di ConfigurationManager sottostante.

Estensioni/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;
    }
}

Poiché ConfigurationManager è un'implementazione sia di IConfigurationBuilder che diIConfigurationRoot, il metodo di estensione può accedere alla configurazione delle stringhe di connessione e aggiungere EntityConfigurationSource.

L'esempio di codice seguente mostra come usare il EntityConfigurationProvider personalizzato 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.

Utilizza provider

Per utilizzare il provider di configurazione personalizzato, è possibile usare il modello di opzioni. Con l'app campione in uso, definire un oggetto di opzioni per rappresentare le impostazioni del widget.

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!;
}

Una chiamata a Configure registra un'istanza di configurazione a cui si associa TOptions.

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.

Il codice precedente configura l'oggetto WidgetOptions dalla sezione "WidgetOptions" della configurazione. In questo modo, viene abilitato il modello di opzioni, esponendo una rappresentazione IOptions<WidgetOptions> pronta per l'inserimento delle dipendenze delle impostazioni di Entity Framework. In ultima istanza, le opzioni vengono fornite da un provider di configurazione personalizzato.

Vedi anche