microsoft / microsoft/aspire

DistributedApplicationBuilder.Build leaves partial host service provider undisposed after host resolution failure

Open
#18,857 1 comment 0 reactions 0 assignees View on GitHub
area-app-model triage:bot-seen
Dominant language
C#
Stars
6.3k
Forks
991
Avg merge
2d 15h
Merged PRs (30d)
196

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Describe the bug

`DistributedApplicationBuilder.Build()` can create a service provider and then
throw while resolving the host. When that happens, provider-owned disposable
services remain undisposed, while the caller receives no application or provider
through which cleanup can be performed.

This matters when Aspire is embedded in a long-lived process such as a test
runner or development tool. The build exception is catchable, and the containing
process can continue running with resources from the failed build still alive.

## Actual behavior

`DistributedApplicationBuilder.Build()` delegates to the underlying generic host
builder. When that build throws after provider creation:

- no `DistributedApplication` is returned;
- the partial provider is not disposed;
- the provider is not exposed to the caller;
- provider-owned resources remain alive until process termination.

### Expected Behavior

If host resolution fails after the service provider has been created,
`DistributedApplicationBuilder.Build()` should:

1. dispose the partial provider and its provider-owned services;
2. preserve the original host-construction exception as the primary exception;
3. leave no provider-owned resources active after `Build()` returns by throwing.

### Steps To Reproduce

Create a .NET 10 console application:

```shell
dotnet new console --framework net10.0
dotnet add package Aspire.Hosting --version 13.4.6
```

Replace `Program.cs` with:

```csharp
using Aspire.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = DistributedApplication.CreateBuilder(
new DistributedApplicationOptions
{
Args = [],
AssemblyName = typeof(DisposalProbe).Assembly.GetName().Name,
ProjectDirectory = Directory.GetCurrentDirectory(),
DisableDashboard = true
});

DisposalProbe? probe = null;

builder.Services.AddSingleton(
_ => probe = new DisposalProbe());

builder.Services.AddSingleton(services =>
{
_ = services.GetRequiredService();

throw new InvalidOperationException(
"Host construction failed after provider creation.");
});

try
{
using var application = builder.Build();
}
catch (InvalidOperationException exception)
{
Console.WriteLine(exception.Message);
}

Console.WriteLine(
$"Provider-owned singleton created: {probe is not null}");

Console.WriteLine(
$"Provider-owned singleton disposed: {probe?.IsDisposed}");

internal sealed class DisposalProbe : IDisposable
{
public bool IsDisposed { get; private set; }

public void Dispose() => IsDisposed = true;
}
```

Run it:

```shell
dotnet run
```

Output:

```text
Host construction failed after provider creation.
Provider-owned singleton created: True
Provider-owned singleton disposed: False
```

The probe is created through the service-provider factory, rather than registered
as an externally owned singleton instance, so it should be disposed with its
owning provider.

### Exceptions (if any)

The reproduction deliberately registers an `IHostLifetime` factory that throws:

```text
System.InvalidOperationException: Host construction failed after provider creation.
```

The exception is caught by the reproduction, so the console output contains its
message rather than an unhandled-exception stack trace. No additional Aspire
exception is output. The reported problem is the provider-owned singleton state
that remains undisposed after the expected build exception is caught.

### Aspire doctor output

Checking Aspire environment...

Aspire Environment Check
========================

Aspire
✅ Aspire CLI version 13.4.6 (channel: stable)

.NET SDK
✅ .NET 10.0.102 installed (arm64)

Container Runtime
✅ Docker v29.5.3: running (auto-detected (default)) ← active

Environment
✅ HTTPS development certificate is trusted

Summary: 4 passed, 0 warnings, 0 failed

Aspire CLI Installations
========================

Path:
/.dotnet/tools/.store/aspire.cli/13.4.6/
aspire.cli.osx-arm64/13.4.6/tools/net10.0/osx-arm64/aspire
Version: 13.4.6+87fe259e4fc244c599019a7b1304c85a1488f248
Channel: stable
Route: dotnet-tool
PATH status: not on PATH

### Anything else?

## Suggested direction

Please clarify the intended failed-build ownership contract.

A complete resolution would dispose the partial provider when host resolution
fails. The cleanup could be implemented in `DistributedApplicationBuilder.Build()`
or coordinated with the underlying generic-host implementation.

If Aspire intentionally cannot own that cleanup, a supported service-provider-
factory or host-builder customization seam would let an embedding caller assume
ownership. That is a fallback rather than the preferred behavior: callers should
not ordinarily need to replace the service-provider factory to make a failed
`Build()` release resources it created.

Reflection over the underlying host builder and global diagnostic interception
do not appear suitable as supported consumer workarounds.

## Related work

- [#4419: Allow configuring of the ServiceProvider on DistributedApplicationBuilder](https://github.com/microsoft/aspire/issues/4419)
requests the customization seam described above. It does not cover disposal
when the default provider has already been created and host resolution fails.
- [#3093: Clean up distributed app builders to fix test failures](https://github.com/microsoft/aspire/pull/3093)
addressed resources retained by builders that were never built. This report
covers a different failure boundary: the provider is created, `Build()` throws,
and the caller receives no host or provider to dispose.

Contributor guide

Open the contributing guide

Research direction

Start at DistributedApplicationBuilder.Build() and run the supplied .NET 10 console reproduction, focusing on the failure after the service provider is created. Done means provider-owned services are disposed when host resolution fails, while the original host-construction exception remains the primary exception.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.