Skip to content

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

Description

@akrock

Is there an existing issue for this?

  • 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:

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

Replace Program.cs with:

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<DisposalProbe>(
    _ => probe = new DisposalProbe());

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

    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:

dotnet run

Output:

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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-app-modelIssues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplicationtriage:bot-seenAspire triage bot has seen this issue

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions