Skip to content

Update appmodel.md #10865

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 11, 2025
Merged
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
191 changes: 103 additions & 88 deletions docs/specs/appmodel.md
Original file line number Diff line number Diff line change
Expand Up @@ -1383,123 +1383,138 @@ This example demonstrates creating a completely custom resource (`TalkingClockRe
- Creating an `AddTalkingClock` extension method to register the resource and its lifecycle hook.

```csharp
// TalkingClockResource and Lifecycle Hook

// 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);

// 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.
}

public sealed class ClockHandResource(string name) : Resource(name);

// 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);
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)
]
});

clockBuilder.OnInitializeResource(static async (resource, initEvent, token) =>
{
// This event is published when the resource is initialized.
// You add custom logic here to establish the lifecycle for your custom resource.

var log = initEvent.Logger; // Get the logger for this resource instance.
var eventing = initEvent.Eventing; // Get the eventing service for publishing events.
var notification = initEvent.Notifications; // Get the notification service for state updates.
var services = initEvent.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);
}
});

AddHandResource(tickHandResource);
AddHandResource(tockHandResource);

return clockBuilder;

void AddHandResource(ClockHandResource clockHand)
{
builder.AddResource(clockHand)
.WithParentRelationship(clockBuilder) // Establish a parent-child relationship with the TalkingClockResource.
.WithInitialState(new()
{
ResourceType = "ClockHand",
CreationTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.NotStarted,
Properties =
[
new(CustomResourceKnownProperties.Source, "Talking Clock")
]
});
}
}
}
```
Expand Down