From 16ac317dad5107308806a3e0a472be48052c2ec3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 02:39:31 +0000 Subject: [PATCH 1/4] Initial plan From a9c0f4248414fa2a4a1702d2e990d939c6895c8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 03:10:13 +0000 Subject: [PATCH 2/4] Update AppHost eventing helper docs Co-authored-by: IEvangelist <7679720+IEvangelist@users.noreply.github.com> --- .../src/content/docs/app-host/eventing.mdx | 16 +- .../docs/architecture/resource-examples.mdx | 157 +++++++++--------- .../content/docs/whats-new/aspire-13-3.mdx | 4 +- .../src/content/docs/whats-new/aspire-9-3.mdx | 13 +- 4 files changed, 99 insertions(+), 91 deletions(-) diff --git a/src/frontend/src/content/docs/app-host/eventing.mdx b/src/frontend/src/content/docs/app-host/eventing.mdx index 7afd6e2c8..dd480c0b4 100644 --- a/src/frontend/src/content/docs/app-host/eventing.mdx +++ b/src/frontend/src/content/docs/app-host/eventing.mdx @@ -73,7 +73,7 @@ await builder.build().run(); -The following builder-level extension methods are available for AppHost events: +The following builder-level helper methods are available for AppHost events: | Method | Event | | ------------------------- | ----------------------------------------------------------------- | @@ -82,6 +82,11 @@ The following builder-level extension methods are available for AppHost events: | `OnBeforePublish` | `BeforePublishEvent` — raised before manifest publishing begins | | `OnAfterPublish` | `AfterPublishEvent` — raised after manifest publishing completes | +For the full API surface, see the [.NET +`DistributedApplicationEventingExtensions`](https://learn.microsoft.com/dotnet/api/aspire.hosting.distributedapplicationeventingextensions?view=dotnet-aspire-13.0) +API reference and the [TypeScript `Aspire.Hosting` API +reference](/reference/api/typescript/aspire.hosting/). + If you need to subscribe via `IDistributedApplicationEventing` directly (for example, inside an `IDistributedApplicationEventingSubscriber`), you can use the lower-level `Eventing.Subscribe()` API: ```csharp title="AppHost.cs" @@ -271,6 +276,7 @@ The preceding code subscribes to the `InitializeResourceEvent`, `ResourceReadyEv - `OnConnectionStringAvailable` / `onConnectionStringAvailable`: Subscribes to the `ConnectionStringAvailableEvent` event. - `OnBeforeResourceStarted` / `onBeforeResourceStarted`: Subscribes to the `BeforeResourceStartedEvent` event. - `OnResourceReady` / `onResourceReady`: Subscribes to the `ResourceReadyEvent` event. +- `OnResourceStopped` / `onResourceStopped`: Subscribes to the `ResourceStoppedEvent` event. When the AppHost is run, by the time the Aspire dashboard is displayed, you should see the following log output in the console: @@ -530,11 +536,11 @@ The `ResourceStoppedEvent` is raised when a resource stops execution: ```csharp title="AppHost.cs" -builder.Eventing.Subscribe( - cache, - (@event, ct) => +cache.OnResourceStopped( + static (resource, @event, ct) => { - logger.LogInformation("Resource {Name} stopped", @event.Resource.Name); + var logger = @event.Services.GetRequiredService>(); + logger.LogInformation("Resource {Name} stopped", resource.Name); return Task.CompletedTask; }); ``` diff --git a/src/frontend/src/content/docs/architecture/resource-examples.mdx b/src/frontend/src/content/docs/architecture/resource-examples.mdx index 339d522a2..387b8b3e8 100644 --- a/src/frontend/src/content/docs/architecture/resource-examples.mdx +++ b/src/frontend/src/content/docs/architecture/resource-examples.mdx @@ -44,20 +44,21 @@ public static class RedisResourceExtensions // Variable to hold the resolved connection string at runtime. string? connectionString = null; - // 4. Subscribe to ConnectionStringAvailableEvent to capture the connection string at runtime + // 4. Use OnConnectionStringAvailable to capture the connection string at runtime. // This event hook allows capturing the connection string *after* it has been resolved // by the Aspire runtime, including potentially allocated ports and resolved parameter values. - builder.Eventing.Subscribe(redis, async (@event, ct) => - { - // Resolve the connection string using the resource's method. - connectionString = await redis.GetConnectionStringAsync(ct).ConfigureAwait(false); - // Ensure the connection string was actually resolved. - if (connectionString == null) + var redisBuilder = builder.AddResource(redis) + .OnConnectionStringAvailable(async (resource, @event, ct) => { - throw new DistributedApplicationException( - $"Connection string for '{redis.Name}' was unexpectedly null."); - } - }); + // Resolve the connection string using the resource's method. + connectionString = await resource.GetConnectionStringAsync(ct).ConfigureAwait(false); + // Ensure the connection string was actually resolved. + if (connectionString == null) + { + throw new DistributedApplicationException( + $"Connection string for '{resource.Name}' was unexpectedly null."); + } + }); // 5. Register a health check that uses the connection string once it becomes available // Define a unique key for the health check. @@ -73,73 +74,73 @@ public static class RedisResourceExtensions // 6. Add & configure container using the fluent builder pattern // Add the RedisResource instance to the application model. - return builder.AddResource(redis) - // 6.a Expose the Redis TCP endpoint - // Map the host port (if provided) to the container's default Redis port (6379). - // Name the endpoint "tcp" for reference. - .WithEndpoint( - port: port, // Optional host port. - targetPort: 6379, // Default Redis port inside the container. - name: RedisResource.PrimaryEndpointName) // Use the constant defined in RedisResource. - // 6.b Specify container image and tag - // Define the Docker image to use for the Redis container. - .WithImage(RedisContainerImageTags.Image, RedisContainerImageTags.Tag) - // 6.c Configure container registry if needed - // Specify a container registry if the image is not on Docker Hub. - .WithImageRegistry(RedisContainerImageTags.Registry) - // 6.d Wire the health check into the resource - // Associate the previously defined health check with this resource. - // Aspire uses this for dashboard status and orchestration. - .WithHealthCheck(healthCheckKey) - // 6.e Define the container's entrypoint - // Override the default container entrypoint if necessary. Here, it's set to use shell. - .WithEntrypoint("/bin/sh") - // 6.f Pass the password ParameterResource into an environment variable - // Set environment variables for the container. This uses a callback to access - // the resource instance (`redis`) and its properties. - .WithEnvironment(context => - { - // If a password parameter exists, expose it as the REDIS_PASSWORD environment variable. - // The actual value resolution happens later via the ParameterResource. - if (redis.PasswordParameter is { } pwd) - { - context.EnvironmentVariables["REDIS_PASSWORD"] = pwd; - } - }) - // 6.g Build the container arguments lazily, preserving annotations - // Define the command-line arguments for the container. This also uses a callback - // to allow dynamic argument construction based on resource state or annotations. - .WithArgs(context => - { - // Start with the basic command to run the Redis server. - var cmd = new List { "redis-server" }; - - // If a password parameter is set, add the necessary Redis CLI arguments. - // Note: It uses the environment variable name set earlier ($REDIS_PASSWORD). - if (redis.PasswordParameter is not null) - { - cmd.Add("--requirepass"); - cmd.Add("$REDIS_PASSWORD"); // Reference the environment variable. - } - - // Check if a PersistenceAnnotation has been added to the resource. - // Annotations allow adding optional configuration or behavior. - if (redis.TryGetLastAnnotation(out var pa)) - { - // If persistence is configured, add the corresponding Redis CLI arguments. - var interval = (pa.Interval ?? TimeSpan.FromSeconds(60)) - .TotalSeconds - .ToString(CultureInfo.InvariantCulture); - cmd.Add("--save"); - cmd.Add(interval); // Save interval in seconds. - cmd.Add(pa.KeysChangedThreshold.ToString(CultureInfo.InvariantCulture)); // Number of key changes threshold. - } - - // Finalize the arguments for the shell entrypoint. - context.Args.Add("-c"); // Argument for /bin/sh to execute a command string. - context.Args.Add(string.Join(' ', cmd)); // Join all parts into a single command string. - return Task.CompletedTask; // Return a completed task as the callback is synchronous. - }); + return redisBuilder + // 6.a Expose the Redis TCP endpoint + // Map the host port (if provided) to the container's default Redis port (6379). + // Name the endpoint "tcp" for reference. + .WithEndpoint( + port: port, // Optional host port. + targetPort: 6379, // Default Redis port inside the container. + name: RedisResource.PrimaryEndpointName) // Use the constant defined in RedisResource. + // 6.b Specify container image and tag + // Define the Docker image to use for the Redis container. + .WithImage(RedisContainerImageTags.Image, RedisContainerImageTags.Tag) + // 6.c Configure container registry if needed + // Specify a container registry if the image is not on Docker Hub. + .WithImageRegistry(RedisContainerImageTags.Registry) + // 6.d Wire the health check into the resource + // Associate the previously defined health check with this resource. + // Aspire uses this for dashboard status and orchestration. + .WithHealthCheck(healthCheckKey) + // 6.e Define the container's entrypoint + // Override the default container entrypoint if necessary. Here, it's set to use shell. + .WithEntrypoint("/bin/sh") + // 6.f Pass the password ParameterResource into an environment variable + // Set environment variables for the container. This uses a callback to access + // the resource instance (`redis`) and its properties. + .WithEnvironment(context => + { + // If a password parameter exists, expose it as the REDIS_PASSWORD environment variable. + // The actual value resolution happens later via the ParameterResource. + if (redis.PasswordParameter is { } pwd) + { + context.EnvironmentVariables["REDIS_PASSWORD"] = pwd; + } + }) + // 6.g Build the container arguments lazily, preserving annotations + // Define the command-line arguments for the container. This also uses a callback + // to allow dynamic argument construction based on resource state or annotations. + .WithArgs(context => + { + // Start with the basic command to run the Redis server. + var cmd = new List { "redis-server" }; + + // If a password parameter is set, add the necessary Redis CLI arguments. + // Note: It uses the environment variable name set earlier ($REDIS_PASSWORD). + if (redis.PasswordParameter is not null) + { + cmd.Add("--requirepass"); + cmd.Add("$REDIS_PASSWORD"); // Reference the environment variable. + } + + // Check if a PersistenceAnnotation has been added to the resource. + // Annotations allow adding optional configuration or behavior. + if (redis.TryGetLastAnnotation(out var pa)) + { + // If persistence is configured, add the corresponding Redis CLI arguments. + var interval = (pa.Interval ?? TimeSpan.FromSeconds(60)) + .TotalSeconds + .ToString(CultureInfo.InvariantCulture); + cmd.Add("--save"); + cmd.Add(interval); // Save interval in seconds. + cmd.Add(pa.KeysChangedThreshold.ToString(CultureInfo.InvariantCulture)); // Number of key changes threshold. + } + + // Finalize the arguments for the shell entrypoint. + context.Args.Add("-c"); // Argument for /bin/sh to execute a command string. + context.Args.Add(string.Join(' ', cmd)); // Join all parts into a single command string. + return Task.CompletedTask; // Return a completed task as the callback is synchronous. + }); } } ``` diff --git a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx index 74ceb4594..1d2a42b0e 100644 --- a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx +++ b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx @@ -603,9 +603,9 @@ Two new convenience extension methods on `IDistributedApplicationBuilder` make i ```csharp title="C# — Subscribe to lifecycle events" // Run a callback right before the AppHost begins starting resources. -builder.SubscribeBeforeStart(async e => { /* ... */ }); +builder.OnBeforeStart(async (e, ct) => { /* ... */ }); // Run a callback once all resources have been created. -builder.SubscribeAfterResourcesCreated(async e => { /* ... */ }); +builder.OnAfterResourcesCreated(async (e, ct) => { /* ... */ }); ``` diff --git a/src/frontend/src/content/docs/whats-new/aspire-9-3.mdx b/src/frontend/src/content/docs/whats-new/aspire-9-3.mdx index 42578b971..afcc2d045 100644 --- a/src/frontend/src/content/docs/whats-new/aspire-9-3.mdx +++ b/src/frontend/src/content/docs/whats-new/aspire-9-3.mdx @@ -98,12 +98,13 @@ For example, this minimal custom resource publishes a running state when initial ```csharp var myCustom = new MyCustomResource("my-resource"); -builder.AddResource(myCustom); -builder.Eventing.Subscribe(myCustom, async (e, ct) => -{ - await e.Notifications.PublishUpdateAsync(e.Resource, - s => s with { State = KnownResourceStates.Running }); -}); +builder.AddResource(myCustom) + .OnInitializeResource(async (resource, e, ct) => + { + await e.Notifications.PublishUpdateAsync( + resource, + s => s with { State = KnownResourceStates.Running }); + }); ``` This replaces awkward patterns like `Task.Run` inside constructors or `Configure()` methods. You can see a more complex version in the [TalkingClock sample](https://github.com/microsoft/aspire-samples/tree/3dee8cd7c7880fe421ea61ba167301eb1369000a/samples/CustomResources/CustomResources.AppHost) in the official Aspire samples repo. From 8327c05c327d5896c42bfd50c62ffc42b9c8c262 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Jun 2026 03:11:13 +0000 Subject: [PATCH 3/4] Finalize eventing helper doc updates Co-authored-by: IEvangelist <7679720+IEvangelist@users.noreply.github.com> --- src/frontend/src/content/docs/app-host/eventing.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/content/docs/app-host/eventing.mdx b/src/frontend/src/content/docs/app-host/eventing.mdx index dd480c0b4..87d1c802a 100644 --- a/src/frontend/src/content/docs/app-host/eventing.mdx +++ b/src/frontend/src/content/docs/app-host/eventing.mdx @@ -83,7 +83,7 @@ The following builder-level helper methods are available for AppHost events: | `OnAfterPublish` | `AfterPublishEvent` — raised after manifest publishing completes | For the full API surface, see the [.NET -`DistributedApplicationEventingExtensions`](https://learn.microsoft.com/dotnet/api/aspire.hosting.distributedapplicationeventingextensions?view=dotnet-aspire-13.0) +`DistributedApplicationEventingExtensions`](https://learn.microsoft.com/dotnet/api/aspire.hosting.distributedapplicationeventingextensions) API reference and the [TypeScript `Aspire.Hosting` API reference](/reference/api/typescript/aspire.hosting/). From 1acc1a0f1a2f9d989a71973490462437eaaec661 Mon Sep 17 00:00:00 2001 From: David Pine Date: Mon, 10 Aug 2026 08:19:12 -0500 Subject: [PATCH 4/4] Address eventing docs review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/content/docs/app-host/eventing.mdx | 43 +++++++++++-------- .../docs/architecture/resource-examples.mdx | 4 +- .../content/docs/whats-new/aspire-13-3.mdx | 5 ++- .../src/content/docs/whats-new/aspire-9-3.mdx | 14 +++--- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/frontend/src/content/docs/app-host/eventing.mdx b/src/frontend/src/content/docs/app-host/eventing.mdx index 87d1c802a..bafa25341 100644 --- a/src/frontend/src/content/docs/app-host/eventing.mdx +++ b/src/frontend/src/content/docs/app-host/eventing.mdx @@ -23,7 +23,7 @@ The following events are available in the AppHost and occur in the following ord ### Subscribe to AppHost events -To subscribe to built-in AppHost events, use the convenience extension methods directly on the builder. These methods return the same `IDistributedApplicationBuilder` instance so calls can be chained: +To subscribe to built-in AppHost events, use the typed API available for each event. C# provides builder extension methods for selected events and the lower-level `Eventing.Subscribe()` API for others. TypeScript provides named subscription methods: @@ -41,12 +41,13 @@ builder.OnBeforeStart(static (@event, cancellationToken) => return Task.CompletedTask; }); -builder.OnAfterResourcesCreated(static (@event, cancellationToken) => -{ - var logger = @event.Services.GetRequiredService>(); - logger.LogInformation("AfterResourcesCreatedEvent"); - return Task.CompletedTask; -}); +builder.Eventing.Subscribe( + static (@event, cancellationToken) => + { + var logger = @event.Services.GetRequiredService>(); + logger.LogInformation("AfterResourcesCreatedEvent"); + return Task.CompletedTask; + }); builder.Build().Run(); ``` @@ -73,19 +74,16 @@ await builder.build().run(); -The following builder-level helper methods are available for AppHost events: +The following C# builder-level helper methods are available for AppHost events: -| Method | Event | -| ------------------------- | ----------------------------------------------------------------- | -| `OnBeforeStart` | `BeforeStartEvent` — raised before the AppHost starts | -| `OnAfterResourcesCreated` | `AfterResourcesCreatedEvent` — raised after resources are created | -| `OnBeforePublish` | `BeforePublishEvent` — raised before manifest publishing begins | -| `OnAfterPublish` | `AfterPublishEvent` — raised after manifest publishing completes | +| Method | Event | +| ----------------- | ---------------------------------------------------------------- | +| `OnBeforeStart` | `BeforeStartEvent` — raised before the AppHost starts | +| `OnBeforePublish` | `BeforePublishEvent` — raised before manifest publishing begins | +| `OnAfterPublish` | `AfterPublishEvent` — raised after manifest publishing completes | -For the full API surface, see the [.NET -`DistributedApplicationEventingExtensions`](https://learn.microsoft.com/dotnet/api/aspire.hosting.distributedapplicationeventingextensions) -API reference and the [TypeScript `Aspire.Hosting` API -reference](/reference/api/typescript/aspire.hosting/). +For the full API surface, see the [C# `DistributedApplicationEventingExtensions` API reference](/reference/api/csharp/aspire.hosting/distributedapplicationeventingextensions/) +and the [TypeScript `Aspire.Hosting` API reference](/reference/api/typescript/aspire.hosting/). If you need to subscribe via `IDistributedApplicationEventing` directly (for example, inside an `IDistributedApplicationEventingSubscriber`), you can use the lower-level `Eventing.Subscribe()` API: @@ -536,6 +534,13 @@ The `ResourceStoppedEvent` is raised when a resource stops execution: ```csharp title="AppHost.cs" +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("cache"); + cache.OnResourceStopped( static (resource, @event, ct) => { @@ -543,6 +548,8 @@ cache.OnResourceStopped( logger.LogInformation("Resource {Name} stopped", resource.Name); return Task.CompletedTask; }); + +builder.Build().Run(); ``` diff --git a/src/frontend/src/content/docs/architecture/resource-examples.mdx b/src/frontend/src/content/docs/architecture/resource-examples.mdx index 387b8b3e8..444a9d12a 100644 --- a/src/frontend/src/content/docs/architecture/resource-examples.mdx +++ b/src/frontend/src/content/docs/architecture/resource-examples.mdx @@ -72,8 +72,8 @@ public static class RedisResourceExtensions ?? throw new InvalidOperationException("Connection string is unavailable"), // Throw if accessed too early. name: healthCheckKey); // Name the health check for identification. - // 6. Add & configure container using the fluent builder pattern - // Add the RedisResource instance to the application model. + // 6. Configure the container using the fluent builder pattern. + // Continue configuring the RedisResource through its existing builder. return redisBuilder // 6.a Expose the Redis TCP endpoint // Map the host port (if provided) to the container's default Redis port (6379). diff --git a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx index 1d2a42b0e..5e14281de 100644 --- a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx +++ b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx @@ -596,7 +596,7 @@ A new **BeforeStart** pipeline phase replaces the bespoke eventing-subscriber cl ### Subscribe extensions for lifecycle events -Two new convenience extension methods on `IDistributedApplicationBuilder` make it easier to wire up lifecycle event handlers without `.Eventing.Subscribe(...)` plumbing: +Lifecycle event subscriptions use the typed API available for each event. In C#, `OnBeforeStart` is a builder-level convenience method, while `AfterResourcesCreatedEvent` uses the lower-level eventing API: @@ -605,7 +605,8 @@ Two new convenience extension methods on `IDistributedApplicationBuilder` make i // Run a callback right before the AppHost begins starting resources. builder.OnBeforeStart(async (e, ct) => { /* ... */ }); // Run a callback once all resources have been created. -builder.OnAfterResourcesCreated(async (e, ct) => { /* ... */ }); +builder.Eventing.Subscribe( + async (e, ct) => { /* ... */ }); ``` diff --git a/src/frontend/src/content/docs/whats-new/aspire-9-3.mdx b/src/frontend/src/content/docs/whats-new/aspire-9-3.mdx index afcc2d045..e610e2b9e 100644 --- a/src/frontend/src/content/docs/whats-new/aspire-9-3.mdx +++ b/src/frontend/src/content/docs/whats-new/aspire-9-3.mdx @@ -98,13 +98,13 @@ For example, this minimal custom resource publishes a running state when initial ```csharp var myCustom = new MyCustomResource("my-resource"); -builder.AddResource(myCustom) - .OnInitializeResource(async (resource, e, ct) => - { - await e.Notifications.PublishUpdateAsync( - resource, - s => s with { State = KnownResourceStates.Running }); - }); +builder.AddResource(myCustom); +builder.Eventing.Subscribe(myCustom, async (e, ct) => +{ + await e.Notifications.PublishUpdateAsync( + e.Resource, + s => s with { State = KnownResourceStates.Running }); +}); ``` This replaces awkward patterns like `Task.Run` inside constructors or `Configure()` methods. You can see a more complex version in the [TalkingClock sample](https://github.com/microsoft/aspire-samples/tree/3dee8cd7c7880fe421ea61ba167301eb1369000a/samples/CustomResources/CustomResources.AppHost) in the official Aspire samples repo.