如何在 appsettings.json 中加载多态对象

How to load polymorphic objects in appsettings.json

有没有办法以强类型的方式从 appsettings.json 中读取多态对象?下面是我需要的一个非常简单的例子。

我有多个应用程序组件,在此处命名为 Features。这些组件由工厂在运行时创建。我的设计意图是每个组件都由其单独的强类型选项配置。在这个例子中 FileSizeCheckerOptionsPersonCheckerOption 是这些的实例。可以使用不同的选项多次包含每个功能。

但是使用现有的 ASP.NET 核心配置系统,我无法读取 polymorphic 强类型选项。如果设置由 JSON 解串器读取,我可以使用 something like this。但这不是 appsettings.json 的情况,其中选项只是键值对。

appsettings.json

{
    "DynamicConfig":
    {
        "Features": [
            {
                "Type": "FileSizeChecker",
                "Options": { "MaxFileSize": 1000 }
            },
            {
                "Type": "PersonChecker",
                "Options": {
                    "MinAge": 10,
                    "MaxAge": 99
                }
            },
            {
                "Type": "PersonChecker",
                "Options": {
                    "MinAge": 15,
                    "MaxAge": 20
                }
            }
        ]
    }
}

Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<FeaturesOptions>(Configuration.GetSection("DynamicConfig"));
        ServiceProvider serviceProvider = services.BuildServiceProvider();
        // try to load settings in strongly typed way
        var options = serviceProvider.GetRequiredService<IOptions<FeaturesOptions>>().Value;
    }

其他定义

public enum FeatureType
{
    FileSizeChecker,
    PersonChecker
}

public class FeaturesOptions
{
    public FeatureConfig[] Features { get; set; }
}

public class FeatureConfig
{
    public FeatureType Type { get; set; }
    // cannot read polymorphic object
    // public object Options { get; set; } 
}

public class FileSizeCheckerOptions
{
    public int MaxFileSize { get; set; }
}

public class PersonCheckerOption
{
    public int MinAge { get; set; }
    public int MaxAge { get; set; }

}

回答这个问题的关键是知道密钥是如何生成的。在您的情况下,键/值对将是:

DynamicConfig:Features:0:Type
DynamicConfig:Features:0:Options:MaxFileSize
DynamicConfig:Features:1:Type
DynamicConfig:Features:1:Options:MinAge
DynamicConfig:Features:1:Options:MaxAge
DynamicConfig:Features:2:Type
DynamicConfig:Features:2:Options:MinAge
DynamicConfig:Features:2:Options:MaxAge

注意数组中的每个元素是如何用 DynamicConfig:Features:{i} 表示的。

要知道的第二件事是,您可以使用 ConfigurationBinder.Bind 方法将配置的任何部分映射到对象实例:

var conf = new PersonCheckerOption();
Configuration.GetSection($"DynamicConfig:Features:1:Options").Bind(conf);

当我们将所有这些放在一起时,我们可以将您的配置映射到您的数据结构:

services.Configure<FeaturesOptions>(opts =>
{
    var features = new List<FeatureConfig>();

    for (var i = 0; ; i++)
    {
        // read the section of the nth item of the array
        var root = $"DynamicConfig:Features:{i}";

        // null value = the item doesn't exist in the array => exit loop
        var typeName = Configuration.GetValue<string>($"{root}:Type");
        if (typeName == null)
            break;

        // instantiate the appropriate FeatureConfig 
        FeatureConfig conf = typeName switch
        {
            "FileSizeChecker" => new FileSizeCheckerOptions(),
            "PersonChecker" => new PersonCheckerOption(),
            _ => throw new InvalidOperationException($"Unknown feature type {typeName}"),
        };

        // bind the config to the instance
        Configuration.GetSection($"{root}:Options").Bind(conf);
        features.Add(conf);
    }

    opts.Features = features.ToArray();
});

注意:所有选项都必须派生自 FeatureConfig 才能正常工作(例如 public class FileSizeCheckerOptions : FeatureConfig)。您甚至可以使用反射来自动检测从 FeatureConfig 继承的所有选项,以避免切换类型名称。

注意 2:您也可以将配置映射到 Dictionary,或者如果您愿意,也可以映射到 dynamic 对象;请参阅我对 .

的回答

基于 ,我创建了可重复使用的扩展方法,它接受接受部分的委托和要绑定到的 returns 实例。

请注意,并非所有边缘情况都得到处理(例如 Features 必须是列表,而不是数组)。

public class FeaturesOptions
{
    public List<FeatureConfigOptions> Features { get; set; }
}

public abstract class FeatureConfigOptions
{
    public string Type { get; set; }
}

public class FileSizeCheckerOptions : FeatureConfigOptions
{
    public int MaxFileSize { get; set; }
}

public class PersonCheckerOptions : FeatureConfigOptions
{
    public int MinAge { get; set; }
    public int MaxAge { get; set; }
}

