Skip to content

Environment conditions, .NET 10 support, and xUnit v3 3.2.2 - #33

Merged
ipjohnson merged 2 commits into
mainfrom
feat/environment-first-class
Aug 10, 2026
Merged

Environment conditions, .NET 10 support, and xUnit v3 3.2.2#33
ipjohnson merged 2 commits into
mainfrom
feat/environment-first-class

Conversation

@ipjohnson

@ipjohnson ipjohnson commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Four strands of work that grew on one branch and share enough files that splitting them now would produce commits that do not build.

Environment conditions become first class (44e8e7c)

[IfEnvironment] and the rest of the family now take effect on [Decorator], so a decorator can exist only where it is wanted. Where the condition does not hold the decorator is never applied, so the service resolves undecorated rather than being wrapped by something that re-tests the environment on every call. Conventions gain the same set as IfEnvironment(…), IfNotEnvironment(…), IfEnvironmentValue(…) and friends, so a whole rule can be gated without repeating the attribute on every class it matches.

.NET 10 support (bbed38d)

Every shipping package multi-targets net8.0 and net10.0 from a single $(LibraryTargetFrameworks) property, so dropping net8.0 when it leaves support in November 2026 is one edit.

A .NET 10 project already worked — a net8.0 assembly loads fine on it — but the packages brought their Microsoft.Extensions.* 8.x dependency along, and on .NET 10 those live in the shared framework, so an older assembly landed in consumer output in place of the one the framework already supplies. Each target framework now carries its own baseline version.

The generators stay on netstandard2.0, which is what Roslyn requires. global.json pins the 10.0 SDK, and CI installs both — the 10 SDK builds net8.0 but cannot run its tests, because roll-forward stops at the major version boundary.

verify-packages.sh now builds a consumer project per target framework and asserts every dependency group is framework-matched. It also restores into a private cache: NuGet keys its global cache on id/version alone, so a stale extraction of the fixed 1.0.0-verify version had been shadowing the packages under test.

xUnit v3 1.0.0 to 3.2.2 (bbed38d)

The extensibility surface moved across two major versions. Module tests now honour the conditional-skip family the way [Fact] and [Theory] do — SkipExceptions on the test case, and per-row SkipType/SkipUnless/SkipWhen/Label — each of which previously had nowhere to go.

Call sites pass arguments by name rather than by position. That is what kept the upgrade to four real errors rather than nineteen misleading ones: a parameter inserted mid-list silently rebinds everything after it.

DependencyModules.xUnit now references xunit.v3.extensibility.core — the supported package for a library that ships test attributes without being a test project — replacing three settings that existed only to stop xunit.v3's targets forcing it to be an executable. xunit.analyzers is referenced explicitly, because that package does not carry it and the rules would otherwise stop being checked rather than being satisfied.

Two trait helpers replace equivalents from Xunit.Internal, which carries no compatibility guarantee; they were verified behaviour-identical by running both over the same input.

A module test also reports where it is declared now. The attribute captured no source location and the discoverer forwarded none, so test explorers had nowhere to navigate to. Naming two or more modules still cannot capture one, because C# does not allow caller-info parameters after a params array — pinned by its own test so the gap stays visible.

Mocking seam (bbed38d)

Mock<T> may be taken directly on a test parameter, and the test-framework interfaces move from DependencyModules.xUnit to DependencyModules.Testing behind ITestMethodContext, so the seam no longer names xUnit types.

Breaking change

ModuleTestCase gained a skipExceptions parameter in fifth position. Positional callers past the fourth argument need updating; [ModuleTest] users are unaffected. This wants a major version.

Verification

result
clean rebuild 0 warnings, 0 errors
DependencyModules.Tests 558 × net8.0 + net10.0
SutProject.Tests 132 × both
WebApiApp.Tests 2 × both
ConsoleTestProject exit 0 × both
coverage 87.7%, clears the 85% ratchet
verify-packages.sh all checks, both target frameworks

New trait and source-location assertions were mutation-tested — each fails when the code it covers is deliberately broken.

🤖 Generated with Claude Code

https://claude.ai/code/session_013me9Eodb4PjJgR4ZNKrC9B

Ian Johnson and others added 2 commits August 9, 2026 17:34
Conditions worked on services and nowhere else. A condition on a [Decorator]
compiled, read as deliberate, and did nothing at all -- decoration never looked
at them, so a decorator marked [IfEnvironment("Development")] wrapped the service
in production too. And a convention could only inherit conditions from the classes
it matched, so gating a whole rule meant repeating the attribute on every class.

