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

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

自定义配置提供程序

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

提供程序具有以下特征:

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

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

namespace CustomProvider.Example.Models;

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

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

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

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

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

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

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

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

由于 ConfigurationManager 同时是 IConfigurationBuilderIConfigurationRoot 的实现,因此扩展方法可以访问连接字符串配置并添加 EntityConfigurationSource

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

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.

使用提供程序

若要使用自定义配置提供程序,可使用选项模式。 准备好示例应用后,定义一个 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!;
}

Configure 的调用会注册一个配置实例,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.

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

另请参阅