Skip to content
docuconf
docuconf on GitHub

Get started / .NET

docuconf for .NET

Your options class, with the DataAnnotations you already use, plus [Secret], [UrlSchemes] and [Description]. AddDocuconf<T>() binds it, loads its files and validates everything on start.

Builds on
Options pattern + appsettings
Package
Docuconf.Options (NuGet)
Requires
.NET 8, .NET 10
Status
v0.1 alpha, not yet released · conformance 132 of 134 (skips json-schema)
Source
docuconf-dotnet · the orders example

Not published yet

Every docuconf SDK is a v0.1 alpha and none is on a package registry yet, so step 1 installs from the main branch. After the first release it will be dotnet add package Docuconf.Options --prerelease.

The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.

1.Install

The package is not on NuGet yet. Pack it from the main branch into a local folder and add it from there:

git clone https://github.com/Docuconf/docuconf-dotnet ../docuconf-dotnet
dotnet pack ../docuconf-dotnet/src/Docuconf -c Release -o ../docuconf-packages
dotnet add package Docuconf.Options --prerelease --source ../docuconf-packages

2.Declare your configuration

An ordinary options class. [Range], [AllowedValues], [MinLength] and [Required] become contract constraints; docuconf adds [ConfigContract], [Secret] and [UrlSchemes]. Every property needs a description: [Description], or the XML doc <summary>, whose <remarks> become the details. The section Orders means Orders:Port is the environment variable ORDERS__PORT.

OrdersOptions.cs
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Docuconf;

namespace Orders.Api;

// The Orders section: Orders:Port is the environment variable ORDERS__PORT, and so on.
// Initializers are the defaults the contract records.
[ConfigContract("orders-api", Section = "Orders")]
public sealed class OrdersOptions
{
    [Range(1, 65535)]
    [Description("HTTP listen port")]
    public int Port { get; set; } = 8080;

    [AllowedValues("debug", "info", "warn", "error")]
    [Description("Minimum level of log messages to write")]
    public string LogLevel { get; set; } = "info";

    // [Secret]: the platform must supply it from a Kubernetes Secret, and docuconf never prints it.
    // [MaxLength] bounds the URL in characters; a longer one fails startup with out_of_range.
    [Required, Secret, UrlSchemes("postgres"), MaxLength(2048)]
    [Description("Postgres connection string for the orders database")]
    public string DatabaseUrl { get; set; } = "";

    // A list arrives as ORDERS__ALLOWEDORIGINS__0, ORDERS__ALLOWEDORIGINS__1, ...
    [MinLength(1)]
    [Description("Origins allowed to call the API from a browser")]
    public List<string> AllowedOrigins { get; set; } = ["http://localhost:3000"];

    // A TimeSpan arrives as hh:mm:ss (00:00:30); the platform writes "30s" and renders it that way.
    [Range(typeof(TimeSpan), "00:00:01", "00:05:00")]
    [Description("Time allowed to handle one request")]
    public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(30);

    // An XML doc comment works instead of [Description]: the <summary> is the description, and the <remarks> are the
    // details, longer docs for docuconf docs (the project sets GenerateDocumentationFile).

    /// <summary>Background workers that process new orders.</summary>
    /// <remarks>
    /// <para>
    /// Each worker holds one connection from the pool of <see cref="DatabaseUrl"/>, so keep this below the database's
    /// connection limit.
    /// </para>
    /// <list type="bullet">
    /// <item><description>Raise it when the order queue backs up.</description></item>
    /// <item><description>Lower it when the database is the bottleneck.</description></item>
    /// </list>
    /// </remarks>
    [Range(1, 64)]
    public int WorkerCount { get; set; } = 4;
}

3.Load it at boot

builder.AddDocuconf<T>() replaces AddOptions<T>().BindConfiguration(...).ValidateOnStart(): it binds the section, loads file inputs and runs every docuconf check. LoadOrExit<T>() returns the validated options, or prints every problem and exits 1. DocuconfExport.RunIfRequested turns the app into its own exporter.

Program.cs
if (DocuconfExport.RunIfRequested(args)) return;

var builder = WebApplication.CreateBuilder(args);
builder.AddDocuconf<OrdersOptions>();   // binds the Orders section and validates it at startup
var app = builder.Build();

// The validated options, or every problem on stderr and exit status 1.
var orders = app.Services.LoadOrExit<OrdersOptions>();

4.Run it, and see an error

Run the example with a valid environment:

ORDERS__DATABASEURL=postgres://orders:pw@localhost:5432/orders dotnet run

With ORDERS__PORT=0 and no ORDERS__DATABASEURL, the service refuses to start, lists every problem with its error code and exits 1. The same lines go to /dev/termination-log.

