Neovolve.Configuration.DependencyInjection 2.0.0-beta.9

This is a prerelease version of Neovolve.Configuration.DependencyInjection.
dotnet add package Neovolve.Configuration.DependencyInjection --version 2.0.0-beta.9
                    
NuGet\Install-Package Neovolve.Configuration.DependencyInjection -Version 2.0.0-beta.9
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Neovolve.Configuration.DependencyInjection" Version="2.0.0-beta.9" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Neovolve.Configuration.DependencyInjection" Version="2.0.0-beta.9" />
                    
Directory.Packages.props
<PackageReference Include="Neovolve.Configuration.DependencyInjection" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Neovolve.Configuration.DependencyInjection --version 2.0.0-beta.9
                    
#r "nuget: Neovolve.Configuration.DependencyInjection, 2.0.0-beta.9"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Neovolve.Configuration.DependencyInjection@2.0.0-beta.9
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Neovolve.Configuration.DependencyInjection&version=2.0.0-beta.9&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Neovolve.Configuration.DependencyInjection&version=2.0.0-beta.9&prerelease
                    
Install as a Cake Tool

Introduction

The Neovolve.Configuration.DependencyInjection NuGet package provides IHostApplicationBuilder, IHostBuilder and IServiceCollection 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

Contents

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.ConfigureWith<RootConfig>();

For other platforms, such as console applications, the host application builder is registered like the following:

var builder = Host.CreateApplicationBuilder();

builder.ConfigureWith<RootConfig>();

Using IHostApplicationBuilder (returned by WebApplication.CreateBuilder() and Host.CreateApplicationBuilder()) is the recommended way to use this library. The classic IHostBuilder is also supported for applications still using Host.CreateDefaultBuilder():

var builder = Host.CreateDefaultBuilder()
    .ConfigureWith<RootConfig>();

For scenarios that work directly with an IServiceCollection and an IConfiguration, an equivalent overload is available. The host builder extension methods are thin wrappers over these.

builder.Services.ConfigureWith<RootConfig>(builder.Configuration);

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 IOptions<>, 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.

Configuration types must be mutable classes to hot reload

Hot reload works by updating the existing singleton instance of a configuration type in place when the configuration changes. That requires the type to be a reference type with writable properties. Two kinds of configuration type cannot be hot reloaded:

  • Value types (struct and record struct). A struct is copied when it is injected, so updating the registered instance would not reach the copies already handed to application classes. Struct configuration types are bound and registered once as a snapshot of the configuration at startup, and they do not receive later updates.
  • Reference types with no writable properties (for example a positional record whose properties are all init-only). There is nowhere to write the updated values, so the injected instance keeps its startup values.

The source generator reports these cases at compile time as warning NCDI001 so the limitation is visible where the type is declared rather than failing silently at runtime. The warning also reports the root configuration type and the configuration path the offending type is bound at (for example Service:Endpoint), so a type buried deep in a complex configuration graph can be traced back to where it is referenced. The accompanying code fix converts a struct (or record struct) configuration type into a class so it can hot reload. For a record with no writable properties, add settable properties or convert it to a mutable class.

If a one-time snapshot is the intended behaviour for a given type, suppress the warning for that type:

[System.Diagnostics.CodeAnalysis.SuppressMessage("Neovolve.Configuration", "NCDI001", Justification = "Snapshot binding is intended for this type.")]
public struct StartupOnlySettings
{
    public string Name { get; set; }
}

The warning can also be disabled project-wide with <NoWarn>$(NoWarn);NCDI001</NoWarn>.

Configuration validation

ConfigureWith<T> enforces validation on the configuration graph so invalid configuration is rejected rather than silently bound. Validation runs through the standard options validation pipeline, so every validation source that produces an IValidateOptions<T> participates:

  • Data annotation attributes ([Range], [Required], [StringLength], and so on) on configuration properties are validated automatically. Each child configuration type is bound with ValidateDataAnnotations(), and the root type and any struct types are validated with their data annotation attributes as they are bound.
  • Source generated validators declared with [OptionsValidator] on a partial IValidateOptions<T> implementation. This keeps validation reflection free and AOT friendly. Register the validator as IValidateOptions<T> so the library picks it up.
  • Custom validators registered as IValidateOptions<T> for rules that cannot be expressed as attributes.
