Implementación de un proveedor de configuración personalizado en .NET

Hay muchos proveedores de configuración disponibles para orígenes de configuración comunes, como archivos JSON, XML e INI. Puede que tenga que implementar un proveedor de configuración personalizado cuando uno de los proveedores disponibles no satisfaga las necesidades de su aplicación. En este artículo, aprenderá a implementar un proveedor de configuración personalizado que depende de una base de datos como origen de configuración.

Proveedores de configuración personalizada

La aplicación de ejemplo muestra cómo crear un proveedor de configuración básica que lee los pares clave-valor de configuración desde una base de datos mediante Entity Framework (EF) Core.

El proveedor tiene las siguientes características:

  • La base de datos en memoria de EF se usa para fines de demostración.
    • Para usar una base de datos que necesita una cadena de conexión, obtenga una cadena de conexión de una configuración provisional.
  • El proveedor lee una tabla de base de datos en la configuración en el inicio. El proveedor no consulta la base de datos por clave.
  • La función de recarga en cambio no se implementa, por lo que actualizar la base de datos después de que la aplicación se haya iniciado no afectará a la configuración de la aplicación.

Defina una entidad de tipo de registro Settings para almacenar los valores de configuración en la base de datos. Por ejemplo, podría agregar un archivo Settings.cs en la carpeta Modelos:

namespace CustomProvider.Example.Models;

public record Settings(string Id, string Value);

Para obtener información sobre los tipos de registro, consulte Tipos de registro en C# 9.

Agregue un EntityConfigurationContext para almacenar y tener acceso a los valores configurados.

Providers/EntityConfigurationContext.cs:

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

namespace CustomProvider.Example.Providers;

public class EntityConfigurationContext : DbContext
{
    private readonly string _connectionString;

    public DbSet<Settings>? Settings { get; set; }

    public EntityConfigurationContext(string connectionString) =>
        _connectionString = connectionString;

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

Al invalidar OnConfiguring(DbContextOptionsBuilder), puede usar la conexión de base de datos adecuada. Por ejemplo, si se ha proporcionado una cadena de conexión, podría conectarse a SQL Server; de lo contrario, podría basarse en una base de datos en memoria.

Cree una clase que implemente IConfigurationSource.

Providers/EntityConfigurationSource.cs:

using Microsoft.Extensions.Configuration;

namespace CustomProvider.Example.Providers;

public class EntityConfigurationSource : IConfigurationSource
{
    private readonly string _connectionString;

    public EntityConfigurationSource(string connectionString) =>
        _connectionString = connectionString;

    public IConfigurationProvider Build(IConfigurationBuilder builder) =>
        new EntityConfigurationProvider(_connectionString);
}

Cree el proveedor de configuración personalizado heredando de ConfigurationProvider. El proveedor de configuración inicializa la base de datos cuando está vacía. Puesto que las claves de configuración no distinguen entre mayúsculas y minúsculas, el diccionario empleado para iniciar la base de datos se crea con el comparador que tampoco hace tal distinción (StringComparer.OrdinalIgnoreCase).

Providers/EntityConfigurationProvider.cs:

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

namespace CustomProvider.Example.Providers;

public class EntityConfigurationProvider : ConfigurationProvider
{
    private readonly string _connectionString;

    public EntityConfigurationProvider(string connectionString) =>
        _connectionString = connectionString;

    public override void Load()
    {
        using var dbContext = new EntityConfigurationContext(_connectionString);

        dbContext.Database.EnsureCreated();

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

    static IDictionary<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(kvp => new Settings(kvp.Key, kvp.Value))
                    .ToArray());

        context.SaveChanges();

        return settings;
    }
}

Un método de extensión AddEntityConfiguration permite agregar el origen de configuración a una instancia de IConfigurationBuilder.

Extensions/ConfigurationBuilderExtensions.cs:

using CustomProvider.Example.Providers;

namespace Microsoft.Extensions.Configuration;

public static class ConfigurationBuilderExtensions
{
    public static IConfigurationBuilder AddEntityConfiguration(
        this IConfigurationBuilder builder)
    {
        var tempConfig = builder.Build();
        var connectionString =
            tempConfig.GetConnectionString("WidgetConnectionString");

        return builder.Add(new EntityConfigurationSource(connectionString));
    }
}

Importante

El uso de un origen de configuración temporal para adquirir la cadena de conexión es importante. En la instancia de builder actual, la configuración se construye temporalmente mediante una llamada a IConfigurationBuilder.Build() y GetConnectionString. Después de obtener la cadena de conexión, builder agrega connectionString al objeto EntityConfigurationSource dado.

En el código siguiente se muestra cómo puede usar el EntityConfigurationProvider personalizado en Program.cs:

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

namespace CustomProvider.Example
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using IHost host = CreateHostBuilder(args).Build();

            var 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.

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((_, configuration) =>
                {
                    configuration.Sources.Clear();
                    configuration.AddEntityConfiguration();
                })
                .ConfigureServices((context, services) =>
                    services.Configure<WidgetOptions>(
                        context.Configuration.GetSection("WidgetOptions")));
    }
}

Consumo del proveedor

Para consumir el proveedor de configuración personalizado, puede usar el patrón de opciones. Con la aplicación de ejemplo implementada, defina un objeto de opciones para representar la configuración del widget.

namespace CustomProvider.Example;

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

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

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

Una llamada a ConfigureServices configura la asignación de las opciones.

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

namespace CustomProvider.Example
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using IHost host = CreateHostBuilder(args).Build();

            var 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.

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((_, configuration) =>
                {
                    configuration.Sources.Clear();
                    configuration.AddEntityConfiguration();
                })
                .ConfigureServices((context, services) =>
                    services.Configure<WidgetOptions>(
                        context.Configuration.GetSection("WidgetOptions")));
    }
}

En el código anterior se configura el objeto WidgetOptions de la sección "WidgetOptions" de la configuración. Esto habilita el patrón de opciones, que expone una representación IOptions<WidgetOptions> lista para la inserción de dependencias de la configuración de EF. En última instancia, las opciones se proporcionan desde el proveedor de configuración personalizado.

Vea también