The Neovolve.Configuration.DependencyInjection NuGet package provides IHostBuilder
extension methods for registering strong typed configuration bindings as services. It supports registration of nested configuration types and hot reload support.
The package can be installed from NuGet using Install-Package Neovolve.Configuration.DependencyInjection
.
This package requires that the application bootstrapping provide a root configuration class that matches the configuration structure that the application uses.
The ConfigureWith<T>
extension method registers the configuration type, all nested configuration types and all interfaces found as services in the host application. It will also ensure that IOptions<>
, IOptionsSnapshot<>
and IOptionsMonitor<>
types are registered with the class types found under the root config type as well as all their interfaces.
For example consider the following nested configuration type structure:
public interface IRootConfig
{
string RootValue { get; }
}
public class RootConfig : IRootConfig
{
public FirstConfig First { get; set; } = new();
public string RootValue { get; set; } = string.Empty;
}
public interface IFirstConfig
{
string FirstValue { get; }
}
public class FirstConfig : IFirstConfig
{
public string FirstValue { get; set; } = string.Empty;
public SecondConfig Second { get; set; } = new();
}
public interface ISecondConfig
{
string SecondValue { get; }
}
public class SecondConfig : ISecondConfig
{
public string SecondValue { get; set; } = string.Empty;
public ThirdConfig Third { get; set; } = new();
}
public interface IThirdConfig
{
string ThirdValue { get; }
TimeSpan Timeout { get; }
}
public class ThirdConfig : IThirdConfig
{
public int TimeoutInSeconds { get; set; } = 123;
public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutInSeconds);
public string ThirdValue { get; set; } = string.Empty;
}
The json configuration source for this data could be something like the following.
{
"RootValue": "This is the root value",
"First": {
"Second": {
"Third": {
"ThirdValue": "This is the third value",
"TimeoutInSeconds": 123
},
"SecondValue": "This is the second value"
},
"FirstValue": "This is the first value"
}
}
For an ASP.Net system, this would be registered like the following:
var builder = WebApplication.CreateBuilder(args);
// Register all configuration types
builder.Host.ConfigureWith<RootConfig>();
For other platforms, such as console applications, this would be registered like the following:
var builder = Host.CreateDefaultBuilder()
.ConfigureWith<RootConfig>();
Given the above example, the following services would be registered with the host application:
| Type | IOptions<T> | IOptionsSnapshot<T> | IOptionsMonitor<T> | Supports hot reload | |-|-|-|-| | RootConfig | No | No | No | No | | IRootConfig | No | No | No | No | | FirstConfig | Yes | Yes | Yes | Yes, except for IOptions<FirstConfig> | | IFirstConfig | Yes | Yes | Yes | Yes, except for IOptions<IFirstConfig> | | SecondConfig | Yes | Yes | Yes | Yes, except for IOptions<SecondConfig> | | ISecondConfig | Yes | Yes | Yes | Yes, except for IOptions<ISecondConfig> | | ThirdConfig | Yes | Yes | Yes | Yes, except for IOptions<ThirdConfig> | | IThirdConfig | Yes | Yes | Yes | Yes, except for IOptions<IThirdConfig> |
See Options pattern in .NET -> Options interfaces for more information on the IOptions<>
, IOptionsSnapshot<>
and IOptionsMonitor<>
types.
The options binding system in .NET Core supports hot reload of configuration data which is implemented by some configuration providers like the providers for json and ini files. This is typically done by watching the configuration source for changes and then reloading the configuration data. This is useful for scenarios where configuration data is stored in a file and the application needs to react to changes in the file without needing to restart the application. This support is provided by the IOptionsSnapshot<>
and IOptionsMonitor<>
services.
One of the benefits of this package is that it supports hot reloading of injected raw configuration services by default. A raw type is a configuration class and its defined interfaces that are found under the root configuration type. In this definition, a raw type is anything other than IOption<>
, IOptionsSnapshot<>
or IOptionsMonitor<>
.
In the above configuration example, the raw types that support hot reloading are:
- IFirstConfig
- FirstConfig
- ISecondConfig
- SecondConfig
- IThirdConfig
- ThirdConfig
This package detects when a configuration change has occurred by watching IOptionsMonitor<>.OnChange
on all configuration services registered under the root configuration type. The package then updates the existing raw type in memory which works because the raw types are registered as singleton services. This allows the application class to receive updated configuration data at runtime by injecting a T
configuration class/interface without needing to use IOptionsMonitor<T>
or IOptionsSnapshot<T>
. Logging is provided as the mechanism for recording that an injected raw type has been updated.
The reason to use IOptionsMonitor<>
instead of the raw type is when the application class wants to hook into the IOptionsMonitor.OnChange
method itself to run some custom code when the configuration changes.
The hot reload support for raw configuration types can be disabled by setting the ReloadInjectedRawTypes
option to false
in the ConfigureWith<T>
overload.
The following are the default options that ConfigureWith<T>
uses.
Option | Type | Default | Description |
---|---|---|---|
CustomLogCategory | string |
string.Empty |
The custom log category used when LogCategoryType is LogCategoryType.Custom |
LogCategoryType | LogCategoryType |
LogCategoryType.TargetType |
The log category to use when logging messages for configuration updates on raw types. Supported values are TargetType or Custom . |
LogPropertyChangeLevel | LogLevel |
LogLevel.Information |
The log level to use when logging that a property on an injected raw type has been updated when ReloadInjectedRawTypes is true . |
LogReadOnlyPropertyLevel | LogLevel |
LogLevel.Warning in Development; otherwise Debug |
The log level to use when logging that updates are detected for read-only properties on an injected raw type has been updated when ReloadInjectedRawTypes is true . |
LogReadOnlyPropertyType | LogReadOnlyPropertyType |
LogReadOnlyPropertyType.ValueTypesOnly |
The types of read-only properties to log when they are updated. Supported values are All , ValueTypesOnly and None. |
ReloadInjectedRawTypes | bool |
true |
Determines if raw types that are injected into the configuration system should be reloaded when the configuration changes |
SkipPropertyTypes | Collection<Type> |
[typeof(IEnumerable), typeof(Type), typeof(Assembly), typeof(Stream)] |
A collection of property types that should be skipped when registering configuration sections. |
These options can be set in the ConfigureWith<T>
overload.
var builder = Host.CreateDefaultBuilder()
.ConfigureWith<RootConfig>(x => {
x.CustomLogCategory = "MyCustomCategory";
x.LogCategoryType = LogCategoryType.Custom;
x.LogReadOnlyPropertyLevel = LogLevel.Information;
x.LogReadOnlyPropertyType = LogReadOnlyPropertyType.All;
x.ReloadInjectedRawTypes = false;
x.SkipPropertyTypes.Add(typeof(ComplexTypeNotToBeRegistered));
});
Configuration class definitions require that properties are mutable to allow the configuration binding system to set the values. There is a risk of an application class mutating the configuration data after it is injected into a class constructor. The way to prevent unintended mutation of configuration data at runtime is to define a read-only interface for the configuration class. This will allow the configuration system to set the values but the application code will not be able to change the values.
The ConfigureWith<T>
extension method supports this by registering any configuration interfaces found under the root configuration class.
Assuming that any configuration interfaces hide unnecessary child configuration types, all properties that represent child configuration types should be defined as their classes rather than interfaces on the parent configuration class. The ConfigureWith<T>
extension method uses reflection to walk the type hierarchy from the root configuration type by finding and recursing through all the properties.
For example, if the First
property on RootConfig
above was defined as IFirstConfig
rather than FirstConfig
then the Second
property on FirstConfig
would not be found and registered as a service. This is because the IFirstConfig
does not define the Second
property but FirstConfig
does.
The root configuration type provided to ConfigureWith<T>
is registered as a service of T
however using this service would typically break Law of Demeter in the application. Additionally, ConfigureWith<T>
explicitly removes the service registrations of the root config for the IOptions<T>
, IOptionsSnapshot<T>
or IOptionsMonitor<T>
services as they do not support hot reload. If you really do need to resolve root level configuration then use an interface like IRootConfig
in the example above. In this case, hot reloaded data will still not be available on these services.