Skip to content

perf(engine): lazy hook metadata registration (#5448)#5750

Merged
thomhurst merged 4 commits intomainfrom
perf/5448-lazy-hook-registration
Apr 26, 2026
Merged

perf(engine): lazy hook metadata registration (#5448)#5750
thomhurst merged 4 commits intomainfrom
perf/5448-lazy-hook-registration

Conversation

@thomhurst
Copy link
Copy Markdown
Owner

Closes #5448

Summary

  • Hook collections store LazyHookEntry (factory + cached singleton); heavy metadata now built once on first execution
  • Module-init cost reduced to a static-lambda + index allocation per hook
  • RegistrationIndex still computed eagerly to preserve registration ordering
  • Source-gen snapshots regenerated

Test plan

  • Source-gen snapshot tests pass on net472, net8.0, net9.0, net10.0
  • Public API snapshot tests pass on all 4 TFMs
  • Engine builds clean (net10.0)
  • Hook integration slices pass (SimpleHookTest, BasicTests, EnsureBeforeEveryClassHit, Verify_TestSession_Hook_In_Referenced_Library_Executed, HelperTest_To_Trigger_Assembly_Hooks)

)

Hooks now register a lightweight LazyHookEntry at module load and only
construct MethodMetadata, ClassMetadata, parameter arrays etc. on first
execution. Mirrors the lazy per-class metadata pattern from #5394.
RegistrationIndex remains computed eagerly to preserve ordering.
@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Apr 25, 2026

Not up to standards ⛔

🔴 Issues 1 minor

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

Results:
1 new issue

Category Results
CodeStyle 1 minor

View in Codacy

🟢 Metrics -17 complexity

Metric Results
Complexity -17

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: perf(engine): lazy hook metadata registration (#5750)

This is a solid, well-executed performance optimization. The core idea — separating "register a hook exists" (cheap, module-init) from "build its metadata" (expensive, deferred) — is architecturally sound and follows established lazy initialization patterns.


What works well

Design of LazyHookEntry<T> is clean. Two constructors (factory-based for source-gen, pre-materialized for reflection mode) mean the lazy wrapper is transparent to callers. Clearing _factory = null after first materialization is a nice touch — it allows the closure to be GC'd early.

Preserving registration order across lazy initialization is the tricky part of this change, and it's handled correctly: RegistrationIndex is captured eagerly at module-init (via GetNext*()) so hook ordering is deterministic even when materialization happens out-of-declaration order.

BuildGlobalHooksAsync generic helper (lines 86–110) nicely eliminates the duplication that would otherwise exist across the 10 global hook types.

SortAndProject with a static lambda (line 115) avoids a per-call closure allocation on the sort comparator.


Concerns and suggestions

1. Missing volatile on _materialized — subtle correctness risk

private T? _materialized;

The double-check locking (DCL) pattern requires either a volatile field or use of Volatile.Read/Volatile.Write to ensure the first null-check outside the lock sees the latest value across all CPU cores.

Without volatile, the JIT is allowed to cache the _materialized read in a register, meaning a second thread could perpetually see null after materialization and keep spinning into the lock (or in a degenerate JIT path, read a partially-constructed object on certain CPU architectures).

In practice the x64 CLR JIT rarely generates such code, but this is spec-undefined behavior and will fail Roslyn's thread-safety analyzers. The fix is one keyword:

private volatile T? _materialized;

Alternatively, replace the manual DCL with Lazy<T> (with LazyThreadSafetyMode.ExecutionAndPublication), which handles the volatile semantics internally and makes the intent self-documenting:

private readonly Lazy<T> _lazy;
// ...
public T Materialize() => _lazy.Value;

The Lazy<T> approach is simpler, provably correct, and the CLR implementation is well-tested. The trade-off is a small extra allocation per entry (the Lazy<T> wrapper), but that's marginal compared to the metadata graph it's guarding.


2. Race in BuildAndCacheBeforeTestHooksAsync causes redundant materialization

private async ValueTask<IReadOnlyList<NamedHookDelegate<TestContext>>> BuildAndCacheBeforeTestHooksAsync(Type testClassType)
{
    var hooks = await BuildBeforeTestHooksAsync(testClassType).ConfigureAwait(false);
    _beforeTestHooksCache.TryAdd(testClassType, hooks);
    return hooks;
}

Two threads racing to the first test of the same class will both call BuildBeforeTestHooksAsync concurrently, both build the full sorted list, and the loser's result is discarded. LazyHookEntry.Materialize() is idempotent so this is safe, but it wastes work (the loser builds the list for nothing).

The conventional fix is GetOrAdd with an async factory — but ConcurrentDictionary doesn't have that natively. A common alternative is Lazy<Task<T>> as the dictionary value, so only one thread ever starts the async build:

private readonly ConcurrentDictionary<Type, Lazy<Task<IReadOnlyList<NamedHookDelegate<TestContext>>>>> _beforeTestHooksCache = new();

public ValueTask<IReadOnlyList<NamedHookDelegate<TestContext>>> CollectBeforeTestHooksAsync(Type testClassType)
{
    var lazyTask = _beforeTestHooksCache.GetOrAdd(
        testClassType,
        static (t, self) => new Lazy<Task<IReadOnlyList<NamedHookDelegate<TestContext>>>>(
            () => self.BuildBeforeTestHooksAsync(t), 
            LazyThreadSafetyMode.ExecutionAndPublication),
        this);
    return new ValueTask<IReadOnlyList<NamedHookDelegate<TestContext>>>(lazyTask.Value);
}

That said, the current behavior is correct and the wasted work is bounded (at most N_threads redundant list builds per type, each happening at most once). If the hook-execution hot path isn't showing contention in practice, this is "nice to have" rather than blocking.


3. Non-static comparator lambda in SortAndAddHooks

// Line 163
hooks.Sort((a, b) => a.order != b.order
    ? a.order.CompareTo(b.order)
    : a.registrationIndex.CompareTo(b.registrationIndex));

SortAndProject (line 115) correctly uses static (a, b) => ... — same pattern should apply here to be consistent and avoid a per-call closure allocation on a method that may be called for every class.


4. BuildGlobalHooksAsync materializes hooks sequentially despite being async

foreach (var entry in sourceHooks)
{
    var hook = entry.Materialize();            // sync
    var namedHook = await createDelegate(hook); // async
    hooks.Add((hook.Order, hook.RegistrationIndex, namedHook));
}

The createDelegate is awaited one at a time. If delegate creation ever becomes non-trivially async (e.g., loading assemblies), the sequential ordering might become a bottleneck. Since this runs during InitializeAsync() before any tests start, it's fine for now — but worth noting if performance ever becomes an issue here.


Minor observations

  • The SortAndProject call in BuildGlobalHooksAsync (line 109) and SortAndAddHooks in BuildBeforeTestHooksAsync (line 273) perform separate but semantically identical sorts. These could share a single static comparator or IComparer<T> to keep the logic in one place.

  • Generated code indentation: the static __registrationIndex => line in snapshots is indented with 4 spaces, while the outer lambda block uses 8 spaces. This is cosmetic but could be cleaned up in the generator.


Summary

The lazy registration approach is the right call — it trades a small per-entry allocation for a large reduction in module-init cost, which is important for large test suites that use many hooks. The RegistrationIndex-at-registration-time trick elegantly preserves ordering without requiring synchronization at sort time.

The primary concern is the missing volatile on _materialized, which is a latent correctness issue. Everything else (the ordering race in the per-type cache, the non-static lambda) is a minor improvement opportunity. Happy to approve once the volatile/Lazy<T> concern is addressed.

- LazyHookEntry: mark _materialized volatile for correct DCL semantics
- HookDelegateBuilder: race-free per-class hook cache via Lazy<Task<>>;
  extract shared GetOrBuildHooksAsync/TryGetCachedHooks helpers and
  cache build delegates to avoid per-call method-group allocation
- Static sort comparator in SortAndAddHooks
- TestBaseModule: skip frameworks the test project did not build for
  (TestProject.props now defaults to net10.0; pipeline still iterates
  net8.0 + net472 and was failing with "process cannot find file" for
  TUnit.Mocks.Tests.exe on net472)
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: perf(engine): lazy hook metadata registration (#5750)

All three critical concerns from the previous review have been addressed. This is now in good shape.


Previous concerns — all resolved ✅

1. Missing volatile on _materialized — FIXED

The field is now correctly declared private volatile T? _materialized (LazyHookEntry.cs:25), and there's even an inline comment explaining exactly why volatile is required for the double-checked-lock pattern on weakly-ordered architectures. Well done.

2. Race causing redundant materialization in per-type hook cache — FIXED

The cache now uses ConcurrentDictionary<Type, Lazy<Task<IReadOnlyList<...>>>> throughout HookDelegateBuilder.cs (lines 24–29), and GetOrBuildHooksAsync (lines 197–213) implements the single-flight Lazy<Task<T>> pattern correctly. Racing threads observe the same Lazy instance and await the same underlying build.

3. Non-static comparator lambda in SortAndAddHooks — FIXED

SortAndAddHooks (line 182) now uses static (a, b) => ..., matching SortAndProject.


Remaining observations (non-blocking)

1. Redundant Materialize() calls in TestDiscovery duplicate check (ReflectionHookDiscoveryService.cs:494–495, 677–678)

if (!Sources.AfterTestDiscoveryHooks.Any(h => h.Materialize().MethodInfo.Name == discoveryMetadata.Name &&
                                               h.Materialize().MethodInfo.Type == discoveryMetadata.Type))

Two issues here:

  • Materialize() is called twice per hook in the predicate — the result should be stored in a local.
  • More importantly, this secondary dedup check appears to be dead code. Both RegisterAfterHook and RegisterAfterEveryHook already guard against re-registration via _registeredMethods.TryAdd(methodKey, true) at the top of each method (lines 441–444, 609–612). By the time execution reaches the TestDiscovery case, the method is already claimed. This means the Any(h => h.Materialize()...) scan iterates and eagerly materializes all existing AfterTestDiscoveryHooks entries for no benefit.

If the intent is to avoid duplicates across [After(TestDiscovery)] and [AfterEvery(TestDiscovery)] on the same method, the _registeredMethods key (derived from GetMethodKey(method)) already handles that. The Any() check can be safely removed.

2. Per-entry _lock object is never freed after materialization

After Materialize() runs, the _factory is nulled out (good — allows the closure to be GC'd), but _lock stays allocated for the lifetime of the entry. For large test suites with many hooks this is a small but persistent overhead. An alternative that avoids the per-entry lock entirely:

public T Materialize()
{
    if (_materialized is not null) return _materialized;
    var built = _factory!(RegistrationIndex);
    // Publish — last writer wins, all readers see a valid object
    Volatile.Write(ref _materialized, built);
    _factory = null;
    return _materialized;
}

This is lock-free and safe because T is a reference type (constructing it twice is idempotent — both produce an equivalent object). The lock pattern prevents the second call entirely; the Volatile.Write pattern accepts that two threads may both construct and the winner is arbitrary but correct. Given that materialization is a one-shot event at startup, either approach is fine — but the lock-free version removes an allocation per hook entry.

3. Generated code indentation (cosmetic)

The static __registrationIndex => line in generated snapshots is still indented with 4 spaces while the surrounding registration block uses 8 spaces. Not a correctness issue.


Verdict

The primary correctness concern (volatile) and the main performance concern (single-flight cache) are both properly addressed. The lazy registration design is sound: registration index is captured eagerly for deterministic ordering, while the heavy MethodMetadata/ClassMetadata graph is deferred to first execution. This is a meaningful startup-performance improvement for test suites with many hooks.

The two remaining points above (redundant Any() materialization, per-entry lock overhead) are minor cleanup opportunities — not blocking. Happy to approve.

The TestBaseModule fix is unrelated to lazy hook registration and now
lives on its own branch so it can land independently and unblock
other open PRs.
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.

Third-Pass Review: perf(engine): lazy hook metadata registration (#5750)

This review checks the current HEAD (96c0b8a) against the two remaining non-blocking observations flagged in the previous follow-up review.


Previous reviews summary

First review raised three critical issues (missing volatile, race condition in per-type hook cache, non-static sort comparator) — all fixed in the follow-up commit.

Second review marked two items as non-blocking:

  1. Redundant Materialize() calls in ReflectionHookDiscoveryService duplicate check
  2. Per-entry _lock object never freed after materialization

Status of remaining observations

1. Redundant Materialize() calls — still present (non-blocking, but worth a note)

ReflectionHookDiscoveryService.cs lines 494–495 and 677–678:

if (!Sources.AfterTestDiscoveryHooks.Any(h => h.Materialize().MethodInfo.Name == discoveryMetadata.Name &&
                                               h.Materialize().MethodInfo.Type == discoveryMetadata.Type))

Materialize() is called twice per element in the bag. Since this is reflection mode and hooks are pre-materialized (the LazyHookEntry(T hook) constructor path), the second call is a no-op (hits the if (_materialized is not null) fast path), so there's no functional bug — but it's still redundant allocation/branching. A var m = h.Materialize(); local would clean this up.

More importantly, as noted last review, this Any() scan appears to be dead code — the _registeredMethods.TryAdd(methodKey, true) guard at the top of RegisterAfterHook/RegisterAfterEveryHook already prevents the same method from being registered twice, making this secondary scan unreachable in practice. If this is intentional (e.g., protecting against future refactors that drop the _registeredMethods guard), a comment explaining why would make this self-documenting.

2. Per-entry _lock allocation — still present (acknowledged trade-off)

LazyHookEntry<T> allocates a private object _lock = new() per hook entry and never clears it after materialization. The previous review suggested a lock-free alternative using Volatile.Write. This remains valid feedback, though for hooks specifically (construction isn't truly idempotent if the factory has side effects like Interlocked.Increment) the lock semantics may be intentional. If the factory is guaranteed side-effect-free at-most-once, the lock-free path would remove one allocation per hook entry. If side-effects matter, the current approach is correct.


New observation

3. _processedHooks keyed on HookMethod reference (minor)

In HookDelegateBuilder.cs, ProcessHookRegistrationAsync uses a ConcurrentDictionary<object, bool> _processedHooks to deduplicate event processing:

if (!_processedHooks.TryAdd(hookMethod, true))
{
    return;
}

This deduplicates on object identity (reference equality), which is correct since each Materialize() returns the same cached instance. However, if HookMethod ever overrides Equals/GetHashCode in the future (e.g., for value semantics), this would silently break. Using a HashSet<HookMethod> with a custom ReferenceEqualityComparer (or using ConditionalWeakTable to avoid preventing GC) would make the intent explicit. Not a current bug, but a subtle coupling assumption.


Overall

The architecture is sound and the critical correctness issues from prior reviews are fully resolved. The two carry-over items from the previous review remain minor. This PR is ready to merge.

One small ask: consider either removing the unreachable Any(h => h.Materialize()...) duplicate checks (they add noise and confusion), or adding a comment explaining why they need to exist despite the _registeredMethods guard.

- ReflectionHookDiscoveryService: hoist Materialize() out of the duplicate-scan predicate; comment on why the guard stays despite _registeredMethods short-circuit
- HookDelegateBuilder: comment on _processedHooks reference-identity dedup assumption
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.

Fourth-Pass Review: perf(engine): lazy hook metadata registration (#5750)

This pass checks the latest commit (9b95cbee) against the two remaining non-blocking observations from the third-pass review.


Previous review history

Review Critical items Status
First pass volatile missing, race in per-type hook cache, non-static comparator All fixed
Second pass Redundant Materialize() calls, _lock never freed Noted as non-blocking
Third pass Both carry-over items still present, _processedHooks identity-dedup contract noted All noted as non-blocking

Status of remaining items

1. Redundant Materialize() calls — FIXED ✅

Both instances in ReflectionHookDiscoveryService.cs (lines 494 and 679) now hoist h.Materialize() into a local variable m before the predicate. The added comment explaining that this is a defence-in-depth guard despite _registeredMethods short-circuiting above is the right call — it prevents future readers from deleting the check as dead code without understanding the intent.

2. _processedHooks reference-identity comment — ADDED ✅

HookDelegateBuilder.cs now has a clear inline comment explaining:

  • dedup relies on reference identity (not value equality)
  • correctness assumption: Materialize() returns the same cached instance
  • what to revisit if HookMethod ever overrides Equals/GetHashCode

This is exactly what was asked for — the contract is now self-documenting.

3. Per-entry _lock allocation — unchanged (acknowledged trade-off)

The object _lock = new() per LazyHookEntry is still present and intentional. As noted previously, since the factory can have side effects (like Interlocked.Increment) and must execute exactly once, the lock semantics are correct and the cost is bounded to one allocation per hook registration. This is a fine trade-off for correctness.


Overall assessment

All critical issues and all non-blocking observations from the prior three reviews have now been addressed — either fixed or explicitly documented. The architecture is sound, the thread safety is correct, and the code is now self-documenting at the key decision points.

This PR is ready to merge. 🟢

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.

perf: defer hook metadata construction to first use (lazy hook registration)

1 participant