Skip to content

feat(assertions): IsEqualTo with implicitly-convertible wrappers (#5720)#5751

Merged
thomhurst merged 3 commits intomainfrom
feat/5720-implicit-wrapper-equals
Apr 26, 2026
Merged

feat(assertions): IsEqualTo with implicitly-convertible wrappers (#5720)#5751
thomhurst merged 3 commits intomainfrom
feat/5720-implicit-wrapper-equals

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Closes #5720

Summary

  • Adds generic IsEqualTo<TValue, TOther> and IsNotEqualTo<TValue, TOther> overloads that detect a user-defined implicit operator at runtime (cached per type-pair) and compare the converted value. This restores Assert.That(productCode).IsEqualTo("Example") for wrapper Value Objects and generalises it to int / decimal / custom wrappers.
  • Defined as partials of the source-generated EqualsAssertionExtensions / NotEqualsAssertionExtensions so [OverloadResolutionPriority(-1)] actually disambiguates calls where source and expected types match (priority is only consulted between overloads in the same containing type).
  • AssertionExtensionGenerator now emits public static partial class to host these partials.
  • Two F# tests call the int-specific extension method directly because F# does not honour OverloadResolutionPriority.

Test plan

  • New Issue5720Tests cover string and int wrapper Value Objects (positive, negative, null, same-type-still-wins)
  • All 2030 TUnit.Assertions.Tests pass (net10.0)
  • Source-generator snapshot tests pass on net8.0 / net9.0 / net10.0 (snapshots updated for the partial keyword)
  • PublicAPI snapshot tests pass on net8.0 / net9.0 / net10.0; Net4_7 snapshot updated by hand (no host runtime locally)
  • F# test project compiles and links

… T (#5720)

Restores the wrapper-vs-primitive ergonomic so e.g.
`Assert.That(productCode).IsEqualTo("Example")` works for Value Object wrappers
around string/int/etc.

- Adds generic `IsEqualTo<TValue, TOther>` and `IsNotEqualTo<TValue, TOther>`
  overloads that detect a user-defined implicit operator at runtime and compare
  the converted value. The reflection lookup is cached per type-pair.
- Defines them as partial of the source-generated `EqualsAssertionExtensions` /
  `NotEqualsAssertionExtensions` so `[OverloadResolutionPriority(-1)]` actually
  disambiguates the call when the source and expected types match (priority is
  only consulted between overloads in the same containing type).
- Updates `AssertionExtensionGenerator` to emit `public static partial class` so
  the partials can extend the generated class.
- F# does not honour `OverloadResolutionPriority`, so two F# tests now call the
  int-specific extension method directly.
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Apr 25, 2026

Not up to standards ⛔

🔴 Issues 2 minor

Alerts:
⚠ 2 issues (≤ 0 issues of at least minor severity)

Results:
2 new issues

Category Results
CodeStyle 2 minor

View in Codacy

🟢 Metrics 31 complexity

Metric Results
Complexity 31

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: IsEqualTo with implicitly-convertible wrappers

Summary

The PR adds generic IsEqualTo<TValue, TOther> and IsNotEqualTo<TValue, TOther> overloads that discover an implicit conversion operator at runtime via reflection (cached per type-pair), then compares the converted value. The source generator is updated to emit partial classes so hand-written partials can live alongside generated ones, allowing [OverloadResolutionPriority(-1)] to work within the same type.

Overall the design approach is sound — caching reflection, using partial classes for OverloadResolutionPriority, and applying [RequiresUnreferencedCode] properly. However there are a few issues to address before merge.


Issues

Bug: FindImplicitOperator fallback searches in the wrong direction

File: TUnit.Assertions/Extensions/ImplicitConversionEqualityExtensions.cs

var op = FindImplicitOperator(fromType, toType)
         ?? FindImplicitOperator(toType, fromType);   // <-- logically wrong

FindImplicitOperator(definedOn, targetType) returns a method defined on definedOn whose ReturnType == targetType. The first call correctly finds an operator on TFrom that converts to TTo. The second call is intended to handle the C# rule that implicit operators may be declared on either participant type — but FindImplicitOperator(toType, fromType) looks for a method on TTo with ReturnType == TFrom, which is the reverse conversion (TTo → TFrom), not the forward one (TFrom → TTo defined on TTo).

The correct second search should look for a method on TTo that has ReturnType == TTo and a single parameter of type TFrom:

// Correct: find "public static implicit operator TTo(TFrom value)" defined on TTo
private static MethodInfo? FindImplicitOperatorOnTarget(Type definedOn, Type fromType)
{
    foreach (var method in definedOn.GetMethods(BindingFlags.Public | BindingFlags.Static))
    {
        if (method.Name != "op_Implicit") continue;
        if (method.ReturnType != definedOn) continue;  // ReturnType == TTo
        var parameters = method.GetParameters();
        if (parameters.Length == 1 && parameters[0].ParameterType == fromType)
            return method;
    }
    return null;
}

This bug is invisible in the current test suite because all test operators are defined on the source type, but it will silently fail for the legitimate C# pattern of defining the conversion on the target type.


Inconsistency: IsNotEqualTo<TValue,TOther> parameter is not nullable

File: TUnit.Assertions/Extensions/ImplicitConversionEqualityExtensions.cs

The companion IsEqualTo<TValue,TOther> takes TOther? expected (nullable), but IsNotEqualTo takes a non-nullable TOther. This means Assert.That(x).IsNotEqualTo((string?)null) won't compile where Assert.That(x).IsEqualTo((string?)null) does (and is tested). The parameter should be TOther? notExpected for consistency, with a corresponding null test case added.


F# template files not updated — will produce broken projects

File: TUnit.Templates/content/TUnit.FSharp/Tests.fs

The F# template files still use the old call pattern. F# does not honour [OverloadResolutionPriority], so after this PR introduces a second applicable overload in EqualsAssertionExtensions, new projects created from the F# template will fail to compile with an ambiguity error. The project's own test files were patched (with explicit type qualification), but the shipped templates were missed. This will affect every user who creates a new F# TUnit project.


Minor / Non-blocking Notes

Cache write pattern — The current code uses a manual TryGetValue + assignment pattern. Under contention, two threads can both miss the cache check and both call BuildConverter. This is benign (equivalent delegates), but the idiomatic pattern Cache.GetOrAdd(key, _ => BuildConverter<TFrom, TTo>()) is more consistent with how PropertyCache is used elsewhere in the codebase.

MethodInfo.Invoke boxes value typesop.Invoke(null, [value]) boxes value-type arguments on every assertion call even though the delegate is cached. An Expression.Lambda<Func<TFrom,TTo>>(...).Compile() would avoid this, but requires [RequiresDynamicCode] making it incompatible with AOT. Worth a brief comment explaining the trade-off so future maintainers don't blindly switch to expression compilation without adding the AOT annotation.

Missing error-path test — There's no test for the case where no implicit operator exists. The error-throwing lambda is cached, and the resulting InvalidOperationException surfaces inside assertion evaluation rather than at the call site, which may produce a confusing failure message. A test confirming the error is reported clearly would help.


Positives

  • The partial class approach for co-locating hand-written and generated overloads is the correct design for OverloadResolutionPriority to work.
  • Caching the reflection lookup per type-pair is the right performance mitigation.
  • [RequiresUnreferencedCode] is consistently applied throughout.
  • The null-guard in the converter lambda correctly handles nullable reference types.
  • Snapshot and PublicAPI tests are fully updated including the net4.7 case.
  • Test coverage for primary use cases (string wrapper, int wrapper, positive/negative, null, same-type) is solid.

Recommendation: REQUEST_CHANGES

The FindImplicitOperator fallback logic is incorrect for the "operator defined on target type" case — a real bug that current tests don't exercise. The IsNotEqualTo non-nullable parameter is a usability inconsistency. The F# template files are not updated and will produce broken projects for new users. These three issues should be fixed before merge.

- FindImplicitOperator now correctly searches the target type for
  op_Implicit(TFrom)->TTo declared on the target (the previous fallback
  searched the reverse direction). Unified the two search call sites
  into a single helper that takes the predicate explicitly.
- IsNotEqualTo<TValue,TOther> parameter is now TOther? for parity with
  IsEqualTo (was non-nullable).
- F# templates updated with the same disambiguation pattern used in the
  project's own .fs files so new F# projects compile (F# does not honour
  [OverloadResolutionPriority]). Also updated the AspNet F# template.
- New regression tests cover (a) operator declared on the target type,
  (b) IsNotEqualTo with a null wrapper, and (c) IsNotEqualTo with a
  non-null wrapper against a null primitive.
- Two analyzer test snippets needed an explicit type argument
  (IsEqualTo<T>) because the analyzer-test harness is pinned to Roslyn
  4.8 (which predates [OverloadResolutionPriority]). Production code is
  unaffected; comment in each test class explains the workaround.
- PublicAPI snapshots and F# template snapshots updated for the
  signature change.
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: All Previous Blocking Issues Resolved ✅

All three blocking issues from the prior review (commit 33b0b9ef) have been addressed in the current HEAD:

  • FindImplicitOperator direction bug — Fixed. The 3-arg signature FindImplicitOperator(toType, fromType, toType) correctly searches for op_Implicit on the target type returning toType with a parameter of fromType.
  • IsNotEqualTo nullability — Fixed. The parameter is now TOther? notExpected.
  • F# template files — Fixed. Both TUnit.FSharp/Tests.fs and TUnit.AspNet.FSharp/TestProject/Tests.fs call the type-specific extension directly with explanatory comments.

Remaining Observations

Minor: Cache write pattern is not race-safe against wasted work

// Current:
if (Cache.TryGetValue(key, out var cached))
    return (Func<TFrom?, TTo?>)cached;
var converter = BuildConverter<TFrom, TTo>();
Cache[key] = converter;
return converter;

Under concurrent first-call pressure for the same type pair, multiple threads can each run BuildConverter and overwrite each other — safe because all converters are equivalent, but wasteful. The idiomatic pattern is:

return (Func<TFrom?, TTo?>)Cache.GetOrAdd(key, _ => BuildConverter<TFrom, TTo>());

GetOrAdd still allows a race on the factory call (it doesn't lock), but the returned value is always the winner's, and future readers always hit the cache. The intent to "build once" is also more clearly expressed. This is non-blocking.

Minor: No error-path test for missing implicit operator

There is no test verifying that calling IsEqualTo when no implicit conversion operator exists throws InvalidOperationException with the diagnostic message. The message is good:

No implicit conversion operator from 'X' to 'Y' was found. IsEqualTo / IsNotEqualTo with a different argument type requires 'X' to define 'public static implicit operator Y(X value)'.

A test for this path would protect the error message from unintentional changes and confirm the feature behaves sensibly for misuse. Non-blocking but recommended.

Noted: F# fluent API regression

The F# template change from:

do! check(Assert.That(result).IsEqualTo(c))

to:

do! check(IntEqualsAssertionExtensions.IsEqualTo(Assert.That(result), c))

…is verbose and breaks the fluent experience for F# users. This is a pre-existing limitation (F# ignores [OverloadResolutionPriority]) that is now more visible because the PR introduces the ambiguity. There is no clean solution within this PR's scope — it's a known F# language constraint — but it's worth tracking as a known rough edge.


Positives

  • The partial class design for co-locating the overload with the source-generated one is the correct architectural choice for [OverloadResolutionPriority] to work.
  • The cache lookup is per-type-pair and lazily populated — correct performance trade-off.
  • [RequiresUnreferencedCode] and [DynamicallyAccessedMembers] are applied correctly and consistently; the [UnconditionalSuppressMessage] on FindImplicitOperator is properly justified.
  • The AOT comment explaining the MethodInfo.Invoke boxing trade-off vs Expression.Lambda is exactly the right level of documentation.
  • Snapshot and PublicAPI tests updated across all TFMs including net4.7.
  • Test coverage for the primary use cases (string wrapper, int wrapper, operator on target type, nulls, IsNotEqualTo) is solid.

Verdict: Approve. The core design is sound, all previous blockers are resolved, and the remaining items are minor and non-blocking.

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.

All three previous blocking issues resolved. Approving — see inline comment for remaining minor observations.

- Cache write switched to ConcurrentDictionary.GetOrAdd (single-call idiom)
- Add error-path test asserting clear diagnostic when no implicit operator exists
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: Final Pass ✅

This is a follow-up to the two prior review rounds. All previous blocking issues and minor observations from those rounds have been addressed in the current HEAD (). Here is a fresh assessment of the final state.


Previous Observations — Status

Issue Status
FindImplicitOperator direction bug ✅ Fixed — correct 3-arg signature
IsNotEqualTo nullability (TOther notExpectedTOther? notExpected) ✅ Fixed
F# template files calling type-specific extension ✅ Fixed with explanatory comments
Cache.GetOrAdd instead of manual TryGetValue/set ✅ Fixed
Error-path test for missing implicit operator ✅ Fixed — IsEqualTo_NoImplicitOperator_Surfaces_Diagnostic + IsNotEqualTo_...

Fresh Assessment

Core implementation () is clean and correct:

  • The with is idiomatic. The keyword is valid here — generic type parameters are not captured variables, they are part of the generic instantiation.
  • Null path in the converter lambda is correct for both reference types and struct-based : boxing a with produces a boxed , which unboxes correctly.
  • on the parameter of protects public operators from trimming at analysis time. The with a clear justification is appropriate.
  • The comment explaining why was not used (would require , breaking Native AOT) is valuable — keep it.
  • The null-forgiving operator in works correctly at runtime; the test covers that path.

Source generator change (): emitting is necessary and correct. This is not a breaking change — only adds the ability to split a class across files; it removes no API surface.

Test coverage () is thorough:

  • Positive and negative cases for both string and int wrappers
  • Operator-on-target-type fallback
  • Null source vs non-null expected and vice versa
  • Same-type still uses the original overload (critical regression guard)
  • Missing-operator diagnostic surfaced correctly

F# workaround (, template snapshots): the direct call to with an explanatory comment is the right approach given that F# does not honour .


One Minor Observation (Non-Blocking)

** in the hand-written partials** uses a single interpolated string:

The source-generated code uses three separate calls to avoid the intermediate allocation. The difference is negligible in assertion paths, but if you ever want strict consistency with the generated style, the three-call pattern would match. Not worth a change on its own.


Summary

The implementation is well-designed and production-ready. The -class approach for is the correct solution to the overload disambiguation problem, the reflection cache is safe and efficient, and the AOT story is handled correctly with appropriate annotations. The test suite is comprehensive.

No further changes required — this is good to merge.

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: Final Pass ✅

This is a follow-up to the two prior review rounds. All previous blocking issues and minor observations from those rounds have been addressed in the current HEAD (bb27ef48). Here is a fresh assessment of the final state.


Previous Observations — Status

Issue Status
FindImplicitOperator direction bug ✅ Fixed — correct 3-arg signature
IsNotEqualTo nullability (TOther notExpectedTOther? notExpected) ✅ Fixed
F# template files calling type-specific extension ✅ Fixed with explanatory comments
Cache.GetOrAdd instead of manual TryGetValue/set ✅ Fixed
Error-path test for missing implicit operator ✅ Fixed — IsEqualTo_NoImplicitOperator_Surfaces_Diagnostic + IsNotEqualTo_...

Fresh Assessment

Core implementation (ImplicitConversionEqualityExtensions.cs) is clean and correct:

  • The ConcurrentDictionary.GetOrAdd with static _ => BuildConverter<TFrom, TTo>() is idiomatic. The static keyword is valid here — generic type parameters are not captured variables; they are part of the generic instantiation.
  • The null path in the converter lambda is correct for both reference types and struct-based Nullable<T>: boxing a Nullable<T> with HasValue = true produces a boxed T, which MethodInfo.Invoke unboxes correctly.
  • [DynamicallyAccessedMembers(PublicMethods)] on the definedOn parameter of FindImplicitOperator protects public operators from trimming at analysis time. The [UnconditionalSuppressMessage] with a clear justification is appropriate.
  • The comment explaining why Expression.Lambda(...).Compile() was not used (would require [RequiresDynamicCode], breaking Native AOT) is valuable — keep it.
  • The notExpected! null-forgiving operator in IsNotEqualTo works correctly at runtime; the test IsNotEqualTo_NonNull_Wrapper_Against_Null_Primitive_Passes verifies that path.

Source generator change (AssertionExtensionGenerator.cs): emitting public static partial class is necessary and correct. This is not a breaking change — partial only adds the ability to split a class across files; it removes no API surface.

Test coverage (Issue5720Tests.cs) is thorough:

  • Positive and negative cases for both string and int wrappers
  • Operator-on-target-type fallback
  • Null source vs non-null expected and vice versa
  • Same-type still uses the original overload (critical regression guard)
  • Missing-operator diagnostic surfaced correctly

F# workaround (AsyncTests.fs, template snapshots): the direct call to IntEqualsAssertionExtensions.IsEqualTo(...) with an explanatory comment is the right approach given that F# does not honour [OverloadResolutionPriority].


One Minor Observation (Non-Blocking)

ExpressionBuilder.Append in the hand-written partials uses a single interpolated string:

source.Context.ExpressionBuilder.Append($".IsEqualTo({expectedExpression})");

The source-generated code uses three separate Append calls to avoid the intermediate allocation. The difference is negligible in assertion paths, but if consistency with the generated style matters, the three-call pattern would match. Not worth a change on its own.


Summary

The implementation is well-designed and production-ready. The partial-class approach for [OverloadResolutionPriority] is the correct solution to the overload disambiguation problem, the reflection cache is safe and efficient, and the AOT story is handled correctly with appropriate annotations. The test suite is comprehensive.

No further changes required — this is good to merge.

@thomhurst thomhurst merged commit 3a4312f into main Apr 26, 2026
14 of 16 checks passed
@thomhurst thomhurst deleted the feat/5720-implicit-wrapper-equals branch April 26, 2026 10:32
thomhurst added a commit that referenced this pull request Apr 26, 2026
…o wrappers (#5751) (#5759)

* fix(tests): disambiguate Count overload broken by IsEqualTo wrappers (#5751)

Chained_Collection_Assertions_WithStrings called
.Count(c => c.IsEqualTo(3)) on string[]. Before #5751 this failed to
bind to the per-item filter overload (int-vs-string mismatch on
IsEqualTo) and resolved to the inline-count assertion form, returning
a chainable assertion. After #5751 added IsEqualTo<TValue, TOther>
with implicit-conversion fallback, both overloads compile and
resolution picks the per-item filter, which returns
CollectionCountSource<,> -- a count source without .And -- breaking
.And.Contains("Bob") with CS1061.

Mirror the int test (Chained_Collection_Assertions) and use
.Count().IsEqualTo(3) so the resulting assertion is the chainable
CollectionCountEqualsAssertion regardless of TItem.

* fix(tests): tighten Count overload comment
thomhurst added a commit that referenced this pull request Apr 26, 2026
…ilment timeout (#5761)

Rebased onto main brings in #5720/#5751 IsEqualTo wrapper overloads. On
netstandard2.0 the [OverloadResolutionPriority] and [RequiresUnreferencedCode]
attributes don't render, so the Net4_7 verified snapshot loses six lines.

Step3_Worker_Fulfills_Order timed out after the previous 120s bump on the
ubuntu-latest runner; widen to 180s to absorb RabbitMQ + Worker delivery
under concurrent load.
thomhurst added a commit that referenced this pull request Apr 27, 2026
) (#5767)

* fix(assertions): gate IsEqualTo<TValue, TOther> overload to net9+ (#5765)

The IsEqualTo<TValue, TOther> / IsNotEqualTo<TValue, TOther> overloads added
in 1.40.0 (#5751) rely on [OverloadResolutionPriority(-1)] to lose to the
source-generated single-generic overload when both apply. That attribute is
only honored by C# 13+ and only present in System.Runtime.CompilerServices
on .NET 9+. On the net8.0 / netstandard2.0 builds of TUnit.Assertions,
Polyfill silently drops the attribute and every same-type IsEqualTo call
becomes ambiguous (CS0121), breaking effectively every existing test suite.

Gating the new overloads behind #if NET9_0_OR_GREATER means net8.0 /
netstandard2.0 consumers fall back to the original well-defined overload
with no contest. net9.0+ consumers using a modern SDK keep the wrapper
Value Object support introduced in #5720.

Public API snapshots for net8.0 and netstandard2.0 (Net4_7) updated to
drop the gated overloads. Issue5720Tests gated to match. Added Issue5765
regression test covering same-type IsEqualTo across enum/primitive/record.

* chore: trim verbose comments and test names from #5765 fix

Per simplify review:
- Shrunk the gate-rationale comment in ImplicitConversionEqualityExtensions
  from 9 lines to 4 — keep the WHY, drop the narration.
- Replaced the duplicate gate-rationale block in Issue5720Tests with a
  one-line pointer to the source file.
- Dropped the redundant XML <summary> on Issue5765Tests (sibling
  IssueNNNNTests files don't carry one) and the "_Compiles_And_Passes"
  suffix on each test name.
This was referenced Apr 27, 2026
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.

[Feature]: IsEqualTo() & co with implicitly convertible wrappers

1 participant