public class ThirdConfig : IThirdConfig
{
    [Range(5, 120)]
    public int TimeoutInSeconds { get; set; } = 30;

    public TimeSpan Timeout => TimeSpan.FromSeconds(TimeoutInSeconds);
}

Fail on start

Invalid configuration fails fast at application startup. Child types are validated through ValidateOnStart(), while the root and struct configuration types are validated as they are bound. A failure throws an OptionsValidationException so the host does not start with invalid configuration.

Validation on hot reload

When a configuration change is detected, the reloaded values are validated before being applied to the injected raw types. If validation fails, the change is rejected as a whole: the failure is logged and the previously valid configuration is retained, so a single invalid value never produces a fragmented update where some properties change and others do not. A reload that passes validation is applied as normal.

To supply your own validation behaviour, register a custom IConfigValidator before calling ConfigureWith<T>; the default implementation runs the registered IValidateOptions<T> validators.

Options

The following are the default options that ConfigureWith<T> uses.

Option Type Default Description
CustomLogCategory string Neovolve.Configuration.DependencyInjection.ConfigureWith 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.None 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.
NestedChangeLogging NestedChangeLogging NestedChangeLogging.Summary How much detail is logged when a class property or a collection of classes changes. Summary logs class properties independently and collections of classes as an entry count only. Deep also walks class properties and the elements of collections of classes, logging individual nested changes with a full property path (for example FilterRules[0].Port).
ReloadInjectedRawTypes bool true Determines if raw types that are injected into the configuration system should be reloaded when the configuration changes

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.NestedChangeLogging = NestedChangeLogging.Deep;
        x.ReloadInjectedRawTypes = false;
    });

To exclude a property or type from the configuration graph, see Excluding properties and types.

Source generator

This package includes a Roslyn source generator that runs in the project that calls ConfigureWith<T>. At compile time it walks the configuration type graph from each ConfigureWith<T> root and emits the registration code plus a strongly typed value applier for each configuration type. Each applier assigns the updated property values directly onto the injected instance, so the library binds and hot reloads configuration without runtime reflection and without boxing value type properties on the update path. The generator ships with the package and requires no configuration.

Because the generator knows each property's type at compile time, it also decides how a property change is logged (when change logging is enabled), without any runtime type inspection:

  • Scalar values (numbers, strings, enums, and so on) are logged as from 'old' to 'new'.
  • Collections of scalar values are logged as an entry count change, or as the individual element values that changed.
  • Collections of complex types are logged only as an entry count change, because logging each element would just repeat the element type name.
  • Child configuration types are assigned but not logged at the parent, because each child type is registered and logs its own changes independently.

For example, given a ServerConfig with a Name string, a Tags collection of strings and an Endpoints collection of Endpoint objects, a reload that renames the server and adds a tag logs the following in the default Summary mode:

Configuration updated on property ServerConfig.Name from 'web-01' to 'web-02'
Configuration updated on property ServerConfig.Tags from 2 entries to 3 entries

A change to an endpoint's port is not logged in Summary mode when the endpoint count is unchanged, because the elements are complex types. Setting the NestedChangeLogging option to Deep also logs the individual nested changes inside class properties and the elements of collections of classes, using a full property path:

Configuration updated on property ServerConfig.Endpoints[0].Port from '80' to '443'

Deep logging costs more on deeper graphs and is off by default. It can also repeat a change that a registered child type already logs on its own, because both the parent (using a nested path) and the child type report it.

Excluding properties and types

The generator always treats IEnumerable, Type, Assembly and Stream as leaf values rather than nested configuration sections. To exclude additional properties or types from the configuration graph (so they are not registered, copied or logged on hot reload), use the exclusion attributes.

Mark an individual property with [SkipConfigProperty]:

using Neovolve.Configuration.DependencyInjection.Generated;

public class ServerConfig
{
    public string Name { get; set; } = string.Empty;

    [SkipConfigProperty]
    public string ComputedToken { get; set; } = string.Empty;
}

Mark a type with [SkipConfigType] to exclude it wherever it appears as a property, or list types at the assembly level:

using Neovolve.Configuration.DependencyInjection.Generated;

[SkipConfigType]
public class DiagnosticState
{
    public string Value { get; set; } = string.Empty;
}

// or, for types declared elsewhere:
[assembly: SkipConfigType(typeof(SomeThirdPartyType))]

