Is there any way how to read polymorphic objects from appsettings.json
in a strongly-typed way? Below is a very simplified example of what I need.
I have multiple app components, named Features
here. These components are created in runtime by a factory. My design intent is that each component is configured by its separate strongly-typed options. In this example FileSizeCheckerOptions
and PersonCheckerOption
are instances of these. Each feature can be included multiple times with different option.
But with the existing ASP.NET Core configuration system, I am not able to read polymorphic strongly typed options. If the settings were read by a JSON deserializer, I could use something like this. But this is not the case of appsettings.json
, where options are just key-value pairs.
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;
}
Other definitions
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; }
}
The key to answer this question is to know how the keys are generated. In your case, the key / value pairs will be:
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
Notice how each element of the array is represented by DynamicConfig:Features:{i}
.
The second thing to know is that you can map any section of a configuration to an object instance, with the ConfigurationBinder.Bind
method:
var conf = new PersonCheckerOption();
Configuration.GetSection($"DynamicConfig:Features:1:Options").Bind(conf);
When we put all this together, we can map your configuration to your data structure:
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();
});
Note: all options must derive from FeatureConfig
for this to work (e.g. public class FileSizeCheckerOptions : FeatureConfig
). You could even use reflection to automatically detect all the options inheriting from FeatureConfig
, to avoid the switch over the type name.
Note 2: you can also map your configuration to a Dictionary
, or a dynamic
object if you prefer; see my answer to Bind netcore IConfigurationSection to a dynamic object.