From 430d71e7cfa165febfbfc1a727db38b72ebfad3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:42:23 +0000 Subject: [PATCH 1/9] Initial plan From 023db73a8f0921c0445b74c6476cf0b3c16069b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:52:41 +0000 Subject: [PATCH 2/9] Set default OutputPath and add IntermediateOutputPath at PipelineContext level Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Cli/Commands/PublishCommand.cs | 9 ++++---- .../AzureEnvironmentResource.cs | 2 +- .../DockerComposePublishingContext.cs | 2 +- .../KubernetesPublishingContext.cs | 2 +- .../Pipelines/PipelineContext.cs | 23 ++++++++++++++++++- .../Pipelines/PipelineStepContext.cs | 7 +++++- src/Shared/PublishingContextUtils.cs | 4 ++-- 7 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/Aspire.Cli/Commands/PublishCommand.cs b/src/Aspire.Cli/Commands/PublishCommand.cs index 86368aa3354..f67bad3cb39 100644 --- a/src/Aspire.Cli/Commands/PublishCommand.cs +++ b/src/Aspire.Cli/Commands/PublishCommand.cs @@ -49,11 +49,10 @@ protected override string[] GetRunArguments(string? fullyQualifiedOutputPath, st { var baseArgs = new List { "--operation", "publish", "--step", "publish" }; - var targetPath = fullyQualifiedOutputPath is not null - ? fullyQualifiedOutputPath - : Path.Combine(Environment.CurrentDirectory, "aspire-output"); - - baseArgs.AddRange(["--output-path", targetPath]); + if (fullyQualifiedOutputPath is not null) + { + baseArgs.AddRange(["--output-path", fullyQualifiedOutputPath]); + } // Add --log-level and --envionment flags if specified var logLevel = parseResult.GetValue(_logLevelOption); diff --git a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs index ed79f706fe4..6af3c37b5aa 100644 --- a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs @@ -122,7 +122,7 @@ private Task PublishAsync(PipelineStepContext context) { var azureProvisioningOptions = context.Services.GetRequiredService>(); var publishingContext = new AzurePublishingContext( - context.OutputPath ?? throw new InvalidOperationException("OutputPath is required for Azure publishing."), + context.OutputPath, azureProvisioningOptions.Value, context.Services, context.Logger, diff --git a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs index 0a3f629f27b..58701582ee0 100644 --- a/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs +++ b/src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs @@ -37,7 +37,7 @@ internal sealed class DockerComposePublishingContext( UnixFileMode.OtherRead | UnixFileMode.OtherWrite; public readonly IResourceContainerImageBuilder ImageBuilder = imageBuilder; - public readonly string OutputPath = outputPath ?? throw new InvalidOperationException("OutputPath is required for Docker Compose publishing."); + public readonly string OutputPath = outputPath; internal async Task WriteModelAsync(DistributedApplicationModel model, DockerComposeEnvironmentResource environment) { diff --git a/src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs b/src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs index a3811463d86..7db532a90e5 100644 --- a/src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs +++ b/src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs @@ -20,7 +20,7 @@ internal sealed class KubernetesPublishingContext( ILogger logger, CancellationToken cancellationToken = default) { - public readonly string OutputPath = outputPath ?? throw new InvalidOperationException("OutputPath is required for Kubernetes publishing."); + public readonly string OutputPath = outputPath; private readonly Dictionary> _helmValues = new() { diff --git a/src/Aspire.Hosting/Pipelines/PipelineContext.cs b/src/Aspire.Hosting/Pipelines/PipelineContext.cs index 54c5455cb83..013997cdd9e 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineContext.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineContext.cs @@ -3,6 +3,8 @@ using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Aspire.Hosting.Pipelines; @@ -53,5 +55,24 @@ public sealed class PipelineContext( /// /// Gets the output path for deployment artifacts. /// - public string? OutputPath { get; } = outputPath; + public string OutputPath { get; } = outputPath ?? Path.Combine(Environment.CurrentDirectory, "aspire-output"); + + /// + /// Gets the intermediate output path for temporary build artifacts. + /// + public string IntermediateOutputPath { get; } = GetIntermediateOutputPath(serviceProvider); + + private static string GetIntermediateOutputPath(IServiceProvider serviceProvider) + { + var configuration = serviceProvider.GetRequiredService(); + var appHostSha = configuration["AppHost:PathSha256"]; + + if (!string.IsNullOrEmpty(appHostSha)) + { + return Directory.CreateTempSubdirectory($"aspire-{appHostSha}").FullName; + } + + // Fallback if AppHost:PathSha256 is not available + return Directory.CreateTempSubdirectory("aspire").FullName; + } } diff --git a/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs b/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs index 9eab4e0af15..07d8b845a66 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs @@ -58,5 +58,10 @@ public sealed class PipelineStepContext /// /// Gets the output path for deployment artifacts. /// - public string? OutputPath => PipelineContext.OutputPath; + public string OutputPath => PipelineContext.OutputPath; + + /// + /// Gets the intermediate output path for temporary build artifacts. + /// + public string IntermediateOutputPath => PipelineContext.IntermediateOutputPath; } \ No newline at end of file diff --git a/src/Shared/PublishingContextUtils.cs b/src/Shared/PublishingContextUtils.cs index d358a0025bb..a7fad60fc49 100644 --- a/src/Shared/PublishingContextUtils.cs +++ b/src/Shared/PublishingContextUtils.cs @@ -15,10 +15,10 @@ public static string GetEnvironmentOutputPath(PipelineStepContext context, IComp if (context.Model.Resources.OfType().Count() > 1) { // If there are multiple compute environments, append the environment name to the output path - return Path.Combine(context.OutputPath!, environment.Name); + return Path.Combine(context.OutputPath, environment.Name); } // If there is only one compute environment, use the root output path - return context.OutputPath!; + return context.OutputPath; } } From 05e6173914f265159616fd4c19500b7bbcdc6cf3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:17:08 +0000 Subject: [PATCH 3/9] Refactor output path handling to use IPipelineOutputService - Create IPipelineOutputService interface with GetOutputDirectory() and GetTempDirectory() methods - Implement PipelineOutputService with better naming than IntermediateOutputPath - Expose service via PipelineContext.OutputService and PipelineStepContext.OutputService - Keep backward compatible OutputPath property for existing consumers - Remove direct IntermediateOutputPath property per feedback Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../Pipelines/IPipelineOutputService.cs | 25 ++++++++++ .../Pipelines/PipelineContext.cs | 22 ++------- .../Pipelines/PipelineOutputService.cs | 48 +++++++++++++++++++ .../Pipelines/PipelineStepContext.cs | 8 ++-- 4 files changed, 81 insertions(+), 22 deletions(-) create mode 100644 src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs create mode 100644 src/Aspire.Hosting/Pipelines/PipelineOutputService.cs diff --git a/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs b/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs new file mode 100644 index 00000000000..ff5d9347da5 --- /dev/null +++ b/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +namespace Aspire.Hosting.Pipelines; + +/// +/// Service for managing pipeline output directories. +/// +[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +public interface IPipelineOutputService +{ + /// + /// Gets the output directory for deployment artifacts. + /// + /// The path to the output directory for deployment artifacts. + string GetOutputDirectory(); + + /// + /// Gets a temporary directory for build artifacts. + /// + /// The path to a temporary directory for build artifacts. + string GetTempDirectory(); +} diff --git a/src/Aspire.Hosting/Pipelines/PipelineContext.cs b/src/Aspire.Hosting/Pipelines/PipelineContext.cs index 013997cdd9e..bd772fcecc3 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineContext.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineContext.cs @@ -53,26 +53,12 @@ public sealed class PipelineContext( public CancellationToken CancellationToken { get; set; } = cancellationToken; /// - /// Gets the output path for deployment artifacts. + /// Gets the service for managing pipeline output directories. /// - public string OutputPath { get; } = outputPath ?? Path.Combine(Environment.CurrentDirectory, "aspire-output"); + public IPipelineOutputService OutputService { get; } = new PipelineOutputService(outputPath, serviceProvider.GetRequiredService()); /// - /// Gets the intermediate output path for temporary build artifacts. + /// Gets the output path for deployment artifacts. /// - public string IntermediateOutputPath { get; } = GetIntermediateOutputPath(serviceProvider); - - private static string GetIntermediateOutputPath(IServiceProvider serviceProvider) - { - var configuration = serviceProvider.GetRequiredService(); - var appHostSha = configuration["AppHost:PathSha256"]; - - if (!string.IsNullOrEmpty(appHostSha)) - { - return Directory.CreateTempSubdirectory($"aspire-{appHostSha}").FullName; - } - - // Fallback if AppHost:PathSha256 is not available - return Directory.CreateTempSubdirectory("aspire").FullName; - } + public string OutputPath => OutputService.GetOutputDirectory(); } diff --git a/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs b/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs new file mode 100644 index 00000000000..b95937d85b8 --- /dev/null +++ b/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; + +namespace Aspire.Hosting.Pipelines; + +/// +/// Default implementation of . +/// +[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +internal sealed class PipelineOutputService : IPipelineOutputService +{ + private readonly string? _outputPath; + private readonly Lazy _tempDirectory; + + public PipelineOutputService(string? outputPath, IConfiguration configuration) + { + _outputPath = outputPath; + _tempDirectory = new Lazy(() => CreateTempDirectory(configuration)); + } + + /// + public string GetOutputDirectory() + { + return _outputPath ?? Path.Combine(Environment.CurrentDirectory, "aspire-output"); + } + + /// + public string GetTempDirectory() + { + return _tempDirectory.Value; + } + + private static string CreateTempDirectory(IConfiguration configuration) + { + var appHostSha = configuration["AppHost:PathSha256"]; + + if (!string.IsNullOrEmpty(appHostSha)) + { + return Directory.CreateTempSubdirectory($"aspire-{appHostSha}").FullName; + } + + // Fallback if AppHost:PathSha256 is not available + return Directory.CreateTempSubdirectory("aspire").FullName; + } +} diff --git a/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs b/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs index 07d8b845a66..5109cfb4e6f 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs @@ -56,12 +56,12 @@ public sealed class PipelineStepContext public CancellationToken CancellationToken => PipelineContext.CancellationToken; /// - /// Gets the output path for deployment artifacts. + /// Gets the service for managing pipeline output directories. /// - public string OutputPath => PipelineContext.OutputPath; + public IPipelineOutputService OutputService => PipelineContext.OutputService; /// - /// Gets the intermediate output path for temporary build artifacts. + /// Gets the output path for deployment artifacts. /// - public string IntermediateOutputPath => PipelineContext.IntermediateOutputPath; + public string OutputPath => PipelineContext.OutputPath; } \ No newline at end of file From 841f796ae3d611c13ab694f129fc7d306d7d5fe3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:38:04 +0000 Subject: [PATCH 4/9] Add resource-specific directory support to IPipelineOutputService - Add GetOutputDirectory(IResource) overload for resource-specific output directories - Add GetTempDirectory(IResource) overload for resource-specific temp directories - Implementation creates subdirectories using resource.Name under base directories - Enables per-resource isolation of output and temp artifacts Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../Pipelines/IPipelineOutputService.cs | 15 +++++++++++++++ .../Pipelines/PipelineOutputService.cs | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs b/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs index ff5d9347da5..c2f0a0e0986 100644 --- a/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs +++ b/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.Pipelines; @@ -17,9 +18,23 @@ public interface IPipelineOutputService /// The path to the output directory for deployment artifacts. string GetOutputDirectory(); + /// + /// Gets the output directory for a specific resource's deployment artifacts. + /// + /// The resource to get the output directory for. + /// The path to the output directory for the resource's deployment artifacts. + string GetOutputDirectory(IResource resource); + /// /// Gets a temporary directory for build artifacts. /// /// The path to a temporary directory for build artifacts. string GetTempDirectory(); + + /// + /// Gets a temporary directory for a specific resource's build artifacts. + /// + /// The resource to get the temporary directory for. + /// The path to a temporary directory for the resource's build artifacts. + string GetTempDirectory(IResource resource); } diff --git a/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs b/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs index b95937d85b8..8ad71fd1aaf 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.Configuration; namespace Aspire.Hosting.Pipelines; @@ -27,12 +28,30 @@ public string GetOutputDirectory() return _outputPath ?? Path.Combine(Environment.CurrentDirectory, "aspire-output"); } + /// + public string GetOutputDirectory(IResource resource) + { + ArgumentNullException.ThrowIfNull(resource); + + var baseOutputDir = GetOutputDirectory(); + return Path.Combine(baseOutputDir, resource.Name); + } + /// public string GetTempDirectory() { return _tempDirectory.Value; } + /// + public string GetTempDirectory(IResource resource) + { + ArgumentNullException.ThrowIfNull(resource); + + var baseTempDir = GetTempDirectory(); + return Path.Combine(baseTempDir, resource.Name); + } + private static string CreateTempDirectory(IConfiguration configuration) { var appHostSha = configuration["AppHost:PathSha256"]; From 9934b6647b3fd50f9ff834de655b3ae6ca17aa7d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:54:56 +0000 Subject: [PATCH 5/9] Remove OutputPath property from PipelineContext and PipelineStepContext - Remove OutputPath convenience property per @davidfowl feedback - Consumers now explicitly call OutputService.GetOutputDirectory() - Update AzureEnvironmentResource to use OutputService.GetOutputDirectory() - Update PublishingContextUtils to use OutputService.GetOutputDirectory() - Update test helper to use OutputService.GetOutputDirectory() - Keep OutputService property for explicit access to output management Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs | 2 +- src/Aspire.Hosting/Pipelines/PipelineContext.cs | 5 ----- src/Aspire.Hosting/Pipelines/PipelineStepContext.cs | 5 ----- src/Shared/PublishingContextUtils.cs | 4 ++-- .../Helpers/JsonDocumentManifestPublisher.cs | 2 +- 5 files changed, 4 insertions(+), 14 deletions(-) diff --git a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs index 6af3c37b5aa..6e8aaade218 100644 --- a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs @@ -122,7 +122,7 @@ private Task PublishAsync(PipelineStepContext context) { var azureProvisioningOptions = context.Services.GetRequiredService>(); var publishingContext = new AzurePublishingContext( - context.OutputPath, + context.OutputService.GetOutputDirectory(), azureProvisioningOptions.Value, context.Services, context.Logger, diff --git a/src/Aspire.Hosting/Pipelines/PipelineContext.cs b/src/Aspire.Hosting/Pipelines/PipelineContext.cs index bd772fcecc3..771d8a8650e 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineContext.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineContext.cs @@ -56,9 +56,4 @@ public sealed class PipelineContext( /// Gets the service for managing pipeline output directories. /// public IPipelineOutputService OutputService { get; } = new PipelineOutputService(outputPath, serviceProvider.GetRequiredService()); - - /// - /// Gets the output path for deployment artifacts. - /// - public string OutputPath => OutputService.GetOutputDirectory(); } diff --git a/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs b/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs index 5109cfb4e6f..9d0cd15a7bd 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs @@ -59,9 +59,4 @@ public sealed class PipelineStepContext /// Gets the service for managing pipeline output directories. /// public IPipelineOutputService OutputService => PipelineContext.OutputService; - - /// - /// Gets the output path for deployment artifacts. - /// - public string OutputPath => PipelineContext.OutputPath; } \ No newline at end of file diff --git a/src/Shared/PublishingContextUtils.cs b/src/Shared/PublishingContextUtils.cs index a7fad60fc49..feacc2e7058 100644 --- a/src/Shared/PublishingContextUtils.cs +++ b/src/Shared/PublishingContextUtils.cs @@ -15,10 +15,10 @@ public static string GetEnvironmentOutputPath(PipelineStepContext context, IComp if (context.Model.Resources.OfType().Count() > 1) { // If there are multiple compute environments, append the environment name to the output path - return Path.Combine(context.OutputPath, environment.Name); + return Path.Combine(context.OutputService.GetOutputDirectory(), environment.Name); } // If there is only one compute environment, use the root output path - return context.OutputPath; + return context.OutputService.GetOutputDirectory(); } } diff --git a/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs b/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs index 4ceb1cd9ab9..67bdd525a45 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs @@ -78,7 +78,7 @@ public static IDistributedApplicationPipeline AddJsonDocumentManifestPublishing( using var stream = new MemoryStream(); using var writer = new Utf8JsonWriter(stream, new() { Indented = true }); - var manifestPath = context.OutputPath ?? "aspire-manifest.json"; + var manifestPath = context.OutputService.GetOutputDirectory(); var publishingContext = new ManifestPublishingContext(executionContext, manifestPath, writer, context.CancellationToken); await publishingContext.WriteModel(context.Model, context.CancellationToken).ConfigureAwait(false); From 4538f8feba3495927bd2fdb5eda59f9a14118549 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:11:46 +0000 Subject: [PATCH 6/9] Remove OutputService from PipelineContext and resolve from DI - Register IPipelineOutputService in DI container with PipelineOptions - Remove OutputService property from PipelineContext and PipelineStepContext - Remove outputPath parameter from PipelineContext constructor - Update all consumers to resolve IPipelineOutputService from Services - AzureEnvironmentResource - PublishingContextUtils - JsonDocumentManifestPublisher test helper - Remove IOptions dependency from PipelineExecutor - Steps now explicitly resolve IPipelineOutputService when needed Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs | 3 ++- src/Aspire.Hosting/DistributedApplicationBuilder.cs | 7 +++++++ src/Aspire.Hosting/Pipelines/PipelineContext.cs | 11 +---------- src/Aspire.Hosting/Pipelines/PipelineStepContext.cs | 5 ----- src/Aspire.Hosting/Publishing/PipelineExecutor.cs | 5 +---- src/Shared/PublishingContextUtils.cs | 7 +++++-- .../Helpers/JsonDocumentManifestPublisher.cs | 3 ++- .../Pipelines/DistributedApplicationPipelineTests.cs | 3 +-- 8 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs index 6e8aaade218..c7883f85b2d 100644 --- a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs @@ -121,8 +121,9 @@ public AzureEnvironmentResource(string name, ParameterResource location, Paramet private Task PublishAsync(PipelineStepContext context) { var azureProvisioningOptions = context.Services.GetRequiredService>(); + var outputService = context.Services.GetRequiredService(); var publishingContext = new AzurePublishingContext( - context.OutputService.GetOutputDirectory(), + outputService.GetOutputDirectory(), azureProvisioningOptions.Value, context.Services, context.Logger, diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 56bcc258599..5143b5c0ff5 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -462,6 +462,13 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(sp => sp.GetRequiredService()); + _innerBuilder.Services.AddSingleton(sp => + { + var options = sp.GetRequiredService>(); + var configuration = sp.GetRequiredService(); + var outputPath = options.Value.OutputPath is not null ? Path.GetFullPath(options.Value.OutputPath) : null; + return new PipelineOutputService(outputPath, configuration); + }); _innerBuilder.Services.AddSingleton(Pipeline); // Configure pipeline logging options diff --git a/src/Aspire.Hosting/Pipelines/PipelineContext.cs b/src/Aspire.Hosting/Pipelines/PipelineContext.cs index 771d8a8650e..9baaa86874d 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineContext.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineContext.cs @@ -3,8 +3,6 @@ using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace Aspire.Hosting.Pipelines; @@ -17,15 +15,13 @@ namespace Aspire.Hosting.Pipelines; /// The service provider for dependency resolution. /// The logger for pipeline operations. /// The cancellation token for the pipeline operation. -/// The output path for deployment artifacts. [Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public sealed class PipelineContext( DistributedApplicationModel model, DistributedApplicationExecutionContext executionContext, IServiceProvider serviceProvider, ILogger logger, - CancellationToken cancellationToken, - string? outputPath) + CancellationToken cancellationToken) { /// /// Gets the distributed application model to be deployed. @@ -51,9 +47,4 @@ public sealed class PipelineContext( /// Gets the cancellation token for the pipeline operation. /// public CancellationToken CancellationToken { get; set; } = cancellationToken; - - /// - /// Gets the service for managing pipeline output directories. - /// - public IPipelineOutputService OutputService { get; } = new PipelineOutputService(outputPath, serviceProvider.GetRequiredService()); } diff --git a/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs b/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs index 9d0cd15a7bd..d3d7faa47fd 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineStepContext.cs @@ -54,9 +54,4 @@ public sealed class PipelineStepContext /// Gets the cancellation token for the pipeline operation. /// public CancellationToken CancellationToken => PipelineContext.CancellationToken; - - /// - /// Gets the service for managing pipeline output directories. - /// - public IPipelineOutputService OutputService => PipelineContext.OutputService; } \ No newline at end of file diff --git a/src/Aspire.Hosting/Publishing/PipelineExecutor.cs b/src/Aspire.Hosting/Publishing/PipelineExecutor.cs index 24b2aa5fad6..21a41c2d801 100644 --- a/src/Aspire.Hosting/Publishing/PipelineExecutor.cs +++ b/src/Aspire.Hosting/Publishing/PipelineExecutor.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Aspire.Hosting.Publishing; @@ -25,7 +24,6 @@ internal sealed class PipelineExecutor( IPipelineActivityReporter activityReporter, IDistributedApplicationEventing eventing, BackchannelService backchannelService, - IOptions options, IPipelineActivityReporter pipelineActivityReporter) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -99,8 +97,7 @@ await eventing.PublishAsync( public async Task ExecutePipelineAsync(DistributedApplicationModel model, CancellationToken cancellationToken) { - var pipelineContext = new PipelineContext(model, executionContext, serviceProvider, logger, cancellationToken, options.Value.OutputPath is not null ? - Path.GetFullPath(options.Value.OutputPath) : null); + var pipelineContext = new PipelineContext(model, executionContext, serviceProvider, logger, cancellationToken); var pipeline = serviceProvider.GetRequiredService(); await pipeline.ExecuteAsync(pipelineContext).ConfigureAwait(false); diff --git a/src/Shared/PublishingContextUtils.cs b/src/Shared/PublishingContextUtils.cs index feacc2e7058..cecc382b62c 100644 --- a/src/Shared/PublishingContextUtils.cs +++ b/src/Shared/PublishingContextUtils.cs @@ -5,6 +5,7 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Pipelines; +using Microsoft.Extensions.DependencyInjection; namespace Aspire.Hosting.Utils; @@ -12,13 +13,15 @@ internal static class PublishingContextUtils { public static string GetEnvironmentOutputPath(PipelineStepContext context, IComputeEnvironmentResource environment) { + var outputService = context.Services.GetRequiredService(); + if (context.Model.Resources.OfType().Count() > 1) { // If there are multiple compute environments, append the environment name to the output path - return Path.Combine(context.OutputService.GetOutputDirectory(), environment.Name); + return Path.Combine(outputService.GetOutputDirectory(), environment.Name); } // If there is only one compute environment, use the root output path - return context.OutputService.GetOutputDirectory(); + return outputService.GetOutputDirectory(); } } diff --git a/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs b/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs index 67bdd525a45..b65913be27b 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs @@ -78,7 +78,8 @@ public static IDistributedApplicationPipeline AddJsonDocumentManifestPublishing( using var stream = new MemoryStream(); using var writer = new Utf8JsonWriter(stream, new() { Indented = true }); - var manifestPath = context.OutputService.GetOutputDirectory(); + var outputService = context.Services.GetRequiredService(); + var manifestPath = outputService.GetOutputDirectory(); var publishingContext = new ManifestPublishingContext(executionContext, manifestPath, writer, context.CancellationToken); await publishingContext.WriteModel(context.Model, context.CancellationToken).ConfigureAwait(false); diff --git a/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs b/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs index 5322c0e6c4f..dd8c1a89fcf 100644 --- a/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs +++ b/tests/Aspire.Hosting.Tests/Pipelines/DistributedApplicationPipelineTests.cs @@ -876,8 +876,7 @@ private static PipelineContext CreateDeployingContext(DistributedApplication app app.Services.GetRequiredService(), app.Services, NullLogger.Instance, - CancellationToken.None, - outputPath: null); + CancellationToken.None); } [Fact] From 922fbdec4debea925bc07c7c49129e6ca33a0abe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 20:20:55 +0000 Subject: [PATCH 7/9] Update PipelineOutputService to inject dependencies and use resource-specific overload - PipelineOutputService now injects IOptions and IConfiguration directly in constructor - Simplified DI registration to use standard AddSingleton without factory lambda - PublishingContextUtils uses GetOutputDirectory(environment) for resource-specific paths - Removes manual path combination logic in favor of service method Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Hosting/DistributedApplicationBuilder.cs | 8 +------- src/Aspire.Hosting/Pipelines/PipelineOutputService.cs | 5 +++-- src/Shared/PublishingContextUtils.cs | 4 ++-- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 5143b5c0ff5..fc94ba22d23 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -462,13 +462,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(sp => sp.GetRequiredService()); - _innerBuilder.Services.AddSingleton(sp => - { - var options = sp.GetRequiredService>(); - var configuration = sp.GetRequiredService(); - var outputPath = options.Value.OutputPath is not null ? Path.GetFullPath(options.Value.OutputPath) : null; - return new PipelineOutputService(outputPath, configuration); - }); + _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(Pipeline); // Configure pipeline logging options diff --git a/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs b/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs index 8ad71fd1aaf..b19fd6a7e72 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; namespace Aspire.Hosting.Pipelines; @@ -16,9 +17,9 @@ internal sealed class PipelineOutputService : IPipelineOutputService private readonly string? _outputPath; private readonly Lazy _tempDirectory; - public PipelineOutputService(string? outputPath, IConfiguration configuration) + public PipelineOutputService(IOptions options, IConfiguration configuration) { - _outputPath = outputPath; + _outputPath = options.Value.OutputPath is not null ? Path.GetFullPath(options.Value.OutputPath) : null; _tempDirectory = new Lazy(() => CreateTempDirectory(configuration)); } diff --git a/src/Shared/PublishingContextUtils.cs b/src/Shared/PublishingContextUtils.cs index cecc382b62c..acb1396736f 100644 --- a/src/Shared/PublishingContextUtils.cs +++ b/src/Shared/PublishingContextUtils.cs @@ -17,8 +17,8 @@ public static string GetEnvironmentOutputPath(PipelineStepContext context, IComp if (context.Model.Resources.OfType().Count() > 1) { - // If there are multiple compute environments, append the environment name to the output path - return Path.Combine(outputService.GetOutputDirectory(), environment.Name); + // If there are multiple compute environments, use resource-specific output path + return outputService.GetOutputDirectory(environment); } // If there is only one compute environment, use the root output path From 04582377fa1c4707043a4659f27da37d4a137146 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 31 Oct 2025 13:50:43 -0700 Subject: [PATCH 8/9] Update src/Shared/PublishingContextUtils.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Shared/PublishingContextUtils.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Shared/PublishingContextUtils.cs b/src/Shared/PublishingContextUtils.cs index acb1396736f..c8cbdb9e0b4 100644 --- a/src/Shared/PublishingContextUtils.cs +++ b/src/Shared/PublishingContextUtils.cs @@ -14,7 +14,6 @@ internal static class PublishingContextUtils public static string GetEnvironmentOutputPath(PipelineStepContext context, IComputeEnvironmentResource environment) { var outputService = context.Services.GetRequiredService(); - if (context.Model.Resources.OfType().Count() > 1) { // If there are multiple compute environments, use resource-specific output path From 69df4c7926f76916ea3b092c3878f7c971986b61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 22:56:56 +0000 Subject: [PATCH 9/9] Add XML documentation and update diagnostic ID to ASPIREPIPELINES004 - Update Experimental attribute from ASPIREPIPELINES001 to ASPIREPIPELINES004 - Add XML documentation for GetOutputDirectory() default behavior - Add XML documentation for private fields in PipelineOutputService - Document purpose of AppHost:PathSha256 usage in CreateTempDirectory - Suppress ASPIREPIPELINES004 diagnostic in all consuming files Co-authored-by: captainsafia <1857993+captainsafia@users.noreply.github.com> --- .../AzureEnvironmentResource.cs | 1 + .../DistributedApplicationBuilder.cs | 1 + .../Pipelines/IPipelineOutputService.cs | 3 ++- .../Pipelines/PipelineOutputService.cs | 15 ++++++++++++++- src/Shared/PublishingContextUtils.cs | 1 + .../Helpers/JsonDocumentManifestPublisher.cs | 1 + 6 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs index c7883f85b2d..c635b3d34f7 100644 --- a/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs +++ b/src/Aspire.Hosting.Azure/AzureEnvironmentResource.cs @@ -4,6 +4,7 @@ #pragma warning disable ASPIREAZURE001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIREPIPELINES003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPIPELINES004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. using System.Diagnostics.CodeAnalysis; using Aspire.Hosting.ApplicationModel; diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index fc94ba22d23..fc88bd556f8 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -4,6 +4,7 @@ #pragma warning disable ASPIREPIPELINES003 #pragma warning disable ASPIREPIPELINES001 #pragma warning disable ASPIREPIPELINES002 +#pragma warning disable ASPIREPIPELINES004 using System.Diagnostics; using System.Reflection; diff --git a/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs b/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs index c2f0a0e0986..826549d077f 100644 --- a/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs +++ b/src/Aspire.Hosting/Pipelines/IPipelineOutputService.cs @@ -9,11 +9,12 @@ namespace Aspire.Hosting.Pipelines; /// /// Service for managing pipeline output directories. /// -[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +[Experimental("ASPIREPIPELINES004", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] public interface IPipelineOutputService { /// /// Gets the output directory for deployment artifacts. + /// If no output path is configured, defaults to {CurrentDirectory}/aspire-output. /// /// The path to the output directory for deployment artifacts. string GetOutputDirectory(); diff --git a/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs b/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs index b19fd6a7e72..4d3eccec412 100644 --- a/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs +++ b/src/Aspire.Hosting/Pipelines/PipelineOutputService.cs @@ -11,10 +11,17 @@ namespace Aspire.Hosting.Pipelines; /// /// Default implementation of . /// -[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +[Experimental("ASPIREPIPELINES004", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] internal sealed class PipelineOutputService : IPipelineOutputService { + /// + /// Stores the resolved output directory path, or null if not specified. + /// private readonly string? _outputPath; + + /// + /// Lazily creates and stores the path to the temporary directory for pipeline output. + /// private readonly Lazy _tempDirectory; public PipelineOutputService(IOptions options, IConfiguration configuration) @@ -53,6 +60,12 @@ public string GetTempDirectory(IResource resource) return Path.Combine(baseTempDir, resource.Name); } + /// + /// Creates a temporary directory for pipeline build artifacts. + /// Uses AppHost:PathSha256 from configuration to create an isolated temp directory per app host, + /// enabling multiple app hosts to run concurrently without conflicts. + /// If AppHost:PathSha256 is not available, falls back to a generic "aspire" temp directory. + /// private static string CreateTempDirectory(IConfiguration configuration) { var appHostSha = configuration["AppHost:PathSha256"]; diff --git a/src/Shared/PublishingContextUtils.cs b/src/Shared/PublishingContextUtils.cs index c8cbdb9e0b4..185aa207c17 100644 --- a/src/Shared/PublishingContextUtils.cs +++ b/src/Shared/PublishingContextUtils.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREPIPELINES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPIPELINES004 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Pipelines; diff --git a/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs b/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs index b65913be27b..c886c180dd1 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/JsonDocumentManifestPublisher.cs @@ -3,6 +3,7 @@ #pragma warning disable CS0618 // Type or member is obsolete #pragma warning disable ASPIREPIPELINES001 +#pragma warning disable ASPIREPIPELINES004 using System.Text.Json; using Aspire.Hosting.Publishing;