Eventing in .NET Aspire

In .NET Aspire, eventing allows you to publish and subscribe to events during various app host life cycles. Eventing is more flexible than life cycle events. Both let you run arbitrary code during event callbacks, but eventing offers finer control of event timing, publishing, and provides supports for custom events.

The eventing mechanisms in .NET Aspire are part of the 📦 Aspire.Hosting NuGet package. This package provides a set of interfaces and classes in the Aspire.Hosting.Eventing namespace that you use to publish and subscribe to events in your .NET Aspire app host project. Eventing is scoped to the app host itself and the resources within.

In this article, you learn how to use the eventing features in .NET Aspire.

App host eventing

The following events are available in the app host and occur in the following order:

  1. BeforeStartEvent: This event is raised before the app host starts.
  2. AfterEndpointsAllocatedEvent: This event is raised after the app host allocated endpoints.
  3. AfterResourcesCreatedEvent: This event is raised after the app host created resources.

All of the preceding events are analogous to the app host life cycles. That is, an implementation of the IDistributedApplicationLifecycleHook could handle these events just the same. With the eventing API, however, you can run arbitrary code when these events are raised and event define custom events—any event that implements the IDistributedApplicationEvent interface.

Subscribe to app host events

To subscribe to the built-in app host events, use the eventing API. After you have a distributed application builder instance, walk up to the IDistributedApplicationBuilder.Eventing property and call the Subscribe<T>(Func<T,CancellationToken,Task>) API. Consider the following sample app host Program.cs file:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

var apiService = builder.AddProject<Projects.AspireApp_ApiService>("apiservice");

builder.AddProject<Projects.AspireApp_Web>("webfrontend")
    .WithExternalHttpEndpoints()
    .WithReference(cache)
    .WaitFor(cache)
    .WithReference(apiService)
    .WaitFor(apiService);

builder.Eventing.Subscribe<BeforeStartEvent>(
    static (@event, cancellationToken) =>
    {
        var logger = @event.Services.GetRequiredService<ILogger<Program>>();

        logger.LogInformation("1. BeforeStartEvent");

        return Task.CompletedTask;
    });

builder.Eventing.Subscribe<AfterEndpointsAllocatedEvent>(
    static (@event, cancellationToken) =>
    {
        var logger = @event.Services.GetRequiredService<ILogger<Program>>();

        logger.LogInformation("2. AfterEndpointsAllocatedEvent");

        return Task.CompletedTask;
    });

builder.Eventing.Subscribe<AfterResourcesCreatedEvent>(
    static (@event, cancellationToken) =>
    {
        var logger = @event.Services.GetRequiredService<ILogger<Program>>();

        logger.LogInformation("3. AfterResourcesCreatedEvent");

        return Task.CompletedTask;
    });

builder.Build().Run();

The preceding code is based on the starter template with the addition of the calls to the Subscribe API. The Subscribe<T> API returns a DistributedApplicationEventSubscription instance that you can use to unsubscribe from the event. It's common to discard the returned subscriptions, as you don't usually need to unsubscribe from events as the entire app is torn down when the app host is shut down.

When the app host is run, by the time the .NET Aspire dashboard is displayed, you should see the following log output in the console:

info: Program[0]
      1. BeforeStartEvent
info: Aspire.Hosting.DistributedApplication[0]
      Aspire version: 9.0.0
info: Aspire.Hosting.DistributedApplication[0]
      Distributed application starting.
info: Aspire.Hosting.DistributedApplication[0]
      Application host directory is: ..\AspireApp\AspireApp.AppHost
info: Program[0]
      2. AfterEndpointsAllocatedEvent
info: Aspire.Hosting.DistributedApplication[0]
      Now listening on: https://localhost:17178
info: Aspire.Hosting.DistributedApplication[0]
      Login to the dashboard at https://localhost:17178/login?t=<YOUR_TOKEN>
info: Program[0]
      3. AfterResourcesCreatedEvent
info: Aspire.Hosting.DistributedApplication[0]
      Distributed application started. Press Ctrl+C to shut down.

The log output confirms that event handlers are executed in the order of the app host life cycle events. The subscription order doesn't affect execution order. The BeforeStartEvent is triggered first, followed by AfterEndpointsAllocatedEvent, and finally AfterResourcesCreatedEvent.

Resource eventing

