在 .NET 中实现自定义配置提供程序

有许多配置提供程序可用于常见的配置源,如 JSON、XML 和 INI 文件。 如果某个可用的提供程序不满足你的应用程序需要,说明你可能需要实现自定义配置提供程序。 在本文中,你将了解如何实现一个依赖数据库作为配置源的自定义配置提供程序。

自定义配置提供程序

此示例应用演示了如何使用实体框架 (EF) Core 创建从数据库读取配置键值对的基本配置提供程序。

提供程序具有以下特征:

  • EF 内存中数据库用于演示目的。
    • 若要使用需要连接字符串的数据库,请从临时配置中获取连接字符串。
  • 提供程序在启动时将数据库表读入配置。 提供程序不会基于每个键查询数据库。
  • 未实现更改时重载,因此在应用启动后更新数据库不会影响应用的配置。

定义一个 Settings 记录类型实体,用于在数据库中存储配置值。 例如,可以将 Settings.cs 文件添加到“Models”文件夹中:

namespace CustomProvider.Example.Models;

public record Settings(string Id, string Value);

有关记录类型的信息,请参阅 C# 9 中的记录类型

添加 EntityConfigurationContext 以存储和访问配置的值。

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 => Set<Settings>();

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

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

通过重写 OnConfiguring(DbContextOptionsBuilder),可使用相应的数据库连接。 例如,如果提供了连接字符串,则可连接到 SQL Server,否则可能要依赖内存数据库。

创建用于实现 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);
}

通过从 ConfigurationProvider 继承来创建自定义配置提供程序。 当数据库为空时,配置提供程序将对其进行初始化。 由于配置密钥不区分大小写,因此用来初始化数据库的字典是用不区分大小写的比较程序 (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<Settings, string, string?>(c => c.Id, c => c.Value, StringComparer.OrdinalIgnoreCase)
            : 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;
    }
}

可以使用 AddEntityConfiguration 扩展方法将配置源添加到 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));
    }
}

重要

使用临时配置源获取连接字符串非常重要。 当前的 builder 通过调用 IConfigurationBuilder.Build()GetConnectionString 临时构造其配置。 获取连接字符串后,builder 将根据 connectionString 添加 EntityConfigurationSource

下面的代码演示如何在 Program.cs 中使用自定义的 EntityConfigurationProvider

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

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((_, configuration) =>
    {
        configuration.Sources.Clear();
        configuration.AddEntityConfiguration();
    })
    .ConfigureServices((context, services) =>
        services.Configure<WidgetOptions>(
            context.Configuration.GetSection("WidgetOptions")))
    .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.

使用提供程序

若要使用自定义配置提供程序,可使用选项模式。 准备好示例应用后,定义一个 options 对象来表示小组件设置。

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

调用 ConfigureServices 可配置 options 的映射。

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

using IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((_, configuration) =>
    {
        configuration.Sources.Clear();
        configuration.AddEntityConfiguration();
    })
    .ConfigureServices((context, services) =>
        services.Configure<WidgetOptions>(
            context.Configuration.GetSection("WidgetOptions")))
    .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.

前面的代码从配置的 "WidgetOptions" 部分配置 WidgetOptions 对象。 这将启用选项模式,同时公开 EF 设置的依赖关系注入就绪 IOptions<WidgetOptions> 表示形式。 这些选项最终是通过自定义配置提供程序提供的。

另请参阅