Skip to content

feat: add ConfigureAppHost hook to AspireFixture#5202

Merged
thomhurst merged 2 commits intomainfrom
feature/aspire-configure-apphost
Mar 21, 2026
Merged

feat: add ConfigureAppHost hook to AspireFixture#5202
thomhurst merged 2 commits intomainfrom
feature/aspire-configure-apphost

Conversation

@thomhurst
Copy link
Owner

Summary

  • Adds a protected virtual void ConfigureAppHost(DistributedApplicationOptions, HostApplicationBuilderSettings) method to AspireFixture<TAppHost>
  • Passes it to DistributedApplicationTestingBuilder.CreateAsync so users can configure low-level AppHost options before the builder is created
  • Follows the same virtual-override pattern as existing hooks (Args, ConfigureBuilder, ResourceTimeout, etc.)

Closes #5199

Usage

public class MyAspireFixture : AspireFixture<Projects.MyAppHost>
{
    protected override void ConfigureAppHost(
        DistributedApplicationOptions options,
        HostApplicationBuilderSettings settings)
    {
        options.DisableDashboard = true;
    }
}

Test plan

  • dotnet build TUnit.Aspire/TUnit.Aspire.csproj passes with 0 errors
  • CI passes

Add a protected virtual ConfigureAppHost method that allows users to
configure DistributedApplicationOptions and HostApplicationBuilderSettings
before the Aspire builder is created.

Closes #5199
Copy link
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: feat: add ConfigureAppHost hook to AspireFixture

Overall assessment: This is a clean, well-scoped addition that follows the established patterns in AspireFixture. The implementation is correct and the documentation is thorough. A few points are worth discussing before merging.


What the PR does

It exposes the configureBuilder callback parameter of DistributedApplicationTestingBuilder.CreateAsync as a protected virtual method on AspireFixture<TAppHost>. This gives subclasses a way to configure low-level DistributedApplicationOptions (e.g. disabling the dashboard) and HostApplicationBuilderSettings before the builder is created, filling the gap identified in issue #5199.


Design observations

1. Method-group vs. lambda — a subtle behavioral difference

The change passes ConfigureAppHost as a method group directly to CreateAsync:

var builder = await DistributedApplicationTestingBuilder.CreateAsync<TAppHost>(Args, ConfigureAppHost);

This is fine in practice, but worth noting for future maintainers: this correctly routes through virtual dispatch, so subclass overrides are called as expected. No issue, just worth documenting since delegate capture can trip people up if the method is ever moved to a static context.

2. Consistency with the proposed alternative in the issue

The issue author proposed an Action<...> property (ConfigureCallback). The chosen approach of a virtual method (ConfigureAppHost) is preferable — it is more discoverable (shows up in IDE override lists), harder to misuse (no accidental null delegate), and consistent with how ConfigureBuilder and WaitForResourcesAsync are structured. Good choice.

3. Empty body notation

The default empty implementation uses an inline empty body:

protected virtual void ConfigureAppHost(DistributedApplicationOptions options, HostApplicationBuilderSettings settings) { }

ConfigureBuilder uses the same style, so this is consistent with the surrounding code.

4. CancellationToken passthrough

The DistributedApplicationTestingBuilder.CreateAsync overload being called accepts an optional CancellationToken that defaults to default here, matching the pre-existing call that was replaced. This is consistent with the rest of InitializeAsync. Worth noting as a potential future improvement: threading a real cancellation token through the builder creation step too.


Suggestions

Consider passing the CancellationToken explicitly (follow-up)

The InitializeAsync method already creates timeout-scoped CancellationTokenSource objects for later phases. For correctness under cancellation, consider threading a top-level token through the builder creation step as well:

var builder = await DistributedApplicationTestingBuilder.CreateAsync<TAppHost>(Args, ConfigureAppHost, cancellationToken);

This is a broader refactor and not strictly required for this PR — noting it as a potential follow-up.

XML doc: <see cref> resolution

The summary cross-references ConfigureBuilder, which is great for discoverability. One small nit: verify the <see cref> for DistributedApplicationTestingBuilder.CreateAsync{TEntryPoint}(...) resolves correctly in generated docs — cref signatures with fully-qualified type names can sometimes produce broken links in DocFX/Sandcastle depending on the using directives in scope.


Summary

