Skip to content

1.0.0-rc9210: per-library mocking packages, service type fixes, and a guide rewrite - #30

Merged
ipjohnson merged 4 commits into
mainfrom
release/rc9210
Aug 9, 2026
Merged

1.0.0-rc9210: per-library mocking packages, service type fixes, and a guide rewrite#30
ipjohnson merged 4 commits into
mainfrom
release/rc9210

Conversation

@ipjohnson

Copy link
Copy Markdown
Owner

Everything for 1.0.0-rc9210. Four commits, summarised by strand below.

Per-library mocking packages

[Mock] required NSubstitute, so anyone on Moq or FakeItEasy could not use it. The pieces mocking
support actually needs — IMockSupportAttribute, IOrderedAttribute, IInjectValueAttribute,
InjectValuesAttribute, AttributeUtility — never referenced xUnit, but living in
DependencyModules.xUnit forced every mocking package to depend on a test framework it does not
use. They move to a new framework-neutral DependencyModules.Testing, with
DependencyModules.Moq and DependencyModules.FakeItEasy joining NSubstitute on top.

Breaking: DependencyModules.xUnit.NSubstitute is now DependencyModules.NSubstitute, and the
moved types changed namespace to match. Naming one mocking package after xUnit would be misleading
standing next to the other two. Consumers update the PackageReference and the using; the
attributes are unchanged.

A capability interface could win the default service type

The service type is inferred from the first interface a class declares, so
class ConnectionPool : IDisposable, IPool registered as IDisposable and was unresolvable as
IPool. Declaring IDisposable first is a normal ordering, and the interface the developer meant
lost silently.

Interfaces describing what a class can do rather than what it is are now passed over when
nobody named a service type. Previously only INotifyPropertyChanged was skipped — the same
reactive patch Autofac and Scrutor each ended up with.

Deliberately a list rather than the namespace rule AsSelfWithInterfaces uses, because the two are
not the same problem. That expansion is additive, so excluding too much costs a bonus registration.
This is an exclusive choice, so excluding too much means the interface the developer wanted is not
registered at all. System holds plenty of genuine service roles — IEqualityComparer<T>,
IJsonTypeInfoResolver, IHttpClientFactory — and a blanket System rule broke the
JsonSerializerContext registrations in SutProject.Tests.

DependencyModules.xUnit was building as a test project

xunit.v3.core's targets set OutputType=Exe and IsTestProject=true on the assumption that
anything referencing them is a test project. This one ships [ModuleTest] for other people's test
projects. It shipped a runtimeconfig.json in lib/, carried eight [RegisterRunnerReporter]
attributes in its public API, and made solution-level dotnet test fail in the VSTest testhost.

ModuleEnvironment

Now a collection of its values, so they can be written inline and enumerated back out. A key not
written falls back to an environment variable of that name; a key written as null hides one.
Leading with false pins an environment to exactly what is at the call site — what a test asserting
registrations wants, since otherwise a variable set on the machine running it reaches a key the test
never mentioned.

Documentation

The guide opened nearly every page with a definition rather than a situation, so it read as though
you already knew why you were there. Each page now states the problem first, then what the library
does about it, then a worked example. The testing pages are built on the sample web app in this
repository, so their examples are known to compile.

Verification

Build clean with 0 warnings. 530 unit, 125 SutProject, 2 WebApiApp — 657 passing, including five new
regression tests for the service-type fix (verified to fail without it).

🤖 Generated with Claude Code

https://claude.ai/code/session_01FasPSEiCXbFwobHLur2pnv

Ian Johnson and others added 4 commits August 9, 2026 14:46
The existing [1.0.0] entry documented a release that was never cut, and it
included work that landed after rc9200 — the decorator ordering PR among it. So
it was describing unreleased changes under a released-looking heading, with a
link to a tag that does not exist.

Retitled to the release actually being cut, and extended with everything since
rc9200: convention registration and its own package, referenced-assembly
scanning, interception, environment-conditional registration, the documentation
site, DM0004 through DM0012, and the defects found along the way. The earlier
prose is kept below a divider rather than restated.

Still an RC. Convention registration is new and large, and the environment API
changed shape late, so the surface is not committed to yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C56x6Vv6HJ6ArfqwKsuSb9
The service type is inferred from the first interface a class declares, so
`class ConnectionPool : IDisposable, IPool` registered as IDisposable and was
unresolvable as IPool. Declaring IDisposable first is a normal ordering, and the
interface the developer meant lost silently.