In addition to the app host events, you can also subscribe to resource events. Resource events are raised specific to an individual resource. Resource events are defined as implementations of the IDistributedApplicationResourceEvent interface. The following resource events are available in the listed order:

  1. ConnectionStringAvailableEvent: Raised when a connection string becomes available for a resource.
  2. BeforeResourceStartedEvent: Raised before the orchestrator starts a new resource.
  3. ResourceReadyEvent: Raised when a resource initially transitions to a ready state.

Subscribe to resource events

To subscribe to resource events, use the eventing API. After you have a distributed application builder instance, walk up to the IDistributedApplicationBuilder.Eventing property and call the Subscribe<T>(IResource, Func<T,CancellationToken,Task>) API. Consider the following sample app host Program.cs file:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

var builder = DistributedApplication.CreateBuilder(args);

var cache = builder.AddRedis("cache");

builder.Eventing.Subscribe<ResourceReadyEvent>(
    cache.Resource,
    static (@event, cancellationToken) =>
    {
        var logger = @event.Services.GetRequiredService<ILogger<Program>>();

        logger.LogInformation("3. ResourceReadyEvent");

        return Task.CompletedTask;
    });

builder.Eventing.Subscribe<BeforeResourceStartedEvent>(
    cache.Resource,
    static (@event, cancellationToken) =>
    {
        var logger = @event.Services.GetRequiredService<ILogger<Program>>();

        logger.LogInformation("2. BeforeResourceStartedEvent");

        return Task.CompletedTask;
    });

builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(
    cache.Resource,
    static (@event, cancellationToken) =>
    {
        var logger = @event.Services.GetRequiredService<ILogger<Program>>();

        logger.LogInformation("1. ConnectionStringAvailableEvent");

        return Task.CompletedTask;
    });

var apiService = builder.AddProject<Projects.AspireApp_ApiService>("apiservice");

builder.AddProject<Projects.AspireApp_Web>("webfrontend")
    .WithExternalHttpEndpoints()
    .WithReference(cache)
    .WaitFor(cache)
    .WithReference(apiService)
    .WaitFor(apiService);

builder.Build().Run();

The preceding code subscribes to the ResourceReadyEvent, ConnectionStringAvailableEvent, and BeforeResourceStartedEvent events on the cache resource. When AddRedis is called, it returns an IResourceBuilder<T> where T is a RedisResource. The resource builder exposes the resource as the IResourceBuilder<T>.Resource property. The resource in question is then passed to the Subscribe API to subscribe to the events on the resource.

When the app host is run, by the time the .NET Aspire dashboard is displayed, you should see the following log output in the console:

info: Aspire.Hosting.DistributedApplication[0]
      Aspire version: 9.0.0
info: Aspire.Hosting.DistributedApplication[0]
      Distributed application starting.
info: Aspire.Hosting.DistributedApplication[0]
      Application host directory is: ..\AspireApp\AspireApp.AppHost
info: Program[0]
      1. ConnectionStringAvailableEvent
info: Program[0]
      2. BeforeResourceStartedEvent
info: Program[0]
      3. ResourceReadyEvent
info: Aspire.Hosting.DistributedApplication[0]
      Now listening on: https://localhost:17222
info: Aspire.Hosting.DistributedApplication[0]
      Login to the dashboard at https://localhost:17222/login?t=<YOUR_TOKEN>
info: Aspire.Hosting.DistributedApplication[0]
      Distributed application started. Press Ctrl+C to shut down.

Nota

Some events are blocking. For example, when the BeforeResourceStartEvent is published, the startup of the resource will be blocked until all subscriptions for that event on a given resource have completed executing. Whether an event is blocking or not depends on how it is published (see the following section).

Publish events

When subscribing to any of the built-in events, you don't need to publish the event yourself as the app host orchestrator manages to publish built-in events on your behalf. However, you can publish custom events with the eventing API. To publish an event, you have to first define an event as an implementation of either the IDistributedApplicationEvent or IDistributedApplicationResourceEvent interface. You need to determine which interface to implement based on whether the event is a global app host event or a resource-specific event.

Then, you can subscribe and publish the event by calling the either of the following APIs:

Provide an EventDispatchBehavior

When events are dispatched, you can control how the events are dispatched to subscribers. The event dispatch behavior is specified with the EventDispatchBehavior enum. The following behaviors are available:

The default behavior is EventDispatchBehavior.BlockingSequential. To override this behavior, when calling a publishing API such as PublishAsync, provide the desired behavior as an argument.