Skip to content

feat(aspire): add ability to manually remove resources#5586

Merged
thomhurst merged 1 commit intothomhurst:mainfrom
Odonno:feat/remove-aspire-resources
Apr 26, 2026
Merged

feat(aspire): add ability to manually remove resources#5586
thomhurst merged 1 commit intothomhurst:mainfrom
Odonno:feat/remove-aspire-resources

Conversation

@Odonno
Copy link
Copy Markdown
Contributor

@Odonno Odonno commented Apr 17, 2026

Description

The goal is to be able to removes specific resources that one knows should not be present/created during a test run. The most important use of this new feature is to exclude/remove UI resources, like pgAdmin, kafka-ui, etc.. that have no use during test runs.

Related Issue

N/A

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Checklist

Required

  • I have read the Contributing Guidelines
  • If this is a new feature, I started a discussion first and received agreement
  • My code follows the project's code style (modern C# syntax, proper naming conventions)
  • I have written tests that prove my fix is effective or my feature works

TUnit-Specific Requirements

  • Dual-Mode Implementation: If this change affects test discovery/execution, I have implemented it in BOTH:
    • Source Generator path (TUnit.Core.SourceGenerator)
    • Reflection path (TUnit.Engine)
  • Snapshot Tests: If I changed source generator output or public APIs:
    • I ran TUnit.Core.SourceGenerator.Tests and/or TUnit.PublicAPI tests
    • I reviewed the .received.txt files and accepted them as .verified.txt
    • I committed the updated .verified.txt files
  • Performance: If this change affects hot paths (test discovery, execution, assertions):
    • I minimized allocations and avoided LINQ in hot paths
    • I cached reflection results where appropriate
  • AOT Compatibility: If this change uses reflection:
    • I added appropriate [DynamicallyAccessedMembers] annotations
    • I verified the change works with dotnet publish -p:PublishAot=true

Testing

  • All existing tests pass (dotnet test)
  • I have added tests that cover my changes
  • I have tested both source-generated and reflection modes (if applicable)

Additional Notes

Note: Behavior already working on concrete projects.

@codacy-production
Copy link
Copy Markdown

codacy-production Bot commented Apr 17, 2026

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0

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.

@Odonno Odonno force-pushed the feat/remove-aspire-resources branch from e40e797 to 1365afa Compare April 17, 2026 09:04
@Odonno Odonno temporarily deployed to Pull Requests April 17, 2026 09:04 — with GitHub Actions Inactive
@Odonno Odonno temporarily deployed to Pull Requests April 17, 2026 09:04 — with GitHub Actions Inactive
@Odonno Odonno temporarily deployed to Pull Requests April 17, 2026 09:04 — with GitHub Actions Inactive
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This is a clean, well-motivated addition — removing UI/observability resources (pgAdmin, kafka-ui, etc.) before the app starts is a common need and the declarative API fits well alongside ResourcesToWaitFor().

Issue 1: RemoveResources is called before ConfigureBuilder — ordering should be reversed

In the current PR, the sequence is:

var builder = await DistributedApplicationTestingBuilder.CreateAsync<TAppHost>(Args, ConfigureAppHost);
RemoveResources(builder);    // ← removal
ConfigureBuilder(builder);   // ← user customisation

This means a user who adds a resource inside ConfigureBuilder (e.g. a mock service injected only during tests) cannot remove it via ResourcesToRemove() — the removal has already run. Calling RemoveResources after ConfigureBuilder is strictly more flexible and doesn't break the primary use-case:

var builder = await DistributedApplicationTestingBuilder.CreateAsync<TAppHost>(Args, ConfigureAppHost);
ConfigureBuilder(builder);
RemoveResources(builder);    // sees both AppHost and ConfigureBuilder resources

The comment on the OTLP block (// Configure OTLP endpoint on project resources AFTER user's ConfigureBuilder) shows this ordering principle is already established — removal should follow the same convention.

Issue 2: Missing LogProgress calls — silent failures are hard to debug

Every other lifecycle step in InitializeAsync emits a LogProgress line. RemoveResources is completely silent: if a name is misspelled or the resource doesn't exist, nothing indicates it was skipped. Suggested addition:

private void RemoveResources(IDistributedApplicationTestingBuilder builder)
{
    foreach (var name in ResourcesToRemove())
    {
        var resource = builder.Resources.SingleOrDefault(r =>
            string.Equals(r.Name, name, StringComparison.Ordinal));

        if (resource is not null)
        {
            builder.Resources.Remove(resource);
            LogProgress($"Removed resource '{name}' from builder.");
        }
        else
        {
            LogProgress($"ResourcesToRemove: resource '{name}' not found in builder (skipped).");
        }
    }
}

This surfaces typos in resource names during test runs without changing behaviour.

Issue 3: SingleOrDefault can throw on duplicate resource names

While duplicate names are unusual in Aspire, SingleOrDefault throws InvalidOperationException if two resources share a name. FirstOrDefault is safer here:

var resource = builder.Resources.FirstOrDefault(r =>
    string.Equals(r.Name, name, StringComparison.Ordinal));

What's good

  • The feature fills a real gap cleanly.
  • The virtual method pattern (ResourcesToRemove() => []) is consistent with ResourcesToWaitFor().
  • Tests cover both the removed resource being absent and the other resources being unaffected — good boundary testing.
  • Documentation is thorough, includes a FAQ update, and clearly explains the case-sensitive matching behaviour.

The two ordering and logging issues are worth fixing before merge; the SingleOrDefaultFirstOrDefault change is low-risk but worth doing for robustness.

@Odonno Odonno force-pushed the feat/remove-aspire-resources branch from 1365afa to bae432e Compare April 17, 2026 09:23
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 — Follow-up

Comparing this version against the previous review: the three raised issues are not yet resolved. Flagging them again for visibility.


Issue 1 (unaddressed): Ordering — RemoveResources should run after ConfigureBuilder

The call sequence is still:

var builder = await DistributedApplicationTestingBuilder.CreateAsync<TAppHost>(...);
RemoveResources(builder);   // ← runs first
ConfigureBuilder(builder);  // ← user customisation runs after

If a user adds a resource inside ConfigureBuilder (e.g. a test-only mock service) and also lists it in ResourcesToRemove(), the removal silently has no effect because it already ran. Swapping the order costs nothing and makes the API consistent with the existing OTLP comment in InitializeAsync that says "AFTER user's ConfigureBuilder":

ConfigureBuilder(builder);
RemoveResources(builder);   // sees both AppHost resources and test-injected ones

Issue 2 (partially addressed): No LogProgress on successful removal

The "not found" log was added — good. But the success path is still silent, so during a test run there's no confirmation that the removal actually happened:

if (resource is not null)
{
    builder.Resources.Remove(resource);
    LogProgress($"Removed resource '{name}' from distributed application.");  // ← add this
}
else
{
    LogProgress($"ResourcesToRemove: resource '{name}' not found (skipped).");
}

Every other lifecycle step in InitializeAsync emits a progress line; removal should too.


Issue 3 (unaddressed): SingleOrDefault can throw on duplicate names

var resource = builder.Resources.SingleOrDefault(r =>
    string.Equals(r.Name, name, StringComparison.Ordinal));

If two resources share a name (unusual but possible in programmatically built apps), this throws InvalidOperationException. FirstOrDefault is drop-in safer:

var resource = builder.Resources.FirstOrDefault(r =>
    string.Equals(r.Name, name, StringComparison.Ordinal));

What's good

The feature itself is solid — clean virtual-method pattern consistent with ResourcesToWaitFor(), good test coverage (removed resource absent + other resources still present), and thorough documentation including a FAQ update. All three issues above are small fixes; none require design changes.

@thomhurst
Copy link
Copy Markdown
Owner

@Odonno are you okay to take a look at claude's suggestions?

@Odonno
Copy link
Copy Markdown
Contributor Author

Odonno commented Apr 21, 2026

@Odonno are you okay to take a look at claude's suggestions?

Already applied some.

I don't really agree on 2 and 3.

For 1, I don't really know. I had the assumption that anyone could override ConfigureBuilder after RemoveResources is applied. But the other way could probably be valid. What is your opinion?

@thomhurst
Copy link
Copy Markdown
Owner

I think Issue 1 we should do, the others can be ignored

@Odonno Odonno force-pushed the feat/remove-aspire-resources branch from bae432e to 5da4ce4 Compare April 23, 2026 07:18
@Odonno
Copy link
Copy Markdown
Contributor Author

Odonno commented Apr 23, 2026

Done.

@thomhurst thomhurst force-pushed the feat/remove-aspire-resources branch from 5da4ce4 to 8a3381e Compare April 26, 2026 11:36
@thomhurst
Copy link
Copy Markdown
Owner

Thanks 😄

@thomhurst thomhurst enabled auto-merge (squash) April 26, 2026 11:41
@thomhurst thomhurst merged commit be39cee into thomhurst:main Apr 26, 2026
9 of 11 checks passed
@Odonno Odonno deleted the feat/remove-aspire-resources branch April 26, 2026 12:18
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.

2 participants