Implementar um provedor de configuração personalizado em .NET

Há muitos provedores de configuração disponíveis para fontes de configuração comuns, como arquivos JSON, XML e INI. Talvez seja necessário implementar um provedor de configuração personalizado quando um dos provedores disponíveis não atender às necessidades do aplicativo. Neste artigo, você aprenderá a implementar um provedor de configuração personalizado que depende de um banco de dados como sua fonte de configuração.

Provedor de Configuração personalizado

O aplicativo de exemplo demonstra como criar um provedor de configuração básico que lê os pares chave-valor da configuração de um banco de dados usando Entity Framework (EF) Core.

O provedor tem as seguintes características:

  • O banco de dados EF na memória é usado para fins de demonstração.
    • Para usar um banco de dados que requer uma cadeia de conexão, obtenha uma cadeia de conexão de uma configuração intermediária.
  • O provedor lê uma tabela de banco de dados na configuração na inicialização. O provedor não consulta o banco de dados em uma base por chave.
  • O recarregamento na alteração não está implementado, portanto, a atualização do banco de dados após a inicialização do aplicativo não terá efeito sobre a configuração do aplicativo.

Defina uma entidade de tipo de registro Settings para armazenar valores de configuração no banco de dados. Por exemplo, você pode adicionar um arquivo Settings.cs na pasta Models:

namespace CustomProvider.Example.Models;

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

Para obter informações sobre tipos de registro, confira Tipos de registro do C#.

Adicione um EntityConfigurationContext para armazenar e acessar os valores configurados.

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

Ao substituir OnConfiguring(DbContextOptionsBuilder), você pode usar a conexão de banco de dados apropriada. Por exemplo, se uma cadeia de conexão for fornecida, você poderá se conectar ao SQL Server, caso contrário, poderá contar com um banco de dados na memória.

Crie uma classe que implementa 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);
}

Crie o provedor de configuração personalizado através da herança de ConfigurationProvider. O provedor de configuração inicializa o banco de dados quando ele está vazio. Como as chaves de configuração não diferenciam maiúsculas de minúsculas, o dicionário usado para inicializar o banco de dados é criado com o comparador que não diferencia maiúsculas de minúsculas (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;
    }
}

Um método de extensão AddEntityConfiguration permite adicionar a fonte de configuração à instância subjacente ConfigurationManager.

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

Como o ConfigurationManager é uma implementação de IConfigurationBuilder e IConfigurationRoot, o método de extensão pode acessar a configuração de cadeias de conexão e adicionar o EntityConfigurationSource.

O código a seguir mostra como usar o EntityConfigurationProvider personalizado em 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.

Consumir provedor

Para consumir o provedor de configuração personalizado, você pode usar o padrão de opções. Com o aplicativo de exemplo em vigor, defina um objeto de opções para representar as configurações do 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!;
}

Uma chamada para Configure registra uma instância de configuração à qual TOptions se associa.

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.

O código anterior configura o objeto WidgetOptions da seção "WidgetOptions" da configuração. Isso habilita o padrão de opções, expondo uma representação IOptions<WidgetOptions> pronta para injeção de dependência das configurações de EF. As opções são fornecidas, em última análise, do provedor de configuração personalizado.

Confira também