MOVED to https://github.com/dotnet/aspire/blob/main/docs/specs/appmodel.md
-
-
Save davidfowl/b408af870d4b5b54a28bf18bffa127e1 to your computer and use it in GitHub Desktop.
Yep let me add that
Much prettier now with the formatting!
2. Built-In Resources and Lifecycle
Is it worth mentioning the Unknown
state that resources start in.
8. Authoring Custom Resources
- Is it worth mentioning that there's a half way house of making specialisatiosn of existing resources type (e.g. the MailDev examples in the doc extending
ContainerResource
) - When should the events be published?
- I feel a sample would really help this out. As a starter for 10
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
var builder = DistributedApplication.CreateBuilder(args);
builder.AddTalkingClock("clock");
builder.Build().Run();
public class TalkingClockResource(string name) : Resource(name)
{
}
public class TalkingClockLifecycleHook(
ResourceNotificationService resourceNotificationService,
IDistributedApplicationEventing eventing,
ResourceLoggerService resourceLoggerService,
IServiceProvider serviceProvider) : IDistributedApplicationLifecycleHook
{
public Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken)
{
foreach (var clock in appModel.Resources.OfType<TalkingClockResource>())
{
var logger = resourceLoggerService.GetLogger(clock);
_ = Task.Run(async () =>
{
// Wait for all BeforeResourceStartedEvent subscribers to process before continuing
var startEvent = new BeforeResourceStartedEvent(clock, serviceProvider);
await eventing.PublishAsync(startEvent, cancellationToken);
logger.LogInformation("Starting Talking Clock");
//Once resource is initailised, signal that it's ready, so that anything waaiting on it can start
var readyEvent = new ResourceReadyEvent(clock, serviceProvider);
await eventing.PublishAsync(readyEvent, cancellationToken);
await resourceNotificationService.PublishUpdateAsync(clock, state => state with
{
StartTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.Running
});
while (!cancellationToken.IsCancellationRequested)
{
logger.LogInformation("The time is {time}", DateTime.UtcNow);
await resourceNotificationService.PublishUpdateAsync(clock, x => x with { State = new ResourceStateSnapshot("Tick", KnownResourceStateStyles.Info) });
await Task.Delay(1000, cancellationToken);
await resourceNotificationService.PublishUpdateAsync(clock, x => x with { State = new ResourceStateSnapshot("Tock", KnownResourceStateStyles.Success) });
await Task.Delay(1000, cancellationToken);
}
}, cancellationToken);
}
return Task.CompletedTask;
}
}
public static class TalkingClockExtensions
{
public static IResourceBuilder<TalkingClockResource> AddTalkingClock(this IDistributedApplicationBuilder builder, string name)
{
builder.Services.TryAddLifecycleHook<TalkingClockLifecycleHook>();
var resource = new TalkingClockResource(name);
return builder.AddResource(new TalkingClockResource(name))
.ExcludeFromManifest()
.WithInitialState(new CustomResourceSnapshot
{
Properties = [
new(CustomResourceKnownProperties.Source, "Talking Clock")
],
ResourceType = "TalkingClock",
CreationTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.NotStarted,
Urls = [
new("Speaking Clock", "https://www.speaking-clock.com/", false)
]
});
}
}
9.Making Resources Publish-Friendly
- Is it worth mentioning
.ExcludeFromManifest()
to not include the resource in the manifest if it's not relevant / want to ignore publishing?
Yea a sample is required before this is live. Iβve been trying to brain dump before making a sample .
Integrated your feedback @afscrome . I need to fix the LLm generated errors
I think you could stick in a one-liner on how IResourceWithParent is similar, yet mutually exclusive with WaitFor for a given parent/child -- the former is implied relationship but a single lifecycle, the latter for connecting two different lifecycles -- I think it deserves a mention
Could we have an isolated, succinct example of a ReferenceExpression constructing a new string from literals and expressions, e.g. an endpoint property, too? That can be a tricky one for newbies.
I added my endpoints doc in there. This needs more massaging but I think the content is looking good
The LLM went into hardcore summary mode and deleted lots of the descriptions
I asked you in discord whether these two things were equivalent, and you said yes -- yet both are in your example above?
await eventing.PublishAsync(
new ResourceReadyEvent(clock, services), token);
await notification.PublishUpdateAsync(clock, s => s with
{
StartTimeStamp = DateTime.UtcNow,
State = KnownResourceStates.Running
});
I think we (I?) need some more clarity around this.
The system will fire ResourceReadyEvent on your behalf if you mark the resource as running. We check for health checks on the resource and if there are none, we will fire the event immediately.
The system will fire ResourceReadyEvent on your behalf if you mark the resource as running. We check for health checks on the resource and if there are none, we will fire the event immediately.
This is what I understood already, but my point is that:
means you're firing ResourceReadyEvent twice, no? While harmless, the implication is that publishing a Running state does not publish a ready event. This is confusing.
Yes, I will update this code.
When creating new resources, I added normal properties to the derived class. Had it been better if I used annotations, instead? π€
When creating new resources, I added normal properties to the derived class. Had it been better if I used annotations, instead? π€
I didn't mention it but the reason we want to use this pattern is because of dotnet/aspire#8984. You can still have properties but they should be backed by annotations (we have to do this work too). Resources are effectively a discriminated union and we want to be able to switch types on the fly without doing nasty hacks like what we have to do currently π
Read up to Values and References so far - some quick thoughts, some of these are very nit picky so feel free to ignore.
- Is it worth somewhere mentioning that this doc focuses on the hosting side, not client integrations.
- Quick Start - Would be pretty cool if the diagram was as screenshot of the resource graph from the dashboard.
- Annotations - is it worth having a sidenote mentioning these are similar to k8s annotations (+ how they differ).
- Fluent Extension Methods - is it worth mentioning that these are often in separate packages.
- Built-In Resources & Lifecycle - is it worth some discussion eventing vs lifecycle hooks? My take is that eventing is generally preferable, but lifecycle hooks do provide a nice way to get a single atomic event registration via
TryAddLifecycleHook
. - Known Resource States - Is it worth mentioning the constants file
Aspire.Hosting.ApplicationModel.KnownResourceStates
- Known Resource States - is it worth discussing Exited vs Finished, and recommend one over the other in light of dotnet/aspire#7373 .
- Resource Logging. Is it worth mentioning where those logs go, and why it is better to use them over a normal
ILogger<T>
from the host? (Possibly with screenshots) - Manual Relationships - what are these used for? (e.g. resource graph on dashboard)
Important: Developers should not manually publish the ResourceReadyEvent. Aspire manages the transition to the ready state based on the presence and outcome of health checks.
Interesting, I'm sure I've had to publish this event myself for several custom resources to make waiting work. Let me check again - maybe this is just a hangover from playing around in early 9.0 previews before everything was fully baked.
Thanks @afscrome !
Example: Cross-Context Communication
- Missing grafana and keycloak resource declaration (or was it omitted on purpose?)
Annotations:
- WithAnnotation(): maybe mention behavior to append (default) or replace an existing one
- Retrieving annotations can also be done from the
resource.Annotations
property, likeresource.Annotations.OfType<ResourceSnapshotAnnotation>()
when there can be many of a given annotation type
IDistributedApplicationLifecycleHook
is only shown in an example. I built many features on top of this primitive - maybe it deserves its own section.
- Methods are blocking
- Most of the time I keep background tasks in fields instead of starting non-observed tasks (like
_ = DoSomethingAsync(ct)
) - mostly for logging and ensuring background tasks work well until the end of the execution (final await in DisposeAsync). - Custom resources implementing
IResourceWithWaitSupport
may depend on other resources, so callnotificationService.WaitForDependenciesAsync(resource, ct)
before doing any custom orchestration
Common interfaces:
- Mention
IResourceWithWaitSupport
WithAnnotation(): maybe mention behavior to append (default) or replace an existing one
Good call out.
Retrieving annotations can also be done from the resource.Annotations property, like resource.Annotations.OfType() when there can be many of a given annotation type
π
Custom resources implementing IResourceWithWaitSupport may depend on other resources, so call notificationService.WaitForDependenciesAsync(resource, ct) before doing any custom orchestration
π
IDistributedApplicationLifecycleHook is only shown in an example. I built many features on top of this primitive - maybe it deserves its own section.
We want people to switch to eventing, I think we need a couple more version then we'll recommend people use the other API as that will drive the custom resource lifecycle (and will solve lots of the problems we have manually building resources today).
When a resource implements the IResourceWithParent interface, it declares true containment β meaning its lifecycle is controlled by its parent:
Startup: The child resource will only start after its parent starts (though readiness is independent).
Shutdown: If the parent is stopped or removed, the child is also stopped automatically.
I find this really confusing as a resource author. It implies that IResourceWithParent imparts direct and automatic control of child resources, when the reality is very different. Resource authors decide which methods comprise startup calls, and also they choose lifecycle hooks and wire up eventing to call these things. How exactly does Aspire enforce these semantics?
There could be an analyzer that validates this -- example, if we say that "start" methods should be annotated with [EntryPoint] or [Startup], then a basic analyzer could ensure that these methods are not being called inside inappropriate eventing callbacks. Even then, this won't cover all scenarios. Something feels "off."
I think we will change this assumption. This really only applies to specific resources...
ResourceLoggerService
is something I find extremely helpful - is it worth mentioning here somewhere?