Skip to content
Merged
12 changes: 7 additions & 5 deletions playground/TestShop/TestShop.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@
});
#endif

var catalogDbApp = builder.AddProject<Projects.CatalogDb>("catalogdbapp")
.WithReference(catalogDb);

var catalogService = builder.AddProject<Projects.CatalogService>("catalogservice")
.WithReference(catalogDb)
.WithReplicas(2);
.WithReplicas(2)
.WaitForCompletion(catalogDbApp);

var messaging = builder.AddRabbitMQ("messaging")
.WithDataVolume()
Expand All @@ -34,15 +38,13 @@
.WithReference(catalogService);

builder.AddProject<Projects.OrderProcessor>("orderprocessor", launchProfileName: "OrderProcessor")
.WithReference(messaging);
.WithReference(messaging)
.WaitFor(messaging);

builder.AddProject<Projects.ApiGateway>("apigateway")
.WithReference(basketService)
.WithReference(catalogService);

builder.AddProject<Projects.CatalogDb>("catalogdbapp")
.WithReference(catalogDb);

#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
// of the dashboard. It is not required in end developer code. Comment out this code
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
Class="severity-icon" />
}
}
else if (Resource.IsStartingOrBuilding())
else if (Resource.IsStartingOrBuildingOrWaiting())
{
<FluentIcon Icon="Icons.Regular.Size16.CircleHint"
Color="Color.Info"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Dashboard.Model;
Expand Down Expand Up @@ -27,9 +27,9 @@ public static bool IsStopped(this ResourceViewModel resource)
return resource.KnownState is KnownResourceState.Exited or KnownResourceState.Finished or KnownResourceState.FailedToStart;
}

public static bool IsStartingOrBuilding(this ResourceViewModel resource)
public static bool IsStartingOrBuildingOrWaiting(this ResourceViewModel resource)
{
return resource.KnownState is KnownResourceState.Starting or KnownResourceState.Building;
return resource.KnownState is KnownResourceState.Starting or KnownResourceState.Building or KnownResourceState.Waiting;
}

public static bool HasNoState(this ResourceViewModel resource) => string.IsNullOrEmpty(resource.State);
Expand Down
5 changes: 3 additions & 2 deletions src/Aspire.Dashboard/Model/KnownResourceState.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Dashboard.Model;
Expand All @@ -11,5 +11,6 @@ public enum KnownResourceState
Starting,
Running,
Building,
Hidden
Hidden,
Waiting
}
5 changes: 5 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,9 @@ public static class KnownResourceStates
/// The finished state. Useful for showing the resource has finished.
/// </summary>
public static readonly string Finished = nameof(Finished);

