From a050ca7bbc4f89519d13ed2a3855797c16da5670 Mon Sep 17 00:00:00 2001 From: afscrome Date: Sun, 25 Jan 2026 15:17:26 +0000 Subject: [PATCH 1/3] Introduce extension methods on `IDistributedApplicationBuilder` for subscribing to application level events. This is similar to #10097, but for app host level events. Moved over what consumers I (copilot really) could to the new helper - the main ones that couldn't be migrated were implementations of `IDistributedApplicationEventingSubscriber`, as they only get access to `IDistributedApplicationEventing`, and not the `IDistributedApplication` these extension method are added to. Since `IDistributedApplicationEventingSubscriber` is a bit of an advanced case, it doesn't seem unreasonable that they don't get the easy helper method. I left out `AfterEndpointsAllocatedEvent` from the new helper methods since it is obsolete. --- .../DotnetTool/DotnetTool.AppHost/AppHost.cs | 2 +- .../AzureContainerRegistryExtensions.cs | 2 +- .../AzureCosmosDBExtensions.cs | 2 +- ...AzureFunctionsProjectResourceExtensions.cs | 2 +- .../AzurePostgresExtensions.cs | 2 +- .../AzureManagedRedisExtensions.cs | 2 +- .../AzureRedisExtensions.cs | 2 +- .../JavaScriptHostingExtensions.cs | 10 +-- .../KeycloakResourceBuilderExtensions.cs | 2 +- src/Aspire.Hosting.Maui/MauiOtlpExtensions.cs | 2 +- .../PythonAppResourceBuilderExtensions.cs | 6 +- .../RedisBuilderExtensions.cs | 4 +- .../YarpResourceExtensions.cs | 2 +- ...tainerRegistryResourceBuilderExtensions.cs | 2 +- ...istributedApplicationEventingExtensions.cs | 79 ++++++++++++++++--- .../OperationModesTests.cs | 2 +- 16 files changed, 88 insertions(+), 35 deletions(-) diff --git a/playground/DotnetTool/DotnetTool.AppHost/AppHost.cs b/playground/DotnetTool/DotnetTool.AppHost/AppHost.cs index 0bb329bcca5..0b109245d9d 100644 --- a/playground/DotnetTool/DotnetTool.AppHost/AppHost.cs +++ b/playground/DotnetTool/DotnetTool.AppHost/AppHost.cs @@ -82,7 +82,7 @@ // Some issues only show up when installing for first time, rather than using existing downloaded versions // Use a specific NUGET_PACKAGES path for these playground tools, so we can easily reset them -builder.Eventing.Subscribe(async (evt, _) => +builder.OnBeforeStart(async (evt, _) => { var nugetPackagesPath = Path.Join(evt.Services.GetRequiredService().BasePath, "nuget"); diff --git a/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryExtensions.cs b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryExtensions.cs index 2be87f443be..595b51cdbe1 100644 --- a/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryExtensions.cs +++ b/src/Aspire.Hosting.Azure.ContainerRegistry/AzureContainerRegistryExtensions.cs @@ -76,7 +76,7 @@ public static IResourceBuilder AddAzureContainer /// private static void SubscribeToAddRegistryTargetAnnotations(IDistributedApplicationBuilder builder, AzureContainerRegistryResource registry) { - builder.Eventing.Subscribe((beforeStartEvent, cancellationToken) => + builder.OnBeforeStart((beforeStartEvent, cancellationToken) => { foreach (var resource in beforeStartEvent.Model.Resources) { diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs index 42d1adfaa80..61d80e70ce2 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBExtensions.cs @@ -408,7 +408,7 @@ public static IResourceBuilder WithAccessKeyAuthenticatio // need to do this later in case builder becomes an emulator after this method is called. if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) { - builder.ApplicationBuilder.Eventing.Subscribe((data, _) => + builder.ApplicationBuilder.OnBeforeStart((data, _) => { if (builder.Resource.IsEmulator) { diff --git a/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs index 961165d1a30..12f514f7314 100644 --- a/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs @@ -153,7 +153,7 @@ private static IResourceBuilder AddAzureFunctions // Register the FuncCoreToolsInstallationManager service for validating Azure Functions Core Tools builder.Services.TryAddSingleton(); - builder.Eventing.Subscribe((data, token) => + builder.OnBeforeStart((data, token) => { var removeStorage = true; // Look at all of the resources and if none of them use the default storage, then we can remove it. diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs index 16b581cc253..e1e2726ff2d 100644 --- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs +++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresExtensions.cs @@ -300,7 +300,7 @@ public static IResourceBuilder WithPassword // need to do this later in case builder becomes an emulator after this method is called. if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) { - builder.ApplicationBuilder.Eventing.Subscribe((data, token) => + builder.ApplicationBuilder.OnBeforeStart((data, token) => { if (builder.Resource.IsContainer()) { diff --git a/src/Aspire.Hosting.Azure.Redis/AzureManagedRedisExtensions.cs b/src/Aspire.Hosting.Azure.Redis/AzureManagedRedisExtensions.cs index c243fd4d5f6..f63f8fdc9b3 100644 --- a/src/Aspire.Hosting.Azure.Redis/AzureManagedRedisExtensions.cs +++ b/src/Aspire.Hosting.Azure.Redis/AzureManagedRedisExtensions.cs @@ -133,7 +133,7 @@ public static IResourceBuilder WithAccessKeyAuthentic // need to do this later in case builder becomes an emulator after this method is called. if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) { - builder.ApplicationBuilder.Eventing.Subscribe((data, token) => + builder.ApplicationBuilder.OnBeforeStart((data, token) => { if (builder.Resource.IsContainer()) { diff --git a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs index 67ffdd24aa0..2f8492d3dcb 100644 --- a/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs +++ b/src/Aspire.Hosting.Azure.Redis/AzureRedisExtensions.cs @@ -206,7 +206,7 @@ public static IResourceBuilder WithAccessKeyAuthenticat // need to do this later in case builder becomes an emulator after this method is called. if (builder.ApplicationBuilder.ExecutionContext.IsRunMode) { - builder.ApplicationBuilder.Eventing.Subscribe((data, token) => + builder.ApplicationBuilder.OnBeforeStart((data, token) => { if (builder.Resource.IsContainer()) { diff --git a/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs b/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs index 93841dc3feb..c3ead80ce09 100644 --- a/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs +++ b/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs @@ -260,7 +260,7 @@ public static IResourceBuilder AddNodeApp(this IDistributedAppl if (builder.ExecutionContext.IsRunMode) { - builder.Eventing.Subscribe((_, _) => + builder.OnBeforeStart((_, _) => { // set the command to the package manager executable if the JavaScriptRunScriptAnnotation is present if (resourceBuilder.Resource.TryGetLastAnnotation(out _) && @@ -462,7 +462,7 @@ private static IResourceBuilder CreateDefaultJavaScriptAppBuilder((_, _) => + builder.OnBeforeStart((_, _) => { if (resourceBuilder.Resource.TryGetLastAnnotation(out var packageManager)) { @@ -630,7 +630,7 @@ public static IResourceBuilder AddViteApp(this IDistributedAppl if (builder.ExecutionContext.IsRunMode) { - builder.Eventing.Subscribe((@event, _) => + builder.OnBeforeStart((@event, _) => { var developerCertificateService = @event.Services.GetRequiredService(); @@ -980,7 +980,7 @@ private static void AddInstaller(IResourceBuilder resource .ExcludeFromManifest() .WithCertificateTrustScope(CertificateTrustScope.None); - resource.ApplicationBuilder.Eventing.Subscribe((_, _) => + resource.ApplicationBuilder.OnBeforeStart((_, _) => { // set the installer's working directory to match the resource's working directory // and set the install command and args based on the resource's annotations @@ -1141,4 +1141,4 @@ private static bool TryParseNodeVersion(string versionString, out string majorVe return false; } -} +} \ No newline at end of file diff --git a/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs b/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs index 94bbb74bd30..71449b78847 100644 --- a/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Keycloak/KeycloakResourceBuilderExtensions.cs @@ -106,7 +106,7 @@ public static IResourceBuilder AddKeycloak( if (builder.ExecutionContext.IsRunMode) { - builder.Eventing.Subscribe((@event, cancellationToken) => + builder.OnBeforeStart((@event, cancellationToken) => { var developerCertificateService = @event.Services.GetRequiredService(); diff --git a/src/Aspire.Hosting.Maui/MauiOtlpExtensions.cs b/src/Aspire.Hosting.Maui/MauiOtlpExtensions.cs index a7af3faf05e..b0835139765 100644 --- a/src/Aspire.Hosting.Maui/MauiOtlpExtensions.cs +++ b/src/Aspire.Hosting.Maui/MauiOtlpExtensions.cs @@ -115,7 +115,7 @@ private static OtlpDevTunnelConfigurationAnnotation CreateOtlpDevTunnelInfrastru // Manually allocate the stub endpoint so dev tunnel can start // Dev tunnels wait for ResourceEndpointsAllocatedEvent before starting - appBuilder.Eventing.Subscribe((evt, ct) => + appBuilder.OnBeforeStart((evt, ct) => { var endpoint = stubResource.Annotations.OfType().FirstOrDefault(); if (endpoint is not null && endpoint.AllocatedEndpoint is null) diff --git a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs index 8fd3a7527c3..2e070267575 100644 --- a/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.Python/PythonAppResourceBuilderExtensions.cs @@ -313,7 +313,7 @@ public static IResourceBuilder AddUvicornApp( if (builder.ExecutionContext.IsRunMode) { - builder.Eventing.Subscribe((@event, cancellationToken) => + builder.OnBeforeStart((@event, cancellationToken) => { var developerCertificateService = @event.Services.GetRequiredService(); @@ -513,7 +513,7 @@ private static IResourceBuilder AddPythonAppCore( // and the dependencies will be established based on which resources actually exist // Only do this in run mode since the installer and venv creator only run in run mode var resourceToSetup = resourceBuilder.Resource; - builder.Eventing.Subscribe((evt, ct) => + builder.OnBeforeStart((evt, ct) => { // Wire up wait dependencies for this resource based on which child resources exist SetupDependencies(builder, resourceToSetup); @@ -1366,7 +1366,7 @@ private static void AddInstaller(IResourceBuilder builder, bool install) w // For other package managers (pip, etc.), Python validation happens via PythonVenvCreatorResource }); - builder.ApplicationBuilder.Eventing.Subscribe((_, _) => + builder.ApplicationBuilder.OnBeforeStart((_, _) => { // Set the installer's working directory to match the resource's working directory // and set the install command and args based on the resource's annotations diff --git a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs index dbbe0716d8c..cfd79d8671d 100644 --- a/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs @@ -175,7 +175,7 @@ public static IResourceBuilder AddRedis( if (builder.ExecutionContext.IsRunMode) { - builder.Eventing.Subscribe((@event, cancellationToken) => + builder.OnBeforeStart((@event, cancellationToken) => { var developerCertificateService = @event.Services.GetRequiredService(); @@ -387,7 +387,7 @@ public static IResourceBuilder WithRedisInsight(this IResourceBui }) .ExcludeFromManifest(); - builder.ApplicationBuilder.Eventing.Subscribe((@event, cancellationToken) => + builder.ApplicationBuilder.OnBeforeStart((@event, cancellationToken) => { var developerCertificateService = @event.Services.GetRequiredService(); diff --git a/src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs b/src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs index a4416fe29a3..b5a674b5025 100644 --- a/src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs +++ b/src/Aspire.Hosting.Yarp/YarpResourceExtensions.cs @@ -55,7 +55,7 @@ public static IResourceBuilder AddYarp( if (builder.ExecutionContext.IsRunMode) { - builder.Eventing.Subscribe((@event, cancellationToken) => + builder.OnBeforeStart((@event, cancellationToken) => { var developerCertificateService = @event.Services.GetRequiredService(); diff --git a/src/Aspire.Hosting/ContainerRegistryResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerRegistryResourceBuilderExtensions.cs index 70f95b21bff..8693a992f81 100644 --- a/src/Aspire.Hosting/ContainerRegistryResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerRegistryResourceBuilderExtensions.cs @@ -118,7 +118,7 @@ public static IResourceBuilder AddContainerRegistry( /// private static void SubscribeToAddRegistryTargetAnnotations(IDistributedApplicationBuilder builder, ContainerRegistryResource registry) { - builder.Eventing.Subscribe((beforeStartEvent, cancellationToken) => + builder.OnBeforeStart((beforeStartEvent, cancellationToken) => { foreach (var resource in beforeStartEvent.Model.Resources) { diff --git a/src/Aspire.Hosting/DistributedApplicationEventingExtensions.cs b/src/Aspire.Hosting/DistributedApplicationEventingExtensions.cs index ab29b72142c..f32b438e7f7 100644 --- a/src/Aspire.Hosting/DistributedApplicationEventingExtensions.cs +++ b/src/Aspire.Hosting/DistributedApplicationEventingExtensions.cs @@ -3,14 +3,59 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Eventing; +using Aspire.Hosting.Publishing; namespace Aspire.Hosting; /// -/// Provides extension methods for subscribing to events on resources. +/// Provides extension methods for subscribing to and events. /// public static class DistributedApplicationEventingExtensions { + /// + /// Subscribes a callback to the event within the AppHost. + /// + /// The distributed application builder. + /// A callback to handle the event. + /// The . + /// If you need to ensure you only subscribe to the event once, see . + public static T OnBeforeStart(this T builder, Func callback) + where T : IDistributedApplicationBuilder + => builder.OnApplicationEvent(callback); + + /// + /// Subscribes a callback to the event within the AppHost. + /// + /// The distributed application builder. + /// A callback to handle the event. + /// The . + /// If you need to ensure you only subscribe to the event once, see . + public static T OnAfterResourcesCreated(this T builder, Func callback) + where T : IDistributedApplicationBuilder + => builder.OnApplicationEvent(callback); + + /// + /// Subscribes a callback to the event within the AppHost. + /// + /// The distributed application builder. + /// A callback to handle the event. + /// The . + /// If you need to ensure you only subscribe to the event once, see . + public static T OnBeforePublish(this T builder, Func callback) + where T : IDistributedApplicationBuilder + => builder.OnApplicationEvent(callback); + + /// + /// Subscribes a callback to the event within the AppHost. + /// + /// The distributed application builder. + /// A callback to handle the event. + /// The . + /// If you need to ensure you only subscribe to the event once, see . + public static T OnAfterPublish(this T builder, Func callback) + where T : IDistributedApplicationBuilder + => builder.OnApplicationEvent(callback); + /// /// Subscribes a callback to the event within the AppHost. /// @@ -20,10 +65,10 @@ public static class DistributedApplicationEventingExtensions /// The . public static IResourceBuilder OnBeforeResourceStarted(this IResourceBuilder builder, Func callback) where T : IResource - => builder.OnEvent(callback); + => builder.OnResourceEvent(callback); /// - /// Subscribes a callback to the event within the AppHost. + /// Subscribes a callback to the event for . /// /// The resource type. /// The resource builder. @@ -31,10 +76,10 @@ public static IResourceBuilder OnBeforeResourceStarted(this IResourceBuild /// The . public static IResourceBuilder OnResourceStopped(this IResourceBuilder builder, Func callback) where T : IResource - => builder.OnEvent(callback); + => builder.OnResourceEvent(callback); /// - /// Subscribes a callback to the event within the AppHost. + /// Subscribes a callback to the event for . /// /// The resource type. /// The resource builder. @@ -42,10 +87,10 @@ public static IResourceBuilder OnResourceStopped(this IResourceBuilder /// The . public static IResourceBuilder OnConnectionStringAvailable(this IResourceBuilder builder, Func callback) where T : IResourceWithConnectionString - => builder.OnEvent(callback); + => builder.OnResourceEvent(callback); /// - /// Subscribes a callback to the event within the AppHost. + /// Subscribes a callback to the event for . /// /// The resource type. /// The resource builder. @@ -53,10 +98,10 @@ public static IResourceBuilder OnConnectionStringAvailable(this IResourceB /// The . public static IResourceBuilder OnInitializeResource(this IResourceBuilder builder, Func callback) where T : IResource - => builder.OnEvent(callback); + => builder.OnResourceEvent(callback); /// - /// Subscribes a callback to the event within the AppHost. + /// Subscribes a callback to the event for . /// /// The resource type. /// The resource builder. @@ -64,10 +109,10 @@ public static IResourceBuilder OnInitializeResource(this IResourceBuilder< /// The . public static IResourceBuilder OnResourceEndpointsAllocated(this IResourceBuilder builder, Func callback) where T : IResourceWithEndpoints - => builder.OnEvent(callback); + => builder.OnResourceEvent(callback); /// - /// Subscribes a callback to the event within the AppHost. + /// Subscribes a callback to the event for . /// /// The resource type. /// The resource builder. @@ -75,9 +120,17 @@ public static IResourceBuilder OnResourceEndpointsAllocated(this IResource /// The . public static IResourceBuilder OnResourceReady(this IResourceBuilder builder, Func callback) where T : IResource - => builder.OnEvent(callback); + => builder.OnResourceEvent(callback); + + private static T OnApplicationEvent(this T builder, Func callback) + where T : IDistributedApplicationBuilder + where TEvent : IDistributedApplicationEvent + { + builder.Eventing.Subscribe(callback); + return builder; + } - private static IResourceBuilder OnEvent(this IResourceBuilder builder, Func callback) + private static IResourceBuilder OnResourceEvent(this IResourceBuilder builder, Func callback) where TResource : IResource where TEvent : IDistributedApplicationResourceEvent { diff --git a/tests/Aspire.Hosting.Tests/OperationModesTests.cs b/tests/Aspire.Hosting.Tests/OperationModesTests.cs index 552a44aa751..a26a877a7e4 100644 --- a/tests/Aspire.Hosting.Tests/OperationModesTests.cs +++ b/tests/Aspire.Hosting.Tests/OperationModesTests.cs @@ -20,7 +20,7 @@ public async Task VerifyBackwardsCompatibleRunModeInvocation() using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(outputHelper); var tcs = new TaskCompletionSource(); - builder.Eventing.Subscribe((e, ct) => { + builder.OnAfterResourcesCreated((e, ct) => { var context = e.Services.GetRequiredService(); tcs.SetResult(context); return Task.CompletedTask; From 2441bfb5ee91c45a969866d3d9d7fd16a4fd8f94 Mon Sep 17 00:00:00 2001 From: afscrome Date: Sun, 25 Jan 2026 19:34:50 +0000 Subject: [PATCH 2/3] Add AspireExport attributes --- ...istributedApplicationEventingExtensions.cs | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/Aspire.Hosting/DistributedApplicationEventingExtensions.cs b/src/Aspire.Hosting/DistributedApplicationEventingExtensions.cs index f32b438e7f7..d0b12a9e0bf 100644 --- a/src/Aspire.Hosting/DistributedApplicationEventingExtensions.cs +++ b/src/Aspire.Hosting/DistributedApplicationEventingExtensions.cs @@ -17,8 +17,9 @@ public static class DistributedApplicationEventingExtensions /// /// The distributed application builder. /// A callback to handle the event. - /// The . + /// The for chaining. /// If you need to ensure you only subscribe to the event once, see . + [AspireExport("onBeforeStart", Description = "Subscribes a callback to the BeforeStartEvent event within the AppHost.")] public static T OnBeforeStart(this T builder, Func callback) where T : IDistributedApplicationBuilder => builder.OnApplicationEvent(callback); @@ -28,8 +29,9 @@ public static T OnBeforeStart(this T builder, Func /// The distributed application builder. /// A callback to handle the event. - /// The . + /// The for chaining. /// If you need to ensure you only subscribe to the event once, see . + [AspireExport("onAfterResourcesCreated", Description = "Subscribes a callback to the AfterResourcesCreatedEvent event within the AppHost.")] public static T OnAfterResourcesCreated(this T builder, Func callback) where T : IDistributedApplicationBuilder => builder.OnApplicationEvent(callback); @@ -39,8 +41,9 @@ public static T OnAfterResourcesCreated(this T builder, Func /// The distributed application builder. /// A callback to handle the event. - /// The . + /// The for chaining. /// If you need to ensure you only subscribe to the event once, see . + [AspireExport("onBeforePublish", Description = "Subscribes a callback to the BeforePublishEvent event within the AppHost.")] public static T OnBeforePublish(this T builder, Func callback) where T : IDistributedApplicationBuilder => builder.OnApplicationEvent(callback); @@ -50,8 +53,9 @@ public static T OnBeforePublish(this T builder, Func /// The distributed application builder. /// A callback to handle the event. - /// The . + /// The for chaining. /// If you need to ensure you only subscribe to the event once, see . + [AspireExport("onAfterPublish", Description = "Subscribes a callback to the AfterPublishEvent event within the AppHost.")] public static T OnAfterPublish(this T builder, Func callback) where T : IDistributedApplicationBuilder => builder.OnApplicationEvent(callback); @@ -62,7 +66,8 @@ public static T OnAfterPublish(this T builder, FuncThe resource type. /// The resource builder. /// A callback to handle the event. - /// The . + /// The for chaining. + [AspireExport("onBeforeResourceStarted", Description = "Subscribes a callback to the BeforeResourceStartedEvent event of the resource.")] public static IResourceBuilder OnBeforeResourceStarted(this IResourceBuilder builder, Func callback) where T : IResource => builder.OnResourceEvent(callback); @@ -73,7 +78,8 @@ public static IResourceBuilder OnBeforeResourceStarted(this IResourceBuild /// The resource type. /// The resource builder. /// A callback to handle the event. - /// The . + /// The for chaining. + [AspireExport("onResourceStopped", Description = "Subscribes a callback to the ResourceStoppedEvent event of the resource.")] public static IResourceBuilder OnResourceStopped(this IResourceBuilder builder, Func callback) where T : IResource => builder.OnResourceEvent(callback); @@ -84,7 +90,8 @@ public static IResourceBuilder OnResourceStopped(this IResourceBuilder /// The resource type. /// The resource builder. /// A callback to handle the event. - /// The . + /// The for chaining. + [AspireExport("onConnectionStringAvailable", Description = "Subscribes a callback to the ConnectionStringAvailableEvent event of the resource.")] public static IResourceBuilder OnConnectionStringAvailable(this IResourceBuilder builder, Func callback) where T : IResourceWithConnectionString => builder.OnResourceEvent(callback); @@ -95,7 +102,8 @@ public static IResourceBuilder OnConnectionStringAvailable(this IResourceB /// The resource type. /// The resource builder. /// A callback to handle the event. - /// The . + /// The for chaining. + [AspireExport("onInitializeResource", Description = "Subscribes a callback to the InitializeResourceEvent event of the resource.")] public static IResourceBuilder OnInitializeResource(this IResourceBuilder builder, Func callback) where T : IResource => builder.OnResourceEvent(callback); @@ -106,7 +114,8 @@ public static IResourceBuilder OnInitializeResource(this IResourceBuilder< /// The resource type. /// The resource builder. /// A callback to handle the event. - /// The . + /// The for chaining. + [AspireExport("onResourceEndpointsAllocated", Description = "Subscribes a callback to the ResourceEndpointsAllocatedEvent event of the resource.")] public static IResourceBuilder OnResourceEndpointsAllocated(this IResourceBuilder builder, Func callback) where T : IResourceWithEndpoints => builder.OnResourceEvent(callback); @@ -117,7 +126,8 @@ public static IResourceBuilder OnResourceEndpointsAllocated(this IResource /// The resource type. /// The resource builder. /// A callback to handle the event. - /// The . + /// The for chaining. + [AspireExport("onResourceReady", Description = "Subscribes a callback to the ResourceReadyEvent event of the resource.")] public static IResourceBuilder OnResourceReady(this IResourceBuilder builder, Func callback) where T : IResource => builder.OnResourceEvent(callback); From 6e475dc4c3285cb355a0f986b91e6445073e13b5 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 27 Jan 2026 08:12:49 +1100 Subject: [PATCH 3/3] Add test coverage for application-level event helper methods - Add tests for OnBeforeStart, OnAfterResourcesCreated, OnBeforePublish, OnAfterPublish - Add tests verifying builder is returned for method chaining - Fix missing newline at EOF in JavaScriptHostingExtensions.cs --- .../JavaScriptHostingExtensions.cs | 2 +- ...tributedApplicationBuilderEventingTests.cs | 129 ++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs b/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs index c3ead80ce09..74607df3577 100644 --- a/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs +++ b/src/Aspire.Hosting.JavaScript/JavaScriptHostingExtensions.cs @@ -1141,4 +1141,4 @@ private static bool TryParseNodeVersion(string versionString, out string majorVe return false; } -} \ No newline at end of file +} diff --git a/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs b/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs index 54b5807b4c0..fe5efb094cf 100644 --- a/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs +++ b/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs @@ -3,6 +3,7 @@ using Aspire.TestUtilities; using Aspire.Hosting.Eventing; +using Aspire.Hosting.Publishing; using Aspire.Hosting.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; @@ -335,6 +336,134 @@ public async Task ResourceStoppedEventFiresWhenResourceStops() await resourceStoppedTcs.Task.DefaultTimeout(); } + [Fact] + public async Task OnBeforeStartSubscribesToBeforeStartEvent() + { + var eventFired = new ManualResetEventSlim(); + + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + builder.OnBeforeStart((e, ct) => + { + Assert.NotNull(e.Services); + Assert.NotNull(e.Model); + eventFired.Set(); + return Task.CompletedTask; + }); + + using var app = builder.Build(); + await app.StartAsync(); + + var fired = eventFired.Wait(TimeSpan.FromSeconds(10)); + Assert.True(fired); + + await app.StopAsync(); + } + + [Fact] + public async Task OnAfterResourcesCreatedSubscribesToAfterResourcesCreatedEvent() + { + var eventFired = new ManualResetEventSlim(); + + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + builder.OnAfterResourcesCreated((e, ct) => + { + Assert.NotNull(e.Services); + Assert.NotNull(e.Model); + eventFired.Set(); + return Task.CompletedTask; + }); + + using var app = builder.Build(); + await app.StartAsync(); + + var fired = eventFired.Wait(TimeSpan.FromSeconds(10)); + Assert.True(fired); + + await app.StopAsync(); + } + + [Fact] + public async Task OnBeforePublishSubscribesToBeforePublishEvent() + { + var eventFired = false; + + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + builder.OnBeforePublish((e, ct) => + { + Assert.NotNull(e.Model); + eventFired = true; + return Task.CompletedTask; + }); + + using var app = builder.Build(); + var eventing = app.Services.GetRequiredService(); + + // Manually publish the event to verify subscription + var testEvent = new BeforePublishEvent(app.Services, new([])); + await eventing.PublishAsync(testEvent, CancellationToken.None); + + Assert.True(eventFired); + } + + [Fact] + public async Task OnAfterPublishSubscribesToAfterPublishEvent() + { + var eventFired = false; + + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + builder.OnAfterPublish((e, ct) => + { + Assert.NotNull(e.Model); + eventFired = true; + return Task.CompletedTask; + }); + + using var app = builder.Build(); + var eventing = app.Services.GetRequiredService(); + + // Manually publish the event to verify subscription + var testEvent = new AfterPublishEvent(app.Services, new([])); + await eventing.PublishAsync(testEvent, CancellationToken.None); + + Assert.True(eventFired); + } + + [Fact] + public void OnBeforeStartReturnsBuilderForChaining() + { + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + var result = builder.OnBeforeStart((e, ct) => Task.CompletedTask); + + Assert.Same(builder, result); + } + + [Fact] + public void OnAfterResourcesCreatedReturnsBuilderForChaining() + { + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + var result = builder.OnAfterResourcesCreated((e, ct) => Task.CompletedTask); + + Assert.Same(builder, result); + } + + [Fact] + public void OnBeforePublishReturnsBuilderForChaining() + { + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + var result = builder.OnBeforePublish((e, ct) => Task.CompletedTask); + + Assert.Same(builder, result); + } + + [Fact] + public void OnAfterPublishReturnsBuilderForChaining() + { + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + var result = builder.OnAfterPublish((e, ct) => Task.CompletedTask); + + Assert.Same(builder, result); + } + public class DummyEvent : IDistributedApplicationEvent { }