惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

SecWiki News
SecWiki News
量子位
The Cloudflare Blog
美团技术团队
T
The Exploit Database - CXSecurity.com
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
P
Proofpoint News Feed
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 三生石上(FineUI控件)
T
Tor Project blog
博客园 - 司徒正美
宝玉的分享
宝玉的分享
T
Threatpost
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
T
Threat Research - Cisco Blogs
Hacker News: Ask HN
Hacker News: Ask HN
Jina AI
Jina AI
博客园 - 聂微东
A
Arctic Wolf
I
Intezer
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Know Your Adversary
Know Your Adversary
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
爱范儿
爱范儿
Hugging Face - Blog
Hugging Face - Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
小众软件
小众软件
T
Tailwind CSS Blog
The Hacker News
The Hacker News
L
LINUX DO - 最新话题
Hacker News - Newest:
Hacker News - Newest: "LLM"
WordPress大学
WordPress大学
S
SegmentFault 最新的问题
TaoSecurity Blog
TaoSecurity Blog
Project Zero
Project Zero
博客园 - 叶小钗
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cloudbric
Cloudbric
雷峰网
雷峰网
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
大猫的无限游戏
大猫的无限游戏
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Troy Hunt's Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
V2EX - 技术
V2EX - 技术
The GitHub Blog
The GitHub Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Privacy & Cybersecurity Law Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Introducing Kiwi Config: Simplifying Configuration Management in .NET Ecosystems
Ajay Jain · 2026-05-06 · via DEV Community

Introduction to Kiwi Config

Kiwi Config (Kiwify.Kiwi.Configuration) is an attribute-driven configuration library for .NET. This article walks through every concept and feature with detailed explanations and practical examples - from the simplest binding to nested hierarchies, dynamic defaults, and collection handling.


The Problem Kiwi Config Solves

Every .NET application reads configuration. The platform gives you IConfiguration, which is flexible but stringly-typed. A call like config["database:port"] compiles regardless of whether that key exists. A typo is silent. A missing key returns null. The code that reads configuration is scattered across the application, and there is no single place that says "this is everything this service reads from config."

ConfigurationBinder.Bind() and IOptions<T> improve on this by binding to typed objects, but they work by name matching convention - the class does not explicitly declare which key it reads, what default to use when the key is absent, or which fields are required. You learn about required-field problems at runtime, not at load time.

Kiwi Config takes a different position: configuration errors should fail at startup, not at runtime. If your service can start, its configuration is valid.

The config class is the schema. Every key a class reads, every default it applies, and every field it requires is declared as an attribute directly on the class. The library reads those declarations once at startup and populates the object. After that, the rest of the application works with a strongly-typed, fully-populated object - no string lookups, no nullable coalescing, no silent failures.


Implicit vs. Explicit Schema

Traditional .NET configuration uses an implicit schema: what a class reads from config is scattered across string lookups, discoverable only by reading the calling code. Kiwi Config uses an explicit schema: the class declares everything it reads, and the library enforces it at load time.

IConfiguration (traditional) Kiwi Config
Schema location Scattered across call sites Declared on the class
Defaults Hardcoded at each call site [ConfigKey("key", defaultValue)]
Required fields Not enforced Required = true - validated at startup
Failure point Runtime - first access of a missing key Startup - before the app handles requests
Discovery Read all callers Open the file

The same information that was once implied is now declared. That is the complete picture of what changes.


Source & Repository

Kiwi Config is open-source and available on GitHub:

https://github.com/kiwifylabs/kiwi-foundation-config-di


Installation

NuGet Package

<PackageReference Include="Kiwify.Kiwi.Configuration" Version="1.0.0" />

Enter fullscreen mode Exit fullscreen mode

.NET CLI

dotnet add package Kiwify.Kiwi.Configuration --version 1.0.0

Enter fullscreen mode Exit fullscreen mode

Source Code

https://github.com/kiwifylabs/kiwi-foundation-config-di

No other packages are required. Microsoft.Extensions.Configuration is included transitively.


Related Articles


The Basics: Your First Config Class

A config class needs two things: a [ConfigSection] attribute on the class that declares where in the configuration hierarchy the class lives, and [ConfigKey] attributes on each property that declare which key to read and what default to use.

using Kiwify.Kiwi.Platform.Configuration.Attributes;

[ConfigSection("app")]
public class AppConfig
{
    [ConfigKey("name", "MyApp")]
    public string Name { get; private set; } = string.Empty;

    [ConfigKey("port", 5000)]
    public int Port { get; private set; }

    [ConfigKey("debug", false)]
    public bool Debug { get; private set; }
}

Enter fullscreen mode Exit fullscreen mode

appsettings.json:

{
  "app": {
    "name": "ProductionApp",
    "port": 8080,
    "debug": false
  }
}

Enter fullscreen mode Exit fullscreen mode

To load it:

using Kiwify.Kiwi.Platform.Configuration;
using Microsoft.Extensions.Configuration;

var configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables()
    .Build();

var appConfig = configuration.LoadConfiguration<AppConfig>();
Console.WriteLine(appConfig.Name);  // ProductionApp
Console.WriteLine(appConfig.Port);  // 8080
Console.WriteLine(appConfig.Debug); // False

Enter fullscreen mode Exit fullscreen mode

LoadConfiguration<T> is an extension method on IConfiguration. It creates a new instance of T and walks every property with a [ConfigKey] attribute, reads the value from configuration, converts it to the property type, and sets it. It returns the fully populated instance.


[ConfigSection] - Declaring the Root Key

[ConfigSection] tells the library which key in the configuration hierarchy this class belongs to.

[ConfigSection("database")]
public class DatabaseConfig { ... }

Enter fullscreen mode Exit fullscreen mode

When you call configuration.LoadConfiguration<DatabaseConfig>(), the library uses "database" as the prefix for every key it reads from that class. A property bound to "port" reads "database:port" from IConfiguration.

What [ConfigSection] is not:

  • It is not an absolute path. The key is always the starting prefix, and nested objects append to it.
  • It cannot use / to escape to the root. Only [ConfigKey] supports absolute paths.
  • Every class passed directly to LoadConfiguration<T>() must have [ConfigSection]. Every class used as a [ConfigObject] property must also have it.

[ConfigKey] - Binding a Property

[ConfigKey] maps a property to a configuration key within the class's section.

[ConfigKey("port", 5432)]
public int Port { get; private set; }

Enter fullscreen mode Exit fullscreen mode

The first argument is the key name (relative to the section). The second argument is the default value used when the key is absent from configuration.

Setter support

Private setters, protected setters, and init-only properties are all supported. The library sets values via reflection. The init restriction is compiler-enforced at compile time, not at runtime, so reflection bypasses it cleanly.

[ConfigKey("timeout", 30)]
public int Timeout { get; private set; }   // private setter - supported

[ConfigKey("name", "default")]
public string Name { get; init; } = string.Empty;  // init-only - supported

Enter fullscreen mode Exit fullscreen mode

No default

If you omit the default argument and the key is absent, the property gets the type's zero value (0, false, null). For strings, that means null. If you want string.Empty instead, use [ConfigKey("key", "")] or a GetDefaultName() method. This is the lowest-priority fallback.

[ConfigKey("retryCount")]
public int RetryCount { get; private set; }  // gets 0 if absent

Enter fullscreen mode Exit fullscreen mode

Property Initializers

Property initializers run before configuration binding. The library always overwrites the property - whether it sets a config value, an attribute default, a GetDefault* result, or a type zero value. Initializers are not part of the fallback chain and have no effect on what value the property ends up with after loading.

[ConfigKey("timeout", 30)]
public int Timeout { get; private set; } = 60;  // 60 is always overwritten by binding

Enter fullscreen mode Exit fullscreen mode

This also affects string properties. If a string property has no config value and no default, it will be null after loading, even if the initializer sets it to string.Empty:

[ConfigKey("name")]
public string Name { get; private set; } = "fallback";  // overwritten - gets null if absent

Enter fullscreen mode Exit fullscreen mode

Use [ConfigKey("name", "")] or GetDefaultName() if you need a non-null fallback.


Type Conversions

The library converts configuration string values to property types automatically.

Integers, longs, doubles, decimals

Standard numeric parsing. A value that cannot be converted throws InvalidOperationException immediately with the key path and failed value in the message.

[ConfigKey("port", 5000)]
public int Port { get; private set; }

[ConfigKey("maxFileSize", 104857600L)]
public long MaxFileSize { get; private set; }

[ConfigKey("threshold", 0.95)]
public double Threshold { get; private set; }

Enter fullscreen mode Exit fullscreen mode

Booleans

Booleans accept a wider set of truthy and falsy strings than strict "true"/"false" parsing. This matches how boolean flags commonly appear in environment variables and configuration files.

Truthy Falsy
true, 1, yes, on, enabled false, 0, no, off, disabled
[ConfigKey("metricsEnabled", false)]
public bool MetricsEnabled { get; private set; }

Enter fullscreen mode Exit fullscreen mode

Config value "yes"true. Config value "off"false. Any other value throws.

Enums

Enum values are parsed case-insensitively. The enum type is inferred from the property type.

public enum AppEnvironment { Development, Staging, Production }

[ConfigKey("environment", AppEnvironment.Development)]
public AppEnvironment Environment { get; private set; }

Enter fullscreen mode Exit fullscreen mode

Config value "production" or "Production"AppEnvironment.Production. An unrecognised value throws InvalidOperationException naming the key and the bad value.

Strings

Strings are passed through as-is - no conversion required.

[ConfigKey("connectionString", "")]
public string ConnectionString { get; private set; } = string.Empty;

Enter fullscreen mode Exit fullscreen mode


Default Value Resolution

When a key is absent from configuration, the library resolves a fallback using four levels of priority:

Priority Source Example
1 (highest) Value present in IConfiguration appsettings.json, environment variable
2 DefaultValue argument on [ConfigKey] [ConfigKey("port", 5432)]
3 GetDefault{PropertyName}() static method private static int GetDefaultPort()
4 (lowest) Type's zero value 0, false, null

If [ConfigKey] carries a DefaultValue, the library never calls GetDefault* - the attribute default takes precedence entirely.

[ConfigKey("port", 5432)]     // uses 5432 if absent; GetDefaultPort() is never called
public int Port { get; private set; }

[ConfigKey("maxConnections")] // no attribute default; GetDefaultMaxConnections() is called
public int MaxConnections { get; private set; }

[ConfigKey("name")]           // no attribute default, no GetDefault*; gets "" if absent
public string Name { get; private set; } = string.Empty;

Enter fullscreen mode Exit fullscreen mode


Required Fields

Mark a field as required when it must always be present in configuration and has no sensible default.

[ConfigKey("apiKey", Required = true)]
public string ApiKey { get; private set; } = string.Empty;

[ConfigKey("connectionString", Required = true)]
public string ConnectionString { get; private set; } = string.Empty;

Enter fullscreen mode Exit fullscreen mode

When the key is absent and Required = true:

  • The library does not fall through to GetDefault* or the type default.
  • LoadConfiguration<T> throws InvalidOperationException immediately.
  • The error message includes the full key path and the property name.
InvalidOperationException: Required configuration key 'app:apiKey' is missing
and no default value is provided. Property: AppConfig.ApiKey.

Enter fullscreen mode Exit fullscreen mode

Required = true combined with a DefaultValue is technically valid but redundant - the default satisfies the requirement before it can fire.


The GetDefault Convention

When a default needs to be computed rather than hardcoded - based on environment variables, machine characteristics, or external state - define a static method named GetDefault{PropertyName}() on the class.

[ConfigSection("database")]
public partial class DatabaseConfig
{
    [ConfigKey("maxConnections")]
    public int MaxConnections { get; private set; }

    [ConfigKey("commandTimeout")]
    public int CommandTimeout { get; private set; }
}

public partial class DatabaseConfig
{
    // Called when "database:maxConnections" is absent from config
    private static int GetDefaultMaxConnections()
        => Environment.ProcessorCount * 4;

    // Called when "database:commandTimeout" is absent from config
    private static int GetDefaultCommandTimeout()
        => int.TryParse(Environment.GetEnvironmentVariable("DEFAULT_TIMEOUT"), out var t) ? t : 30;
}

Enter fullscreen mode Exit fullscreen mode

The method is split into a partial class here to keep the defaults separate from the config declarations - a common pattern, not a requirement.

Rules for the method:

  • Must be static. Access modifier does not matter (private, internal, public all work).
  • Return type must exactly match the property type. A mismatch is detected at load time and throws.
  • Must accept no parameters. A method with parameters is detected at load time and throws.
  • If the method throws, the exception propagates uncaught through LoadConfiguration<T>. Guard against exceptions within the method if needed.
  • GetDefault* is never called when [ConfigKey] carries an attribute default - it is only the third-priority fallback.

Nested Configuration with [ConfigObject]

