OptionsProvider 0.5.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package OptionsProvider --version 0.5.1
                    
NuGet\Install-Package OptionsProvider -Version 0.5.1
                    
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="OptionsProvider" Version="0.5.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OptionsProvider" Version="0.5.1" />
                    
Directory.Packages.props
<PackageReference Include="OptionsProvider" />
                    
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 OptionsProvider --version 0.5.1
                    
#r "nuget: OptionsProvider, 0.5.1"
                    
#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 OptionsProvider@0.5.1
                    
#: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=OptionsProvider&version=0.5.1
                    
Install as a Cake Addin
#tool nuget:?package=OptionsProvider&version=0.5.1
                    
Install as a Cake Tool

OptionsProvider

Enables loading configurations from JSON files, YAML files, or your own IConfigurationSources to manage options for experiments.

Features:

  • Each feature flag can be represented by a JSON or YAML file which contains options to override default configuration values when processing feature names or experiment names in a request. Note that YAML support is still experimental and parsing may change.
  • Reads separate files in parallel to keep independent configurations clear and easily maintainable.
  • Supports clear file names and aliases for feature names.
  • Uses the same logic that ConfigurationBuilder uses to load files so that it's easy to understand as it's the same as how appsettings*.json files are loaded.
  • Caching: Built configuration objects are cached by default in IMemoryCache to avoiding rebuilding the same objects for the same feature names.

This project mainly focuses on supporting features backed by configurations in files with your source code because that's the most clear way for developers to see what values are supported for different configurable options. Tools like Azure App Configuration to control options externally while the service is running can be used with this library as this library accepts custom IConfigurationSources and overrides the current default IConfiguration when given feature names.

Installation

dotnet add package OptionsProvider

See more at NuGet.org.

Example

Suppose you have a class that you want to use to configure your logic:

internal sealed class MyConfiguration
{
    public string[]? Array { get; set; }
    public MyObject? Object { get; set; }
}

You probably already use Configuration in .NET and build an IConfiguration to use in your service based on some default settings in an appsettings.json file. Suppose you have an appsettings.json like this to configure MyConfiguration:

{
    "config": {
        "array": [
            "default item 1"
        ],
        "object": {
            "one": 1,
            "two": 2
        }
    },
    "another config": {
        ...
    }
}

Note: Do not put default values directly in the class because they cannot be overriden to null. Instead, put defaults in appsettings.json for clarity because it's easier to see them in configuration files instead of classes. Default values in appsettings.json cannot be overridden to null either. If a value needs to be set to null for a feature, then do not set a default in appsettings.json either.

Now you want to start experimenting with different values deep within MyConfiguration.

Create a new folder for configurations files, for this example, we'll call it Configurations and add some files to it. All *.json, *.yaml, and *.yml files in Configurations and any of its subdirectories will be loaded into memory.

Configurations/feature_A.json:

{
    "metadata": {
        "aliases": [ "a" ],
        "owners": "a-team@company.com"
    },
    "options": {
        "config": {
            "array": [
                "example item 1"
            ]
        }
    }
}

Configurations/feature_B/initial.yaml:

metadata:
    aliases:
        - "b"
    owners: "team-b@company.com"
options:
    config:
        array:
            - "different item 1"
            - "item 2"
        object:
            one: 11
            two: 22
            three: 3

When setting up your IServiceCollection for your service, do the following:

services
    .AddOptionsProvider("Configurations")
    .ConfigureOptions<MyConfiguration>("config")

There are two simple ways to get the right version of MyConfiguration for the current request based on the enabled features.

Using IOptionsProvider Directly

You can the inject IOptionsProvider into classes to get options for a given set of features. Features names are not case-sensitive.

using OptionsProvider;

class MyClass(IOptionsProvider optionsProvider)
{
    void DoSomething(...)
    {
        MyConfiguration options = optionsProvider.GetOptions<MyConfiguration>("config", ["A"]);
        // `options` be a result of merging the default values from appsettings.json, then applying Configurations/feature_A.json
        // because "a" is an alias for feature_A.json and aliases are case-insensitive.
    }
}

Using IOptionsSnapshot

