fix(flags): allow the documented tuple config for flags and entitlements - #55
fix(flags): allow the documented tuple config for flags and entitlements#55windischb wants to merge 2 commits into
Conversation
IFeatureFlags<TConfig> and IEntitlements<TConfig> were constrained to `where TConfig : class`, while their own typeparam docs promised "the configuration type (or value tuple of types)" and guide/flags/defining-flags.md documents "Multiple Config Sources" with `IFeatureFlags<(FeatureConfig, TenantConfig)>`. A tuple is a ValueTuple and therefore a struct, so the documented pattern never compiled: error CS0452: The type '(FeatureConfig, TenantConfig)' must be a reference type in order to use it as parameter 'TConfig' Dropping the constraint is the whole fix. The generator maps TConfig straight onto IReactiveConfig<TConfig>, which is itself unconstrained, so tuples work end to end once the interfaces stop rejecting them. Adds tests covering a flag class and an entitlement class over a named tuple, both reading from either element. Also converts the preferPendingState <param> added in #54 to <remarks>: documenting a single parameter made the compiler warn about the five undocumented ones (CS1573). The two remaining CS1574 cref warnings predate this branch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropping the class constraint made the documented tuple pattern compile, but only the non-DI path actually worked. A flag or entitlement class is registered as its own implementation type, so the container builds it and must resolve the IReactiveConfig<TConfig> its generated constructor asks for. Per-config-type reactive services are emitted from the rule plan, so IReactiveConfig<(FeatureConfig, TenantConfig)> was never registered: System.InvalidOperationException: Unable to resolve service for type 'IReactiveConfig`1[ValueTuple`2[DiFeatureConfig,DiTenantConfig]]' while attempting to activate 'DiRolloutFlags'. That is very likely why the constraint existed: it turned an unsupported combination into a compile error instead of an obscure container failure. Removing it without this would have traded CS0452 for a runtime crash. EmitFlagsServices/EmitEntitlementsServices now derive TConfig from the class's IFeatureFlags<T>/IEntitlements<T> interface and emit the reactive registration when it is not already present. EmitReactiveService was already reflection-based and works for tuples unchanged. Adds container-level tests for both a flag class and an entitlement class over a tuple config; the earlier tests constructed the classes directly and never exercised this path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up: the constraint was load-bearing after allReview feedback questioned whether History first: the constraint was not switched on later. It arrived in But the functional reason is real. My first commit only proved the tuple pattern compiles and works when the flag class is constructed directly. That skipped the path that matters. A flag or entitlement class is registered as its own implementation type, so the container builds it and has to resolve the So the constraint was doing real work: it turned an unsupported combination into a clear compile error instead of an obscure container failure at startup. Shipping the first commit alone would have traded CS0452 for a runtime crash — strictly worse.
Added container-level tests for both a flag class and an entitlement class over a tuple config. Full suite: 776 passing, 4 skipped, 0 failing. |
What was broken
guide/flags/defining-flags.mddocuments "Multiple Config Sources" — a flag class reading from several configs at once:That never compiled. Both interfaces were declared
where TConfig : class, and a tuple is aValueTuple, i.e. a struct:The interfaces even contradicted themselves — the
typeparamdoc on the very same line promised "the configuration type (or value tuple of types)". The same applied toIEntitlements<TConfig>.Found by the library audit as one of two critical findings; the other one was the config-aware staleness fixed in #54.
The fix
Dropping the constraint is the entire change. The generator maps
TConfigstraight ontoIReactiveConfig<TConfig>, which is itself unconstrained (IReactiveConfig<out T>), so tuples flow through end to end once the interfaces stop rejecting them. No generator change was needed — verified by the new tests, which exercise the generated constructor andConfigproperty over a named tuple.Verification
Three tests covering a flag class and an entitlement class over
(FeatureConfig Features, TenantConfig Tenant), reading from either element and requiring both. Confirmed failing with CS0452 before the change.Full suite: 772 passing, 4 skipped, 0 failing.
Drive-by
Converts the
preferPendingState<param>added in #54 into<remarks>. Documenting one parameter of that constructor made the compiler warn about the five undocumented ones (CS1573) — visible in the post-merge build log for #54. The two remaining CS1574 cref warnings (ConfigManager.cs:247,AggregateRuleBuilder.cs:8) predate this branch and are untouched.🤖 Generated with Claude Code