Skip to content

roryprimrose/Neovolve.Configuration.DependencyInjection

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Introduction

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.

GitHub license   Nuget   Nuget

Actions Status

Installation

The package can be installed from NuGet using Install-Package Neovolve.Configuration.DependencyInjection.

Usage

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.

Hot reload support

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.

Options

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

Recommendations

Use read-only interface definitions for configuration types

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.

Properties for child configuration types should be classes

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.

Avoid resolving the root config service

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.

About

Configuration binding dependency injection support with hot reload

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages