Changelog
[6.1.0] — 2026-06-03
Added
Cocoar.Configuration.Yaml— new opt-in YAML file provider (FromYamlFile) with reactive file-watching. Plain YAML scalars map to JSON types (true/false→ boolean, integers/floats → number,null/~→ null); quoted and block scalars stay strings.Cocoar.Configuration.Toml— new opt-in TOML file provider (FromTomlFile) with reactive file-watching (takes a Tomlyn dependency). TOML is strongly typed, so its values (strings, integers, floats, booleans, dates, arrays, tables, arrays-of-tables) map unambiguously to JSON — no scalar-style guessing as in YAML. Date/time values are emitted as ISO-8601 strings.- dotenv —
FromDotEnv(path)built into the core package (no dependency):KEY=valuelines,#comments, optionalexportprefix, single/double quotes, inline comments, and:/__key nesting; reactive. - INI —
FromIniFile(path)built into the core package (no dependency): classic[section]headers,key=valuelines,;/#whole-line comments,./:nesting (matching the environment-variable convention), and quote stripping. Inline comments are not stripped, so values containing;/#(e.g. connection strings) survive intact. Values are strings; the binder coerces. Reactive. - Kubernetes ConfigMap / Secret support — opt-in
followSymlinksparameter onFromFile/FromYamlFile/FromDotEnv(andFileSourceProviderOptions.FollowSymlinks). A ConfigMap-mounted file is a symlink whose content is updated by an atomic swap of the sibling..datasymlink rather than by rewriting the file. WithfollowSymlinksenabled, the symlinked file is read (its resolved final target must still resolve within the configured directory — an escaping symlink is rejected) and the atomic swap is detected and hot-reloaded (via Cocoar.FileSystem 2.3.0 symlink-target tracking). Off by default — symlinks remain rejected as defense in depth.
Changed
- Extracted a reusable
FileBackedProviderbase (watching, path/symlink security, debounce, disposal) so file-format providers share one implementation;FileSourceProvideris now a thin subclass (behavior unchanged). - Documented the provider-contract invariants on
ConfigurationProvider/IProviderConfiguration. - Added a parameterless
FromCommandLine()(default--, no key filter).
Fixed
- Observable provider: fetch no longer hangs on a cold / complete-without-emit source, and no longer leaks its one-shot subscription on synchronous replay.
- HTTP provider: the
HttpResponseMessageis now disposed after fetch; added optionalSseReadIdleTimeoutthat reconnects a half-open SSE stream. FileSourceProvider.Disposeis race-safe and guards cancellation.
Removed
- Dead internal
MicrosoftConfigurationSource*types (theFromMicrosoftSourceAPI was removed in 6.0.0).
[6.0.0] — 2026-06-01
Major release. The headline change is the move off .NET 8.
Breaking
- Dropped .NET 8 support. All packages now multi-target
net9.0andnet10.0(wasnet8.0/net9.0). Consumers must target .NET 9 or later. Microsoft.Extensions.*dependencies moved to the10.0.xline, aligned with .NET 10.10.0.xships a nativenet9.0target, so .NET 9 consumers take no runtime hit.
Removed
IConfigurationAccessor.GetRequiredConfig<T>()/GetRequiredConfig(Type)— deprecated since v5; useGetConfig<T>()/GetConfig(Type)(identical throw-on-missing behavior).FromMicrosoftSource(...)— useFromIConfiguration(IConfiguration).X509CertificateGenerator.GenerateAndSave(...)— useGenerateAndSavePfx(...)/GenerateAndSavePem(...).
Added
Marten store backend (Cocoar.Configuration.WritableStore.Marten)
- New opt-in package:
MartenStoreBackendpersists WritableStore overrides in Marten (PostgreSQL) — oneCocoarConfigDocumentper configuration type. FromMartenStore()service-backed (Layer-2) rule extension resolves theIDocumentStorefrom DI. Combine with.TenantScoped()for database-per-tenant configuration: each tenant's overlay lives in its own database (Marten multi-tenancy), selected fromaccessor.Tenant.
WritableStore — batch writes (PatchAsync)
IWritableStore<T>.PatchAsync(b => b.Set(...).SetSecret(...).Reset(...))applies any number of mutations as one atomic write and one recompute — a form save no longer fires one recompute (and one backend round-trip) per field. The single-valueSetAsync/SetSecretAsync/ResetAsyncnow delegate to it.- Async overload
PatchAsync(async b => …)for when gathering values is asynchronous (e.g. encrypting aSecretEnvelope<T>inline). - New
IWritableStorePatch<T>builder:Set(value, including an explicitnull),SetSecret(pre-encrypted envelope),Reset. Presence-based semantics — present = set, absent = untouched,Reset= remove the override.
Fixed
- Resetting a secret-typed member (
ResetAsync(x => x.Secret), orResetinside a patch) no longer throwsNotSupportedException— removing an override exposes no plaintext, so it is now allowed (symmetry withSetSecretAsync).
Changed
- Configuration layer merging is now case-insensitive on property names (via Cocoar.Json.Mutable 1.2.0), consistent with how the effective config is read back (System.Text.Json case-insensitive, like
IConfiguration). A higher layer overrides a lower-layer key regardless of casing — no more case-variant sibling keys. Invisible to typed access; internal-only for most consumers.
[5.1.0] — 2026-05-31
Added
WritableStore — writable override layer
- A writable, application-controlled layer for overridable defaults: the normal sources supply defaults; the app overrides individual values at runtime.
IWritableStore<T>(type-safe facade) andIWritableStoreOverlay<T>(raw key-path surface) inCocoar.Configuration.Abstractions- Sparse writes —
SetAsync(x => x.Smtp.Port, value)persists only the touched leaf; unset keys keep inheriting from lower layers ResetAsync(...)removes an override (falls back to the inherited default); an explicitnulloverride is distinct from resetDescribeAsync()returns per-key provenance (StoreEntry: base, effective,IsSet) for management UIs.FromStore()rule extension; file-based backend by default, pluggableIStoreBackendIWritableStore<T>/IWritableStoreOverlay<T>are DI-injectable (single shared singleton) — write your own endpoints with your own validation/normalization/logging- Secret-typed members: a plaintext override throws
NotSupportedException; override them viaSetSecretAsyncwith a pre-encryptedSecretEnvelope<T> IProviderServiceRegistrationgained resolve-time factory registration support
Multi-Tenancy (ADR-005)
- The same configuration type resolves to different values per tenant, layered on a shared global base
.TenantScoped()rule marker +TenantonIConfigurationAccessor— author one flat rule list (no second surface)ITenantConfigurationAccessorlifecycle:InitializeTenantAsync/EnsureTenantInitializedAsync/RemoveTenantAsync- Per-tenant access:
GetConfigForTenant/GetReactiveConfigForTenant/GetFeatureFlagsForTenant/GetEntitlementsForTenant/GetWritableStoreForTenant - Tenant-only types excluded from the global DI plan; per-tenant flags/entitlements need no source-generator change
- Tenant config consumption (DI, no ASP.NET dependency): scoped
ITenantReactiveConfig<T>+ITenantContext;AddCocoarTenantResolver<TService>(s => s.TenantId)resolves the current tenant from any DI service (HTTP viaIHttpContextAccessor) — no hand-written adapter - ASP.NET Core:
MapTenantFeatureFlagEndpoints()/MapTenantEntitlementEndpoints()
Service-Backed (DI-aware) configuration (ADR-006)
- Two-layer model: eager
UseConfiguration(Layer 1) + lazyUseServiceBackedConfiguration(Layer 2), whose provider factories receive theIServiceProvider FromStore((sp, a) => …),FromHttp((sp, a) => …),FromService<TService>(s => …)— useIHttpClientFactory/ Marten / EF without giving up the no-DI core- Activated on host start via
IHostedLifecycleService(a recompute, never a rebuild — live reactive views stay valid) - Public
ServiceBackedProviderBuilder<T>seam for third-party(sp, a)provider overloads
Secrets — encryption-key publishing
- Publish the public half of the secrets encryption key so a browser/CLI can build
cocoar.secretenvelopes —ISecretEncryptionKeyProvider(GetCurrentKey()/GetCurrentKeyForTenant(tenantId)) returns exactly one current public key (the newest cert; older certs stay decrypt-only) - ASP.NET Core
MapSecretEncryptionKey()(single-tenant) /MapTenantSecretEncryptionKey()(per-tenant; tenant fromITenantContext) at/.well-known/cocoar/encryption-key— one key per request, never a list, no cross-tenant exposure SecretEnvelope<T>typed secret-overlay writes; WritableStoreSetSecretAsync/SetSecretEnvelopeAsyncaccept pre-encrypted envelopes (per tenant viaGetWritableStoreForTenant<T>(id).SetSecretAsync(...))
Custom-provider authoring
- Public
ProviderObservable/ProviderDisposablehelpers (inCocoar.Configuration.Providers.Abstractions) for a provider's change stream without referencing System.Reactive FromFile(a => …)config-aware file-path overload — the natural shape for per-tenant file rules (resolves the path from the accessor per recompute)
Changed
Secrets — robust enum & casing handling
- Secret payloads now (de)serialize with lenient options: enums as names (safe against enum reordering) and case-insensitive property matching.
- Reading still accepts numeric enums and any casing → existing encrypted secrets remain fully readable, no migration.
- Recommendation: when encrypting an enum secret with the CLI, pass the name (e.g.
Active) rather than the ordinal.
Multi-tenancy — clearer error for a tenant-only type read globally
- Reading a type whose every rule is
.TenantScoped()from the global pipeline now throws a targeted error pointing atGetConfigForTenant<T>(id)/GetReactiveConfigForTenant<T>(id)— for bothGetConfig<T>()andGetReactiveConfig<T>()— instead of the generic "no rule registered" message (a rule does exist; it is just tenant-scoped).
[5.0.0] — 2026-03-24
Added
Feature Flags & Entitlements
- Strongly-typed feature flags and entitlements built into the core
Cocoar.Configurationpackage IFeatureFlags<TConfig>interface withFeatureFlag<T>properties — bare lambdas directly assignableIEntitlements<TConfig>interface withEntitlement<T>properties- Fluent registration:
.UseFeatureFlags(f => f.Register<T>())and.UseEntitlements(e => e.Register<T>()) IFeatureFlagsDescriptors/IEntitlementsDescriptorsfor descriptor metadata- Health integration — expired flag classes report
Degradedautomatically - Roslyn source generator emits
CocoarFlagsDescriptorsat compile time IFeatureFlagEvaluator/IEntitlementEvaluatorfor REST evaluation endpoints- Context resolvers at global, class, and property levels
ConfigManager Builder API
ConfigManager.Create()static factory with fluent builder — replacesnew ConfigManager(...).Initialize()ConfigManager.CreateAsync()for async initialization withCancellationToken- Builder groups concerns:
.UseConfiguration(),.UseLogger(),.UseDebounce(),.UseSecretsSetup()
Package Consolidation (10+ → 7)
- Secrets, X509Encryption, Flags merged into core
Cocoar.Configuration - Flags.Generator merged into
Cocoar.Configuration.Analyzers - Secrets.Abstractions merged into
Cocoar.Configuration.Abstractions - Same types, same namespaces — fewer packages to install
Zero External Dependencies
- Removed
System.Reactivefrom all shipped packages - Lightweight internal reactive primitives (~200 lines)
- Public API unchanged —
IObservable<T>is BCL
Aggregate Rules
FromFiles(params string[])for concise file layering:rule.For<T>().FromFiles("base.json", $"base.{env}.json").Aggregate(r => [...])for general-purpose rule grouping with full provider flexibilityAggregateRuleManager— isolated execution boundary for grouped rules (inner Required stays within aggregate)TypedProviderBuilder<T>base class for provider extension methods (prevents recursive nesting)IRuleManagerinterface extracted fromRuleManagerfor uniform engine handlingSubManagersproperty for ConfigHub drill-down into aggregate structure- ADR-004: Aggregate Rules with Isolated Execution Boundary
Other Improvements
- Runtime recomputes are now fully async (no sync-over-async)
- Secrets memory safety: direct UTF-8 deserialization, DEK zeroing, no plaintext in error messages
- File provider security: symlink rejection, improved path traversal validation
- Health model simplification with
HealthTracker
Breaking Changes
ConfigManagerconstructors andInitialize()→internal. UseConfigManager.Create()AddCocoarConfiguration()→ builder API:c => c.UseConfiguration(rule => [...])- Secrets setup →
UseSecretsSetup()extension instead ofsetup.Secrets() - Testing:
ReplaceAllRules()→ReplaceConfiguration(),AppendTestRules()→AppendConfiguration()
See Migration Guide v4 → v5 for details.
[4.2.1] — 2026-02-03
Fixed
- Interface reactive configs:
IReactiveConfig<IInterface>now works for interfaces exposed viaExposeAs<IInterface>()
[4.2.0] — 2026-02-03
Added
- Abstractions packages:
Cocoar.Configuration.AbstractionsandCocoar.Configuration.Secrets.Abstractions AllowPlaintext(): Conditionally allow plaintext values inSecret<T>for development/testing- Testing setup overrides:
WithSetup()and optional setup parameter on test override methods
Fixed
- Deserialization failure logging (EventId 5100) instead of silent
null - DI registration ordering is now deterministic (sorted by type full name)
- Provider rebuild callback ordering fixed in
RuleProviderLease
[4.1.0] — 2026-01-11
Fixed
- Provider consistency: optional rules now return empty objects with C# defaults consistently across all providers
[4.0.0] — 2026-01-08
Added
- Testing Configuration Overrides:
CocoarTestConfigurationwithAsyncLocal<T>isolation - Secrets Package:
Secret<T>, X.509 hybrid encryption (RSA-OAEP + AES-256-GCM), password-less certificates - Secrets CLI:
generate-cert,convert-cert,cert-info,encrypt,decrypt - Analyzers Package: COCFG001–006
Breaking
- Provider contract:
byte[]instead ofJsonElement(internal — consuming apps unaffected)
[3.3.0] — 2025-10-23
Added
- Rule naming:
.Named("name")for health snapshots - Enhanced health monitoring:
RuleHealthEntrywith Name, ProviderType, ConfigType, Skipped status IConfigurationHealthServiceauto-registered in DI
[3.2.0] — 2025-10-23
Added
- CommandLine provider with multiple switch prefix support
[3.1.1] — 2025-10-19
Fixed
- Enum string conversion:
JsonStringEnumConverterfor string-to-enum deserialization
[3.1.0] — 2025-10-19
Added
- Interface deserialization:
setup.Interface<I>().DeserializeTo<T>()
[3.0.0]
Breaking
- Type-First API:
rule.File("...").For<T>()→rule.For<T>().FromFile("...") - When() signature:
Func<bool>→Func<IConfigurationAccessor, bool>
Added
- Config-aware conditional rules via
IConfigurationAccessorin.When()
See Migration Guide v2 → v3 for details.
[2.0.0] — 2025-09-30
Breaking
Rule.From.File()→rule.File()(lambda builder)Bind.Type<T>().To<I>()→setup.ConcreteType<T>().ExposeAs<I>()
[1.1.0] — 2025-09-25
IReactiveConfig<T>tuple support for atomic multi-config updates
[1.0.0] — 2025-09-21
- First stable release: 204 tests, health monitoring, reactive configuration
[0.9.0] — 2025-09-14
- Initial release