Skip to content

fix(flags): allow the documented tuple config for flags and entitlements - #55

Open
windischb wants to merge 2 commits into
developfrom
fix/flags-tuple-config
Open

fix(flags): allow the documented tuple config for flags and entitlements#55
windischb wants to merge 2 commits into
developfrom
fix/flags-tuple-config

Conversation

@windischb

Copy link
Copy Markdown
Contributor

What was broken

guide/flags/defining-flags.md documents "Multiple Config Sources" — a flag class reading from several configs at once:

public partial class RolloutFlags : IFeatureFlags<(FeatureConfig, TenantConfig)>

That never compiled. Both interfaces were declared where TConfig : class, and a tuple is a ValueTuple, i.e. a struct:

error CS0452: The type '(FeatureConfig, TenantConfig)' must be a reference type
in order to use it as parameter 'TConfig' in the generic type or method
'IFeatureFlags<TConfig>'

The interfaces even contradicted themselves — the typeparam doc on the very same line promised "the configuration type (or value tuple of types)". The same applied to IEntitlements<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 TConfig straight onto IReactiveConfig<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 and Config property 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

windischb and others added 2 commits August 4, 2026 10:46
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>
@windischb

Copy link
Copy Markdown
Contributor Author

Follow-up: the constraint was load-bearing after all

Review feedback questioned whether where TConfig : class had been there for a reason. It had — just not the one the archaeology suggested.

History first: the constraint was not switched on later. It arrived in 044ec13 (v5.0) together with the interfaces themselves, and with the same self-contradicting typeparam doc ("or value tuple of types") already in place. It survived the base-class → interface refactor in 585c1a3 untouched.

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 IReactiveConfig<TConfig> its generated constructor asks for — and reactive services are emitted per config type from the rule plan, so the tuple service type was never registered:

System.InvalidOperationException: Unable to resolve service for type
'IReactiveConfig`1[ValueTuple`2[DiFeatureConfig,DiTenantConfig]]'
while attempting to activate 'DiRolloutFlags'.

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.

7d10d89 completes the fix: EmitFlagsServices/EmitEntitlementsServices now derive TConfig from the class's interface and emit the reactive registration when it is not already present. EmitReactiveService was already reflection-based and handles tuples unchanged.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant