Skip to content

Fix what five trial applications ran into, and lead the README with the problem - #38

Merged
ipjohnson merged 7 commits into
mainfrom
docs/onboarding-and-diagnostics
Aug 16, 2026
Merged

Fix what five trial applications ran into, and lead the README with the problem#38
ipjohnson merged 7 commits into
mainfrom
docs/onboarding-and-diagnostics

Conversation

@ipjohnson

Copy link
Copy Markdown
Owner

Built five applications against the released package — a console tool, a three-project minimal API, a worker, an xUnit suite and a Native AOT binary — and fixed everything that got in the way.

Bugs

A nullable type argument in a service type broke the build. class GetBookHandler : IHandler<GetBook, Book?> emits typeof(…Book?), and the registrations file was the one generated file that never opened a nullable context. CS8669 on a find-by-id handler: a warning the consumer cannot silence from their own source, and a hard failure under TreatWarningsAsErrors. One call fixes both the attribute and the convention path, since both emit through the same writer. Regression test fails 2/2 without it.

Two async interception tests raced a wall clock. Both assert nothing has logged an exit between the task being handed back and awaited, and manufactured that window with await Task.Delay(20). Any stall longer than 20ms fails them — the log arrives complete rather than partial. A 60ms stall reproduces it 3/3; the gated version survives the same stall. InterceptorGenerationTests had the identical shape and had simply not lost the coin flip yet.

[InjectValues] did nothing where it was written. The only one of the three testing attributes without an AttributeUsage, so a method-level usage compiled, was never read, and failed inside ActivatorUtilities naming System.String. Now CS0592. Breaking, deliberately.

Documentation

The README opened by naming the implementation, and the most persuasive thing in it — a generated registration that is plainly ordinary C# — sat at the bottom under four hundred lines of reference. It now opens with the hook, a link to the docs site, the attribute beside the code it generates, and a table answering why-not-Scrutor. The reference links out instead of duplicating the site, and integ-tests/ is pointed at as the sample gallery it already is.

Three documented examples did not compile, all found by building them: a missing attributes using, and two places naming a module without importing its namespace. integ-tests/ConsoleTestProject has always carried that using; the docs omitted it.

Verification

  • Full solution green on both TFMs, coverage 89.3% against the 85% gate
  • scripts/verify-packages.sh passes for net8.0 and net10.0
  • All six trial applications build clean under TreatWarningsAsErrors, including Native AOT with zero IL warnings
  • Every README snippet compiled and run before committing — which is how two of the three doc bugs were found

Not done

I had proposed emitting the auto-generated ApplicationModule into the global namespace so the quickstart needs no using. Investigation reversed that: the RootNamespace placement is deliberate and covered by ConfigurationTests.RootNamespace_NamesTheAutoGeneratedApplicationModule, and a global using alias works but is not something to introduce days before 1.0. Documented instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CMV4J1eVscS5EBMUhhWc2x

Ian Johnson and others added 7 commits August 16, 2026 06:28
Both tests assert that nothing has logged an exit between the task being
handed back and being awaited, and manufactured that window with
`await Task.Delay(20)`. The timer and its continuation are independent of
the test thread, so any stall longer than 20ms lets the whole chain finish
before the assertion reads the log — the failure is the log arriving
complete rather than partial. A 60ms stall reproduces it 3/3.

The work now awaits a gate the test releases, so the assertion is about a
suspension rather than a stopwatch. It survives the same 60ms stall, and it
is a stronger claim than before: the old shape would also have passed on a
lucky margin, with the work at 19ms and the assertion at 5ms.

The runtime gate defaults to Task.CompletedTask so a test with no interest
in it cannot deadlock, and the generator one hangs off the Recorder the
harness already owns, which lives in the generated assembly and so cannot
leak between compilations.

InterceptorGenerationTests had the identical shape and had simply not lost
the coin flip yet; fixing only the test that failed would have left it live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMV4J1eVscS5EBMUhhWc2x
A registered service type carries whatever nullable annotation its
declaration used, so `class GetBookHandler : IHandler<GetBook, Book?>`
emits `typeof(...Book?)`. Roslyn requires generated code to open a nullable
context explicitly however the consuming project is configured, and the
registrations file was the one generated file that never did — the module,
attribute and interceptor writers all already call EnableNullable.

The result was CS8669 on a find-by-id handler, which is about as ordinary a
shape as exists: a warning the consumer cannot fix from their own source
without dropping the annotation from their own domain signatures, and a hard
build failure under TreatWarningsAsErrors. It reproduced on both the
attribute and the convention path, because the convention path emits through
the same writer — which is also why one call fixes both files.

Nullability is not stripped from the emitted typeof. It is inert there,
since `typeof(Book?)` and `typeof(Book)` are one runtime type, and removing
it would mean touching type modelling that decoration and interception rely
on: ConstructorArgumentWriter reads nullability to choose GetService over
GetRequiredService, and the interceptor wrappers need the annotations to keep
implementing the interfaces they wrap. Probing a decorator declared against
`IStore<Document>` over a registration of `IStore<Document?>` confirmed the
two still match, so the annotation is cosmetic rather than a silent miss.

