Skip to content

Add ResourceNotificationService.WaitForResourceAsync #4445

@DamianEdwards

Description

@DamianEdwards

Background and Motivation

When writing tests with Aspire.Hosting.Testing against AppHost projects, it's beneficial to have a programmatic way to wait on a resource to transition to a known state from within a test, so that tests don't needlessly attempt to connect to a resource when it's known to be in a state where that attempt will fail, e.g. before it transitions to the "Running" state as shown in the dashboard, or reported by ResourceNotificationService.

Proposed API

We should consider adding a new method Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync to aid in these scenarios, along with adding more common states to Aspire.Hosting.ApplicationModel.KnownResourceStates.

namespace Aspire.Hosting.ApplicationModel;

public class ResourceNotificationService
{
    public ResourceNotificationService(ILogger<ResourceNotificationService> logger);
+   public ResourceNotificationService(ILogger<ResourceNotificationService> logger, IHostApplicationLifetime hostApplicationLifetime);
+   public Task WaitForResourceAsync(string resourceName, string targetState = KnownResourceStates.Running, CancellationToken? cancellationToken = null);
+   public Task<string> WaitForResourceAsync(string resourceName, IEnumerable<string> targetStates, CancellationToken? cancellationToken = null);
}

public static class KnownResourceStates
{
    public static readonly string Hidden = "Hidden";
+   public static readonly string Starting = "Starting";
+   public static readonly string Running = "Running";
+   public static readonly string FailedToStart = "FailedToStart";
+   public static readonly string Stopping = "Stopping";
+   public static readonly string Exited = "Exited";
+   public static readonly string Finished = "Finished";
}

Usage Examples

[Fact]
public async Task GetWebResourceRootReturnsOkStatusCode()
{
    // Arrange
    var appHost = await DistributedApplicationTestingBuilder.CreateAsync<Projects.MyAspireApp_AppHost>();
    appHost.Services.ConfigureHttpClientDefaults(client =>
    {
        client.AddStandardResilienceHandler();
    });

    await using var app = await appHost.BuildAsync();
    var resourceNotification = app.Services.GetRequiredService<ResourceNotificationService>();
    await app.StartAsync();

    // Act
    await resourceNotification.WaitForResourceAsync("webfrontend", KnownResourceStates.Running);
    var httpClient = app.CreateHttpClient("webfrontend");
    var response = await httpClient.GetAsync("/");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Alternative Designs

We initially thought about adding a new public type in Aspire.Hosting.Testing that could be resolved from DI manually and have the WaitForResourceAsync method on that but given the target state is aligned with the state text reported by RequestNotificationService it seems appropriate to add it directly there.

Perhaps WaitForResourceAsync could just be WaitForAsync given it's already on the RequestNotificationService.

Risks

The resource states are just string values and aren't enforced anywhere, or even used to set states by Aspire.Hosting, as the states generally come from DCP. It seems reasonable that we should have a set of common known states though.

The overload WaitForResource("myresource") where the targetState parameter is defaulted could be something we'd prefer to map to a different behavior in the future, e.g. once health checks are added it could wait for health checks to be healthy before returning rather than just status update text matching.

Metadata

Metadata

Assignees

Labels

area-app-modelIssues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplicationarea-app-testingIssues pertaining to the APIs in Aspire.Hosting.Testing

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions