Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 31 additions & 18 deletions src/frontend/src/content/docs/app-host/eventing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>()` API for others. TypeScript provides named subscription methods:

<Tabs syncKey="aspire-lang">
<TabItem id="csharp" label="C#">
Expand All @@ -41,12 +41,13 @@ builder.OnBeforeStart(static (@event, cancellationToken) =>
return Task.CompletedTask;
});

builder.OnAfterResourcesCreated(static (@event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("AfterResourcesCreatedEvent");
return Task.CompletedTask;
});
builder.Eventing.Subscribe<AfterResourcesCreatedEvent>(
static (@event, cancellationToken) =>
{
var logger = @event.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("AfterResourcesCreatedEvent");
return Task.CompletedTask;
});

builder.Build().Run();
```
Expand All @@ -73,14 +74,16 @@ await builder.build().run();
</TabItem>
</Tabs>

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<T>()` API:

Expand Down Expand Up @@ -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:

Expand Down Expand Up @@ -530,13 +534,22 @@ The `ResourceStoppedEvent` is raised when a resource stops execution:
<TabItem id="csharp" label="C#">

```csharp title="AppHost.cs"
builder.Eventing.Subscribe<ResourceStoppedEvent>(
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<ILogger<Program>>();
logger.LogInformation("Resource {Name} stopped", resource.Name);
return Task.CompletedTask;
});
Comment thread
IEvangelist marked this conversation as resolved.

builder.Build().Run();
```

</TabItem>
Expand Down
161 changes: 81 additions & 80 deletions src/frontend/src/content/docs/architecture/resource-examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ConnectionStringAvailableEvent>(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.
Expand All @@ -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<string> { "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<PersistenceAnnotation>(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<string> { "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<PersistenceAnnotation>(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.
});
}
}
```
Expand Down
7 changes: 4 additions & 3 deletions src/frontend/src/content/docs/whats-new/aspire-13-3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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:

<Tabs syncKey='aspire-lang'>
<TabItem id='csharp' label='AppHost.cs'>

```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<AfterResourcesCreatedEvent>(
async (e, ct) => { /* ... */ });
```

</TabItem>
Expand Down
3 changes: 2 additions & 1 deletion src/frontend/src/content/docs/whats-new/aspire-9-3.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ var myCustom = new MyCustomResource("my-resource");
builder.AddResource(myCustom);
builder.Eventing.Subscribe<InitializeResourceEvent>(myCustom, async (e, ct) =>
{
await e.Notifications.PublishUpdateAsync(e.Resource,
await e.Notifications.PublishUpdateAsync(
e.Resource,
s => s with { State = KnownResourceStates.Running });
});
```
Expand Down
Loading