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, vea Tipos de registro en C#.

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

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 sealed class EntityConfigurationSource(
    string? connectionString) : IConfigurationSource
{
    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 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 método de extensión AddEntityConfiguration permite agregar el origen de configuración a la instancia subyacente de 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;
    }
}

Dado que ConfigurationManager es una implementación de IConfigurationBuilder y IConfigurationRoot, el método de extensión puede acceder a la configuración de las cadenas de conexión y agregar EntityConfigurationSource.

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

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 required Guid EndpointId { get; set; }

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

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

Una llamada a Configure registra una instancia de configuración, a la que se une 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.

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