Interfaces describing what a class can do rather than what it is are now passed
over when nobody named a service type: IDisposable, IAsyncDisposable,
IEquatable<T>, IComparable, ICloneable, IConvertible, IFormattable, IParsable<T>,
ISerializable, IEnumerable/IEnumerable<T> and the INotify* family. When one is
the only interface, the class registers as itself. IEnumerable earns its place
twice over, since registering a service as IEnumerable<T> collides with how the
container represents every registration of T.

A list rather than the namespace rule AsSelfWithInterfaces uses, because the two
are not the same problem. That expansion is additive, so excluding too much costs
a bonus registration. This is an exclusive choice, so excluding too much means the
interface the developer wanted is not registered at all. System holds plenty of
genuine service roles -- IEqualityComparer<T>, IJsonTypeInfoResolver,
IHttpClientFactory -- and a blanket System rule broke the JsonSerializerContext
registrations in SutProject.Tests. Previously only INotifyPropertyChanged was
skipped, which was the same reactive patch Autofac and Scrutor each ended up with.

Also stops a skipped interface becoming the symbol walked for a base class
interface, which would have let `class Foo : IEnumerable<int>` hand back
IEnumerable.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FasPSEiCXbFwobHLur2pnv
Three strands, landing together because the documentation describes the new
package names and would be wrong without them.

Mocking packages. `[Mock]` needed NSubstitute, so anyone using Moq or FakeItEasy
could not use it at all. The pieces the mocking support actually needs --
IMockSupportAttribute, IOrderedAttribute, IInjectValueAttribute,
InjectValuesAttribute and AttributeUtility -- never referenced xUnit, but living
in DependencyModules.xUnit forced every mocking package to depend on a test
framework it does not use. They move to a new test-framework-neutral
DependencyModules.Testing, and DependencyModules.Moq and
DependencyModules.FakeItEasy join NSubstitute on top of it. With NSubstitute and
FakeItEasy the injected instance is also what you configure; Moq separates the
two, so the container receives Mock<T>.Object and the mock is reached through
Mock.Get(instance).

DependencyModules.xUnit.NSubstitute becomes DependencyModules.NSubstitute, since
naming one mocking package after xUnit would be misleading standing next to the
other two. Consumers update the PackageReference and the using; the attribute is
unchanged.

ModuleEnvironment. A ModuleEnvironment is now a collection of its values, so they
can be written inline and enumerated back out. A key not written falls back to an
environment variable of that name, and a key written as null hides one. Leading
with false pins an environment to exactly what is at the call site, which is what
a test asserting registrations wants -- otherwise a variable set on the machine
running it reaches a key the test never mentioned.

Documentation. The guide opened nearly every page with a definition rather than a
situation, so it read as though the reader already knew why they were there. Each
page now states the problem first, in code where that is clearer, then what the
library does about it, then a worked example. The testing pages are built on the
sample web app in this repository, so their examples are known to compile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FasPSEiCXbFwobHLur2pnv
xunit.v3 brings in xunit.v3.core, whose targets set OutputType to Exe and
IsTestProject to true on the assumption that anything referencing them is a test
project. This one ships the [ModuleTest] attribute for other people's test
projects; it is not one itself.

Three consequences, all now gone. The package shipped a runtimeconfig.json in
lib/, which a library has no business carrying. The assembly carried eight
[RegisterRunnerReporter] attributes injected by those targets, so xUnit's
AppVeyor, TeamCity and VSTS reporters were part of this package's public API
surface. And solution-level dotnet test tried to run the library as a test
project, failing in the VSTest testhost resolving its own Newtonsoft.Json --
nothing here has ever depended on Newtonsoft, it is the testhost's own manifest.

OutputType has to be corrected through ExcludeAssets rather than a property:
xunit.v3.core.targets sets it unconditionally and imports after the csproj body,
so there is nothing to override. Only build assets are dropped -- the reference
and the package dependency are untouched. Consumers are unaffected either way,
since xunit.v3.core ships those targets under build/ rather than
buildTransitive/, so they never flowed transitively to begin with.

The API snapshot loses only the eight injected attributes; every type and member
is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FasPSEiCXbFwobHLur2pnv
@ipjohnson
ipjohnson merged commit 76e4444 into main Aug 9, 2026
2 checks passed
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