/// <summary>
/// The waiting state. Useful for showing the resource is waiting for a dependency.
/// </summary>
public static readonly string Waiting = nameof(Waiting);
}
3 changes: 3 additions & 0 deletions src/Aspire.Hosting/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Aspire.Hosting.DistributedApplicationExecutionContextOptions.DistributedApplicat
Aspire.Hosting.DistributedApplicationExecutionContextOptions.Operation.get -> Aspire.Hosting.DistributedApplicationOperation
Aspire.Hosting.DistributedApplicationExecutionContextOptions.ServiceProvider.get -> System.IServiceProvider?
Aspire.Hosting.DistributedApplicationExecutionContextOptions.ServiceProvider.set -> void
static Aspire.Hosting.ResourceBuilderExtensions.WaitFor<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T>! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.IResource!>! dependency) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T>!
static Aspire.Hosting.ResourceBuilderExtensions.WaitForCompletion<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T>! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.IResource!>! dependency) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T>!
static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Exited -> string!
static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.FailedToStart -> string!
static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Finished -> string!
Expand All @@ -72,3 +74,4 @@ static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildArg<T>(this As
static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildSecret<T>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>! builder, string! name, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ParameterResource!>! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<T!>!
static Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! projectPath, System.Action<Aspire.Hosting.ProjectResourceOptions!>! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>!
static Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject<TProject>(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action<Aspire.Hosting.ProjectResourceOptions!>! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.ProjectResource!>!
static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Waiting -> string!
80 changes: 80 additions & 0 deletions src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
using System.Net.Sockets;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Publishing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -549,4 +551,82 @@ public static IResourceBuilder<T> ExcludeFromManifest<T>(this IResourceBuilder<T
{
return builder.WithAnnotation(ManifestPublishingCallbackAnnotation.Ignore);
}

/// <summary>
/// Waits for the dependency resource to enter the Running state before starting the resource.
/// </summary>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <param name="builder">The resource builder for the resource that will be waiting.</param>
/// <param name="dependency">The resource builder for the dependency resource.</param>
/// <returns>The resource builder.</returns>
/// <remarks>
/// <para>This method is useful when a resource should wait until another has started running. This can help
/// reduce errors in logs during local development where dependency resources.</para>
/// </remarks>
/// <example>
/// Start message queue before starting the worker service.
/// <code lang="C#">
/// var builder = DistributedApplication.CreateBuilder(args);
/// var messaging = builder.AddRabbitMQ("messaging");
/// builder.AddProject&lt;Projects.MyApp&gt;("myapp")
/// .WithReference(messaging)
/// .WaitFor(messaging);
/// </code>
/// </example>
public static IResourceBuilder<T> WaitFor<T>(this IResourceBuilder<T> builder, IResourceBuilder<IResource> dependency) where T : IResource
{
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (e, ct) =>
Copy link
Member

Choose a reason for hiding this comment

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

Another downside to doing it this way instead of a single wait for callback with lots of data is that first one to throw will stop other errors from coming through since the wait is sequential.

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe for this event we should invoke them all in parallel and then await them all together.

{
var rls = e.Services.GetRequiredService<ResourceLoggerService>();
var resourceLogger = rls.GetLogger(builder.Resource);
resourceLogger.LogInformation($"Waiting for resource '{dependency.Resource.Name}' to enter the '{KnownResourceStates.Running}' state.");

var rns = e.Services.GetRequiredService<ResourceNotificationService>();
await rns.PublishUpdateAsync(builder.Resource, s => s with { State = KnownResourceStates.Waiting }).ConfigureAwait(false);
await rns.WaitForResourceAsync(dependency.Resource.Name, cancellationToken: ct).ConfigureAwait(false);
});

return builder;
}

/// <summary>
/// Waits for the dependency resource to enter the Exited or Finished state before starting the resource.
/// </summary>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <param name="builder">The resource builder for the resource that will be waiting.</param>
/// <param name="dependency">The resource builder for the dependency resource.</param>
/// <returns>The resource builder.</returns>
/// <remarks>
/// <para>This method is useful when a resource should wait until another has completed. A common usage pattern
/// would be to include a console application that initializes the database schema or performs other one off
/// initialization tasks.</para>
/// <para>Note that this method has no impact at deployment time and only works for local development.</para>
/// </remarks>
/// <example>
/// Wait for database initialization app to complete running.
/// <code lang="C#">
/// var builder = DistributedApplication.CreateBuilder(args);
/// var pgsql = builder.AddPostgres("postgres");
/// var dbprep = builder.AddProject&lt;Projects.DbPrepApp&gt;("dbprep")
/// .WithReference(pgsql);
/// builder.AddProject&lt;Projects.DatabasePrepTool&gt;("dbprep")
/// .WithReference(pgsql)
/// .WaitForCompletion(dbprep);
/// </code>
/// </example>
public static IResourceBuilder<T> WaitForCompletion<T>(this IResourceBuilder<T> builder, IResourceBuilder<IResource> dependency) where T : IResource
{
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(builder.Resource, async (e, ct) =>
{
var rls = e.Services.GetRequiredService<ResourceLoggerService>();
var resourceLogger = rls.GetLogger(builder.Resource);
resourceLogger.LogInformation($"Waiting for resource '{dependency.Resource.Name}' to enter the 'Finished' state.");

var rns = e.Services.GetRequiredService<ResourceNotificationService>();
await rns.PublishUpdateAsync(builder.Resource, s => s with { State = KnownResourceStates.Waiting }).ConfigureAwait(false);
await rns.WaitForResourceAsync(dependency.Resource.Name, targetState: "Finished", cancellationToken: ct).ConfigureAwait(false);
});

return builder;
}
}