Terminal
$ ORDERS__PORT=0 dotnet run
docuconf: 2 configuration problems:
  [missing_required] ORDERS__DATABASEURL: is required (Orders:DatabaseUrl)
  [out_of_range] ORDERS__PORT: '0' is below the minimum 1

5.Test your configuration

Bind from an in-memory configuration instead of the environment, then read IOptions<T>.Value: it throws OptionsValidationException with one failure per problem. This is an xUnit test project next to the app.

Orders.Tests/OrdersOptionsTests.cs
using Docuconf;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Orders.Api;
using Xunit;

public class OrdersOptionsTests
{
    // Build the options from an in-memory configuration: no environment variables are read or set.
    static OrdersOptions Load(Dictionary<string, string?> values)
    {
        var configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build();
        var services = new ServiceCollection().AddSingleton<IConfiguration>(configuration);
        services.AddDocuconf<OrdersOptions>(s => s.TerminationLogPath = Path.GetTempFileName());
        return services.BuildServiceProvider().GetRequiredService<IOptions<OrdersOptions>>().Value;
    }

    [Fact]
    public void Defaults()
    {
        var orders = Load(new() { ["Orders:DatabaseUrl"] = "postgres://orders@db/orders" });
        Assert.Equal(8080, orders.Port);
        Assert.Equal(TimeSpan.FromSeconds(30), orders.RequestTimeout);
    }

    [Fact]
    public void RejectsBadValues()
    {
        var e = Assert.Throws<OptionsValidationException>(() => Load(new() { ["Orders:Port"] = "70000" }));
        Assert.Contains(e.Failures, f => f.StartsWith("[out_of_range] ORDERS__PORT"));
        Assert.Contains(e.Failures, f => f.StartsWith("[missing_required] ORDERS__DATABASEURL"));
    }
}
dotnet test

6.Export the contract

Export runs the built app with docuconf export, so it includes the appsettings*.json files that ship with it: their values become defaults and per-environment profiles. There is no build-time source generator yet.

dotnet build -c Release
dotnet bin/Release/net10.0/Orders.Api.dll docuconf export contract.cue
contract.cue
// Code generated by docuconf. DO NOT EDIT.
package orders_api

import "docuconf.dev/contract"

contract.#Contract & {
	apiVersion: "docuconf.dev/v1alpha1"
	kind: "ConfigContract"
	metadata: {
		name: "orders-api"
		generator: {language: "dotnet", sdk: "Docuconf.Options", version: "0.1.0-alpha.1"}
	}
	vars: {
		ORDERS__ALLOWEDORIGINS: {
			type: "list"
			description: "Origins allowed to call the API from a browser"
			configKey: "Orders:AllowedOrigins"
			items: "string"
			encoding: "indexed"
			minItems: 1
			default: ["http://localhost:3000"]
		}
		ORDERS__DATABASEURL: {
			type: "url"
			description: "Postgres connection string for the orders database"
			required: true
			secret: true
			configKey: "Orders:DatabaseUrl"
			schemes: ["postgres"]
			maxLength: 2048
		}
		ORDERS__LOGLEVEL: {
			type: "enum"
			description: "Minimum level of log messages to write"
			configKey: "Orders:LogLevel"
			values: ["debug", "info", "warn", "error"]
			default: "info"
		}
		ORDERS__PORT: {
			type: "int"
			description: "HTTP listen port"
			configKey: "Orders:Port"
			min: 1
			max: 65535
			default: 8080
		}
		ORDERS__REQUESTTIMEOUT: {
			type: "duration"
			description: "Time allowed to handle one request"
			configKey: "Orders:RequestTimeout"
			encoding: "timespan"
			min: "1s"
			max: "5m"
			default: "30s"
		}
		ORDERS__WORKERCOUNT: {
			type: "int"
			description: "Background workers that process new orders"
			details: "Each worker holds one connection from the pool of `OrdersOptions.DatabaseUrl`, so keep this below the database's connection limit.\n\n- Raise it when the order queue backs up.\n- Lower it when the database is the bottleneck."
			configKey: "Orders:WorkerCount"
			min: 1
			max: 64
			default: 4
		}
	}
}

The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.

7.ASP.NET Core and the generic host

AddDocuconf<T>() is an IServiceCollection extension, so it works in ASP.NET Core, worker services and any generic host. Inject IOptions<T>, or IOptionsMonitor<T> for values that reload. A platform-mounted appsettings.Production.json overlay loads through builder.Configuration.AddDocuconfOverlays<T>(); see the README.

Next

Every snippet on this page is compiled or run against docuconf-dotnet's main branch in CI. Facts checked 2026-10-08.