ConfigContraband 0.1.7

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

<p align="center"> <img src="assets/configcontraband-icon.png" width="96" height="96" alt="ConfigContraband icon"> </p>

ConfigContraband

CI CodeQL codecov

Stop smuggling broken appsettings into production.

ConfigContraband is a high-signal Roslyn analyzer for .NET configuration, ASP.NET Core Options, appsettings.json, ValidateOnStart(), and ValidateDataAnnotations(). It catches the configuration mistakes that compile cleanly, pass code review, and then fail at startup or, worse, on first use.

It focuses on the boring production failures:

  • a section name typo in BindConfiguration(...)
  • validation that exists but does not run on startup
  • [Required] properties that are never wired into Options validation
  • nested options that look validated but are silently skipped
  • misspelled JSON keys hiding under a bound section

Use it when your app relies on strongly typed options and you want configuration validation feedback in the editor, in pull requests, and in CI before a bad setting reaches production.

Feature Snapshot

Area What ConfigContraband does
Section binding Checks supported options bindings against visible appsettings*.json files.
Startup validation Flags options validation that is registered but not forced to run at startup.
DataAnnotations Finds [Required], [Range], and inherited validation attributes without ValidateDataAnnotations().
Nested validation Detects nested options objects and collections that need recursive validation attributes.
JSON key drift Reports likely misspelled keys under bound sections while staying conservative for flexible binding shapes.

Install

<PackageReference Include="ConfigContraband" Version="0.1.6" PrivateAssets="all" />

The package includes buildTransitive props that pass visible appsettings*.json files to the analyzer automatically. Add the package, build, and let your editor or CI tell you when your options contract and configuration drift apart.

No runtime dependency is added to your app. ConfigContraband runs as an analyzer during build and in supported IDEs.

What It Looks At

ConfigContraband analyzes options registrations shaped like this:

services.AddOptions<StripeOptions>()
    .BindConfiguration("Stripe");

It also recognizes the common explicit-section style:

services.AddOptions<StripeOptions>()
    .Bind(configuration.GetSection("Stripe"));

services.Configure<StripeOptions>(
    configuration.GetSection("Stripe"));

The section name must be a compile-time string literal. The analyzer follows normal fluent chains:

services.AddOptions<StripeOptions>()
    .BindConfiguration("Stripe")
    .ValidateDataAnnotations()
    .ValidateOnStart();

It also follows immediate same-block local OptionsBuilder<T> chains:

var optionsBuilder = services.AddOptions<StripeOptions>()
    .BindConfiguration("Stripe");

optionsBuilder.ValidateDataAnnotations();
optionsBuilder.ValidateOnStart();

When the analyzer cannot prove a configuration shape statically, it stays quiet. The goal is high-signal feedback, not noisy guesses.

Rules

ID Rule Default Catches
CFG001 Bound configuration section does not exist Warning BindConfiguration("Strpie") when only Stripe exists.
CFG003 Options validation does not run on startup Warning Validation is registered but ValidateOnStart() is missing.
CFG004 DataAnnotations are not enabled for options validation Warning [Required], [Range], and inherited annotations without ValidateDataAnnotations().
CFG005 Nested options validation is not recursive Warning Nested objects or item types with annotations but no recursive validation attribute.
CFG006 Unknown configuration key under bound section Info JSON keys that do not match bindable options properties or aliases.

Fast Feedback Loop

The repository includes a showcase project with one intentional example for each rule:

dotnet build samples/ConfigContraband.Showcase/ConfigContraband.Showcase.csproj --configuration Release --no-incremental

The sample stays out of the main solution so normal development builds remain clean.

Rule Details

CFG001: The Section Must Exist

If your code binds "Stripe", a visible appsettings*.json file should contain a matching Stripe section.

Before:

services.AddOptions<StripeOptions>()
    .BindConfiguration("Strpie")
    .ValidateDataAnnotations()
    .ValidateOnStart();
{
  "Stripe": {
    "ApiKey": "secret"
  }
}

After:

services.AddOptions<StripeOptions>()
    .BindConfiguration("Stripe")
    .ValidateDataAnnotations()
    .ValidateOnStart();

When ConfigContraband sees a likely typo, it can offer a code fix. Nested section paths use the same colon-separated shape as .NET configuration:

services.AddOptions<StripeOptions>()
    .BindConfiguration("Features:Stripe")
    .ValidateDataAnnotations()
    .ValidateOnStart();
{
  "Features": {
    "Stripe": {
      "ApiKey": "secret"
    }
  }
}

For nested typos, the fix keeps the parent path and replaces only the bad leaf section. If the code says Features:Strpie and the file contains Features:Stripe, the fix changes it to Features:Stripe.

The analyzer checks every visible appsettings*.json additional file for section existence, including commented files and duplicate JSON section members when resolving nested section paths. It stays quiet when no appsettings files are available because it cannot prove what configuration exists at runtime.

CFG003: Validation Should Run When The App Starts

Options validation often runs later, when options are first used. ValidateOnStart() moves that failure to startup, where it belongs.

Before:

services.AddOptions<StripeOptions>()
    .BindConfiguration("Stripe")
    .ValidateDataAnnotations();

After:

services.AddOptions<StripeOptions>()
    .BindConfiguration("Stripe")
    .ValidateDataAnnotations()
    .ValidateOnStart();