The generator also reports diagnostic NCDI001 when a configuration type in the graph cannot be hot reloaded (see Configuration types must be mutable classes to hot reload). A code fix is included to convert a struct configuration type into a class. It reports diagnostic NCDI002 for a get-only collection property that binds at startup but cannot be hot reloaded (see Collection properties must be settable on the class to hot reload), with code fixes that add the setter or apply [SkipConfigProperty].

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.

Declare the properties as settable on the class so the configuration binder can assign the values and hot reload can update them, and expose them as get-only on the interface so application code cannot mutate the configuration after it is injected:

public interface IServerConfig
{
    // Get-only on the interface: application code cannot change the values.
    string Name { get; }
    int Port { get; }
}

public class ServerConfig : IServerConfig
{
    // Get/set on the class: the binder assigns the values and hot reload updates them.
    public string Name { get; set; } = string.Empty;
    public int Port { get; set; }
}

Inject IServerConfig (rather than ServerConfig) into application classes so they get read-only access to the configuration while still receiving hot reload updates.

Collection properties must be settable on the class to hot reload

A collection configuration property (for example ICollection<T>, IList<T>, ISet<T> or a concrete List<T>) is bound at application startup whether or not the property has a setter, because the configuration binder adds the items into the existing collection instance. This happens during startup before the instance is injected anywhere, so the in-place population is safe.

Hot reload is different. The library hot reloads by assigning the reloaded value onto the injected singleton, which is a single atomic reference assignment, so a reader sees either the whole previous value or the whole new value. A get-only collection property cannot be assigned, so the only way to update it at runtime would be to clear and refill the existing collection in place. That is not safe: application code enumerating the collection on another thread during the reload would observe a torn result or throw, because the mutation is not atomic. For this reason a get-only collection property is bound once at startup and is not hot reloaded.

This limitation applies only to the raw injected types (the configuration class and its interfaces). It does not affect IOptionsSnapshot<T> or IOptionsMonitor<T>, which always reflect the latest configuration because each access binds a fresh instance with the collection populated from the current configuration. If a configuration type has a get-only collection that must observe changes at runtime, either make the property settable on the class (see below) or consume the configuration through IOptionsSnapshot<T>/IOptionsMonitor<T> instead of the raw injected type.

To make a collection hot reload, declare it as a settable property ({ get; set; }) on the class so the reloaded collection is assigned atomically, and expose it as a get-only property on any interface so application code still cannot replace it:

using System.Collections.Generic;
using System.Collections.ObjectModel;

public interface IRequiredDataConfig
{
    // Get-only on the interface: callers cannot replace the collection.
    ICollection<ConfiguredData> RequiredData { get; }
}

public class RequiredDataConfig : IRequiredDataConfig
{
    // Get/set on the class: hot reload assigns the reloaded collection atomically.
    public ICollection<ConfiguredData> RequiredData { get; set; } = new Collection<ConfiguredData>();
}

The source generator reports a get-only collection property as warning NCDI002. Two code fixes are offered: one adds the setter for you, and one applies [SkipConfigProperty] to the property. If startup-only binding is intentional and the collection does not need to hot reload, keep the get-only property and either suppress NCDI002 for it or exclude it with [SkipConfigProperty].

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 walks the type hierarchy from the root configuration type at compile time using a source generator, 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.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0-beta.9 39 6/30/2026
2.0.0-beta.8 35 6/30/2026
2.0.0-beta.7 44 6/29/2026
2.0.0-beta.6 46 6/29/2026
2.0.0-beta.5 48 6/29/2026
2.0.0-beta.4 55 6/29/2026
2.0.0-beta.3 45 6/29/2026
2.0.0-beta.2 44 6/29/2026
1.4.1-beta.1 52 6/27/2026
1.4.0 669 8/4/2024
1.3.2-beta0001 185 6/9/2024
1.3.1 264 5/23/2024
1.3.1-beta0001 219 5/23/2024
1.3.0 261 5/23/2024
1.3.0-beta0002 183 5/23/2024
1.2.1-beta0001 212 5/23/2024
1.2.0 333 4/13/2024
1.2.0-beta0002 228 4/13/2024
1.2.0-beta0001 194 4/13/2024
1.1.0 260 9/28/2023
Loading failed