Based on Metoule answer, I've created reusable extension method, that accepts delegate that accepts section and returns instance to bind to.
Please note that not all edge cases are handled (e.g. Features
must be list, not array).
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;
}
}
}
"FileSizeCheckerOptions": { "MaxFileSize": 1000 }
? – ElmerelminaFileSizeChecker
) - this is a key for the factory to create a new feature. And I need to load options for this feature. – Prisca