FeaturesOptions options = new FeaturesOptions();

IConfiguration configuration = new ConfigurationBuilder()
    .AddJsonFile("path-to-the-appsettings.json")
    .Build();

configuration.Bind(options, (propertyType, section) =>
{
    string type = section.GetValue<string>("Type");
    switch (type)
    {
        case "FileSizeChecker": return new FileSizeCheckerOptions();
        case "PersonChecker": return new PersonCheckerOptions();
        default: throw new InvalidOperationException($"Unknown feature type {type}"); // or you can return null to skip the binding.
    };
});

appsettings.json

{
    "Features":
    [
        {
            "Type": "FileSizeChecker",
            "MaxFileSize": 1000
        },
        {
            "Type": "PersonChecker",
            "MinAge": 10,
            "MaxAge": 99
        },
        {
            "Type": "PersonChecker",
            "MinAge": 15,
            "MaxAge": 20
        }
    ]
}

IConfigurationExtensions.cs

using System.Collections;

namespace Microsoft.Extensions.Configuration
{
    /// <summary>
    /// </summary>
    /// <param name="requestedType">Abstract type or interface that is about to be bound.</param>
    /// <param name="configurationSection">Configuration section to be bound from.</param>
    /// <returns>Instance of object to be used for binding, or <c>null</c> if section should not be bound.</returns>
    public delegate object? ObjectFactory(Type requestedType, IConfigurationSection configurationSection);

    public static class IConfigurationExtensions
    {
        public static void Bind(this IConfiguration configuration, object instance, ObjectFactory objectFactory)
        {
            if (configuration is null)
                throw new ArgumentNullException(nameof(configuration));

            if (instance is null)
                throw new ArgumentNullException(nameof(instance));
            
            if (objectFactory is null)
                throw new ArgumentNullException(nameof(objectFactory));

            // first, bind all bindable instance properties.
            configuration.Bind(instance);

            // then scan for all interfaces or abstract types
            foreach (var property in instance.GetType().GetProperties())
            {
                var propertyType = property.PropertyType;
                if (propertyType.IsPrimitive || propertyType.IsValueType || propertyType.IsEnum || propertyType == typeof(string))
                    continue;

                var propertySection = configuration.GetSection(property.Name);
                if (!propertySection.Exists())
                    continue;

                object? propertyValue;
                
                if (propertyType.IsAbstract || propertyType.IsInterface)
                {
                    propertyValue = CreateAndBindValueForAbstractPropertyTypeOrInterface(propertyType, objectFactory, propertySection);
                    property.SetValue(instance, propertyValue);
                }
                else
                {
                    propertyValue = property.GetValue(instance);
                }

                if (propertyValue is null)
                    continue;

                var isGenericList = propertyType.IsAssignableTo(typeof(IList)) && propertyType.IsGenericType;
                if (isGenericList)
                {
                    var listItemType = propertyType.GenericTypeArguments[0];
                    if (listItemType.IsPrimitive || listItemType.IsValueType || listItemType.IsEnum || listItemType == typeof(string))
                        continue;

                    if (listItemType.IsAbstract || listItemType.IsInterface)
                    {
                        var newListPropertyValue = (IList)Activator.CreateInstance(propertyType)!;

                        for (int i = 0; ; i++)
                        {
                            var listItemSection = propertySection.GetSection(i.ToString());
                            if (!listItemSection.Exists())
                                break;

                            var listItem = CreateAndBindValueForAbstractPropertyTypeOrInterface(listItemType, objectFactory, listItemSection);
                            if (listItem is not null)
                                newListPropertyValue.Add(listItem);
                        }

                        property.SetValue(instance, newListPropertyValue);
                    }
                    else
                    {
                        var listPropertyValue = (IList)property.GetValue(instance, null)!;
                        for (int i = 0; i < listPropertyValue.Count; i++)
                        {
                            var listItem = listPropertyValue[i];
                            if (listItem is not null)
                            {
                                var listItemSection = propertySection.GetSection(i.ToString());
                                listItemSection.Bind(listItem, objectFactory);
                            }
                        }
                    }
                }
                else
                {
                    propertySection.Bind(propertyValue, objectFactory);
                }
            }
        }

        private static object? CreateAndBindValueForAbstractPropertyTypeOrInterface(Type abstractPropertyType, ObjectFactory objectFactory, IConfigurationSection section)
        {
            if (abstractPropertyType is null)
                throw new ArgumentNullException(nameof(abstractPropertyType));

            if (objectFactory is null)
                throw new ArgumentNullException(nameof(objectFactory));

            if (section is null)
                throw new ArgumentNullException(nameof(section));

            var propertyValue = objectFactory(abstractPropertyType, section);

            if (propertyValue is not null)
                section.Bind(propertyValue, objectFactory);

            return propertyValue;
        }
    }
}