The nine snapshots re-approve with two lines each and no other change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMV4J1eVscS5EBMUhhWc2x
It was the only one of the three testing attributes without an
AttributeUsage — MockAttribute is pinned to parameters and TestExportAttribute
to methods — so writing it on a test method compiled, was never read, and then
failed inside ActivatorUtilities with "Multiple constructors accepting all
given argument types have been found in type 'System.String'", which names
neither the parameter nor the real mistake. It is now CS0592 at the attribute.

The remarks also say what the values are, since the guide's table listing this
against "String parameters" describes the one case it cannot do: they are the
parameter type's constructor arguments, combined with what the container
supplies. A parameter that should simply be a value wants [InlineData].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMV4J1eVscS5EBMUhhWc2x
It opened by naming the implementation — "a C# source generator package that
uses attributes to create dependency injection registration modules" — which
says what the thing is before the reader knows why they want one. The single
most persuasive artefact in the document, a generated registration that is
plainly ordinary C#, sat at the very bottom below four hundred lines of
reference.

Now: the hook, a link to the documentation site, the attribute and the code it
generates side by side, and a table answering the question every reader of a
.NET DI library arrives with, which is why not Scrutor. The reference material
that the site covers in depth is condensed to a lookup table that links out,
so the README is a pitch and an index rather than a second copy of the docs.

Two corrections to code that did not compile. The quick start now carries the
`using DependencyModules.Runtime.Attributes;` its services need and the
`using YourRootNamespace;` that top-level statements need to name the generated
ApplicationModule — the latter being a real papercut, since the module takes
the project's root namespace while top-level statements sit in the global one.
integ-tests/ConsoleTestProject has always had that using; the README omitted it
and sent every reader into a CS0246 that names a type they never wrote.

Every snippet in this file was compiled and run before committing, which is how
both omissions were found. Sample links are absolute, since this file also ships
as the NuGet package readme where relative paths do not resolve.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMV4J1eVscS5EBMUhhWc2x
Three fixes, all found by building the examples rather than reading them.

[InjectValues] was introduced as being for "a string, an id, a record
combining both", and the comparison table said "the parameter is data, not a
service". A bare string is the one thing it cannot do: the values are the
parameter type's constructor arguments, so asking for a string tries to
construct System.String from a string and fails with "A suitable constructor
for type 'System.String' could not be located". The prose below it was already
right; only the framing promised something else. Data rows are what a parameter
that simply is a value wants, and [InlineData] composing with [ModuleTest] is
now shown, since nothing said so.

The testing bootstrap and the top-level statements example both referenced a
module without importing its namespace. A module generates its attribute in its
own namespace and an assembly attribute has no namespace context, so the first
fails on a type the reader never wrote; the generated ApplicationModule takes
the project's RootNamespace while top-level statements sit in the global one,
so the second fails on the module itself. Both now carry the using, and say why
it is load-bearing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMV4J1eVscS5EBMUhhWc2x
…shot

The API snapshot moves because AttributeUsage is part of the public surface,
which is the test doing its job on a deliberate change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMV4J1eVscS5EBMUhhWc2x
A module generates its attribute in the module's own namespace, and an
assembly-level attribute has no namespace context to inherit — a `using`
written inside a namespace declaration cannot reach it, because assembly
attributes precede every namespace in the file. So `[assembly: ApplicationModule]`
without the import fails with CS0246 naming `ApplicationModuleAttribute`: a type
the developer never wrote, generated into a namespace the error does not
mention, by a generator whose output they have probably never opened. Every part
of that message points away from the fix, which is one line.

Alone among these diagnostics it is read from syntax rather than the semantic
model, and has to be: the attribute is written by the generator that is running,
so it does not exist in the compilation being examined and nothing about it
resolves. Every assembly-level module attribute looks unresolved from here,
which is why the question is "is there a module by this name, and could this
file see it" rather than "did it bind".

That makes the false positives the part worth testing, and they are what most of
the tests cover. It stays quiet for an attribute matching no module in the
compilation, a module in the global namespace with nothing to import, a usage
already written qualified, and a namespace supplied by a `global using` in any
file — the last being why the check reads every compilation unit rather than
only the one the attribute sits in. A `using` alias is deliberately not
accepted, since it imports one name rather than a namespace and does not bring
the attribute into scope under the name written.

Registered from SourceGenerator rather than the base class, so a framework
generator loaded alongside this one does not report the same usage twice.

Confirmed against the trial project the papercut was found in: it reports on the
right line, names both fixes, and disappears when either is applied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMV4J1eVscS5EBMUhhWc2x
@ipjohnson
ipjohnson merged commit 261ad93 into main Aug 16, 2026
2 checks passed
@ipjohnson
ipjohnson deleted the docs/onboarding-and-diagnostics branch August 16, 2026 10:52
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