diff --git a/playground/CustomResources/CustomResources.AppHost/TestResource.cs b/playground/CustomResources/CustomResources.AppHost/TestResource.cs index a69539ac49d..f47381ccda6 100644 --- a/playground/CustomResources/CustomResources.AppHost/TestResource.cs +++ b/playground/CustomResources/CustomResources.AppHost/TestResource.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.Logging; @@ -9,7 +10,7 @@ static class TestResourceExtensions { public static IResourceBuilder AddTestResource(this IDistributedApplicationBuilder builder, string name) { - builder.Services.TryAddLifecycleHook(); + builder.Services.TryAddEventingSubscriber(); var rb = builder.AddResource(new TestResource(name)) .WithInitialState(new() @@ -27,13 +28,16 @@ public static IResourceBuilder AddTestResource(this IDistributedAp } } -internal sealed class TestResourceLifecycleHook(ResourceNotificationService notificationService, ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook, IAsyncDisposable +internal sealed class TestResourceLifecycle( + ResourceNotificationService notificationService, + ResourceLoggerService loggerService + ) : IDistributedApplicationEventingSubscriber { private readonly CancellationTokenSource _tokenSource = new(); - public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + public Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken = default) { - foreach (var resource in appModel.Resources.OfType()) + foreach (var resource in @event.Model.Resources.OfType()) { var states = new[] { "Starting", "Running", "Finished", "Uploading", "Downloading", "Processing", "Provisioning" }; var stateStyles = new[] { "info", "success", "warning", "error" }; @@ -76,6 +80,12 @@ public ValueTask DisposeAsync() _tokenSource.Cancel(); return default; } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + eventing.Subscribe(OnBeforeStartAsync); + return Task.CompletedTask; + } } sealed class TestResource(string name) : Resource(name) diff --git a/playground/HealthChecks/HealthChecksSandbox.AppHost/Program.cs b/playground/HealthChecks/HealthChecksSandbox.AppHost/Program.cs index 2912470dd05..84f53e2f004 100644 --- a/playground/HealthChecks/HealthChecksSandbox.AppHost/Program.cs +++ b/playground/HealthChecks/HealthChecksSandbox.AppHost/Program.cs @@ -1,13 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; var builder = DistributedApplication.CreateBuilder(args); -builder.Services.TryAddLifecycleHook(); +builder.Services.TryAddEventingSubscriber(); AddTestResource("healthy", HealthStatus.Healthy, "I'm fine, thanks for asking."); AddTestResource("unhealthy", HealthStatus.Unhealthy, "I can't do that, Dave.", exceptionMessage: "Feeling unhealthy."); @@ -60,11 +61,11 @@ void AddTestResource(string name, HealthStatus status, string? description = nul internal sealed class TestResource(string name) : Resource(name); -internal sealed class TestResourceLifecycleHook(ResourceNotificationService notificationService) : IDistributedApplicationLifecycleHook +internal sealed class TestResourceLifecycle(ResourceNotificationService notificationService) : IDistributedApplicationEventingSubscriber { - public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken) { - foreach (var resource in appModel.Resources.OfType()) + foreach (var resource in @event.Model.Resources.OfType()) { Task.Run( async () => @@ -80,4 +81,10 @@ await notificationService.PublishUpdateAsync( return Task.CompletedTask; } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + eventing.Subscribe(OnBeforeStartAsync); + return Task.CompletedTask; + } } diff --git a/playground/Stress/Stress.AppHost/TestResource.cs b/playground/Stress/Stress.AppHost/TestResource.cs index cb8420c788d..a9115588c9d 100644 --- a/playground/Stress/Stress.AppHost/TestResource.cs +++ b/playground/Stress/Stress.AppHost/TestResource.cs @@ -3,6 +3,7 @@ using System.Globalization; using Aspire.Dashboard.Model; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.Logging; @@ -10,7 +11,7 @@ static class TestResourceExtensions { public static IResourceBuilder AddTestResource(this IDistributedApplicationBuilder builder, string name) { - builder.Services.TryAddLifecycleHook(); + builder.Services.TryAddEventingSubscriber(); var rb = builder.AddResource(new TestResource(name)) .WithInitialState(new() @@ -46,13 +47,16 @@ public static IResourceBuilder AddNestedResource(this IDistr } } -internal sealed class TestResourceLifecycleHook(ResourceNotificationService notificationService, ResourceLoggerService loggerService) : IDistributedApplicationLifecycleHook, IAsyncDisposable +internal sealed class TestResourceLifecycle( + ResourceNotificationService notificationService, + ResourceLoggerService loggerService + ) : IDistributedApplicationEventingSubscriber, IAsyncDisposable { private readonly CancellationTokenSource _tokenSource = new(); - public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + public Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken = default) { - foreach (var resource in appModel.Resources.OfType()) + foreach (var resource in @event.Model.Resources.OfType()) { var states = new[] { "Starting", "Running", "Finished" }; @@ -94,6 +98,12 @@ public ValueTask DisposeAsync() _tokenSource.Cancel(); return default; } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + eventing.Subscribe(OnBeforeStartAsync); + return Task.CompletedTask; + } } sealed class TestResource(string name) : Resource(name) diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs index 10a884662ef..009b0b21311 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppExtensions.cs @@ -42,7 +42,7 @@ internal static IDistributedApplicationBuilder AddAzureContainerAppsInfrastructu // so Azure resources don't need to add the default role assignments themselves builder.Services.Configure(o => o.SupportsTargetedRoleAssignments = true); - builder.Services.TryAddLifecycleHook(); + builder.Services.TryAddEventingSubscriber(); return builder; } diff --git a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs index e1f1a4240e1..0103c919add 100644 --- a/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs +++ b/src/Aspire.Hosting.Azure.AppContainers/AzureContainerAppsInfrastructure.cs @@ -5,6 +5,7 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure.AppContainers; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,25 +14,19 @@ namespace Aspire.Hosting.Azure; /// /// Represents the infrastructure for Azure Container Apps within the Aspire Hosting environment. -/// Implements the interface to provide lifecycle hooks for distributed applications. /// internal sealed class AzureContainerAppsInfrastructure( ILogger logger, - IOptions provisioningOptions, - DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook + DistributedApplicationExecutionContext executionContext, + IOptions options) : IDistributedApplicationEventingSubscriber { - public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken = default) { - if (executionContext.IsRunMode) - { - return; - } - - var caes = appModel.Resources.OfType().ToArray(); + var caes = @event.Model.Resources.OfType().ToArray(); if (caes.Length == 0) { - EnsureNoPublishAsAcaAnnotations(appModel); + EnsureNoPublishAsAcaAnnotations(@event.Model); return; } @@ -42,9 +37,9 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell executionContext, environment); - foreach (var r in appModel.GetComputeResources()) + foreach (var r in @event.Model.GetComputeResources()) { - var containerApp = await containerAppEnvironmentContext.CreateContainerAppAsync(r, provisioningOptions.Value, cancellationToken).ConfigureAwait(false); + var containerApp = await containerAppEnvironmentContext.CreateContainerAppAsync(r, options.Value, cancellationToken).ConfigureAwait(false); // Capture information about the container registry used by the // container app environment in the deployment target information @@ -68,4 +63,14 @@ private static void EnsureNoPublishAsAcaAnnotations(DistributedApplicationModel } } } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + if (!executionContext.IsRunMode) + { + eventing.Subscribe(OnBeforeStartAsync); + } + + return Task.CompletedTask; + } } diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs index a03ea9c56ce..38d68f29324 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceEnvironmentExtensions.cs @@ -26,7 +26,7 @@ internal static IDistributedApplicationBuilder AddAzureAppServiceInfrastructureC builder.Services.Configure(options => options.SupportsTargetedRoleAssignments = true); - builder.Services.TryAddLifecycleHook(); + builder.Services.TryAddEventingSubscriber(); return builder; } diff --git a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs index 88475e25e38..0fc89849a96 100644 --- a/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs +++ b/src/Aspire.Hosting.Azure.AppService/AzureAppServiceInfrastructure.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -11,21 +12,20 @@ namespace Aspire.Hosting.Azure.AppService; internal sealed class AzureAppServiceInfrastructure( ILogger logger, IOptions provisioningOptions, - DistributedApplicationExecutionContext executionContext) : - IDistributedApplicationLifecycleHook + DistributedApplicationExecutionContext executionContext) : IDistributedApplicationEventingSubscriber { - public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken = default) { if (!executionContext.IsPublishMode) { return; } - var appServiceEnvironments = appModel.Resources.OfType().ToArray(); + var appServiceEnvironments = @event.Model.Resources.OfType().ToArray(); if (appServiceEnvironments.Length == 0) { - EnsureNoPublishAsAzureAppServiceWebsiteAnnotations(appModel); + EnsureNoPublishAsAzureAppServiceWebsiteAnnotations(@event.Model); return; } @@ -36,7 +36,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell executionContext, appServiceEnvironment); - foreach (var resource in appModel.GetComputeResources()) + foreach (var resource in @event.Model.GetComputeResources()) { // Support project resources and containers with Dockerfile if (resource is not ProjectResource && !(resource.IsContainer() && resource.TryGetAnnotationsOfType(out _))) @@ -67,4 +67,10 @@ private static void EnsureNoPublishAsAzureAppServiceWebsiteAnnotations(Distribut } } } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + eventing.Subscribe(OnBeforeStartAsync); + return Task.CompletedTask; + } } diff --git a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs index f2423151be8..e3da23bd2c4 100644 --- a/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs +++ b/src/Aspire.Hosting.Azure/AzureResourcePreparer.cs @@ -3,6 +3,7 @@ using Aspire.Dashboard.Model; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Azure.Provisioning; using Azure.Provisioning.Authorization; @@ -16,34 +17,32 @@ namespace Aspire.Hosting.Azure; /// This includes preparing role assignment annotations for Azure resources. /// internal sealed class AzureResourcePreparer( - IOptions provisioningOptions, - DistributedApplicationExecutionContext executionContext - ) : IDistributedApplicationLifecycleHook + IOptions options, + DistributedApplicationExecutionContext executionContext) : IDistributedApplicationEventingSubscriber { - public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken) { - var azureResources = GetAzureResourcesFromAppModel(appModel); + var azureResources = GetAzureResourcesFromAppModel(@event.Model); if (azureResources.Count == 0) { return; } - var options = provisioningOptions.Value; - if (!EnvironmentSupportsIdentitiesAndAssignments(options)) + if (!EnvironmentSupportsIdentitiesAndAssignments()) { // If the app infrastructure does not support targeted identities and role assignments, then we need to ensure that // there are no identity or role assignment annotations in the app model because they won't be honored otherwise. - EnsureNoIdentityOrRoleAssignmentAnnotations(appModel); + EnsureNoIdentityOrRoleAssignmentAnnotations(@event.Model); } - await BuildRoleAssignmentAnnotations(appModel, azureResources, options, cancellationToken).ConfigureAwait(false); + await BuildRoleAssignmentAnnotations(@event.Model, azureResources, cancellationToken).ConfigureAwait(false); // set the ProvisioningBuildOptions on the resource, if necessary foreach (var r in azureResources) { if (r.AzureResource is AzureProvisioningResource provisioningResource) { - provisioningResource.ProvisioningBuildOptions = options.ProvisioningBuildOptions; + provisioningResource.ProvisioningBuildOptions = options.Value.ProvisioningBuildOptions; } } } @@ -78,11 +77,11 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell return azureResources; } - private bool EnvironmentSupportsIdentitiesAndAssignments(AzureProvisioningOptions options) + private bool EnvironmentSupportsIdentitiesAndAssignments() { // run mode always supports targeted role assignments // publish mode only supports targeted role assignments if the environment supports it - return executionContext.IsRunMode || options.SupportsTargetedRoleAssignments; + return executionContext.IsRunMode || options.Value.SupportsTargetedRoleAssignments; } private static void EnsureNoIdentityOrRoleAssignmentAnnotations(DistributedApplicationModel appModel) @@ -101,11 +100,11 @@ private static void EnsureNoIdentityOrRoleAssignmentAnnotations(DistributedAppli } } - private async Task BuildRoleAssignmentAnnotations(DistributedApplicationModel appModel, List<(IResource Resource, IAzureResource AzureResource)> azureResources, AzureProvisioningOptions options, CancellationToken cancellationToken) + private async Task BuildRoleAssignmentAnnotations(DistributedApplicationModel appModel, List<(IResource Resource, IAzureResource AzureResource)> azureResources, CancellationToken cancellationToken) { var globalRoleAssignments = new Dictionary>(); - if (!EnvironmentSupportsIdentitiesAndAssignments(options)) + if (!EnvironmentSupportsIdentitiesAndAssignments()) { // when the app infrastructure doesn't support targeted role assignments, just copy all the default role assignments to applied role assignments foreach (var resource in azureResources.Select(r => r.AzureResource).OfType()) @@ -187,7 +186,7 @@ private async Task BuildRoleAssignmentAnnotations(DistributedApplicationModel ap var roleAssignments = GetAllRoleAssignments(resource); if (roleAssignments.Count > 0) { - var (identityResource, roleAssignmentResources) = CreateIdentityAndRoleAssignmentResources(options, resource, roleAssignments, executionContext); + var (identityResource, roleAssignmentResources) = CreateIdentityAndRoleAssignmentResources(resource, roleAssignments); if (resource != identityResource) { @@ -230,7 +229,7 @@ private async Task BuildRoleAssignmentAnnotations(DistributedApplicationModel ap if (globalRoleAssignments.Count > 0) { - CreateGlobalRoleAssignments(appModel, globalRoleAssignments, options); + CreateGlobalRoleAssignments(appModel, globalRoleAssignments); } // We can derive role assignments for compute resources and declared @@ -254,11 +253,9 @@ private static Dictionary return result; } - private static (AzureUserAssignedIdentityResource IdentityResource, List RoleAssignmentResources) CreateIdentityAndRoleAssignmentResources( - AzureProvisioningOptions provisioningOptions, + private (AzureUserAssignedIdentityResource IdentityResource, List RoleAssignmentResources) CreateIdentityAndRoleAssignmentResources( IResource resource, - Dictionary> roleAssignments, - DistributedApplicationExecutionContext executionContext) + Dictionary> roleAssignments) { AzureUserAssignedIdentityResource identityResource; @@ -278,29 +275,27 @@ private static (AzureUserAssignedIdentityResource IdentityResource, List CreateRoleAssignmentsResources( - AzureProvisioningOptions provisioningOptions, + private List CreateRoleAssignmentsResources( IResource resource, Dictionary> roleAssignments, - AzureUserAssignedIdentityResource appIdentityResource, - DistributedApplicationExecutionContext executionContext) + AzureUserAssignedIdentityResource appIdentityResource) { var roleAssignmentResources = new List(); foreach (var (targetResource, roles) in roleAssignments) { var roleAssignmentResource = new AzureProvisioningResource( $"{resource.Name}-roles-{targetResource.Name}", - infra => AddRoleAssignmentsInfrastructure(infra, executionContext, targetResource, roles, appIdentityResource)) + infra => AddRoleAssignmentsInfrastructure(infra, targetResource, roles, appIdentityResource)) { - ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions, + ProvisioningBuildOptions = options.Value.ProvisioningBuildOptions, }; // existing resource role assignments need to be scoped to the resource's resource group @@ -316,9 +311,8 @@ private static List CreateRoleAssignmentsResources( return roleAssignmentResources; } - private static void AddRoleAssignmentsInfrastructure( + private void AddRoleAssignmentsInfrastructure( AzureResourceInfrastructure infra, - DistributedApplicationExecutionContext executionContext, AzureProvisioningResource azureResource, IEnumerable roles, AzureUserAssignedIdentityResource appIdentityResource) @@ -485,11 +479,11 @@ private static void AppendGlobalRoleAssignments(Dictionary> globalRoleAssignments, AzureProvisioningOptions provisioningOptions) + private void CreateGlobalRoleAssignments(DistributedApplicationModel appModel, Dictionary> globalRoleAssignments) { foreach (var (azureResource, roles) in globalRoleAssignments) { - var roleAssignmentResource = CreateGlobalRoleAssignmentsResource(provisioningOptions, azureResource, roles, executionContext); + var roleAssignmentResource = CreateGlobalRoleAssignmentsResource(azureResource, roles); appModel.Resources.Add(roleAssignmentResource); azureResource.Annotations.Add(new RoleAssignmentResourceAnnotation(roleAssignmentResource)); @@ -498,17 +492,15 @@ private void CreateGlobalRoleAssignments(DistributedApplicationModel appModel, D } } - private static AzureProvisioningResource CreateGlobalRoleAssignmentsResource( - AzureProvisioningOptions provisioningOptions, + private AzureProvisioningResource CreateGlobalRoleAssignmentsResource( AzureProvisioningResource targetResource, - IEnumerable roles, - DistributedApplicationExecutionContext executionContext) + IEnumerable roles) { var roleAssignmentResource = new AzureProvisioningResource( $"{targetResource.Name}-roles", - infra => AddGlobalRoleAssignmentsInfrastructure(infra, executionContext, targetResource, roles)) + infra => AddGlobalRoleAssignmentsInfrastructure(infra, targetResource, roles)) { - ProvisioningBuildOptions = provisioningOptions.ProvisioningBuildOptions, + ProvisioningBuildOptions = options.Value.ProvisioningBuildOptions, }; // existing resource role assignments need to be scoped to the resource's resource group @@ -521,9 +513,8 @@ private static AzureProvisioningResource CreateGlobalRoleAssignmentsResource( return roleAssignmentResource; } - private static void AddGlobalRoleAssignmentsInfrastructure( + private void AddGlobalRoleAssignmentsInfrastructure( AzureResourceInfrastructure infra, - DistributedApplicationExecutionContext executionContext, AzureProvisioningResource azureResource, IEnumerable roles) { @@ -544,4 +535,10 @@ ProvisioningParameter CreatePrincipalParam(string name) azureResource.AddRoleAssignments(context); } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + eventing.Subscribe(OnBeforeStartAsync); + return Task.CompletedTask; + } } diff --git a/src/Aspire.Hosting.Azure/Provisioning/AzureProvisionerExtensions.cs b/src/Aspire.Hosting.Azure/Provisioning/AzureProvisionerExtensions.cs index e15bc5a04d0..ce8cc36a2b9 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/AzureProvisionerExtensions.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/AzureProvisionerExtensions.cs @@ -26,8 +26,8 @@ public static IDistributedApplicationBuilder AddAzureProvisioning(this IDistribu builder.AddAzureEnvironment(); #pragma warning restore ASPIREAZURE001 - builder.Services.TryAddLifecycleHook(); - builder.Services.TryAddLifecycleHook(); + builder.Services.TryAddEventingSubscriber(); + builder.Services.TryAddEventingSubscriber(); // Attempt to read azure configuration from configuration builder.Services.AddOptions() diff --git a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs index 8a13f998170..46fcfb01cfa 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs @@ -13,7 +13,6 @@ namespace Aspire.Hosting.Azure; // Provisions azure resources for development purposes internal sealed class AzureProvisioner( - DistributedApplicationExecutionContext executionContext, IConfiguration configuration, IServiceProvider serviceProvider, IBicepProvisioner bicepProvisioner, @@ -22,28 +21,22 @@ internal sealed class AzureProvisioner( IDistributedApplicationEventing eventing, IProvisioningContextProvider provisioningContextProvider, IUserSecretsManager userSecretsManager - ) : IDistributedApplicationLifecycleHook + ) : IDistributedApplicationEventingSubscriber { internal const string AspireResourceNameTag = "aspire-resource-name"; private ILookup? _parentChildLookup; - public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken = default) { - // AzureProvisioner only applies to RunMode - if (executionContext.IsPublishMode) - { - return; - } - - var azureResources = AzureResourcePreparer.GetAzureResourcesFromAppModel(appModel); + var azureResources = AzureResourcePreparer.GetAzureResourcesFromAppModel(@event.Model); if (azureResources.Count == 0) { return; } // Create a map of parents to their children used to propagate state changes later. - _parentChildLookup = appModel.Resources.OfType().ToLookup(r => r.Parent); + _parentChildLookup = @event.Model.Resources.OfType().ToLookup(r => r.Parent); // Sets the state of the resource and all of its children async Task UpdateStateAsync((IResource Resource, IAzureResource AzureResource) resource, Func stateFactory) @@ -288,4 +281,14 @@ async Task PublishConnectionStringAvailableEventRecursiveAsync(IResource targetR } } } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + if (executionContext.IsRunMode) + { + eventing.Subscribe(OnBeforeStartAsync); + } + + return Task.CompletedTask; + } } diff --git a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentExtensions.cs b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentExtensions.cs index 0506e4b851b..025f2d351fe 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeEnvironmentExtensions.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeEnvironmentExtensions.cs @@ -15,7 +15,7 @@ public static class DockerComposeEnvironmentExtensions { internal static IDistributedApplicationBuilder AddDockerComposeInfrastructureCore(this IDistributedApplicationBuilder builder) { - builder.Services.TryAddLifecycleHook(); + builder.Services.TryAddEventingSubscriber(); return builder; } diff --git a/src/Aspire.Hosting.Docker/DockerComposeInfrastructure.cs b/src/Aspire.Hosting.Docker/DockerComposeInfrastructure.cs index ae7fc761656..7cd5e1519c2 100644 --- a/src/Aspire.Hosting.Docker/DockerComposeInfrastructure.cs +++ b/src/Aspire.Hosting.Docker/DockerComposeInfrastructure.cs @@ -3,6 +3,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.Logging; @@ -10,13 +11,13 @@ namespace Aspire.Hosting.Docker; /// /// Represents the infrastructure for Docker Compose within the Aspire Hosting environment. -/// Implements the interface to provide lifecycle hooks for distributed applications. +/// Implements and subscribes to to configure Docker Compose resources before publish. /// internal sealed class DockerComposeInfrastructure( ILogger logger, - DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook + DistributedApplicationExecutionContext executionContext) : IDistributedApplicationEventingSubscriber { - public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken = default) { if (executionContext.IsRunMode) { @@ -24,11 +25,11 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell } // Find Docker Compose environment resources - var dockerComposeEnvironments = appModel.Resources.OfType().ToArray(); + var dockerComposeEnvironments = @event.Model.Resources.OfType().ToArray(); if (dockerComposeEnvironments.Length == 0) { - EnsureNoPublishAsDockerComposeServiceAnnotations(appModel); + EnsureNoPublishAsDockerComposeServiceAnnotations(@event.Model); return; } @@ -47,7 +48,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell }); } - foreach (var r in appModel.GetComputeResources()) + foreach (var r in @event.Model.GetComputeResources()) { // Configure OTLP for resources if dashboard is enabled (before creating the service resource) if (environment.DashboardEnabled && environment.Dashboard?.Resource.OtlpGrpcEndpoint is EndpointReference otlpGrpcEndpoint) @@ -93,4 +94,10 @@ private static void ConfigureOtlp(IResource resource, EndpointReference otlpEndp })); } } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + eventing.Subscribe(OnBeforeStartAsync); + return Task.CompletedTask; + } } diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentExtensions.cs b/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentExtensions.cs index c564353463f..8673dfa1fcb 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentExtensions.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesEnvironmentExtensions.cs @@ -15,7 +15,7 @@ public static class KubernetesEnvironmentExtensions { internal static IDistributedApplicationBuilder AddKubernetesInfrastructureCore(this IDistributedApplicationBuilder builder) { - builder.Services.TryAddLifecycleHook(); + builder.Services.TryAddEventingSubscriber(); return builder; } diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesInfrastructure.cs b/src/Aspire.Hosting.Kubernetes/KubernetesInfrastructure.cs index 065275d5bab..45a73815d9b 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesInfrastructure.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesInfrastructure.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Microsoft.Extensions.Logging; @@ -9,13 +10,13 @@ namespace Aspire.Hosting.Kubernetes; /// /// Represents the infrastructure for Kubernetes within the Aspire Hosting environment. -/// Implements the interface to provide lifecycle hooks for distributed applications. +/// Implements and subscribes to to configure Kubernetes resources before publish. /// internal sealed class KubernetesInfrastructure( ILogger logger, - DistributedApplicationExecutionContext executionContext) : IDistributedApplicationLifecycleHook + DistributedApplicationExecutionContext executionContext) : IDistributedApplicationEventingSubscriber { - public async Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + private async Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken = default) { if (executionContext.IsRunMode) { @@ -23,11 +24,11 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell } // Find Kubernetes environment resources - var kubernetesEnvironments = appModel.Resources.OfType().ToArray(); + var kubernetesEnvironments = @event.Model.Resources.OfType().ToArray(); if (kubernetesEnvironments.Length == 0) { - EnsureNoPublishAsKubernetesServiceAnnotations(appModel); + EnsureNoPublishAsKubernetesServiceAnnotations(@event.Model); return; } @@ -35,7 +36,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell { var environmentContext = new KubernetesEnvironmentContext(environment, logger); - foreach (var r in appModel.GetComputeResources()) + foreach (var r in @event.Model.GetComputeResources()) { // Create a Kubernetes compute resource for the resource var serviceResource = await environmentContext.CreateKubernetesResourceAsync(r, executionContext, cancellationToken).ConfigureAwait(false); @@ -61,4 +62,10 @@ private static void EnsureNoPublishAsKubernetesServiceAnnotations(DistributedApp } } } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + eventing.Subscribe(OnBeforeStartAsync); + return Task.CompletedTask; + } } diff --git a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs similarity index 97% rename from src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs rename to src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs index b500e431b8d..db935d2947f 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs @@ -26,7 +26,7 @@ namespace Aspire.Hosting.Dashboard; -internal sealed class DashboardLifecycleHook(IConfiguration configuration, +internal sealed class DashboardEventHandlers(IConfiguration configuration, IOptions dashboardOptions, ILogger distributedApplicationLogger, IDashboardEndpointProvider dashboardEndpointProvider, @@ -38,7 +38,7 @@ internal sealed class DashboardLifecycleHook(IConfiguration configuration, IHostApplicationLifetime hostApplicationLifetime, IDistributedApplicationEventing eventing, CodespacesUrlRewriter codespaceUrlRewriter - ) : IDistributedApplicationLifecycleHook, IAsyncDisposable + ) : IDistributedApplicationEventingSubscriber, IAsyncDisposable { // Internal for testing internal const string OtlpGrpcEndpointName = "otlp-grpc"; @@ -58,21 +58,21 @@ CodespacesUrlRewriter codespaceUrlRewriter private CancellationTokenSource? _dashboardLogsCts; private string? _customRuntimeConfigPath; - public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken) { Debug.Assert(executionContext.IsRunMode, "Dashboard resource should only be added in run mode"); - if (appModel.Resources.SingleOrDefault(r => StringComparers.ResourceName.Equals(r.Name, KnownResourceNames.AspireDashboard)) is { } dashboardResource) + if (@event.Model.Resources.SingleOrDefault(r => StringComparers.ResourceName.Equals(r.Name, KnownResourceNames.AspireDashboard)) is { } dashboardResource) { ConfigureAspireDashboardResource(dashboardResource); // Make the dashboard first in the list so it starts as fast as possible. - appModel.Resources.Remove(dashboardResource); - appModel.Resources.Insert(0, dashboardResource); + @event.Model.Resources.Remove(dashboardResource); + @event.Model.Resources.Insert(0, dashboardResource); } else { - AddDashboardResource(appModel); + AddDashboardResource(@event.Model); } // Stop watching logs from the dashboard when the app host is stopping. Part of the app host shutdown is tearing down the dashboard service. @@ -85,41 +85,6 @@ public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationT return Task.CompletedTask; } - public async ValueTask DisposeAsync() - { - // Stop listening to logs if the lifecycle hook is disposed without the app being shutdown. - _dashboardLogsCts?.Cancel(); - - if (_dashboardLogsTask is not null) - { - try - { - await _dashboardLogsTask.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Expected when the application is shutting down. - } - catch (Exception ex) - { - distributedApplicationLogger.LogError(ex, "Unexpected error while watching dashboard logs."); - } - } - - // Clean up the temporary runtime config file - if (_customRuntimeConfigPath is not null) - { - try - { - File.Delete(_customRuntimeConfigPath); - } - catch (Exception ex) - { - distributedApplicationLogger.LogWarning(ex, "Failed to delete temporary runtime config file: {Path}", _customRuntimeConfigPath); - } - } - } - private static (string NetCoreVersion, string AspNetCoreVersion) GetAppHostFrameworkVersions() { try @@ -246,7 +211,7 @@ private string CreateCustomRuntimeConfig(string dashboardPath) // In test environments or when the dashboard runtime config doesn't exist, // create a default configuration using the AppHost's framework versions var (appHostNetCoreVersion, appHostAspNetCoreVersion) = GetAppHostFrameworkVersions(); - + var defaultConfig = new { runtimeOptions = new @@ -259,10 +224,10 @@ private string CreateCustomRuntimeConfig(string dashboardPath) } } }; - + var customConfigPath = Path.ChangeExtension(Path.GetTempFileName(), ".json"); File.WriteAllText(customConfigPath, JsonSerializer.Serialize(defaultConfig, new JsonSerializerOptions { WriteIndented = true })); - + _customRuntimeConfigPath = customConfigPath; return customConfigPath; } @@ -335,7 +300,7 @@ private void AddDashboardResource(DistributedApplicationModel model) // Handle Windows (.exe), Unix (no extension), and direct DLL cases var directory = Path.GetDirectoryName(fullyQualifiedDashboardPath)!; var fileName = Path.GetFileName(fullyQualifiedDashboardPath); - + string baseName; if (fileName.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) { @@ -357,9 +322,9 @@ private void AddDashboardResource(DistributedApplicationModel model) // Unix executable (no extension) or other: use full filename as base baseName = fileName; } - + dashboardDll = Path.Combine(directory, $"{baseName}.dll"); - + if (!File.Exists(dashboardDll)) { distributedApplicationLogger.LogError("Dashboard DLL not found: {Path}", dashboardDll); @@ -794,6 +759,51 @@ private static void LogMessage(ILoggerFactory loggerFactory, ConcurrentDictionar (s, _) => (logMessage.Exception is { } e) ? s + Environment.NewLine + e : s); } } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext execContext, CancellationToken cancellationToken) + { + if (execContext.IsRunMode) + { + eventing.Subscribe(OnBeforeStartAsync); + } + + return Task.CompletedTask; + } + + public async ValueTask DisposeAsync() + { + // Stop listening to logs if the lifecycle hook is disposed without the app being shutdown. + _dashboardLogsCts?.Cancel(); + + if (_dashboardLogsTask is not null) + { + try + { + await _dashboardLogsTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Expected when the application is shutting down. + } + catch (Exception ex) + { + distributedApplicationLogger.LogError(ex, "Unexpected error while watching dashboard logs."); + } + } + + // Clean up the temporary runtime config file + if (_customRuntimeConfigPath is not null) + { + try + { + File.Delete(_customRuntimeConfigPath); + } + catch (Exception ex) + { + distributedApplicationLogger.LogWarning(ex, "Failed to delete temporary runtime config file: {Path}", _customRuntimeConfigPath); + } + } + } } internal sealed class DashboardLogMessage diff --git a/src/Aspire.Hosting/Devcontainers/DevcontainerPortForwardingLifecycleHook.cs b/src/Aspire.Hosting/Devcontainers/DevcontainerPortForwardingLifecycleHook.cs index 5e3405718c3..bd11c846af5 100644 --- a/src/Aspire.Hosting/Devcontainers/DevcontainerPortForwardingLifecycleHook.cs +++ b/src/Aspire.Hosting/Devcontainers/DevcontainerPortForwardingLifecycleHook.cs @@ -3,62 +3,67 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Devcontainers.Codespaces; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Aspire.Hosting.Devcontainers; -internal sealed class DevcontainerPortForwardingLifecycleHook : IDistributedApplicationLifecycleHook +internal sealed class DevcontainerPortForwardingLifecycleHook : IDistributedApplicationEventingSubscriber { - private readonly ILogger _hostingLogger; private readonly IOptions _codespacesOptions; private readonly IOptions _devcontainersOptions; private readonly IOptions _sshRemoteOptions; private readonly DevcontainerSettingsWriter _settingsWriter; - public DevcontainerPortForwardingLifecycleHook(ILoggerFactory loggerFactory, IOptions codespacesOptions, IOptions devcontainersOptions, IOptions sshRemoteOptions, DevcontainerSettingsWriter settingsWriter) + public DevcontainerPortForwardingLifecycleHook( + IOptions codespacesOptions, + IOptions devcontainersOptions, + IOptions sshRemoteOptions, + DevcontainerSettingsWriter settingsWriter) { - _hostingLogger = loggerFactory.CreateLogger("Aspire.Hosting"); _codespacesOptions = codespacesOptions; _devcontainersOptions = devcontainersOptions; _sshRemoteOptions = sshRemoteOptions; _settingsWriter = settingsWriter; } - public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public async Task OnResourceEndpointsAllocatedAsync(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken) { - if (!_devcontainersOptions.Value.IsDevcontainer && !_codespacesOptions.Value.IsCodespace && !_sshRemoteOptions.Value.IsSshRemote) + if (@event.Resource is not IResourceWithEndpoints resourceWithEndpoints) { - // We aren't a codespace, devcontainer, or SSH remote so there is nothing to do here. return; } - foreach (var resource in appModel.Resources) + foreach (var endpoint in resourceWithEndpoints.Annotations.OfType()) { - if (resource is not IResourceWithEndpoints resourceWithEndpoints) + if (_codespacesOptions.Value.IsCodespace && !(endpoint.UriScheme is "https" or "http")) { + // Codespaces only does port forwarding over HTTPS. If the protocol is not HTTP or HTTPS + // it cannot be forwarded because it can't intercept access to the endpoint without breaking + // the non-HTTP protocol to do GitHub auth. continue; } - foreach (var endpoint in resourceWithEndpoints.Annotations.OfType()) - { - if (_codespacesOptions.Value.IsCodespace && !(endpoint.UriScheme is "https" or "http")) - { - // Codespaces only does port forwarding over HTTPS. If the protocol is not HTTP or HTTPS - // it cannot be forwarded because it can't intercept access to the endpoint without breaking - // the non-HTTP protocol to do GitHub auth. - continue; - } - - _settingsWriter.AddPortForward( - endpoint.AllocatedEndpoint!.UriString, - endpoint.AllocatedEndpoint!.Port, - endpoint.UriScheme, - $"{resource.Name}-{endpoint.Name}"); - } + _settingsWriter.AddPortForward( + endpoint.AllocatedEndpoint!.UriString, + endpoint.AllocatedEndpoint!.Port, + endpoint.UriScheme, + $"{@event.Resource.Name}-{endpoint.Name}"); } await _settingsWriter.FlushAsync(cancellationToken).ConfigureAwait(false); } -} \ No newline at end of file + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext execContext, CancellationToken cancellationToken) + { + if (!_devcontainersOptions.Value.IsDevcontainer && !_codespacesOptions.Value.IsCodespace && !_sshRemoteOptions.Value.IsSshRemote) + { + // We aren't a codespace, devcontainer, or SSH remote so there is nothing to do here. + return Task.CompletedTask; + } + + eventing.Subscribe(OnResourceEndpointsAllocatedAsync); + return Task.CompletedTask; + } +} diff --git a/src/Aspire.Hosting/DistributedApplication.cs b/src/Aspire.Hosting/DistributedApplication.cs index 763e55f07f2..f814019a0ba 100644 --- a/src/Aspire.Hosting/DistributedApplication.cs +++ b/src/Aspire.Hosting/DistributedApplication.cs @@ -478,11 +478,20 @@ internal async Task ExecuteBeforeStartHooksAsync(CancellationToken cancellationT try { - var beforeStartEvent = new BeforeStartEvent(_host.Services, _host.Services.GetRequiredService()); + var eventSubscribers = _host.Services.GetServices(); var eventing = _host.Services.GetRequiredService(); + var execContext = _host.Services.GetRequiredService(); + foreach (var subscriber in eventSubscribers) + { + await subscriber.SubscribeAsync(eventing, execContext, cancellationToken).ConfigureAwait(false); + } + + var beforeStartEvent = new BeforeStartEvent(_host.Services, _host.Services.GetRequiredService()); await eventing.PublishAsync(beforeStartEvent, cancellationToken).ConfigureAwait(false); +#pragma warning disable CS0618 // Hooks are obsolete, but still need to be supported until fully removed. var lifecycleHooks = _host.Services.GetServices(); +#pragma warning restore CS0618 // Hooks are obsolete, but still need to be supported until fully removed. var appModel = _host.Services.GetRequiredService(); foreach (var lifecycleHook in lifecycleHooks) diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index be5812ac632..4fee048e4c3 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -345,7 +345,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddHostedService(sp => sp.GetRequiredService()); _innerBuilder.Services.AddSingleton(); - _innerBuilder.Services.AddLifecycleHook(); + _innerBuilder.Services.AddEventingSubscriber(); _innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureDefaultDashboardOptions>()); _innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ValidateDashboardOptions>()); } @@ -363,7 +363,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) _innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureDevcontainersOptions>()); _innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureSshRemoteOptions>()); _innerBuilder.Services.AddSingleton(); - _innerBuilder.Services.TryAddLifecycleHook(); + _innerBuilder.Services.TryAddEventingSubscriber(); } if (ExecutionContext.IsRunMode) diff --git a/src/Aspire.Hosting/Lifecycle/EventingSubscriberServiceCollectionExtensions.cs b/src/Aspire.Hosting/Lifecycle/EventingSubscriberServiceCollectionExtensions.cs new file mode 100644 index 00000000000..0f03cc18757 --- /dev/null +++ b/src/Aspire.Hosting/Lifecycle/EventingSubscriberServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Aspire.Hosting.Lifecycle; + +/// +/// Provides extension methods for adding event subscribers to the . +/// +public static class EventingSubscriberServiceCollectionExtensions +{ + /// + /// Adds a singleton event subscriber of type to the service collection. + /// + /// A service that implements + /// The to add the event subscriber to. + public static void AddEventingSubscriber(this IServiceCollection services) where T : class, IDistributedApplicationEventingSubscriber + { + services.AddSingleton(); + } + + /// + /// Attempts to add a singleton event subscriber of type to the service collection. + /// + /// A service that implements + /// The to add the event subscriber to. + public static void TryAddEventingSubscriber(this IServiceCollection services) where T : class, IDistributedApplicationEventingSubscriber + { + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + } +} diff --git a/src/Aspire.Hosting/Lifecycle/IDistributedApplicationEventingSubscriber.cs b/src/Aspire.Hosting/Lifecycle/IDistributedApplicationEventingSubscriber.cs new file mode 100644 index 00000000000..411f32d026a --- /dev/null +++ b/src/Aspire.Hosting/Lifecycle/IDistributedApplicationEventingSubscriber.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.Eventing; + +namespace Aspire.Hosting.Lifecycle; + +/// +/// Defines an interface for services that want to subscribe to events from IDistributedApplicationEventing. +/// This allows a service to subscribe to BeforeStartEvent before the application actually starts. +/// +public interface IDistributedApplicationEventingSubscriber +{ + /// + /// Callback during which the service should subscribe to global events from IDistributedApplicationEventing. + /// + /// The service to subscribe to events from. + /// The instance for the run. + /// Cancellation token from the service collection + /// A task indicating event registration is complete + Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken); +} diff --git a/src/Aspire.Hosting/Lifecycle/IDistributedApplicationLifecycleHook.cs b/src/Aspire.Hosting/Lifecycle/IDistributedApplicationLifecycleHook.cs index 3e0726a4911..3224ee1327d 100644 --- a/src/Aspire.Hosting/Lifecycle/IDistributedApplicationLifecycleHook.cs +++ b/src/Aspire.Hosting/Lifecycle/IDistributedApplicationLifecycleHook.cs @@ -8,6 +8,7 @@ namespace Aspire.Hosting.Lifecycle; /// /// Defines an interface for hooks that are executed during the lifecycle of a distributed application. /// +[Obsolete("Use IDistributedApplicationEventingSubscriber instead.")] public interface IDistributedApplicationLifecycleHook { /// diff --git a/src/Aspire.Hosting/Lifecycle/LifecycleHookServiceCollectionExtensions.cs b/src/Aspire.Hosting/Lifecycle/LifecycleHookServiceCollectionExtensions.cs index 0168460467b..9b1cab2e930 100644 --- a/src/Aspire.Hosting/Lifecycle/LifecycleHookServiceCollectionExtensions.cs +++ b/src/Aspire.Hosting/Lifecycle/LifecycleHookServiceCollectionExtensions.cs @@ -16,6 +16,7 @@ public static class LifecycleHookServiceCollectionExtensions /// /// The type of the distributed application lifecycle hook to add. /// The to add the distributed application lifecycle hook to. + [Obsolete("Use EventingSubscriberServiceCollectionExtensions.AddEventingSubscriber instead.")] public static void AddLifecycleHook(this IServiceCollection services) where T : class, IDistributedApplicationLifecycleHook { services.AddSingleton(); @@ -26,6 +27,7 @@ public static void AddLifecycleHook(this IServiceCollection services) where T /// /// The type of the distributed application lifecycle hook to add. /// The to add the distributed application lifecycle hook to. + [Obsolete("Use EventingSubscriberServiceCollectionExtensions.TryAddEventingSubscriber instead.")] public static void TryAddLifecycleHook(this IServiceCollection services) where T : class, IDistributedApplicationLifecycleHook { services.TryAddEnumerable(ServiceDescriptor.Singleton()); @@ -37,17 +39,19 @@ public static void TryAddLifecycleHook(this IServiceCollection services) wher /// The type of the distributed application lifecycle hook. /// The service collection to add the hook to. /// A factory function that creates the hook implementation. + [Obsolete("Use EventingSubscriberServiceCollectionExtensions.AddEventingSubscriber instead.")] public static void AddLifecycleHook(this IServiceCollection services, Func implementationFactory) where T : class, IDistributedApplicationLifecycleHook { services.AddSingleton(implementationFactory); } /// - /// Attempts to add a distributed application lifecycle hook to the service collection. + /// Attempts to add a distributed application lifecycle hook to the service collection. /// /// The type of the distributed application lifecycle hook. /// The service collection to add the hook to. /// A factory function that creates the hook implementation. + [Obsolete("Use EventingSubscriberServiceCollectionExtensions.TryAddEventingSubscriber instead.")] public static void TryAddLifecycleHook(this IServiceCollection services, Func implementationFactory) where T : class, IDistributedApplicationLifecycleHook { services.TryAddEnumerable(ServiceDescriptor.Singleton(implementationFactory)); diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 67aa9f3cb5f..be066652e54 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -19,7 +19,9 @@ internal sealed class ApplicationOrchestrator private readonly IDcpExecutor _dcpExecutor; private readonly DistributedApplicationModel _model; private readonly ILookup _parentChildLookup; +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be supported until fully removed. private readonly IDistributedApplicationLifecycleHook[] _lifecycleHooks; +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be supported until fully removed. private readonly ResourceNotificationService _notificationService; private readonly ResourceLoggerService _loggerService; private readonly IDistributedApplicationEventing _eventing; @@ -31,7 +33,9 @@ internal sealed class ApplicationOrchestrator public ApplicationOrchestrator(DistributedApplicationModel model, IDcpExecutor dcpExecutor, DcpExecutorEvents dcpExecutorEvents, +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be supported until fully removed. IEnumerable lifecycleHooks, +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be supported until fully removed. ResourceNotificationService notificationService, ResourceLoggerService loggerService, IDistributedApplicationEventing eventing, diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs index 903c5116557..6b950d89b77 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs @@ -78,8 +78,8 @@ public void AzureExtensionsAutomaticallyAddAzureProvisioning(Func(); - Assert.Single(hooks.OfType()); + var eventingServices = app.Services.GetServices(); + Assert.Single(eventingServices.OfType()); } [Theory] diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsTests.cs index 72d579d913c..efec69b3a70 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureFunctionsTests.cs @@ -311,10 +311,10 @@ public async Task AddAzureFunctionsProject_WorksWithAddAzureContainerAppsInfrast var app = builder.Build(); - var model = app.Services.GetRequiredService(); - await ExecuteBeforeStartHooksAsync(app, default); + var model = app.Services.GetRequiredService(); + var (_, bicep) = await GetManifestWithBicep(funcApp.Resource.GetDeploymentTargetAnnotation()!.DeploymentTarget); var projRolesStorage = Assert.Single(model.Resources.OfType(), r => r.Name == "funcapp-roles-funcstorage634f8"); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureManifestUtils.cs b/tests/Aspire.Hosting.Azure.Tests/AzureManifestUtils.cs index 25fd4e0fc12..37c90da537b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureManifestUtils.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureManifestUtils.cs @@ -5,6 +5,7 @@ using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; +using Aspire.Hosting.Tests.Utils; using Microsoft.Extensions.Options; namespace Aspire.Hosting.Utils; @@ -23,7 +24,7 @@ public sealed class AzureManifestUtils { var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish); var azurePreparer = new AzureResourcePreparer(Options.Create(new AzureProvisioningOptions()), executionContext); - await azurePreparer.BeforeStartAsync(appModel, cancellationToken: default); + await azurePreparer.OnBeforeStartAsync(new BeforeStartEvent(new TestServiceProvider(), appModel), cancellationToken: default); } string manifestDir = Directory.CreateTempSubdirectory(resource.Name).FullName; diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs index 47f75ad3c1c..75c9ce09ddf 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs @@ -43,7 +43,7 @@ public async Task WatchDashboardLogs_WrittenToHostLoggerFactory(DateTime? timest var hook = CreateHook(resourceLoggerService, resourceNotificationService, configuration, loggerFactory: factory); var model = new DistributedApplicationModel(new ResourceCollection()); - await hook.BeforeStartAsync(model, CancellationToken.None).DefaultTimeout(); + await hook.OnBeforeStartAsync(new BeforeStartEvent(new TestServiceProvider(), model), CancellationToken.None).DefaultTimeout(); await resourceNotificationService.PublishUpdateAsync(model.Resources.Single(), s => s).DefaultTimeout(); @@ -86,7 +86,7 @@ public async Task BeforeStartAsync_ExcludeLifecycleCommands_CommandsNotAddedToDa var model = new DistributedApplicationModel(new ResourceCollection()); // Act - await hook.BeforeStartAsync(model, CancellationToken.None).DefaultTimeout(); + await hook.OnBeforeStartAsync(new BeforeStartEvent(new TestServiceProvider(), model), CancellationToken.None).DefaultTimeout(); var dashboardResource = model.Resources.Single(r => string.Equals(r.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)); dashboardResource.AddLifeCycleCommands(); @@ -134,7 +134,7 @@ public async Task BeforeStartAsync_DashboardContainsDebugSessionInfo(string? deb var model = new DistributedApplicationModel(new ResourceCollection()); // Act - await hook.BeforeStartAsync(model, CancellationToken.None).DefaultTimeout(); + await hook.OnBeforeStartAsync(new BeforeStartEvent(new TestServiceProvider(), model), CancellationToken.None).DefaultTimeout(); var dashboardResource = model.Resources.Single(r => string.Equals(r.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)); var context = new DistributedApplicationExecutionContext(new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run) { ServiceProvider = TestServiceProvider.Instance }); var dashboardEnvironmentVariables = new ConcurrentDictionary(); @@ -228,7 +228,7 @@ public async Task AddDashboardResource_CreatesExecutableResourceWithCustomRuntim var model = new DistributedApplicationModel(new ResourceCollection()); // Act - await hook.BeforeStartAsync(model, CancellationToken.None); + await hook.OnBeforeStartAsync(new BeforeStartEvent(new TestServiceProvider(), model), CancellationToken.None); // Assert var dashboardResource = Assert.Single(model.Resources); @@ -316,7 +316,7 @@ public async Task AddDashboardResource_WithExecutablePath_CreatesCorrectArgument var model = new DistributedApplicationModel(new ResourceCollection()); // Act - await hook.BeforeStartAsync(model, CancellationToken.None); + await hook.OnBeforeStartAsync(new BeforeStartEvent(new TestServiceProvider(), model), CancellationToken.None); // Assert var dashboardResource = Assert.Single(model.Resources); @@ -388,7 +388,7 @@ public async Task AddDashboardResource_WithUnixExecutablePath_CreatesCorrectArgu var model = new DistributedApplicationModel(new ResourceCollection()); // Act - await hook.BeforeStartAsync(model, CancellationToken.None); + await hook.OnBeforeStartAsync(new BeforeStartEvent(new TestServiceProvider(), model), CancellationToken.None); // Assert var dashboardResource = Assert.Single(model.Resources); @@ -458,7 +458,7 @@ public async Task AddDashboardResource_WithDirectDllPath_CreatesCorrectArguments var model = new DistributedApplicationModel(new ResourceCollection()); // Act - await hook.BeforeStartAsync(model, CancellationToken.None); + await hook.OnBeforeStartAsync(new BeforeStartEvent(new TestServiceProvider(), model), CancellationToken.None); // Assert var dashboardResource = Assert.Single(model.Resources); @@ -485,7 +485,7 @@ public async Task AddDashboardResource_WithDirectDllPath_CreatesCorrectArguments } } - private static DashboardLifecycleHook CreateHook( + private static DashboardEventHandlers CreateHook( ResourceLoggerService resourceLoggerService, ResourceNotificationService resourceNotificationService, IConfiguration configuration, @@ -498,7 +498,7 @@ private static DashboardLifecycleHook CreateHook( dashboardOptions ??= Options.Create(new DashboardOptions { DashboardPath = "test.dll" }); var rewriter = new CodespacesUrlRewriter(codespacesOptions); - return new DashboardLifecycleHook( + return new DashboardEventHandlers( configuration, dashboardOptions, NullLogger.Instance, diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs index 972015dfdf4..69ab62fa815 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs @@ -448,7 +448,7 @@ public void ContainerIsValidWithDashboardIsDisabled() [InlineData(LogLevel.Information)] [InlineData(LogLevel.Debug)] [InlineData(LogLevel.Trace)] - public async Task DashboardLifecycleHookWatchesLogs(LogLevel logLevel) + public async Task DashboardLifecycleEventsWatchesLogs(LogLevel logLevel) { using var builder = TestDistributedApplicationBuilder.Create( options => options.DisableDashboard = false, @@ -555,11 +555,11 @@ static void SetDashboardAllocatedEndpoints(IResource dashboard, int otlpGrpcPort { foreach (var endpoint in dashboard.Annotations.OfType()) { - if (endpoint.Name == DashboardLifecycleHook.OtlpGrpcEndpointName) + if (endpoint.Name == DashboardEventHandlers.OtlpGrpcEndpointName) { endpoint.AllocatedEndpoint = new(endpoint, "localhost", otlpGrpcPort, targetPortExpression: otlpGrpcPort.ToString()); } - else if (endpoint.Name == DashboardLifecycleHook.OtlpHttpEndpointName) + else if (endpoint.Name == DashboardEventHandlers.OtlpHttpEndpointName) { endpoint.AllocatedEndpoint = new(endpoint, "localhost", otlpHttpPort, targetPortExpression: otlpHttpPort.ToString()); } diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs index a66056e3d59..7537cf06fd4 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs @@ -41,10 +41,10 @@ public void BuilderAddsDefaultServices() var appModel = app.Services.GetRequiredService(); Assert.Empty(appModel.Resources); - var lifecycles = app.Services.GetServices(); + var eventingSubscribers = app.Services.GetServices(); Assert.Collection( - lifecycles, - s => Assert.IsType(s), + eventingSubscribers, + s => Assert.IsType(s), s => Assert.IsType(s) ); diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index a9523717a4e..b3359f70af7 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -47,6 +47,7 @@ public async Task RegisteredLifecycleHookIsExecutedWhenRunAsynchronously() var exceptionMessage = "Exception from lifecycle hook to prove it ran!"; using var testProgram = CreateTestProgram("lifecycle-hook-executed-async"); +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. testProgram.AppBuilder.Services.AddLifecycleHook((sp) => { return new CallbackLifecycleHook((appModel, cancellationToken) => @@ -56,6 +57,7 @@ public async Task RegisteredLifecycleHookIsExecutedWhenRunAsynchronously() throw new DistributedApplicationException(exceptionMessage); }); }); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. var ex = await Assert.ThrowsAsync(async () => { @@ -77,6 +79,7 @@ public async Task MultipleRegisteredLifecycleHooksAreExecuted() using var testProgram = CreateTestProgram("multiple-lifecycle-hooks"); // Lifecycle hook 1 +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. testProgram.AppBuilder.Services.AddLifecycleHook((sp) => { return new CallbackLifecycleHook((app, cancellationToken) => @@ -85,8 +88,10 @@ public async Task MultipleRegisteredLifecycleHooksAreExecuted() return Task.CompletedTask; }); }); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. // Lifecycle hook 2 +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. testProgram.AppBuilder.Services.AddLifecycleHook((sp) => { return new CallbackLifecycleHook((app, cancellationToken) => @@ -97,6 +102,7 @@ public async Task MultipleRegisteredLifecycleHooksAreExecuted() throw new DistributedApplicationException(exceptionMessage); }); }); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. var ex = await Assert.ThrowsAsync(async () => { @@ -390,6 +396,7 @@ public void RegisteredLifecycleHookIsExecutedWhenRunSynchronously() var exceptionMessage = "Exception from lifecycle hook to prove it ran!"; using var testProgram = CreateTestProgram("lifecycle-hook-executed-sync"); +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. testProgram.AppBuilder.Services.AddLifecycleHook((sp) => { return new CallbackLifecycleHook((appModel, cancellationToken) => @@ -399,6 +406,7 @@ public void RegisteredLifecycleHookIsExecutedWhenRunSynchronously() throw new DistributedApplicationException(exceptionMessage); }); }); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. var ex = Assert.Throws(testProgram.Run); Assert.IsType(ex.InnerExceptions.First()); @@ -411,12 +419,18 @@ public void TryAddWillNotAddTheSameLifecycleHook() using var testProgram = CreateTestProgram("lifecycle-hook-duplicates"); var callback1 = (IServiceProvider sp) => new DummyLifecycleHook(); +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. testProgram.AppBuilder.Services.TryAddLifecycleHook(callback1); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. var callback2 = (IServiceProvider sp) => new DummyLifecycleHook(); +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. testProgram.AppBuilder.Services.TryAddLifecycleHook(callback2); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. var lifecycleHookDescriptors = testProgram.AppBuilder.Services.Where(sd => sd.ServiceType == typeof(IDistributedApplicationLifecycleHook)); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. Assert.Single(lifecycleHookDescriptors, sd => sd.ImplementationFactory == callback1); Assert.DoesNotContain(lifecycleHookDescriptors, sd => sd.ImplementationFactory == callback2); @@ -427,7 +441,9 @@ public async Task AllocatedPortsAssignedAfterHookRuns() { using var testProgram = CreateTestProgram("ports-assigned-after-hook-runs"); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. testProgram.AppBuilder.Services.AddLifecycleHook(sp => new CheckAllocatedEndpointsLifecycleHook(tcs)); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. await using var app = testProgram.Build(); @@ -444,7 +460,9 @@ public async Task AllocatedPortsAssignedAfterHookRuns() } } +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. private sealed class CheckAllocatedEndpointsLifecycleHook(TaskCompletionSource tcs) : IDistributedApplicationLifecycleHook +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. { public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) { @@ -1320,12 +1338,16 @@ public async Task AfterResourcesCreatedLifecycleHookWorks() using var builder = TestDistributedApplicationBuilder.Create(); builder.AddRedis($"{testName}-redis"); +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. builder.Services.TryAddLifecycleHook(); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. using var app = builder.Build(); var s = app.Services.GetRequiredService(); +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. var lifecycles = app.Services.GetServices(); +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. var kubernetesLifecycle = (KubernetesTestLifecycleHook)lifecycles.Where(l => l.GetType() == typeof(KubernetesTestLifecycleHook)).First(); kubernetesLifecycle.KubernetesService = s; @@ -1349,7 +1371,9 @@ private void SetupXUnitLogging(IServiceCollection services) }); } +#pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. private sealed class KubernetesTestLifecycleHook : IDistributedApplicationLifecycleHook +#pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. { private readonly TaskCompletionSource _tcs = new(); diff --git a/tests/Aspire.Hosting.Tests/Helpers/CallbackLifecycleHook.cs b/tests/Aspire.Hosting.Tests/Helpers/CallbackLifecycleHook.cs index fd609b664eb..03b6b9afd91 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/CallbackLifecycleHook.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/CallbackLifecycleHook.cs @@ -5,7 +5,9 @@ namespace Aspire.Hosting.Tests.Helpers; +#pragma warning disable CS0618 // Type or member is obsolete internal sealed class CallbackLifecycleHook : IDistributedApplicationLifecycleHook +#pragma warning restore CS0618 // Type or member is obsolete { private readonly Func _beforeStartCallback; diff --git a/tests/Aspire.Hosting.Tests/Helpers/DummyLifecycleHook.cs b/tests/Aspire.Hosting.Tests/Helpers/DummyLifecycleHook.cs index 1d73d4b3e5d..b5c44149732 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/DummyLifecycleHook.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/DummyLifecycleHook.cs @@ -5,6 +5,8 @@ namespace Aspire.Hosting.Tests.Helpers; +#pragma warning disable CS0618 // Type or member is obsolete internal sealed class DummyLifecycleHook : IDistributedApplicationLifecycleHook +#pragma warning restore CS0618 // Type or member is obsolete { } diff --git a/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationTestFactory.cs b/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationTestFactory.cs index 5ee1528467c..430e58bf765 100644 --- a/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationTestFactory.cs +++ b/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationTestFactory.cs @@ -2,8 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Lifecycle; +using Aspire.Hosting.Eventing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using SamplesIntegrationTests.Infrastructure; using Xunit; @@ -22,7 +23,7 @@ public static async Task CreateAsync(Type // Custom hook needed because we want to only override the registry when // the original is from `docker.io`, but the options.ContainerRegistryOverride will // always override. - builder.Services.AddLifecycleHook(); + builder.Services.AddHostedService(); builder.WithRandomParameterValues(); builder.WithRandomVolumeNames(); @@ -47,12 +48,12 @@ public static async Task CreateAsync(Type return builder; } - internal sealed class ContainerRegistryHook : IDistributedApplicationLifecycleHook + internal sealed class ContainerRegistryHook(DistributedApplicationEventing eventing) : IHostedService { public const string AspireTestContainerRegistry = "netaspireci.azurecr.io"; - public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken = default) + public Task OnBeforeStartAsync(BeforeStartEvent @event, CancellationToken cancellationToken = default) { - var resourcesWithContainerImages = appModel.Resources + var resourcesWithContainerImages = @event.Model.Resources .SelectMany(r => r.Annotations.OfType() .Select(cia => new { Resource = r, Annotation = cia })); @@ -67,6 +68,17 @@ public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationT return Task.CompletedTask; } + + public Task StartAsync(CancellationToken cancellationToken) + { + eventing.Subscribe(OnBeforeStartAsync); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } } } diff --git a/tests/Shared/TemplatesTesting/AspireProject.cs b/tests/Shared/TemplatesTesting/AspireProject.cs index 7e1e471a822..d62c0232930 100644 --- a/tests/Shared/TemplatesTesting/AspireProject.cs +++ b/tests/Shared/TemplatesTesting/AspireProject.cs @@ -270,7 +270,7 @@ public async Task StartAppHostAsync(string[]? extraArgs = default, Action(); + builder.Services.TryAddEventingSubscriber(); var app = builder.Build(); diff --git a/tests/Shared/TemplatesTesting/data/EndPointWriterHook_cs b/tests/Shared/TemplatesTesting/data/EndPointWriterHook_cs index 433429eaee3..560f88e012b 100644 --- a/tests/Shared/TemplatesTesting/data/EndPointWriterHook_cs +++ b/tests/Shared/TemplatesTesting/data/EndPointWriterHook_cs @@ -1,3 +1,4 @@ +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using System.Text.Json; using System.Text.Json.Nodes; @@ -6,12 +7,12 @@ using System.Text.Json.Nodes; /// Writes the allocated endpoints to the console in JSON format. /// This allows for easier consumption by the external test process. /// -public sealed class EndPointWriterHook : IDistributedApplicationLifecycleHook +public sealed class EndPointWriterHook : IDistributedApplicationEventingSubscriber { - public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public async Task OnAfterResourcesCreated(AfterResourcesCreatedEvent @event, CancellationToken cancellationToken) { var root = new JsonObject(); - foreach (var project in appModel.Resources.OfType()) + foreach (var project in @event.Model.Resources.OfType()) { var projectJson = new JsonObject(); root[project.Name] = projectJson; @@ -39,4 +40,11 @@ public sealed class EndPointWriterHook : IDistributedApplicationLifecycleHook // write the whole json in a single line so it's easier to parse by the external process await Console.Out.WriteLineAsync("$ENDPOINTS: " + JsonSerializer.Serialize(root, JsonSerializerOptions.Default)); } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + // We can assume endpoints are allocated before project resources are created + eventing.Subscribe(OnAfterResourcesCreated); + return Task.CompletedTask; + } } diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index f3b5fafeb3f..6c679e9cc1a 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; +using Aspire.Hosting.Eventing; using Aspire.Hosting.Lifecycle; using Aspire.TestProject; using Microsoft.Extensions.DependencyInjection; @@ -88,7 +89,7 @@ private TestProgram( } } - AppBuilder.Services.AddLifecycleHook(); + AppBuilder.Services.TryAddEventingSubscriber(); AppBuilder.Services.AddHttpClient(); } @@ -153,12 +154,12 @@ public void Run() /// Writes the allocated endpoints to the console in JSON format. /// This allows for easier consumption by the external test process. /// - private sealed class EndPointWriterHook : IDistributedApplicationLifecycleHook + private sealed class EndPointWriterHook : IDistributedApplicationEventingSubscriber { - public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public async Task OnAfterResourcesCreated(AfterResourcesCreatedEvent @event, CancellationToken cancellationToken) { var root = new JsonObject(); - foreach (var project in appModel.Resources.OfType()) + foreach (var project in @event.Model.Resources.OfType()) { var projectJson = new JsonObject(); root[project.Name] = projectJson; @@ -186,6 +187,13 @@ public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appMo // write the whole json in a single line so it's easier to parse by the external process await Console.Out.WriteLineAsync("$ENDPOINTS: " + JsonSerializer.Serialize(root, JsonSerializerOptions.Default)); } + + public Task SubscribeAsync(IDistributedApplicationEventing eventing, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken) + { + // We can assume endpoints are allocated before project resources are created + eventing.Subscribe(OnAfterResourcesCreated); + return Task.CompletedTask; + } } }