diff --git a/src/frontend/src/content/docs/app-host/eventing.mdx b/src/frontend/src/content/docs/app-host/eventing.mdx index 7afd6e2c8..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,14 +74,16 @@ await builder.build().run(); -The following builder-level extension 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 [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: @@ -271,6 +274,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,13 +534,22 @@ The `ResourceStoppedEvent` is raised when a resource stops execution: ```csharp title="AppHost.cs" -builder.Eventing.Subscribe( - cache, - (@event, ct) => +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +var builder = DistributedApplication.CreateBuilder(args); + +var cache = builder.AddRedis("cache"); + +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; }); + +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 339d522a2..444a9d12a 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. @@ -71,75 +72,75 @@ 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. - 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. - }); + // 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). + // 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..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,16 +596,17 @@ 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: ```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.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 42578b971..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 @@ -101,7 +101,8 @@ var myCustom = new MyCustomResource("my-resource"); builder.AddResource(myCustom); builder.Eventing.Subscribe(myCustom, async (e, ct) => { - await e.Notifications.PublishUpdateAsync(e.Resource, + await e.Notifications.PublishUpdateAsync( + e.Resource, s => s with { State = KnownResourceStates.Running }); }); ```