1.0.0-rc9210: per-library mocking packages, service type fixes, and a guide rewrite - #30
Merged
Conversation
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
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.
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 mockingsupport actually needs —
IMockSupportAttribute,IOrderedAttribute,IInjectValueAttribute,InjectValuesAttribute,AttributeUtility— never referenced xUnit, but living inDependencyModules.xUnitforced every mocking package to depend on a test framework it does notuse. They move to a new framework-neutral
DependencyModules.Testing, withDependencyModules.MoqandDependencyModules.FakeItEasyjoining NSubstitute on top.Breaking:
DependencyModules.xUnit.NSubstituteis nowDependencyModules.NSubstitute, and themoved types changed namespace to match. Naming one mocking package after xUnit would be misleading
standing next to the other two. Consumers update the
PackageReferenceand theusing; theattributes 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, IPoolregistered asIDisposableand was unresolvable asIPool. DeclaringIDisposablefirst is a normal ordering, and the interface the developer meantlost 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
INotifyPropertyChangedwas skipped — the samereactive patch Autofac and Scrutor each ended up with.
Deliberately a list rather than the namespace rule
AsSelfWithInterfacesuses, because the two arenot 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.
Systemholds plenty of genuine service roles —IEqualityComparer<T>,IJsonTypeInfoResolver,IHttpClientFactory— and a blanketSystemrule broke theJsonSerializerContextregistrations inSutProject.Tests.DependencyModules.xUnit was building as a test project
xunit.v3.core's targets setOutputType=ExeandIsTestProject=trueon the assumption thatanything referencing them is a test project. This one ships
[ModuleTest]for other people's testprojects. It shipped a
runtimeconfig.jsoninlib/, carried eight[RegisterRunnerReporter]attributes in its public API, and made solution-level
dotnet testfail 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
nullhides one.Leading with
falsepins an environment to exactly what is at the call site — what a test assertingregistrations 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