Skip to content
Open
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
209 changes: 115 additions & 94 deletions src/frontend/src/content/docs/architecture/resource-examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -212,133 +212,154 @@ public class RedisResource(string name)

This example demonstrates creating a completely custom resource (`TalkingClockResource`) that doesn't derive from built-in types. It shows:

- Defining a simple resource class.
- Implementing a custom lifecycle hook (`TalkingClockLifecycleHook`) to manage the resource's behavior (starting, logging, state updates).
- Using `ResourceLoggerService` for per-resource logging.
- Defining a simple resource class with child resources.
- Using the `OnInitializeResource` event to establish resource lifecycle behavior.
- Using per-resource logging and state management.
- Using `ResourceNotificationService` to publish state updates.
- Creating an `AddTalkingClock` extension method to register the resource and its lifecycle hook.
- Creating an `AddTalkingClock` extension method to register the resource and configure its behavior.

```csharp title="C# — TalkingClockResource.cs"
// Define the custom resource type. It inherits from the base Aspire 'Resource' class.
// This class is primarily a data container; Aspire behavior is added via lifecycle hooks and extension methods.
public sealed class TalkingClockResource(string name) : Resource(name);
```

```csharp title="C# — TalkingClockLifecycleHook.cs"
// Define an Aspire lifecycle hook that implements the behavior for the TalkingClockResource.
// Lifecycle hooks allow plugging into the application's startup and shutdown sequences.
public sealed class TalkingClockLifecycleHook(
// Aspire service for publishing resource state updates (e.g., Running, Starting).
ResourceNotificationService notification,
// Aspire service for publishing and subscribing to application-wide events.
IDistributedApplicationEventing eventing,
// Aspire service for getting a logger scoped to a specific resource.
ResourceLoggerService loggerSvc,
// General service provider for dependency injection if needed.
IServiceProvider services) : IDistributedApplicationLifecycleHook // Implement the Aspire hook interface.
// This class is primarily a data container; Aspire behavior is added via eventing and extension methods.
public sealed class TalkingClockResource(string name, ClockHandResource tickHand, ClockHandResource tockHand) : Resource(name)
{
// This method is called by Aspire after all resources have been initially added to the application model.
public Task AfterResourcesCreatedAsync(
DistributedApplicationModel model, // The Aspire application model containing all resources.
CancellationToken token) // Cancellation token for graceful shutdown.
{
// Find all instances of TalkingClockResource in the Aspire application model.
foreach (var clock in model.Resources.OfType<TalkingClockResource>())
{
// Get an Aspire logger specifically for this clock instance. Logs will be associated with this resource in the dashboard.
var log = loggerSvc.GetLogger(clock);

// Start a background task to manage the clock's lifecycle and behavior.
_ = Task.Run(async () =>
{
// Publish an Aspire event indicating that this resource is about to start.
// Other components could subscribe to this event for pre-start actions.
await eventing.PublishAsync(
new BeforeResourceStartedEvent(clock, services), token);

// Log an informational message associated with the resource.
log.LogInformation("Starting Talking Clock...");

// Publish an initial state update to the Aspire notification service.
// This sets the resource's state to 'Running' and records the start time.
// The Aspire dashboard and other orchestrators observe these state updates.
await notification.PublishUpdateAsync(clock, s => s with
{
StartTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.Running // Use an Aspire well-known state.
});

// Enter the main loop that runs as long as cancellation is not requested.
while (!token.IsCancellationRequested)
{
// Log the current time, associated with the resource.
log.LogInformation("The time is {time}", DateTime.UtcNow);

// Publish a custom state update "Tick" using Aspire's ResourceStateSnapshot.
// This demonstrates using custom state strings and styles in the Aspire dashboard.
await notification.PublishUpdateAsync(clock,
s => s with { State = new ResourceStateSnapshot("Tick", KnownResourceStateStyles.Info) });

await Task.Delay(1000, token);

// Publish another custom state update "Tock" using Aspire's ResourceStateSnapshot.
await notification.PublishUpdateAsync(clock,
s => s with { State = new ResourceStateSnapshot("Tock", KnownResourceStateStyles.Success) });

await Task.Delay(1000, token);
}
}, token);
}

// Indicate that this hook's work (starting the background tasks) is complete for now.
return Task.CompletedTask;
}
// Other Aspire lifecycle hook methods (e.g., BeforeStartAsync, AfterEndpointsAllocatedAsync) could be implemented here if needed.
public ClockHandResource TickHand { get; } = tickHand; // The tick hand resource instance.
public ClockHandResource TockHand { get; } = tockHand; // The tock hand resource instance.
}

// Define a child resource type representing a clock hand (tick or tock).
// This demonstrates how to create hierarchical resource relationships in Aspire.
public sealed class ClockHandResource(string name) : Resource(name);
```