Alternatively, you can also use IOptionsSnapshot<MyConfiguration> and follow .NET's Options pattern.

When a request starts, set the feature names based on the enabled features in your system (for example, the enabled features could be passed in a request body or from headers):

using OptionsProvider;

class MyController(IFeaturesContext context)
{
    public void InitializeContext(string[] enabledFeatures)
    {
        context.FeatureNames = enabledFeatures;
    }
}

Then while processing the request, IFeaturesContext will automatically be used to get the right configuration for the current request based on the enabled features. To use this method, MyConfiguration must have public setters for all of its properties.

In your code, you can use IOptionsSnapshot<MyConfiguration> to get the right configuration for the current request based on the enabled features:

class MyClass(IOptionsSnapshot<MyConfiguration> options)
{
    void DoSomething(...)
    {
        MyConfiguration options = options.Value;
    }
}

If enabledFeatures is ["A", "B"], then MyConfiguration will be built in this order:

  1. Apply the default values the injected IConfiguration, i.e. the values from appsettings.json under "config".
  2. Apply the values from Configurations/feature_A.json.
  3. Apply the values from Configurations/feature_B/initial.yaml.

Caching

["A", "B"] is treated the same as ["a", "FeAtuRe_B/iNiTiAl"] because using an alias is equivalent to using the path to the file and names and aliases are case-insensitive. Both examples would retrieve the same instance from the cache and IOptionsProvider would return the same instance. If IOptionsSnapshot<MyConfiguration> is used, then MyConfiguration will still only be built once and cached, but a different instance would be returned from IOptionsSnapshot<MyConfiguration>.Value for each scope because the options pattern creates a new instance each time.

Preserving Configuration Files

To ensure that the latest configuration files are used when running your service or tests, you may need to ensure the Configuration folder gets copied to the output folder. In your .csproj files with the Configurations folder, add a section like:

<ItemGroup>
  <Content Include="Configurations/**">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </Content>
</ItemGroup>

or there are already rule about including files, but the configuration file for a feature isn't found, you can try:

<ItemGroup>
  <None Include="Configurations/**">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
</ItemGroup>

Configuration Building Examples

Arrays/Lists

Note that ConfigurationBuilder does not concatenate lists, it merges them and overwrites entries because it treats each item in a list like a value in a dictionary indexed by the item's index.

For example, if the following features are applied:

Configurations/feature_A.yaml:

options:
  config:
    array:
      - 1
      - 2

Configurations/feature_B.yaml:

options:
  config:
    array:
      - 3

The resulting MyConfiguration for ["feature_A", "feature_B"] will have array set to [3, 2] because the second list is applied 'on top of' the first list. The builder views the lists as:

array from Configurations/feature_A.yaml:
array:0 = 1
array:1 = 2

array from Configurations/feature_B.yaml:
array:0 = 3

so the merged result is:
array:0 = 3
array:1 = 2

So array becomes [3, 2].

For more details, see here.

Development

Code Formatting

CI enforces:

dotnet format --verify-no-changes --severity info --no-restore src/*/*.sln

To automatically format code, run:

dotnet format --severity info --no-restore src/*/*.sln

Publishing

From the dotnet folder in the root of the repo, run:

api_key=<your NuGet API key>
cd src/OptionsProvider
dotnet pack --configuration Release
dotnet nuget push OptionsProvider/bin/Release/OptionsProvider.*.nupkg  --source https://api.nuget.org/v3/index.json -k $api_key --skip-duplicate
Product Compatible and additional computed target framework versions.
.NET 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 was computed.  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 was computed.  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. 
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
1.5.0 181 12/20/2024
1.4.0 144 12/20/2024
1.3.0 130 12/19/2024
1.2.2 162 12/5/2024
1.2.1 141 11/29/2024
1.2.0 138 11/28/2024
1.1.0 141 11/27/2024
1.0.1 154 11/25/2024
1.0.0 186 10/3/2024
0.6.0 145 10/2/2024
0.5.1 156 9/11/2024
0.5.0 170 9/11/2024
0.4.0 191 7/21/2024
0.3.0 141 7/9/2024
0.2.1 148 6/26/2024
0.2.0 148 6/24/2024
0.1.0 166 6/24/2024