Configuration naturally forms hierarchies. A database section might contain a credentials sub-section. An app section might contain a caching sub-section. [ConfigObject] composes config classes to match the JSON structure.

How path resolution works

When the library encounters a [ConfigObject] property, it reads the nested type's [ConfigSection] key and appends it to the current path prefix using :. All properties of the nested type are then resolved relative to this new prefix.

[ConfigSection("app")]
public class AppConfig
{
    [ConfigKey("name")]
    public string Name { get; private set; } = string.Empty;
    // reads: "app:name"

    [ConfigObject]
    public DatabaseConfig Database { get; private set; } = null!;
    // nested prefix: "app:database"
}

[ConfigSection("database")]
public class DatabaseConfig
{
    [ConfigKey("server")]
    public string Server { get; private set; } = string.Empty;
    // reads: "app:database:server"

    [ConfigKey("port", 5432)]
    public int Port { get; private set; }
    // reads: "app:database:port"

    [ConfigObject]
    public CredentialsConfig Credentials { get; private set; } = null!;
    // nested prefix: "app:database:credentials"
}

[ConfigSection("credentials")]
public class CredentialsConfig
{
    [ConfigKey("username")]
    public string Username { get; private set; } = string.Empty;
    // reads: "app:database:credentials:username"

    [ConfigKey("password", Required = true)]
    public string Password { get; private set; } = string.Empty;
    // reads: "app:database:credentials:password"
}

Enter fullscreen mode Exit fullscreen mode

appsettings.json:

