Refuse open generic decoration and cross-wiring, and cut 1.0.0-rc9330 - #37
Merged
Conversation
…lently Decoration and cross-wiring both replace a registration with a factory, and the container refuses a factory for an open generic service type. Neither path said so, and the two failed in different ways. A generic decorator is expanded against the closed constructions a compilation registers. An open generic registration closes nothing, so the expansion produced no decorations and the declaration was dropped in silence. A non-generic decorator named against an unbound service needed no expansion at all, so nothing caught it: it reached emission carrying IStore<> and produced Decorate<IStore<>>, which is CS7003 inside generated code. [CrossWireService] on a generic type was worse again, leaking the type parameter into typeof(ILedger<T>) beside GetRequiredService<Ledger<>>() for CS0246 and CS7003 together. DM0013 covers all three decorator shapes, whichever way the decorator was declared. DM0014 covers cross-wiring, dropping the whole registration rather than the cross-wired half, since keeping the implementation's own registration would leave the instance unreachable through any of its interfaces. A decorator naming a service the compilation does not register stays quiet. Naming a service someone else registers is what [Decorate] exists for, so reporting there would fire on the feature's primary use. HasUnboundServiceType was written to describe the CS7003 case and was never consulted; it is now the guard. DecoratorExpansion.Expand takes an out parameter carrying what it refused, which is a break on the generator extension points. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Np5LZpJ2PPStWVcrNXfmD5
The Unreleased section holds one change — the open generic refusals — so it is stamped as rc9330 rather than left to grow. VersionSuffix is the fallback local and CI builds use. A release still takes its version from the tag, so cutting rc9330 means pushing v1.0.0-rc9330. Assembly and file versions carry no prerelease part and are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Np5LZpJ2PPStWVcrNXfmD5
…phabet Four registrations that were silently not happening, and the diagnostics for two more that were refused without saying so. Attribute usages were compared as written, so a namespace-qualified spelling, a global:: prefix or a using alias missed and the class went unregistered with no diagnostic at all. Service attributes now resolve through the semantic model; module attributes match on the name the usage ends in, which covers every qualified form. A using alias of a module attribute is still not seen, because a predicate that must stay syntax-only cannot resolve one, and that case surfaces as CS0311 rather than silence. Replace and Try act on a registration that has to already be there, and registrations are emitted sorted by implementation type name. Named so the sort put them first they ran before their target existed, so Replace replaced nothing, added itself, and lost to the registration it meant to displace. They are emitted after the plain Add registrations now, the rule that already put conditional registrations last. [Mock] registered its double unkeyed even when the parameter carried [FromKeyedServices], leaving the keyed registration the consumer injects untouched: the test held a double it believed was wired in while the service under test kept the real implementation. DM0015 reports an interceptor absent from members whose shape it cannot serve. The sharpest form — an IInterceptor on an all-async service, where it never runs — was invisible to the generator too, which discarded the model before anything could report on it. DM0008 now says that one unsupported member costs the whole interface, which the guide had implied otherwise. CopyToOutputDirectory on a Compile item copies the source, and the metadata flowed to every referencing project: 50 files of generator internals in consumers' bin and publish output, this repository's own benchmarks included. The NuGet package was never affected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Np5LZpJ2PPStWVcrNXfmD5
A generic implementation registers as an open generic and was refused outright, because decoration cannot touch one and interception inherited the constraint without needing it. Decoration rewrites a registration into a factory, which an open generic service type cannot carry. Interception generates a type, and an open generic implementation type is exactly what the container does accept. The wrapper is now generic over the same parameters, and takes the implementation by its own type rather than the service — asking for the service would resolve the wrapper, which is registered as that service, and recurse. Its nested state classes close the self reference over the outer parameters, which is CS0305 written bare. DecoratorHelper.InterceptOpenGeneric swaps the registration and registers the implementation alongside it, carrying lifetime and service key across, and is idempotent so two modules carrying the same registration do not double-wrap. A constrained type parameter is still refused. The wrapper would have to repeat the constraint to reference the implementation, and there is no way to emit one; the message says that rather than describing the old factory reason. Verified under Native AOT, where the limit belongs to the container rather than to this: a published binary closes an open generic over reference types only, and throws for a value type argument. A plain [SingletonService] on a generic class behaves identically, so interception adds no restriction. Both the AOT guide and the interception guide now say so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Np5LZpJ2PPStWVcrNXfmD5
Interception refused a constrained generic implementation because the wrapper has to repeat the constraints to reference what it wraps, and there was no way to emit one. There is now: CSharpAuthor 1.1.1010 adds AddConstraint, which takes the parts and writes them in the order C# requires. Repository<T> where T : class, IEntity, new() is wrapped by Repository_Intercepted<T> : IRepository<T> where T : class, IEntity, new(). struct and unmanaged already guarantee a default constructor and Roslyn reports one for them, so new() is dropped rather than repeated - writing it out is CS0451. The method path moves onto the same model. Constraints were read into a rendered string for generic methods and, once classes needed them too, would have been read a second way for classes - two copies of the same precedence rules, including that unmanaged has to be tested before struct because it implies it. TypeParameterReader reads a parameter once for both, and the writer hands the parts to the library rather than formatting a clause itself. Verified by compiling and running the result: a service constrained to class, IMarker, new() resolves as its wrapper and is intercepted, and one constrained to struct does too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Np5LZpJ2PPStWVcrNXfmD5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Decoration and cross-wiring both replace a registration with a factory, and the container refuses a factory for an open generic service type —
Open generic service type 'IRepository1[T]' requires registering an open generic implementation type`. Neither path said so, and the three shapes of the mistake failed in two different ways.What was wrong
[Decorator]on class[Decorate]on module[CrossWireService]on a generic typeThe two silent cases came from expansion: a generic decorator is expanded against the closed constructions a compilation registers, and an open generic registration closes nothing, so the declaration was dropped with nothing to say. The non-generic decorator never went through expansion at all, so nothing caught it and it reached emission carrying
IStore<>.HasUnboundServiceTypewas written to describe exactly that case and was never consulted; it is now the guard.What this adds
DM0013 — a service registered as an open generic cannot be decorated. Covers all three decorator shapes, whichever way the decorator was declared.
DM0014 — a generic type cannot be cross-wired. The whole registration is dropped rather than the cross-wired half, since keeping the implementation's own registration would leave the instance unreachable through any of its interfaces.
Deliberately not reported: a decorator naming a service the compilation does not register. Naming a service someone else registers is what
[Decorate]exists for, so a diagnostic there would fire on the feature's primary use.Docs
DecorateAttribute'sserviceparameter was documented as "may be an open generic", which contradicted the README. The decorators guide described the failure as anInvalidOperationExceptionfromDecoratorHelper— that guard is unreachable from generated code, because expansion drops the decorator before anything is emitted, so it now points at DM0013.Breaking
DecoratorExpansion.Expandtakes an additionalout IReadOnlyList<DecoratorModel>carrying what it refused. That is on the generator extension points, which the extending guide documents as unversioned. The public API snapshot is updated.Verification
GeneratedAssembly.Create, which fails on any emit error — these would have failed before this change) and two negative tests guarding the open-generic-decorator-over-closed-registrations case and the cross-assembly caseignoreDeadLinks: falseRelease
Second commit stamps
1.0.0-rc9330and moves the Unreleased section under it, matching how rc9230 was cut.dotnet packproducesDependencyModules.Runtime.1.0.0-rc9330.nupkg. Merging this means pushingv1.0.0-rc9330to publish.🤖 Generated with Claude Code
https://claude.ai/code/session_01Np5LZpJ2PPStWVcrNXfmD5