Decorators now carry conditions the way services do. DecoratorModel gained them,
DecoratorModelUtility reads them off the class, and the writer guards the Decorate
call rather than the registration -- a decorator that does not apply is simply
never run, so the service resolves undecorated instead of being wrapped by
something that re-tests the environment on every call. Ordering is untouched: a
condition decides whether a decorator applies, never where it sits.

Reaching the environment at decoration time needed DecoratorRegistration to hold
an EnvironmentRegistryFunc, mirroring what RegistryFunc/EnvironmentRegistryFunc
already do for services. Unconditional decorators keep the old shape and are
adapted, so conditional and unconditional ones still sort against each other on
Order alone. ApplyDecorators finds the environment already registered by
ApplyServices rather than resolving a second answer to "what environment is this".

Conventions gained IfEnvironment, IfNotEnvironment, IfEnvironmentValue and
IfNotEnvironmentValue on the fluent chain, named after the attributes so the two
ways of saying the same thing read alike. Emission was free: the conditions land
on the ServiceModel the existing writer already guards.

Convention and class conditions combine with and. Letting either win would mean
one declaration silently discarding a condition written in the other. Because the
writer emits one guard per ServiceModel, matches are now grouped by implementation
*and* conditions -- merged, two conventions matching one class under different
conditions would have had the stricter guard swallow the other's registrations.

BuildCondition moved to a shared EnvironmentConditionWriter so the service and
decorator writers cannot drift into testing the same attributes differently.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FasPSEiCXbFwobHLur2pnv
Three strands of work that ended up sharing files and are committed together
rather than split into commits that would not build on their own.

.NET 10 support. Every shipping package multi-targets net8.0 and net10.0 from a
single $(LibraryTargetFrameworks) property, so dropping net8.0 when it leaves
support in November 2026 is one edit. A .NET 10 project already worked, but the
packages brought their Microsoft.Extensions.* 8.x dependency along, and on .NET
10 those live in the shared framework — an older assembly landed in consumer
output in place of the one the framework supplies. Each target framework now
carries its own baseline version. The generators stay on netstandard2.0, which
is what Roslyn requires. global.json pins the 10.0 SDK; CI installs both SDKs,
since the 10 SDK builds net8.0 but cannot run its tests. verify-packages.sh
builds a consumer per target framework and asserts the dependency groups are
framework-matched; it also restores into a private cache, without which a stale
extraction of the fixed verify version shadowed the packages under test.

xUnit v3 1.0.0 to 3.2.2. The extensibility surface moved across two majors.
Module tests now honour the conditional-skip family the way [Fact] and [Theory]
do — SkipExceptions on the case, per-row SkipType/SkipUnless/SkipWhen/Label —
each of which previously had nowhere to go. Call sites pass arguments by name
rather than by position, which is what kept the upgrade to four real errors
instead of nineteen misleading ones. DependencyModules.xUnit now references
xunit.v3.extensibility.core, the supported package for a library shipping test
attributes, replacing three settings that existed only to stop xunit.v3's
targets forcing it to be an executable; xunit.analyzers is referenced
explicitly because that package does not carry it. Two trait helpers replace
equivalents from Xunit.Internal, which carries no compatibility guarantee.

A module test also reports where it is declared. The attribute captured no
source location and the discoverer forwarded none, so test explorers had
nowhere to navigate. Naming two or more modules still cannot capture one — C#
does not allow caller-info parameters after a params array — which is pinned by
its own test.

Mock<T> may be taken directly on a test parameter, and the test-framework
interfaces move from DependencyModules.xUnit to DependencyModules.Testing
behind ITestMethodContext, so the seam no longer names xUnit types.

Breaking: ModuleTestCase gained a skipExceptions parameter in fifth position.
Positional callers past the fourth argument need updating; [ModuleTest] users
are unaffected.

558 unit, 132 integration and 2 web tests pass on both target frameworks.
Coverage 87.7%, package verification green for both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013me9Eodb4PjJgR4ZNKrC9B
@ipjohnson ipjohnson changed the title Make environment conditions first class on decorators and conventions Environment conditions, .NET 10 support, and xUnit v3 3.2.2 Aug 10, 2026
@ipjohnson
ipjohnson merged commit 6d4c99b into main Aug 10, 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