{
  "app": {
    "name": "OrderService",
    "database": {
      "server": "db.prod.example.com",
      "port": 5432,
      "credentials": {
        "username": "svc_orders",
        "password": "s3cr3t"
      }
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Loading AppConfig populates the entire hierarchy in one call:

var appConfig = configuration.LoadConfiguration<AppConfig>();

Console.WriteLine(appConfig.Name);                          // OrderService
Console.WriteLine(appConfig.Database.Server);               // db.prod.example.com
Console.WriteLine(appConfig.Database.Port);                 // 5432
Console.WriteLine(appConfig.Database.Credentials.Username); // svc_orders
Console.WriteLine(appConfig.Database.Credentials.Password); // s3cr3t

Enter fullscreen mode Exit fullscreen mode

Nested types and DI registration

When using Kiwi Config with Kiwi DI, only root config classes need [ConfigService] to be auto-loaded. Nested types like DatabaseConfig and CredentialsConfig in the example above are loaded as part of their parent - they do not need their own [ConfigService] unless you want them registered independently as well.

Circular reference detection

If a [ConfigObject] chain references a type that is already in its own ancestor chain, the library detects the cycle and throws InvalidOperationException. The check is per-load-call and tracks only the current path, so the same type can safely appear in separate, independent branches of the hierarchy.


Absolute Key Paths

Sometimes a property needs to read a value from an entirely different part of the configuration hierarchy - perhaps a global setting that is shared across sections. The / prefix on a [ConfigKey] key escapes the current section prefix and reads from the configuration root.

[ConfigSection("app")]
public class AppConfig
{
    [ConfigKey("name")]
    public string Name { get; private set; } = string.Empty;
    // reads "app:name" (normal, relative to section)

    [ConfigKey("/globalTimeout", 30)]
    public int GlobalTimeout { get; private set; }
    // reads root-level "globalTimeout" - the "app:" prefix is ignored
}

Enter fullscreen mode Exit fullscreen mode

appsettings.json:

{
  "globalTimeout": 60,
  "app": {
    "name": "MyApp"
  }
}

Enter fullscreen mode Exit fullscreen mode

var appConfig = configuration.LoadConfiguration<AppConfig>();
Console.WriteLine(appConfig.GlobalTimeout); // 60 - from root level

Enter fullscreen mode Exit fullscreen mode

Important: The / prefix applies only to [ConfigKey]. There is no equivalent for [ConfigSection] - section keys are always appended to the parent prefix. Only individual properties can escape the hierarchy.

Absolute paths also work inside nested types:

[ConfigSection("database")]
public class DatabaseConfig
{
    [ConfigKey("port", 5432)]
    public int Port { get; private set; }
    // reads "app:database:port" when nested under AppConfig

    [ConfigKey("/sharedSecret", Required = true)]
    public string SharedSecret { get; private set; } = string.Empty;
    // reads root-level "sharedSecret" regardless of nesting depth
}

Enter fullscreen mode Exit fullscreen mode


Collections

Properties typed as arrays or lists are populated by splitting a comma-separated configuration string.

[ConfigSection("app")]
public class AppConfig
{
    [ConfigKey("allowedPorts", "80,443,8080")]
    public int[] AllowedPorts { get; private set; } = Array.Empty<int>();

    [ConfigKey("allowedOrigins")]
    public List<string> AllowedOrigins { get; private set; } = new();

    [ConfigKey("featureFlags")]
    public string[] FeatureFlags { get; private set; } = Array.Empty<string>();
}

Enter fullscreen mode Exit fullscreen mode

appsettings.json:

{
  "app": {
    "allowedPorts": "80,443,8080",
    "allowedOrigins": "https://app.example.com,https://admin.example.com",
    "featureFlags": "analytics,darkMode,betaApi"
  }
}

Enter fullscreen mode Exit fullscreen mode

Supported collection types: T[], List<T>, and any type implementing IEnumerable<T>, IList<T>, or ICollection<T>.

⚠️ Collection Format Limitation

Kiwi Config expects collections as comma-separated strings. JSON array syntax is not supported.

Supported:

{ "app": { "allowedPorts": "80,443,8080" } }

Enter fullscreen mode Exit fullscreen mode

Not supported - will not be parsed as a collection:

{ "app": { "allowedPorts": [80, 443, 8080] } }

Enter fullscreen mode Exit fullscreen mode

If your configuration source uses JSON array syntax, use IConfiguration.GetSection(...).Get<T[]>() directly instead of [ConfigKey].

Edge cases

Input Behaviour
"8080, 8443" (spaces around commas) Whitespace is trimmed - produces [8080, 8443]
"value1,,value2" (double comma) Empty entries are removed - produces ["value1", "value2"]
"" (empty string) Returns an empty collection
"80,abc" for int[] Throws InvalidOperationException naming the key and the bad element

Using LoadConfiguration<T> Standalone

No DI container is required. LoadConfiguration<T> works anywhere you have an IConfiguration - whether or not a service collection exists.

CLI tools

[ConfigSection("tool")]
public class ToolConfig
{
    [ConfigKey("outputPath", "./output")]
    public string OutputPath { get; private set; } = string.Empty;

    [ConfigKey("verbose", false)]
    public bool Verbose { get; private set; }

    [ConfigKey("maxThreads")]
    public int MaxThreads { get; private set; }

    private static int GetDefaultMaxThreads() => Environment.ProcessorCount;
}

var configuration = new ConfigurationBuilder()
    .AddJsonFile("tool.json", optional: true)
    .AddEnvironmentVariables(prefix: "TOOL_")
    .AddCommandLine(args)
    .Build();

var toolConfig = configuration.LoadConfiguration<ToolConfig>();
// Use toolConfig directly - no service collection needed

Enter fullscreen mode Exit fullscreen mode

Test helpers

var inMemoryConfig = new ConfigurationBuilder()
    .AddInMemoryCollection(new Dictionary<string, string?>
    {
        ["database:host"] = "localhost",
        ["database:port"] = "5432",
        ["database:name"] = "testdb"
    })
    .Build();

var dbConfig = inMemoryConfig.LoadConfiguration<DatabaseConfig>();
// Use dbConfig to construct the class under test directly

Enter fullscreen mode Exit fullscreen mode

In-memory configuration is particularly useful in tests because it does not require files on disk and the values are explicit in the test code.

Pre-container startup code

// Read config before the DI container is built - for use in middleware setup, logging, etc.
var appConfig = configuration.LoadConfiguration<AppConfig>();
SetupLogging(appConfig.LogLevel);
ConfigureMiddleware(appConfig.Port);

// Later: pass the already-loaded instance to the DI container
services.AddKiwiServices(configuration, preLoadedConfigs: appConfig);

Enter fullscreen mode Exit fullscreen mode


Logging

Pass an ILogger to LoadConfiguration<T> to receive a detailed trace of what the library does during binding. This is useful during development and debugging.

var logger = loggerFactory.CreateLogger<DatabaseConfig>();
var dbConfig = configuration.LoadConfiguration<DatabaseConfig>(logger);

Enter fullscreen mode Exit fullscreen mode

Example output at Debug level:

dbg: Loading configuration for DatabaseConfig from section 'database'
dbg: Loaded config 'database:host' = db.prod.example.com
dbg: Loaded config 'database:port' = 5432 (default from: attribute)
dbg: Loaded config 'database:maxConnections' = 16 (default from: method:GetDefaultMaxConnections)
dbg: Loading nested configuration object 'Credentials' from section 'database:credentials'
dbg: Loaded config 'database:credentials:username' = svc_orders
dbg: Loaded config 'database:credentials:password' = *****
inf: Successfully loaded configuration for DatabaseConfig

Enter fullscreen mode Exit fullscreen mode

The library logs the raw value it resolves for each property. Sensitive values should be masked by the caller before logging - LoadConfiguration<T> has no built-in concept of sensitive keys.

Event Level
Starting load for a config type Debug
Property bound from config Debug
Property resolved from a default Debug (includes default source: attribute, method:GetDefaultXxx, type-default)
Nested object starting Debug
Config type loaded successfully Information
Property load failure Propagates as exception - no log entry

Logging is optional. Omit the logger argument when you do not need the output.


Error Messages

Errors include the full key path, the property name, and a description of what went wrong. They are all thrown as InvalidOperationException.

Configuration error at 'database:port': Cannot convert value 'abc' to type Int32.
  Check your configuration file. Details: Input string was not in a correct format.

Required configuration key 'app:apiKey' is missing and no default value is provided.
  Property: AppConfig.ApiKey.

Nested configuration type 'DatabaseConfig' used with [ConfigObject] must have [ConfigSection].
  Property: AppConfig.Database.

Circular reference detected in configuration hierarchy: Type 'DatabaseConfig' references itself
  either directly or through nested objects.

Default method 'DatabaseConfig.GetDefaultPort()' has wrong return type.
  Expected 'Int32', but got 'String'.

Default method 'DatabaseConfig.GetDefaultPort()' must have no parameters.

Enter fullscreen mode Exit fullscreen mode

All errors surface at load time - at startup, before the application enters its request-handling loop. There are no silent failures.


Validation Beyond Required Fields

Kiwi Config intentionally does not support range checks, format validation, or cross-property rules. The library validates structure (required keys, correct types, valid enum values) and stops there.

For additional constraints, validate the populated object immediately after loading:

var dbConfig = configuration.LoadConfiguration<DatabaseConfig>();

if (dbConfig.MaxConnections is < 1 or > 1000)
    throw new InvalidOperationException("MaxConnections must be between 1 and 1000.");

if (!Uri.TryCreate(dbConfig.BrokerUrl, UriKind.Absolute, out _))
    throw new InvalidOperationException($"BrokerUrl is not a valid URI: {dbConfig.BrokerUrl}");

Enter fullscreen mode Exit fullscreen mode

This keeps validation visible and co-located with the loading code. It also keeps the library focused.


Performance Guidance

LoadConfiguration<T> uses reflection to discover and populate properties.

  • Call once, store the result. The library does not cache. Each call does a full reflection walk and creates a new instance. Call LoadConfiguration<T>() once at startup and store or register the result.
  • Not for hot paths. Do not call it inside request handlers, loops, or frequently-executed code.
  • Thread safe. Multiple concurrent calls are safe - the method operates on independent instances with no shared mutable state.
  • Startup cost scales with depth. A deep nested hierarchy with many properties costs more than a flat class, but the cost is paid once at startup, not per request. Typical startup cost is in the range of microseconds to a few milliseconds, depending on the size of the configuration graph.

The expected pattern:

// At startup - once
var dbConfig = configuration.LoadConfiguration<DatabaseConfig>();
services.AddSingleton(dbConfig);

// In application code - injected, no further loading
public class OrderRepository(DatabaseConfig db) { ... }

Enter fullscreen mode Exit fullscreen mode


Complete Real-World Example

A microservice configuration with every feature of the library in use.

appsettings.json:

{
  "globalRateLimit": 1000,
  "service": {
    "name": "OrderService",
    "environment": "Production",
    "port": 5000,
    "allowedOrigins": "https://app.example.com,https://admin.example.com"
  },
  "database": {
    "connectionString": "Host=db.prod.example.com;Database=orders;Username=svc_orders",
    "commandTimeoutSeconds": 30,
    "retryAttempts": 3,
    "credentials": {
      "username": "svc_orders",
      "password": "s3cr3t"
    }
  },
  "messaging": {
    "brokerUrl": "amqp://rabbit.prod.example.com",
    "exchangeName": "orders",
    "prefetchCount": 10
  }
}

Enter fullscreen mode Exit fullscreen mode

Config classes:

using Kiwify.Kiwi.Platform.Configuration.Attributes;

public enum AppEnvironment { Development, Staging, Production }

// Root service config - reads a root-level value via absolute path
[ConfigSection("service")]
public class ServiceConfig
{
    [ConfigKey("name", Required = true)]
    public string Name { get; private set; } = string.Empty;

    [ConfigKey("environment", AppEnvironment.Development)]
    public AppEnvironment Environment { get; private set; }

    [ConfigKey("port", 5000)]
    public int Port { get; private set; }

    [ConfigKey("allowedOrigins")]
    public string[] AllowedOrigins { get; private set; } = Array.Empty<string>();

    // Reads root-level "globalRateLimit", not "service:globalRateLimit"
    [ConfigKey("/globalRateLimit", 500)]
    public int RateLimit { get; private set; }
}

// Database config with nested credentials and a computed default
[ConfigSection("database")]
public partial class DatabaseConfig
{
    [ConfigKey("connectionString", Required = true)]
    public string ConnectionString { get; private set; } = string.Empty;

    [ConfigKey("commandTimeoutSeconds", 30)]
    public int CommandTimeoutSeconds { get; private set; }

    [ConfigKey("retryAttempts", 3)]
    public int RetryAttempts { get; private set; }

    // No attribute default - computed dynamically
    [ConfigKey("maxConnections")]
    public int MaxConnections { get; private set; }

    [ConfigObject]
    public CredentialsConfig Credentials { get; private set; } = null!;
}

public partial class DatabaseConfig
{
    private static int GetDefaultMaxConnections() => Environment.ProcessorCount * 4;
}

// Nested credentials - loaded as part of DatabaseConfig
[ConfigSection("credentials")]
public class CredentialsConfig
{
    [ConfigKey("username", Required = true)]
    public string Username { get; private set; } = string.Empty;

    [ConfigKey("password", Required = true)]
    public string Password { get; private set; } = string.Empty;
}

// Messaging config - simple flat section
[ConfigSection("messaging")]
public class MessagingConfig
{
    [ConfigKey("brokerUrl", Required = true)]
    public string BrokerUrl { get; private set; } = string.Empty;

    [ConfigKey("exchangeName", "default")]
    public string ExchangeName { get; private set; } = string.Empty;

    [ConfigKey("prefetchCount", 10)]
    public int PrefetchCount { get; private set; }
}

Enter fullscreen mode Exit fullscreen mode

Loading all configs standalone - no DI container required:

var configuration = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables()
    .Build();

var service   = configuration.LoadConfiguration<ServiceConfig>();
var database  = configuration.LoadConfiguration<DatabaseConfig>();
var messaging = configuration.LoadConfiguration<MessagingConfig>();

Console.WriteLine($"{service.Name} [{service.Environment}] on :{service.Port}");
// OrderService [Production] on :5000

Console.WriteLine($"Rate limit: {service.RateLimit} req/s");
// Rate limit: 1000 req/s  (from root-level "globalRateLimit")

Console.WriteLine($"DB: {database.ConnectionString}");
Console.WriteLine($"DB connections: {database.MaxConnections}");
// DB connections: 16  (from GetDefaultMaxConnections(), 4 cores * 4)

Console.WriteLine($"DB credentials: {database.Credentials.Username}@...");
// DB credentials: svc_orders@...

Console.WriteLine($"MQ: {messaging.BrokerUrl} / {messaging.ExchangeName} (prefetch={messaging.PrefetchCount})");
// MQ: amqp://rabbit.prod.example.com / orders (prefetch=10)

Enter fullscreen mode Exit fullscreen mode

Validation after load:

if (database.CommandTimeoutSeconds is < 1 or > 300)
    throw new InvalidOperationException("CommandTimeoutSeconds must be between 1 and 300.");

if (messaging.PrefetchCount < 1)
    throw new InvalidOperationException("PrefetchCount must be at least 1.");

Enter fullscreen mode Exit fullscreen mode

When used with Kiwi DI, adding [ConfigService] to each class means a single services.AddKiwiServices(configuration) call loads and registers all of them automatically.


Configuration Reloading

Kiwi Config loads configuration once and produces an immutable object. There is no mechanism to re-read the configuration source and update the object in place, and the library does not observe IConfiguration change tokens.

This is intentional. Immutable config objects are safe to inject as singletons and share across threads without locks. For most applications - especially server applications where configuration changes require a restart to reason about reliably - this is the right default.

If you need configuration values to update at runtime without restarting the application, use IOptionsMonitor<T> from Microsoft.Extensions.Options. It observes configuration change tokens and provides access to the current value at each call site:

public class MyService(IOptionsMonitor<AppOptions> options)
{
    public void Do()
    {
        var current = options.CurrentValue; // always reflects the latest config
    }
}

Enter fullscreen mode Exit fullscreen mode

Kiwi Config and IOptionsMonitor<T> serve different needs and can coexist in the same application. Use Kiwi Config for startup-critical settings where immutability is a feature; use IOptionsMonitor<T> for settings that must update without a restart.


When to Use Kiwi Config vs IOptions<T>

Both libraries bind configuration to typed classes. The difference is in intent and guarantees.

Kiwi Config IOptions<T> / IOptionsMonitor<T>
How binding is declared Attributes on the class Convention (property names match config keys) or Configure<T>() calls
Default values Declared on the attribute Set in code via Configure<T>() or property initializers
Required fields Required = true - throws at load time Not built-in; validate after binding
Failure point Startup - before the app serves requests First access of IOptions<T>.Value
Reload support No - object is immutable after load Yes - IOptionsMonitor<T> observes change tokens
DI integration Optional - works without a container Requires IServiceCollection
Nested hierarchy [ConfigObject] with explicit path composition Automatic by property name matching

Choose Kiwi Config when:

  • You want the config class to be self-documenting - all keys, defaults, and requirements visible in one place.
  • Startup-time validation matters. A missing required key should fail immediately, not on the first request that happens to read the option.
  • You need computed defaults (GetDefault* methods) or absolute key paths.
  • You are outside a DI container (CLI tools, test fixtures, startup bootstrapping).

Choose IOptions<T> / IOptionsMonitor<T> when:

  • You need live reload - configuration values that update without restarting the application.
  • You are binding to a large set of settings where name-matching convention is less overhead than explicit attributes.
  • You are working with third-party libraries that already publish their own options types.

The two approaches can coexist in the same application. A common pattern is to use Kiwi Config for your own application settings (where explicitness matters) and IOptionsMonitor<T> for settings that require live reload.


Mental Model

Everything LoadConfiguration<T> does maps to five steps. Once you have this, the rest of the article is detail.

1. Start at [ConfigSection]
The attribute on the class sets the root key. All property lookups are relative to it: [ConfigSection("database")] means every key in the class reads from database:*.

2. Walk properties
For each property on T:

  • [ConfigKey] - read a scalar value from configuration at section:key
  • [ConfigObject] - recurse into the nested type, appending its [ConfigSection] key to the current prefix

3. Resolve the value (four-level priority)

1. Value present in IConfiguration          ← wins
2. DefaultValue on [ConfigKey]
3. GetDefault{PropertyName}() static method
4. Type's zero value (0, false, null)       ← last resort

Enter fullscreen mode Exit fullscreen mode

If Required = true and no value is found at levels 1-3, throw immediately.

4. Convert and assign
The resolved string is converted to the property type (int, bool, enum, collection, ...). Conversion failure throws with the key path and bad value in the message. The value is set via reflection - private and init setters are supported.

5. Fail fast on any error
Missing required keys, bad values, circular references, wrong GetDefault* signatures - all throw InvalidOperationException at load time. If LoadConfiguration<T> returns, the object is valid.


Feature Summary

Feature Attribute / API
Declare config section [ConfigSection("key")] on the class
Bind a property to a key [ConfigKey("key")] on the property
Set a static default [ConfigKey("key", defaultValue)]
Mark a field as required [ConfigKey("key", Required = true)]
Computed dynamic default private static T GetDefault{Prop}() method
Nested config object [ConfigObject] on a property
Read from config root [ConfigKey("/rootKey")] (slash prefix)
Collections (arrays/lists) Comma-separated config values
Flexible booleans true/1/yes/on/enabled, false/0/no/off/disabled
Case-insensitive enums Auto-parsed by property type
Load a config object configuration.LoadConfiguration<T>()
Load with diagnostics configuration.LoadConfiguration<T>(logger)