The code fix appends ValidateOnStart() in the same style as the existing registration chain, including multiline chains and immediate same-block local OptionsBuilder<T> chains. Registrations that start with AddOptionsWithValidateOnStart<TOptions>() already run validation at startup, so CFG003 stays quiet for that shape.

CFG004: DataAnnotations Must Be Switched On

Attributes such as [Required] do nothing for Options validation unless ValidateDataAnnotations() is registered. Inherited bindable properties count too, so a base options class with DataAnnotations still needs validation enabled on the derived options registration.

Before:

public class BillingOptions
{
    [Required]
    public string ApiKey { get; set; } = "";
}

public sealed class StripeOptions : BillingOptions
{
    public string WebhookSecret { get; set; } = "";
}

services.AddOptions<StripeOptions>()
    .BindConfiguration("Stripe")
    .ValidateOnStart();

After:

services.AddOptions<StripeOptions>()
    .BindConfiguration("Stripe")
    .ValidateDataAnnotations()
    .ValidateOnStart();

Validate(...) counts as validation for CFG003, but it does not satisfy CFG004 when DataAnnotations attributes are present.

The code fix preserves existing fluent-chain formatting, adds ValidateDataAnnotations(), and only adds ValidateOnStart() when startup validation is not already present, including registrations started with AddOptionsWithValidateOnStart<TOptions>().

CFG005: Nested Options Need Recursive Validation

DataAnnotations do not automatically walk into child objects or collection items. If a nested class or collection item has validation attributes anywhere in its bindable object graph, mark each parent property that should be checked recursively.

Before:

public sealed class AppOptions
{
    public DatabaseOptions Database { get; set; } = new();
}

public sealed class DatabaseOptions
{
    [Required]
    public string ConnectionString { get; set; } = "";
}

After:

using Microsoft.Extensions.Options;

public sealed class AppOptions
{
    [ValidateObjectMembers]
    public DatabaseOptions Database { get; set; } = new();
}

public sealed class DatabaseOptions
{
    [Required]
    public string ConnectionString { get; set; } = "";
}

For arrays and other IEnumerable<T> option collections, use [ValidateEnumeratedItems]. The code fix updates the file that owns the options property, adds using Microsoft.Extensions.Options; when needed, and keeps existing property comments in place. CFG005 does not report interface-typed nested properties or system scalar types because the Options validator cannot safely infer a concrete object graph for those shapes.

CFG006: Config Keys Should Match Options Properties

Keys under a bound section should match public bindable properties, or a [ConfigurationKeyName] alias.

Before:

{
  "Stripe": {
    "ApiKey": "secret",
    "WebookSecret": "typo"
  }
}
public sealed class StripeOptions
{
    public string ApiKey { get; set; } = "";
    public string WebhookSecret { get; set; } = "";
}

After:

{
  "Stripe": {
    "ApiKey": "secret",
    "WebhookSecret": "secret"
  }
}

CFG006 is informational because .NET configuration binding allows flexible shapes. It is still useful for catching the typos that hide in environment-specific settings.

Visible appsettings*.json files are treated as a merged configuration view for unknown-key checks, including files with // or /* ... */ comments. If a bound section appears in appsettings.json and appsettings.Production.json, keys from both files are checked. Nested options objects, arrays or lists of nested options objects, strongly typed dictionary values, and dictionary values that bind to collections of nested options objects are checked recursively, so typos under Servers:0:Port, Servers:primary:Port, or ServersByRegion:eu:0:Port-style data can still be found.

Dictionary entry names and scalar array items are treated as values rather than property names. Arbitrary keys under Dictionary<string, string> and values inside string[] are not reported as unknown options properties.

Design Principles

  • Prefer warnings for configuration failures that are likely to break production.
  • Keep flexible binding shapes quiet when static proof is weak.
  • Offer fixes only when the rewrite is narrow and deterministic.
  • Treat appsettings*.json as the contract your options classes are supposed to honor.

Current Scope

ConfigContraband currently focuses on:

  • appsettings*.json files.
  • AddOptions<T>().BindConfiguration("Section") registrations.
  • AddOptions<T>().Bind(configuration.GetSection("Section")) and GetRequiredSection(...) registrations.
  • Direct Configure<T>(configuration.GetSection("Section")) registrations for section and JSON-key drift.
  • String-literal section names.
  • Public bindable properties on options types, including inherited bindable properties.
  • [ConfigurationKeyName] aliases.
  • Normal fluent chains and immediate same-block local OptionsBuilder<T> chains.

It does not try to prove every possible dynamic configuration shape. When the analyzer cannot see enough static information, it stays quiet.

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

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
0.3.1 91 5/20/2026
0.2.0 97 5/19/2026
0.1.11 95 5/5/2026
0.1.10 90 5/3/2026
0.1.9 89 5/3/2026
0.1.8 95 4/29/2026
0.1.7 97 4/29/2026
0.1.6 99 4/29/2026
0.1.5 94 4/29/2026
0.1.4 95 4/29/2026
0.1.3 100 4/29/2026
0.1.2 93 4/29/2026
0.1.1 106 4/28/2026
0.1.0 95 4/28/2026

Honor AddOptionsWithValidateOnStart registrations when checking startup validation.