Skip to content

fix(assertions): preserve specialised source in .Count(itemAssertion) (#5707)#5749

Merged
thomhurst merged 6 commits intomainfrom
fix/5707-count-itemassertion-source
Apr 26, 2026
Merged

fix(assertions): preserve specialised source in .Count(itemAssertion) (#5707)#5749
thomhurst merged 6 commits intomainfrom
fix/5707-count-itemassertion-source

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Closes #5707

Summary

Test plan

  • Snapshot updates accepted
  • Unit test asserting message contains specialised source for Count(item-assertion)

…tion) (#5707)

The per-item overload of Count wrapped the inner condition in a way that
discarded the specialised assertion source, leaving failure messages
generic. Inner-condition source is now propagated through to the
collection-level diagnostic.
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Apr 25, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 19 complexity

Metric Results
Complexity 19

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This PR fixes issue #5707 by refactoring CollectionCountSource to accept a raw Func<TItem, int, IAssertion?> factory instead of the typed Func<IAssertionSource<TItem>, Assertion<TItem>?>, then adds specialised extension method overloads for concrete item shapes. The core approach is the right one — the extension-method dispatch strategy is the correct C# idiom given the type-system constraints, and the test coverage for the happy path is solid.

Several issues are worth addressing:


Issue 1 — IReadOnlySet<TInner> items have no specialised overload (Correctness Gap)

AssertionExtensions.cs adds a Count overload for ISet<TInner> items, but IReadOnlySet<TInner> items (which Assert.That routes to ReadOnlySetAssertion<TInner>) have no matching overload. Calls with IReadOnlySet<TInner> items silently fall through to the generic instance method, leaving IsSubsetOf, IsSupersetOf, etc. unreachable — exactly the bug this PR claims to fix.

The existing test (Count_Set_Items_Reach_IsSubsetOf_On_Inner) only covers ISet<int>, not IReadOnlySet<int>. A companion overload backed by ReadOnlySetAssertion<TInner> is needed to close this gap.


Issue 2 — Redundant legacy constructor on CollectionCountEqualsAssertion (Design Debt)

CollectionCountSource.cs (lines ~172–191) now has two constructors: the new Func<TItem, int, IAssertion?> one and an old Func<IAssertionSource<TItem>, Assertion<TItem>?> shim that just calls WrapWithValueAssertion and delegates to the new one. If there are no external call sites for the legacy constructor, it should be removed — it adds cognitive overhead and the WrapWithValueAssertion static is only there to serve it. Keeping both also means the public snapshot files lock in an extra constructor that carries no semantic value.


Issue 3 — WrapWithValueAssertion is internal static on a public class (API Coupling)

WrapWithValueAssertion is declared internal static on CollectionCountSource<TCollection, TItem> (a public class) and then called by CollectionCountEqualsAssertion via a cross-class static reference. One public class reaching into the internals of a sibling public class makes ownership unclear. If Issues 2 and 3 are fixed together — drop the legacy constructor, inline WrapWithValueAssertion at its single remaining call site — this coupling disappears entirely.


Issue 4 — Bare catch swallows OperationCanceledException (Safety)

CollectionCountSource.cs (lines ~230–238):

try
{
    await resultingAssertion.AssertAsync();
    _actualCount++;
}
catch
{
    // Item did not satisfy the assertion, don't count it
}

This pre-existed the PR, but the PR now routes every specialised-source assertion through this path, making the impact broader. A cancellation during test teardown could be silently dropped here, causing a hang or wrong count. The fix is simple:

catch (Exception ex) when (ex is not OperationCanceledException)
{
    // Item did not satisfy the assertion
}

or narrowing to catch (AssertionException) if only assertion failures should be suppressed.


Issue 5 — Array<TItem> items have no specialised overload (Feature Parity Gap)

ArrayAssertion<TItem> inherits CollectionAssertionBase<TItem[], TItem> and provides index-based operations like HasItemAt. When a collection's items are arrays (e.g. List<int[]>), there is no specialised overload — the PR covers IList<TInner> and IReadOnlyList<TInner> but not TInner[]. Users wanting HasItemAt on inner arrays will hit the same wall the issue describes.


Issue 6 — The string overload widens the lambda return type silently (Subtle Contract Change)

The existing generic instance method expects Func<IAssertionSource<TItem>, Assertion<TItem>?>, but all the new extension overloads (including for string) accept Func<IAssertionSource<TItem>, IAssertion?>. A string lambda that returns Assertion<string>? still compiles and resolves to the extension method — but the compile-time contract is now different depending on which overload wins. This is harmless today, but documenting the widening explicitly (or standardising on IAssertion? in the instance method too, if that's the long-term direction) would prevent future confusion.


Issue 7 — CountSpecialised uses a fragile explicit interface cast (Minor Design)

var context = ((IAssertionSource<TCollection>)source).Context;

AssertionExtensions is in the same assembly as Assertion<TCollection>, so the protected Context property should be accessible directly as source.Context without the cast. The cast couples the helper to the detail of which interface exposes Context explicitly, and will break at runtime (not compile time) if that explicit implementation changes.


Issue 8 — Failure-message tests are missing for most specialised shapes (Test Coverage)

The PR's motivation includes preserving typed diagnostic messages in the failure path. Only Count_Specialised_Source_Failure_Message_Mentions_Inner_Expectation checks the message content, and only for the IEnumerable<TInner> shape. Without failure-path tests for IList, IDictionary, ISet, and IReadOnlyList shapes, regressions on message quality for those shapes would go undetected.


Overall Assessment

The core fix is correct and the extension-method dispatch strategy is the right design for this problem. The main items to address before merging:

  1. Add the IReadOnlySet<TInner> specialised overload (Issue 1 — correctness gap).
  2. Remove the legacy Func<IAssertionSource<TItem>, Assertion<TItem>?> constructor if it has no external callers (Issues 2 & 3 — simplification).
  3. Narrow the bare catch to guard against OperationCanceledException (Issue 4 — safety).

Issues 5–8 are lower priority but worth a follow-up.

- Add Count(itemAssertion) overload for IReadOnlySet<TInner>
- Drop legacy constructor on CollectionCountEqualsAssertion (unused)
- Narrow bare catch to preserve OperationCanceledException semantics
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up Review

The second commit addresses all three primary issues from the previous review cleanly:

  • IReadOnlySet overload added - the IReadOnlySet<TInner> gap is closed, with a matching test.
  • Legacy constructor on CollectionCountEqualsAssertion removed - the dead overload and its WrapWithValueAssertion helper are gone.
  • Bare catch narrowed - OperationCanceledException is now preserved correctly.

A few smaller observations on what remains:


1 — Public constructor on CollectionCountSource still carries a legacy signature

The public constructor on CollectionCountSource<TCollection, TItem> still accepts Func<IAssertionSource<TItem>, Assertion<TItem>?>?. The internal design has moved to Func<TItem, int, IAssertion?>, and the public constructor just wraps it. Keeping the old signature in the public API surface (confirmed by the verified snapshot files) means external callers are permanently coupled to Assertion<TItem>? as the return type even though all new overloads use IAssertion?. If narrowing that constructor to internal is feasible (i.e., no external callers exist), it would keep the public surface consistent and prevent future divergence. The snapshot update would be small.


2 — Explicit interface cast in CountSpecialised is necessary but deceptive

var context = ((IAssertionSource<TCollection>)source).Context;

This is actually required because Context is protected on Assertion<TValue> and AssertionExtensions is a different class (even in the same assembly). The explicit interface implementation on CollectionAssertionBase re-exposes it publicly. However, Assertion<TValue> also declares internal AssertionContext<TValue> InternalContext => Context; — since AssertionExtensions is in the same assembly, source.InternalContext would be a cleaner and more self-documenting access pattern. The rest of the file takes IAssertionSource<T> parameters and calls .Context directly; CountSpecialised is the only method that takes a concrete base class and then has to cast it. This is a minor readability issue, not a correctness problem.


3 — Array-typed items still have no specialised overload (gap)

ArrayAssertion<TItem> inherits from CollectionAssertionBase<TItem[], TItem> and exposes the same collection surface as ListAssertion. When collection items are arrays (e.g. List<int[]>), there is no specialised Count overload — the generic instance method on CollectionAssertionBase wins, exposing only IAssertionSource<TItem[]> instead of ArrayAssertion<TItem>. This was raised in the previous review as Issue 5. It is a lower-priority follow-up, but it is worth a separate issue to track so it doesn't get forgotten — the same pattern that broke IReadOnlySet would apply here.


4 — Failure-message coverage for non-enumerable shapes remains missing

The only failure-path test (Count_Specialised_Source_Failure_Message_Mentions_Inner_Expectation) covers the IEnumerable<TInner> shape. For IList, IDictionary, ISet, and IReadOnlyList shapes there is no test asserting that the failure message contains the typed inner expectation. This means a regression (e.g., accidentally using ValueAssertion instead of the typed source) would only be caught for the enumerable shape. This was raised as Issue 8 in the previous review. It would be valuable to add at least one failure-message test per specialised shape, or to track this as a follow-up.


Overall

The core fix is correct, the addressed issues are cleanly resolved, and the PR is in good shape. Issues 3 and 4 above are lower-priority gaps that could land as follow-up issues rather than blocking this PR. Issue 1 (public constructor signature) is the only one worth considering before merge if API surface consistency matters.

- CollectionCountSource public ctor narrowed to internal (legacy signature; no external callers)
- CountSpecialised uses Assertion.InternalContext instead of explicit IAssertionSource cast
- New T[] specialised Count overload (ArrayAssertion<TInner>) closing the array gap
- Failure-message coverage extended to IList, IDictionary, ISet, IReadOnlyList, IReadOnlySet, T[]
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: fix(assertions): preserve specialised source in .Count(itemAssertion)

Summary

This PR fixes a real bug (#5707): when items inside a collection are themselves collections, dictionaries, or sets, the per-item .Count(itemAssertion) overload previously exposed only a generic IAssertionSource<TItem> (backed by ValueAssertion<TItem>), making specialised assertion methods like HasItemAt, ContainsKey, and IsSubsetOf unreachable. The fix is correct. The OperationCanceledException handling improvement is also valuable. However, there are architectural concerns with the chosen approach.


Correctness

OperationCanceledException fix — correct and important

The old bare catch { } in CollectionCountEqualsAssertion.CheckAsync would silently swallow task cancellations. The new catch (Exception ex) when (ex is not OperationCanceledException) is correct. Notably, CollectionAllSatisfyAssertion still uses the broader catch (Exception ex) which also swallows OperationCanceledException — that's a pre-existing inconsistency worth a follow-up.


Architectural Concern: Combinatorial Explosion at the API Surface

The chosen approach — one extension method overload per TItem shape — is a "closed world" enumeration. Every time a new specialised assertion source is introduced, a developer must remember to add a matching Count(...) extension. The PR already lists nine overloads and misses at least two practical cases.

Gap 1: Concrete types are not covered by their interface overloads.

C# generic type inference resolves TItem to the exact declared type, not to an interface. So if a user writes:

var listOfHashSets = new List<HashSet<int>> { ... };
await Assert.That(listOfHashSets).Count(s => s.IsSubsetOf(universe)).IsEqualTo(2);

TItem is inferred as HashSet<int>, not ISet<int>, so the ISet<TInner> extension overload does not match. The call falls through to the instance method and SetAssertion-specific members (IsSubsetOf, IsSupersetOf, Overlaps) are unreachable in the lambda. The same gap exists for List<T> items vs the IList<TInner> overload, Dictionary<K,V> items vs IDictionary<K,V>, etc.

Gap 2: The string overload appears redundant.

The description states the string overload prevents strings from being treated as IEnumerable<char>. In practice, C# type inference does not unify string with IEnumerable<TInner> when matching a generic constraint, so the IEnumerable<TInner> overload would never be selected for string items anyway. The instance method already wraps items in ValueAssertion<string>, which is identical to what the extension does.


Alternative Design Worth Considering

The root problem is that the instance method hardcodes IAssertionSource<TItem> as the lambda parameter type. The most maintainable solution would be a single method that accepts a user-supplied per-item factory:

public CollectionCountSource<TCollection, TItem> Count<TSource>(
    Func<TItem, TSource> sourceFactory,
    Func<TSource, IAssertion?> itemAssertion,
    [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null)
    where TSource : IAssertionSource<TItem>

This removes the closed-world maintenance burden entirely. A middle-ground that preserves ergonomics: instead of per-type extension methods, shift ownership of "how to wrap an item" to the assertion source type itself (e.g., a static factory delegate), and have a single CountSpecialised helper call it. This way, adding a new assertion source type doesn't require touching the Count extension layer.

Neither is a blocker for merging — the current approach does solve the immediate problem — but the gap with concrete types (HashSet<T>, List<T>, Dictionary<K,V> as items) means the fix is incomplete for those cases.


Breaking Change — Public Constructor Removed

The public constructor of CollectionCountSource<TCollection, TItem> has been made internal. The snapshot tests correctly capture this. Since CollectionCountSource is returned from factory methods and rarely constructed directly by test authors, the real-world impact is low, but it should be noted in release notes if this library follows semver.


Minor: Inconsistency in Lambda Return Type

The instance method Count(Func<IAssertionSource<TItem>, Assertion<TItem>?> itemAssertion) uses Assertion<TItem>? as the lambda return type, while all new extension overloads use IAssertion?. The broader IAssertion? in the extensions is arguably better design. Worth unifying the instance method signature in a follow-up.


Tests

The 249-line test file is well-structured and the failure-message assertions (Contains(".Count(l => l.Count().IsEqualTo(3))")) are a good regression guard. Two suggestions:

  1. Add test cases where TItem is a concrete type (List<int>, HashSet<int>, Dictionary<K,V>) rather than only interface types — this would document the current limitation (type inference gap) and detect if it's ever addressed.
  2. The Count_String_Items_Use_String_Assertion_Source test would pass even without the string extension (the instance method handles it identically). If the extension is genuinely needed, the test should be designed to fail without it.

Summary

Concern Severity
OperationCanceledException fix ✅ Correct, good improvement
Concrete-type gap (HashSet<T>, List<T>, etc.) ⚠️ Medium — fix is partially incomplete
String overload appears redundant ℹ️ Low — harmless but adds noise
Removed public constructor is a breaking API change ℹ️ Low — acceptable given usage patterns
catch in CollectionAllSatisfyAssertion still bare 📝 Pre-existing, out of scope
Test coverage of concrete types missing ℹ️ Low

The core bug fix is correct and the approach is understandable, but the concrete-type inference gap means users relying on HashSet<int>, List<int>, etc. as item types will still hit the original issue. Consider either documenting this limitation or addressing it with a factory-based design that doesn't require per-type overloads.

…5707)

C# generic inference resolves TItem to the exact declared type, never to
an interface, so List<HashSet<int>> items wouldn't bind to the
ISet<TInner> Count overload and HashSet-specific assertions stayed
unreachable. Same gap for List<List<T>>, List<Dictionary<K,V>>, etc.

Adds Count(itemAssertion) extension overloads for the common BCL
concrete types (List<T>, HashSet<T>, Dictionary<K,V>) wired to
ListAssertion / HashSetAssertion / MutableDictionaryAssertion so the
specialised assertion surface is reachable in the lambda.

Also drops the redundant string overload from the previous round: C#
inference cannot unify `string` with `IEnumerable<TInner>` for a
generic type parameter, so the generic instance method already wraps
each string in ValueAssertion<string> — the dedicated overload was
dead code. Adds concrete-type regression tests covering List<List<T>>,
List<HashSet<T>> and List<Dictionary<K,V>>.
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Third Iteration

This iteration has addressed all the blocking issues raised in the previous two reviews. Here is the assessment of what remains.

What's been resolved

All three primary items from the previous review are cleanly addressed:

  • Concrete-type overloads addedList<TInner>, HashSet<TInner>, and Dictionary<TKey, TValue> overloads are now present, with tests including the concrete-type cases that were previously a gap (Count_ConcreteList_Items_Reach_HasItemAt_On_Inner, Count_ConcreteHashSet_Items_Reach_IsSubsetOf_On_Inner, Count_ConcreteDictionary_Items_Reach_ContainsKey_On_Inner).
  • IReadOnlySet<TInner> overload added — the gap from the first review is closed; the #if NET5_0_OR_GREATER guard is correct.
  • Array<TInner> overload added — the gap from the second review is closed, with a matching test.
  • Failure-message tests added for all specialised shapes — each new overload now has a corresponding failure-path test. This is good regression protection.
  • OperationCanceledException fix is correctcatch (Exception ex) when (ex is not OperationCanceledException) is the right pattern for this "count items that pass" semantics.
  • Public constructor removed from snapshot — the API snapshots correctly reflect the narrowing of the constructor from public to internal.

Remaining observations (non-blocking)

1. AllSatisfyAssertion still catches OperationCanceledException (pre-existing, out-of-scope)

CollectionAllSatisfyAssertion and CollectionAllSatisfyMappedAssertion both have catch (Exception ex) that wraps any exception — including OperationCanceledException — into AssertionResult.Failed. This is semantically different from the Count path (it's re-surfacing, not swallowing), but it still converts a cancellation into a test failure rather than propagating it. This is pre-existing and not touched by this PR; worth a follow-up issue.

2. The internal constructor ambiguity is a minor readability concern

CollectionCountSource<TCollection, TItem> now has two internal constructors:

  • CollectionCountSource(AssertionContext<TCollection>, Func<IAssertionSource<TItem>, Assertion<TItem>?>?) — used by the generic instance method on CollectionAssertionBase
  • CollectionCountSource(AssertionContext<TCollection>, Func<TItem, int, IAssertion?>?) — used by CountSpecialised

These are distinguished only by the delegate signature. The XML doc comments explain the distinction clearly, which is adequate. The design is functional; this is just worth flagging in case a future maintainer adds a Func<TItem, IAssertion?> variant and is surprised that it would match the first overload via implicit conversion. No action needed.

3. Instance-method lambda return type remains Assertion<TItem>? while extensions use IAssertion?

The instance method on CollectionAssertionBase:

public CollectionCountSource<TCollection, TItem> Count(
    Func<IAssertionSource<TItem>, Assertion<TItem>?> itemAssertion, ...)

...still has the narrower Assertion<TItem>? return type while all new extension overloads use the broader IAssertion?. This was raised in the first review. The asymmetry is harmless today (both resolve through the same internal factory path), but it creates an inconsistent contract for users who glance at the method signatures. A follow-up to unify the instance method to IAssertion? would be clean.


Overall

The bug fix is correct and complete for all the cases covered. The extension-method combinatorial approach has now been well-documented with inline comments explaining why both interface and concrete-type overloads are needed. Test coverage is solid including failure-message assertions and concrete-type cases. The API snapshot updates are correct. This is ready to merge.

…sertion? (#5707)

The instance method on CollectionAssertionBase still returned
Assertion<TItem>? while every new extension overload used IAssertion?.
Tightening the asymmetry so the signature reads the same across the
whole Count(itemAssertion) family.
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #5749: fix(assertions): preserve specialised source in .Count(itemAssertion)

Summary

The PR correctly identifies and fixes a real usability bug: the Count(itemAssertion) overload was wrapping each item in a generic ValueAssertion<TItem>, which meant users working with collections-of-collections couldn't reach typed assertion methods like HasItemAt, ContainsKey, or IsSubsetOf inside the lambda.

The fix is broadly sound — but the chosen approach carries significant long-term maintenance costs that are worth discussing.


Primary Concern: The Overload-Explosion Pattern is a Scalability Trap

The fix adds 11 new extension overloads (interface shapes + concrete types) to the already large AssertionExtensions.cs (now ~2,077 lines). This is the same pattern that was used in #5702, and it is fragile by design.

Why this will continue to compound:

  1. The same problem exists in All().Satisfy() right now. CollectionAllSatisfyAssertion also uses new ValueAssertion<TItem>(item, ...) and exposes only IAssertionSource<TItem> to its lambda — meaning users with collections-of-collections face the same limitation there. Every new *Satisfy-style method added in the future will repeat this gap.

  2. Every new assertion source type (e.g. a hypothetical SortedSetAssertion, ImmutableListAssertion) would require adding more overloads here.

  3. The pattern requires both interface-shape overloads and concrete-type overloads due to C# type inference not unifying concrete types with interfaces — so the number of overloads scales as O(2 x assertion_types).

Alternative worth considering: A factory-based approach would eliminate the overload explosion and apply universally. Rather than adding per-type extension methods, if Count accepted an Func<TItem, IAssertion?> where the user themselves calls Assert.That(item) to produce a typed assertion source, the dispatch would resolve to the right source type automatically — no new overloads needed per collection type. This would mean the issue dissolves naturally for all current and future collection types.

If that redesign is out of scope for this PR, it would be worth opening a tracking issue to avoid the overload pattern spreading further to All().Satisfy() and any future per-item assertion methods.


Correctness: Good Fix for OperationCanceledException

// Before
catch
{
    // Item did not satisfy the assertion, don't count it
}

// After
catch (Exception ex) when (ex is not OperationCanceledException)
{
    // Item did not satisfy the assertion, don't count it
}

This is a correct and important fix. Swallowing OperationCanceledException (and TaskCanceledException) would silently abort test cancellation, leaving tests hanging or producing misleading results. This fix is solid and should also be applied to the analogous catch (Exception ex) blocks in CollectionAllSatisfyAssertion and CollectionAllSatisfyMappedAssertion in CollectionAssertions.cs (lines ~689 and ~756), which currently catch Exception without filtering out cancellation exceptions.


Breaking Change: Public Constructor Removed

The public constructor CollectionCountSource(AssertionContext<TCollection>, Func<IAssertionSource<TItem>, Assertion<TItem>?>?) has been removed from the public API (visible in all four snapshot diffs). Both constructors are now internal.

This is a breaking change for any external code that was directly constructing CollectionCountSource. The change seems intentional but is worth explicitly calling out in the PR description.


Minor: Count() Null-Factory Cast

// After
return new CollectionCountSource<TCollection, TItem>(
    Context, (Func<TItem, int, IAssertion?>?)null);

The explicit cast disambiguates the two internal constructors. The approach is correct. As an alternative, a named static factory or a dedicated private constructor that takes no factory at all would eliminate the need for the cast, but this is a minor point.


Test Coverage

The regression tests in Issue5707Tests.cs are thorough — interface shapes, concrete types, failure message content, and array items are all covered. The approach of checking that exception messages contain the specialised lambda expression text is a good regression guard.


Verdict

The fix solves the reported bug correctly, the OperationCanceledException improvement is a genuine safety enhancement, and the tests are solid. The main concern is architectural: the overload-per-type pattern will need to be repeated for every similar method and every new collection assertion type going forward. It is worth discussing whether a user-side Assert.That(item) factory approach can avoid this before the pattern becomes further entrenched.

…sertions (#5707)

Mirrors the same fix applied to CollectionCountSource — bare catch
was swallowing test cancellations along with assertion failures.
intellitect-bot pushed a commit to IntelliTect/EssentialCSharp.Web that referenced this pull request Apr 27, 2026
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.39.0 to
1.40.5.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.40.5

<!-- Release notes generated using configuration in .github/release.yml
at v1.40.5 -->

## What's Changed
### Other Changes
* Fix reflection property injection reuse by @​thomhurst in
thomhurst/TUnit#5763
* fix(assertions): gate IsEqualTo<TValue, TOther> overload to net9+
(#​5765) by @​thomhurst in thomhurst/TUnit#5767
### Dependencies
* chore(deps): update tunit to 1.40.0 by @​thomhurst in
thomhurst/TUnit#5762


**Full Changelog**:
thomhurst/TUnit@v1.40.0...v1.40.5

## 1.40.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.40.0 -->

## What's Changed
### Other Changes
* perf(engine): collapse async forwarding wrappers in test execution
(#​5714) by @​thomhurst in thomhurst/TUnit#5725
* perf(engine): skip Console.Out/Err FlushAsync when no output captured
(#​5712) by @​thomhurst in thomhurst/TUnit#5724
* perf(engine): collapse async state machines on hook cache-hit /
empty-hook path (#​5713) by @​thomhurst in
thomhurst/TUnit#5726
* perf: eliminate per-test closure + GetOrAdd factory alloc (#​5710) by
@​thomhurst in thomhurst/TUnit#5727
* perf(engine): replace global lock in EventReceiverRegistry with
lock-free CAS by @​thomhurst in
thomhurst/TUnit#5731
* perf(engine): batch per-test overhead cleanups (#​5719) by @​thomhurst
in thomhurst/TUnit#5730
* #​5733 handling all arguments for Fact and Theory by @​inyutin-maxim
in thomhurst/TUnit#5734
* fix(assertions): prefer string overload of Member() over
IEnumerable<char> (#​5702) by @​thomhurst in
thomhurst/TUnit#5721
* fix(migration): preserve comments/XML docs when removing sole
attributes (#​5698) by @​thomhurst in
thomhurst/TUnit#5739
* perf(build): trim test TFMs and skip viewer dump by default by
@​thomhurst in thomhurst/TUnit#5741
* fix(pipeline): skip TestBaseModule frameworks with missing binaries by
@​thomhurst in thomhurst/TUnit#5752
* feat(assertions): focused diff messages for IsEqualTo/IsEquivalentTo
(#​5732) by @​thomhurst in thomhurst/TUnit#5747
* fix(analyzers): remove incorrect AOT rules TUnit0300/0301/0302
(#​5722) by @​thomhurst in thomhurst/TUnit#5746
* perf(engine): lazy hook metadata registration (#​5448) by @​thomhurst
in thomhurst/TUnit#5750
* chore(templates): unify TUnit version pinning to 1.* (#​5709) by
@​thomhurst in thomhurst/TUnit#5743
* fix(templates): floating TUnit.Aspire version (#​5708) by @​thomhurst
in thomhurst/TUnit#5742
* fix(assertions): preserve specialised source in .Count(itemAssertion)
(#​5707) by @​thomhurst in thomhurst/TUnit#5749
* feat(assertions): IsEqualTo with implicitly-convertible wrappers
(#​5720) by @​thomhurst in thomhurst/TUnit#5751
* feat(aspire): add ability to manually remove resources by @​Odonno in
thomhurst/TUnit#5586
* fix(fscheck): register default CancellationToken arbitrary that
surfaces TestContext token by @​JohnVerheij in
thomhurst/TUnit#5758
* fix(engine): allow keyed NotInParallel tests to run alongside
unconstrained tests (#​5700) by @​thomhurst in
thomhurst/TUnit#5740
* perf: skip TimeoutHelper wrap when no explicit [Timeout] is set
(#​5711) by @​thomhurst in thomhurst/TUnit#5728
### Dependencies
* chore(deps): update tunit to 1.39.0 by @​thomhurst in
thomhurst/TUnit#5701
* chore(deps): update aspire to 13.2.4 by @​thomhurst in
thomhurst/TUnit#5735
* chore(deps): bump postcss from 8.5.6 to 8.5.10 in /docs by
@​dependabot[bot] in thomhurst/TUnit#5736
* chore(deps): update dependency fscheck to 3.3.3 by @​thomhurst in
thomhurst/TUnit#5760

## New Contributors
* @​inyutin-maxim made their first contribution in
thomhurst/TUnit#5734
* @​Odonno made their first contribution in
thomhurst/TUnit#5586

**Full Changelog**:
thomhurst/TUnit@v1.39.0...v1.40.0

Commits viewable in [compare
view](thomhurst/TUnit@v1.39.0...v1.40.5).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.39.0&new-version=1.40.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
github-actions Bot pushed a commit to IntelliTect/CodingGuidelines that referenced this pull request Apr 27, 2026
Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.37.0 to
1.40.10.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit.Core's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.40.10

<!-- Release notes generated using configuration in .github/release.yml
at v1.40.10 -->

## What's Changed
### Other Changes
* refactor(opentelemetry): depend on TUnit.Core instead of umbrella
TUnit by @​thomhurst in thomhurst/TUnit#5774
### Dependencies
* chore(deps): update tunit to 1.40.5 by @​thomhurst in
thomhurst/TUnit#5769


**Full Changelog**:
thomhurst/TUnit@v1.40.5...v1.40.10

## 1.40.5

<!-- Release notes generated using configuration in .github/release.yml
at v1.40.5 -->

## What's Changed
### Other Changes
* Fix reflection property injection reuse by @​thomhurst in
thomhurst/TUnit#5763
* fix(assertions): gate IsEqualTo<TValue, TOther> overload to net9+
(#​5765) by @​thomhurst in thomhurst/TUnit#5767
### Dependencies
* chore(deps): update tunit to 1.40.0 by @​thomhurst in
thomhurst/TUnit#5762


**Full Changelog**:
thomhurst/TUnit@v1.40.0...v1.40.5

## 1.40.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.40.0 -->

## What's Changed
### Other Changes
* perf(engine): collapse async forwarding wrappers in test execution
(#​5714) by @​thomhurst in thomhurst/TUnit#5725
* perf(engine): skip Console.Out/Err FlushAsync when no output captured
(#​5712) by @​thomhurst in thomhurst/TUnit#5724
* perf(engine): collapse async state machines on hook cache-hit /
empty-hook path (#​5713) by @​thomhurst in
thomhurst/TUnit#5726
* perf: eliminate per-test closure + GetOrAdd factory alloc (#​5710) by
@​thomhurst in thomhurst/TUnit#5727
* perf(engine): replace global lock in EventReceiverRegistry with
lock-free CAS by @​thomhurst in
thomhurst/TUnit#5731
* perf(engine): batch per-test overhead cleanups (#​5719) by @​thomhurst
in thomhurst/TUnit#5730
* #​5733 handling all arguments for Fact and Theory by @​inyutin-maxim
in thomhurst/TUnit#5734
* fix(assertions): prefer string overload of Member() over
IEnumerable<char> (#​5702) by @​thomhurst in
thomhurst/TUnit#5721
* fix(migration): preserve comments/XML docs when removing sole
attributes (#​5698) by @​thomhurst in
thomhurst/TUnit#5739
* perf(build): trim test TFMs and skip viewer dump by default by
@​thomhurst in thomhurst/TUnit#5741
* fix(pipeline): skip TestBaseModule frameworks with missing binaries by
@​thomhurst in thomhurst/TUnit#5752
* feat(assertions): focused diff messages for IsEqualTo/IsEquivalentTo
(#​5732) by @​thomhurst in thomhurst/TUnit#5747
* fix(analyzers): remove incorrect AOT rules TUnit0300/0301/0302
(#​5722) by @​thomhurst in thomhurst/TUnit#5746
* perf(engine): lazy hook metadata registration (#​5448) by @​thomhurst
in thomhurst/TUnit#5750
* chore(templates): unify TUnit version pinning to 1.* (#​5709) by
@​thomhurst in thomhurst/TUnit#5743
* fix(templates): floating TUnit.Aspire version (#​5708) by @​thomhurst
in thomhurst/TUnit#5742
* fix(assertions): preserve specialised source in .Count(itemAssertion)
(#​5707) by @​thomhurst in thomhurst/TUnit#5749
* feat(assertions): IsEqualTo with implicitly-convertible wrappers
(#​5720) by @​thomhurst in thomhurst/TUnit#5751
* feat(aspire): add ability to manually remove resources by @​Odonno in
thomhurst/TUnit#5586
* fix(fscheck): register default CancellationToken arbitrary that
surfaces TestContext token by @​JohnVerheij in
thomhurst/TUnit#5758
* fix(engine): allow keyed NotInParallel tests to run alongside
unconstrained tests (#​5700) by @​thomhurst in
thomhurst/TUnit#5740
* perf: skip TimeoutHelper wrap when no explicit [Timeout] is set
(#​5711) by @​thomhurst in thomhurst/TUnit#5728
### Dependencies
* chore(deps): update tunit to 1.39.0 by @​thomhurst in
thomhurst/TUnit#5701
* chore(deps): update aspire to 13.2.4 by @​thomhurst in
thomhurst/TUnit#5735
* chore(deps): bump postcss from 8.5.6 to 8.5.10 in /docs by
@​dependabot[bot] in thomhurst/TUnit#5736
* chore(deps): update dependency fscheck to 3.3.3 by @​thomhurst in
thomhurst/TUnit#5760

## New Contributors
* @​inyutin-maxim made their first contribution in
thomhurst/TUnit#5734
* @​Odonno made their first contribution in
thomhurst/TUnit#5586

**Full Changelog**:
thomhurst/TUnit@v1.39.0...v1.40.0

## 1.39.0

<!-- Release notes generated using configuration in .github/release.yml
at v1.39.0 -->

## What's Changed
### Other Changes
* perf(mocks): shrink MethodSetup + cache stateless matchers by
@​thomhurst in thomhurst/TUnit#5669
* fix(mocks): handle base classes with explicit interface impls (#​5673)
by @​thomhurst in thomhurst/TUnit#5674
* fix(mocks): implement indexer in generated mock (#​5676) by
@​thomhurst in thomhurst/TUnit#5683
* fix(mocks): disambiguate IEquatable<T>.Equals from object.Equals
(#​5675) by @​thomhurst in thomhurst/TUnit#5680
* fix(mocks): escape C# keyword identifiers at all emit sites (#​5679)
by @​thomhurst in thomhurst/TUnit#5684
* fix(mocks): emit [SetsRequiredMembers] on generated mock ctor (#​5678)
by @​thomhurst in thomhurst/TUnit#5682
* fix(mocks): skip MockBridge for class targets with static-abstract
interfaces (#​5677) by @​thomhurst in
thomhurst/TUnit#5681
* chore(mocks): regenerate source generator snapshots by @​thomhurst in
thomhurst/TUnit#5691
* perf(engine): collapse async state-machine layers on hot test path
(#​5687) by @​thomhurst in thomhurst/TUnit#5690
* perf(engine): reduce lock contention in scheduling and hook caches
(#​5686) by @​thomhurst in thomhurst/TUnit#5693
* fix(assertions): prevent implicit-to-string op from NREing on null
(#​5692) by @​thomhurst in thomhurst/TUnit#5696
* perf(engine/core): reduce per-test allocations (#​5688) by @​thomhurst
in thomhurst/TUnit#5694
* perf(engine): reduce message-bus contention on test start (#​5685) by
@​thomhurst in thomhurst/TUnit#5695
### Dependencies
* chore(deps): update tunit to 1.37.36 by @​thomhurst in
thomhurst/TUnit#5667
* chore(deps): update verify to 31.16.2 by @​thomhurst in
thomhurst/TUnit#5699


**Full Changelog**:
thomhurst/TUnit@v1.37.36...v1.39.0

## 1.37.36

<!-- Release notes generated using configuration in .github/release.yml
at v1.37.36 -->

## What's Changed
### Other Changes
* fix(telemetry): remove duplicate HTTP client spans by @​thomhurst in
thomhurst/TUnit#5668


**Full Changelog**:
thomhurst/TUnit@v1.37.35...v1.37.36

## 1.37.35

<!-- Release notes generated using configuration in .github/release.yml
at v1.37.35 -->

## What's Changed
### Other Changes
* Add TUnit.TestProject.Library to the TUnit.Dev.slnx solution file by
@​Zodt in thomhurst/TUnit#5655
* fix(aspire): preserve user-supplied OTLP endpoint (#​4818) by
@​thomhurst in thomhurst/TUnit#5665
* feat(aspire): emit client spans for HTTP by @​thomhurst in
thomhurst/TUnit#5666
### Dependencies
* chore(deps): update dependency dotnet-sdk to v10.0.203 by @​thomhurst
in thomhurst/TUnit#5656
* chore(deps): update microsoft.aspnetcore to 10.0.7 by @​thomhurst in
thomhurst/TUnit#5657
* chore(deps): update tunit to 1.37.24 by @​thomhurst in
thomhurst/TUnit#5659
* chore(deps): update microsoft.extensions to 10.0.7 by @​thomhurst in
thomhurst/TUnit#5658
* chore(deps): update aspire to 13.2.3 by @​thomhurst in
thomhurst/TUnit#5661
* chore(deps): update dependency microsoft.net.test.sdk to 18.5.0 by
@​thomhurst in thomhurst/TUnit#5664

## New Contributors
* @​Zodt made their first contribution in
thomhurst/TUnit#5655

**Full Changelog**:
thomhurst/TUnit@v1.37.24...v1.37.35

## 1.37.24

<!-- Release notes generated using configuration in .github/release.yml
at v1.37.24 -->

## What's Changed
### Other Changes
* docs: add Tluma Ask AI widget to Docusaurus site by @​thomhurst in
thomhurst/TUnit#5638
* Revert "chore(deps): update dependency docusaurus-plugin-llms to
^0.4.0 (#​5637)" by @​thomhurst in
thomhurst/TUnit#5640
* fix(asp-net): forward disposal in FlowSuppressingHostedService
(#​5651) by @​JohnVerheij in
thomhurst/TUnit#5652
### Dependencies
* chore(deps): update dependency docusaurus-plugin-llms to ^0.4.0 by
@​thomhurst in thomhurst/TUnit#5637
* chore(deps): update tunit to 1.37.10 by @​thomhurst in
thomhurst/TUnit#5639
* chore(deps): update opentelemetry to 1.15.3 by @​thomhurst in
thomhurst/TUnit#5645
* chore(deps): update opentelemetry by @​thomhurst in
thomhurst/TUnit#5647
* chore(deps): update dependency dompurify to v3.4.1 by @​thomhurst in
thomhurst/TUnit#5648
* chore(deps): update dependency system.commandline to 2.0.7 by
@​thomhurst in thomhurst/TUnit#5650
* chore(deps): update dependency microsoft.entityframeworkcore to 10.0.7
by @​thomhurst in thomhurst/TUnit#5649
* chore(deps): update dependency microsoft.templateengine.authoring.cli
to v10.0.203 by @​thomhurst in
thomhurst/TUnit#5653
* chore(deps): update dependency
microsoft.templateengine.authoring.templateverifier to 10.0.203 by
@​thomhurst in thomhurst/TUnit#5654


**Full Changelog**:
thomhurst/TUnit@v1.37.10...v1.37.24

## 1.37.10

<!-- Release notes generated using configuration in .github/release.yml
at v1.37.10 -->

## What's Changed
### Other Changes
* docs(test-filters): add migration callout for --filter →
--treenode-filter by @​johnkattenhorn in
thomhurst/TUnit#5628
* fix: re-enable RPC tests and modernize harness (#​5540) by @​thomhurst
in thomhurst/TUnit#5632
* fix(mocks): propagate [Obsolete] and null-forgiving raise dispatch
(#​5626) by @​JohnVerheij in
thomhurst/TUnit#5631
* ci: use setup-dotnet built-in NuGet cache by @​thomhurst in
thomhurst/TUnit#5635
* feat(playwright): propagate W3C trace context into browser contexts by
@​thomhurst in thomhurst/TUnit#5636
### Dependencies
* chore(deps): update tunit to 1.37.0 by @​thomhurst in
thomhurst/TUnit#5625

## New Contributors
* @​johnkattenhorn made their first contribution in
thomhurst/TUnit#5628
* @​JohnVerheij made their first contribution in
thomhurst/TUnit#5631

**Full Changelog**:
thomhurst/TUnit@v1.37.0...v1.37.10

Commits viewable in [compare
view](thomhurst/TUnit@v1.37.0...v1.40.10).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit.Core&package-manager=nuget&previous-version=1.37.0&new-version=1.40.10)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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.

.Count(itemAssertion) per-item overload loses specialised source

1 participant