The change is small, correct, well-documented, and follows the established patterns in AspireFixture. There are no architectural concerns or anti-patterns introduced. The main open question (CancellationToken threading) can reasonably be deferred as a follow-up.

Verdict: approve with minor optional nits.

Update lifecycle diagram, virtual methods table, and Args/ConfigureBuilder
tip to include the new ConfigureAppHost hook.
Copy link
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 (new commit: 0e4ede5c)

The second commit adds documentation for ConfigureAppHost in docs/docs/examples/aspire.md. This directly addresses the discoverability concern from my previous review.

What changed

  • Lifecycle diagram: Added ↳ ConfigureAppHost() as a sub-step under CreateAsync, clearly showing it runs before ConfigureBuilder. This is well-placed and the visual hierarchy (↳ arrow) communicates the relationship intuitively.
  • Override reference table: ConfigureAppHost(options, settings) is now listed in the correct order between Args and ConfigureBuilder, with a clear default value (No-op) and description.
  • Tip block: Updated from "Args vs ConfigureBuilder" to "Args vs ConfigureAppHost vs ConfigureBuilder" — the added bullet explains when to use each hook with a concrete example (DisableDashboard), making the decision easy for users.

Assessment

The documentation is accurate, ordered correctly relative to the lifecycle, and provides actionable guidance. The concrete DisableDashboard example in the tip is exactly the right level of specificity.

Previous observations (CancellationToken threading as a follow-up, XML doc cref resolution) remain open but are not introduced or worsened by this commit.

This PR is ready to merge. Both the implementation and documentation are clean, consistent, and complete.