```csharp title="C# — TalkingClockExtensions.cs"

// Define Aspire extension methods for adding the TalkingClockResource to the application builder.
// This provides a fluent API for users to add the custom resource.
public static class TalkingClockExtensions
{
// The main Aspire extension method to add a TalkingClockResource.
public static IResourceBuilder<TalkingClockResource> AddTalkingClock(
this IDistributedApplicationBuilder builder, // Extends the Aspire application builder.
string name) // The name for this resource instance.
string name) // The name for this resource instance.
{
// Register the TalkingClockLifecycleHook with the DI container using Aspire's helper method.
// The Aspire hosting infrastructure will automatically discover and run registered lifecycle hooks.
builder.Services.TryAddLifecycleHook<TalkingClockLifecycleHook>();

// Create a new instance of the TalkingClockResource.
var clockResource = new TalkingClockResource(name);
// Create a new instance of the TalkingClockResource with child resources for tick and tock hands.
var tickHandResource = new ClockHandResource(name + "-tick-hand");
var tockHandResource = new ClockHandResource(name + "-tock-hand");
var clockResource = new TalkingClockResource(name, tickHandResource, tockHandResource);

// Add the resource instance to the Aspire application builder and configure it using fluent APIs.
return builder.AddResource(clockResource)
var clockBuilder = builder.AddResource(clockResource)
// Use Aspire's ExcludeFromManifest to prevent this resource from being included in deployment manifests.
.ExcludeFromManifest()
// Set a URL for the resource, which will be displayed in the Aspire dashboard.
.WithUrl("https://www.speaking-clock.com/", "Speaking Clock")
// Use Aspire's WithInitialState to set an initial state snapshot for the resource.
// This provides initial metadata visible in the Aspire dashboard.
.WithInitialState(new CustomResourceSnapshot // Aspire type for custom resource state.
{
ResourceType = "TalkingClock", // A string identifying the type of resource for Aspire.
ResourceType = "TalkingClock", // A string identifying the type of resource for Aspire, this shows in the dashboard.
CreationTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.NotStarted, // Use an Aspire well-known state.
State = KnownResourceStates.NotStarted, // Use an Aspire well-known state.
// Add custom properties displayed in the Aspire dashboard's resource details.
Properties =
[
// Use Aspire's known property key for source information.
new(CustomResourceKnownProperties.Source, "Talking Clock")
],
// Add URLs associated with the resource, displayed as links in the Aspire dashboard.
Urls =
[
// Define a URL using Aspire's UrlSnapshot type.
new("Speaking Clock", "https://www.speaking-clock.com/", isInternal: false)
]
});

// Use the OnInitializeResource event to establish the lifecycle behavior for this custom resource.
// This event fires after a resource is added but before endpoints are allocated.
// It's the preferred way to add custom logic to resources that don't have a built-in lifecycle.
clockBuilder.OnInitializeResource(static async (resource, @event, token) =>
{
// This event is published when the resource is initialized.
// You can add custom logic here to establish the lifecycle for your custom resource.

var log = @event.Logger; // Get the logger for this resource instance.
var eventing = @event.Eventing; // Get the eventing service for publishing events.
var notification = @event.Notifications; // Get the notification service for state updates.
var services = @event.Services; // Get the service provider for dependency injection.

// Publish an Aspire event indicating that this resource is about to start.
// Other components could subscribe to this event for pre-start actions.
await eventing.PublishAsync(new BeforeResourceStartedEvent(resource, services), token);
await eventing.PublishAsync(new BeforeResourceStartedEvent(resource.TickHand, services), token);
await eventing.PublishAsync(new BeforeResourceStartedEvent(resource.TockHand, services), token);

// Log an informational message associated with the resource.
log.LogInformation("Starting Talking Clock...");

// Publish an initial state update to the Aspire notification service.
// This sets the resource's state to 'Running' and records the start time.
// The Aspire dashboard and other orchestrators observe these state updates.
await notification.PublishUpdateAsync(resource, s => s with
{
StartTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.Running // Use an Aspire well-known state.
});
await notification.PublishUpdateAsync(resource.TickHand, s => s with
{
StartTimeStamp = DateTime.UtcNow,
State = "Waiting on clock tick" // Custom state string for the tick hand.
});
await notification.PublishUpdateAsync(resource.TockHand, s => s with
{
StartTimeStamp = DateTime.UtcNow,
State = "Waiting on clock tock" // Custom state string for the tock hand.
});

// Enter the main loop that runs as long as cancellation is not requested.
while (!token.IsCancellationRequested)
{
// Log the current time, associated with the resource.
log.LogInformation("The time is {time}", DateTime.UtcNow);

// Publish a custom state update "Tick" using Aspire's ResourceStateSnapshot.
// This demonstrates using custom state strings and styles in the Aspire dashboard.
await notification.PublishUpdateAsync(resource,
s => s with { State = new ResourceStateSnapshot("Tick", KnownResourceStateStyles.Success) });
await notification.PublishUpdateAsync(resource.TickHand,
s => s with { State = new ResourceStateSnapshot("On", KnownResourceStateStyles.Success) });
await notification.PublishUpdateAsync(resource.TockHand,
s => s with { State = new ResourceStateSnapshot("Off", KnownResourceStateStyles.Info) });

await Task.Delay(1000, token);

// Publish another custom state update "Tock" using Aspire's ResourceStateSnapshot.
await notification.PublishUpdateAsync(resource,
s => s with { State = new ResourceStateSnapshot("Tock", KnownResourceStateStyles.Success) });
await notification.PublishUpdateAsync(resource.TickHand,
s => s with { State = new ResourceStateSnapshot("Off", KnownResourceStateStyles.Info) });
await notification.PublishUpdateAsync(resource.TockHand,
s => s with { State = new ResourceStateSnapshot("On", KnownResourceStateStyles.Success) });

await Task.Delay(1000, token);
}
});
Comment on lines +312 to +339
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The background task started in OnInitializeResource runs an infinite loop without being properly managed. If the initialization completes but the task continues running, there's no mechanism to handle cancellation when the application shuts down gracefully. The task should be tracked or wrapped in a Task.Run to ensure it doesn't block initialization and can be properly cancelled.

Consider wrapping the loop logic in Task.Run to run it in the background without blocking the initialization event handler, similar to how it was done in the old lifecycle hook pattern.

Copilot uses AI. Check for mistakes.

AddHandResource(tickHandResource);
AddHandResource(tockHandResource);

return clockBuilder;

void AddHandResource(ClockHandResource clockHand)
{
builder.AddResource(clockHand)
// Establish a parent-child relationship with the TalkingClockResource.
// This creates a hierarchical structure in the dashboard and coordinates lifecycle management.
.WithParentRelationship(clockBuilder)
.WithInitialState(new()
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The WithInitialState call on line 352 uses new() without specifying the type explicitly. While this works in modern C#, for documentation purposes it would be clearer to explicitly specify new CustomResourceSnapshot() to match the pattern used on line 258 and help readers understand what type is being instantiated.

Suggested change
.WithInitialState(new()
.WithInitialState(new CustomResourceSnapshot

Copilot uses AI. Check for mistakes.
{
ResourceType = "ClockHand",
CreationTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.NotStarted,
Properties =
[
new(CustomResourceKnownProperties.Source, "Talking Clock")
]
});
}
}
}
```
Loading
Loading