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
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-packages2.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.
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.
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 runWith 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.
$ 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 15.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.
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 test6.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// 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
- The docuconf-dotnet README: every type, file input and option.
- The orders example, with its smoke test.
- The same service in every language, side by side.
- What every SDK supports, and this one's known gaps: Platform-mounted appsettings overlays load through AddDocuconfOverlays<T>(). Export runs at runtime; a build-time source generator is planned.
Every snippet on this page is compiled or run against docuconf-dotnet's main branch in CI. Facts checked 2026-10-08.