@thomhurst thomhurst merged commit 6212cf3 into main Mar 21, 2026
14 of 16 checks passed
@thomhurst thomhurst deleted the feature/aspire-configure-apphost branch March 21, 2026 13:18
This was referenced Mar 23, 2026
github-actions bot pushed a commit to IntelliTect/CodingGuidelines that referenced this pull request Mar 23, 2026
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Updated [TUnit.Core](https://github.com/thomhurst/TUnit) from 1.19.57 to
1.21.6.

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

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

## 1.21.6

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

## What's Changed
### Other Changes
* perf: replace object locks with Lock type for efficient
synchronization by @​thomhurst in
thomhurst/TUnit#5219
* perf: parallelize test metadata collection for source-generated tests
by @​thomhurst in thomhurst/TUnit#5221
* perf: use GetOrAdd args overload to eliminate closure allocations in
event receivers by @​thomhurst in
thomhurst/TUnit#5222
* perf: self-contained TestEntry<T> with consolidated switch invokers
eliminates per-test JIT by @​thomhurst in
thomhurst/TUnit#5223
### Dependencies
* chore(deps): update tunit to 1.21.0 by @​thomhurst in
thomhurst/TUnit#5220


**Full Changelog**:
thomhurst/TUnit@v1.21.0...v1.21.6

## 1.21.0

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

## What's Changed
### Other Changes
* perf: reduce ConcurrentDictionary closure allocations in hot paths by
@​thomhurst in thomhurst/TUnit#5210
* perf: reduce async state machine overhead in test execution pipeline
by @​thomhurst in thomhurst/TUnit#5214
* perf: reduce allocations in EventReceiverOrchestrator and
TestContextExtensions by @​thomhurst in
thomhurst/TUnit#5212
* perf: skip timeout machinery when no timeout configured by @​thomhurst
in thomhurst/TUnit#5211
* perf: reduce allocations and lock contention in ObjectTracker by
@​thomhurst in thomhurst/TUnit#5213
* Feat/numeric tolerance by @​agray in
thomhurst/TUnit#5110
* perf: remove unnecessary lock in ObjectTracker.TrackObjects by
@​thomhurst in thomhurst/TUnit#5217
* perf: eliminate async state machine in
TestCoordinator.ExecuteTestAsync by @​thomhurst in
thomhurst/TUnit#5216
* perf: eliminate LINQ allocation in ObjectTracker.UntrackObjectsAsync
by @​thomhurst in thomhurst/TUnit#5215
* perf: consolidate module initializers into single .cctor via partial
class by @​thomhurst in thomhurst/TUnit#5218
### Dependencies
* chore(deps): update tunit to 1.20.0 by @​thomhurst in
thomhurst/TUnit#5205
* chore(deps): update dependency nunit3testadapter to 6.2.0 by
@​thomhurst in thomhurst/TUnit#5206
* chore(deps): update dependency cliwrap to 3.10.1 by @​thomhurst in
thomhurst/TUnit#5207


**Full Changelog**:
thomhurst/TUnit@v1.20.0...v1.21.0

## 1.20.0

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

## What's Changed
### Other Changes
* Fix inverted colors in HTML report ring chart due to locale-dependent
decimal formatting by @​Copilot in
thomhurst/TUnit#5185
* Fix nullable warnings when using Member() on nullable properties by
@​Copilot in thomhurst/TUnit#5191
* Add CS8629 suppression and member access expression matching to
IsNotNullAssertionSuppressor by @​Copilot in
thomhurst/TUnit#5201
* feat: add ConfigureAppHost hook to AspireFixture by @​thomhurst in
thomhurst/TUnit#5202
* Fix ConfigureTestConfiguration being invoked twice by @​thomhurst in
thomhurst/TUnit#5203
* Add IsEquivalentTo assertion for Memory<T> and ReadOnlyMemory<T> by
@​thomhurst in thomhurst/TUnit#5204
### Dependencies
* chore(deps): update dependency gitversion.tool to v6.6.2 by
@​thomhurst in thomhurst/TUnit#5181
* chore(deps): update dependency gitversion.msbuild to 6.6.2 by
@​thomhurst in thomhurst/TUnit#5180
* chore(deps): update tunit to 1.19.74 by @​thomhurst in
thomhurst/TUnit#5179
* chore(deps): update verify to 31.13.3 by @​thomhurst in
thomhurst/TUnit#5182
* chore(deps): update verify to 31.13.5 by @​thomhurst in
thomhurst/TUnit#5183
* chore(deps): update aspire to 13.1.3 by @​thomhurst in
thomhurst/TUnit#5189
* chore(deps): update dependency stackexchange.redis to 2.12.4 by
@​thomhurst in thomhurst/TUnit#5193
* chore(deps): update microsoft/setup-msbuild action to v3 by
@​thomhurst in thomhurst/TUnit#5197


**Full Changelog**:
thomhurst/TUnit@v1.19.74...v1.20.0

## 1.19.74

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

## What's Changed
### Other Changes
* feat: per-hook activity spans with method names by @​thomhurst in
thomhurst/TUnit#5159
* fix: add tooltip to truncated span names in HTML report by @​thomhurst
in thomhurst/TUnit#5164
* Use enum names instead of numeric values in test display names by
@​Copilot in thomhurst/TUnit#5178
* fix: resolve CS8920 when mocking interfaces whose members return
static-abstract interfaces by @​lucaxchaves in
thomhurst/TUnit#5154
### Dependencies
* chore(deps): update tunit to 1.19.57 by @​thomhurst in
thomhurst/TUnit#5157
* chore(deps): update dependency gitversion.msbuild to 6.6.1 by
@​thomhurst in thomhurst/TUnit#5160
* chore(deps): update dependency gitversion.tool to v6.6.1 by
@​thomhurst in thomhurst/TUnit#5161
* chore(deps): update dependency polyfill to 9.20.0 by @​thomhurst in
thomhurst/TUnit#5163
* chore(deps): update dependency polyfill to 9.20.0 by @​thomhurst in
thomhurst/TUnit#5162
* chore(deps): update dependency polyfill to 9.21.0 by @​thomhurst in
thomhurst/TUnit#5166
* chore(deps): update dependency polyfill to 9.21.0 by @​thomhurst in
thomhurst/TUnit#5167
* chore(deps): update dependency polyfill to 9.22.0 by @​thomhurst in
thomhurst/TUnit#5168
* chore(deps): update dependency polyfill to 9.22.0 by @​thomhurst in
thomhurst/TUnit#5169
* chore(deps): update dependency coverlet.collector to 8.0.1 by
@​thomhurst in thomhurst/TUnit#5177

## New Contributors
* @​lucaxchaves made their first contribution in
thomhurst/TUnit#5154

**Full Changelog**:
thomhurst/TUnit@v1.19.57...v1.19.74

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

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit.Core&package-manager=nuget&previous-version=1.19.57&new-version=1.21.6)](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]: Allow access to the configuration builder in DistributedApplicationTestingBuilder.CreateAsync in TUnit.Aspire

1 participant