Skip to content
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

API Proposal: App Model Eventing #5234

Closed
mitchdenny opened this issue Aug 9, 2024 · 2 comments
Closed

API Proposal: App Model Eventing #5234

mitchdenny opened this issue Aug 9, 2024 · 2 comments
Assignees
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication
Milestone

Comments

@mitchdenny
Copy link
Member

Background and Motivation

Today .NET Aspire has a coarse-grained mechanism for executing code at various stages of the local development lifecycle. A developer can implement the IDistributedApplicationLifecycleHook interface and inject it into the DI container, and when the application starts methods on the implementation are called.

This mechanism is used by features such as WithPgAdmin and many other internal mechanisms. In the lead up to GA for .NET Aspire 8.0 we identified that it might be useful to have a much more general and extensible mechanism for publishing and subscribing to events that occur during the local development lifecycle.

Proposed API

Core Eventing API

These are the core interfaces and types that support the eventing model. The first change is that we add a new property to implementors of IDistributedApplicationBuilder. This eventing API allows developers to publish and subscribe to events before DI is even available which is needed for the builder pattern in Aspire. This IDistributedApplicationEventing instance is a singleton for the builder/app host and is also available via DI.

namespace Aspire.Hosting;

public interface IDistributedApplicationBuilder
{
+    IDistributedApplicationEventing Eventing { get; }
}

The IDistributedApplicationEventing interface is the core interface for publishing and subscribing to events.

+ namespace Aspire.Hosting.Eventing;

+ public interface IDistributedApplicationEventing
+ {
+    DistributedApplicationEventSubscription Subscribe<T>(Func<T, CancellationToken, Task> callback) where T : IDistributedApplicationEvent;
+    DistributedApplicationResourceEventSubscription Subscribe<T>(IResource resource, Func<T, CancellationToken, Task> callback) where T : IDistributedApplicationResourceEvent;
+    void Unsubscribe(DistributedApplicationEventSubscription subscription);
+    Task PublishAsync<T>(T @event, CancellationToken cancellationToken) where T : IDistributedApplicationEvent;
+}

Event subscriptions can be either global or scoped to a resource (where applicable).

+ namespace Aspire.Hosting.Eventing;

+ public class DistributedApplicationEventSubscription(Func<IDistributedApplicationEvent, CancellationToken, Task> callback)
+ {
+     public Func<IDistributedApplicationEvent, CancellationToken, Task> Callback { get; } = callback;
+ }

+ public class DistributedApplicationResourceEventSubscription(IResource? resource, Func<IDistributedApplicationResourceEvent, CancellationToken, Task> callback)
+     : DistributedApplicationEventSubscription((@event, cancellationToken) => callback((IDistributedApplicationResourceEvent)@event, cancellationToken))
+ {
+     public IResource? Resource { get; } = resource;
+ }

An event is any type that implements the following interfaces:

+ namespace Aspire.Hosting.Eventing;

+ public interface IDistributedApplicationEvent
+ {
+ }

+ public interface IDistributedApplicationResourceEvent : IDistributedApplicationEvent
+ {
+     IResource Resource { get; }
+ }

Usage Examples

Here is an example of creating an extension to our Postgres APIs to support database initialization:

    public static IResourceBuilder<PostgresDatabaseResource> WithDatabaseInitialization(this IResourceBuilder<PostgresDatabaseResource> builder)
    {
        builder.ApplicationBuilder.Eventing.Subscribe<ContainerResourceStartedEvent>(builder.Resource.Parent, async (@event, cancellationToken) =>
        {
            var connectionString = await builder.Resource.Parent.ConnectionStringExpression.GetValueAsync(cancellationToken).ConfigureAwait(false);
            var connection = new NpgsqlConnection(connectionString);

            // TODO: Use Polly.
            await Task.Delay(10000, cancellationToken).ConfigureAwait(false);

            await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
            var createDatabaseCommand = connection.CreateCommand();
            createDatabaseCommand.CommandText = $"CREATE DATABASE {builder.Resource.DatabaseName};";
            await createDatabaseCommand.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        });

        return builder;
    }

This is using the resource-scoped variant for Subscribe(...). The publishing code looks like the following (in this case its in the application executor internal to Aspire):

var containerStartedEvent = new ContainerResourceStartedEvent(modelContainerResource);
await eventing.PublishAsync(containerStartedEvent, cancellationToken).ConfigureAwait(false);

The eventing variable is an instance of IDistributedApplicationEventing which is resolved from DI (since this event is raised post DI build).

Alternative Designs

Explored the use of a more DI heavy publishing/subscribe model with dispatchers/handlers here: #5104

In the end the approach proposed here feels more ergonomic and less heavy for end users of the API (no types to define unless you are publishing a new kind of event).

Risks

We need to consider concurrency here in the algorithm but also whether there is any potential for storms of events due to loops in the logic.

@dotnet-issue-labeler dotnet-issue-labeler bot added the area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication label Aug 9, 2024
@mitchdenny mitchdenny added this to the 8.2 milestone Aug 13, 2024
@mitchdenny mitchdenny self-assigned this Aug 13, 2024
@joperezr joperezr modified the milestones: 8.2, 9.0 Aug 19, 2024
@joperezr
Copy link
Member

Moving to 9.0. For 8.2, we will ship this as Experimental but we still want to finish the work for 9.0.

@mitchdenny
Copy link
Member Author

This is actually in now as an experimental API and we are starting to build on top of it (many of the lifecycle hooks have been replaced. So I am going to close this issue and treat any subsequent updates as distinct work.

@github-actions github-actions bot locked and limited conversation to collaborators Sep 20, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-app-model Issues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplication
Projects
None yet
Development

No branches or pull requests

3 participants