From 26eb2dd8cec9aa56a27e3217cb4bff32250b92a2 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 11 May 2026 17:28:28 -0700 Subject: [PATCH 01/38] Add persistent executable support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ExecutableLifetimeAnnotation.cs | 34 +++++ .../ApplicationModel/ResourceExtensions.cs | 19 +++ src/Aspire.Hosting/Dcp/DcpExecutor.cs | 3 + src/Aspire.Hosting/Dcp/DcpNameGenerator.cs | 7 +- src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs | 11 ++ src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 38 +++++- src/Aspire.Hosting/Dcp/Model/Executable.cs | 17 ++- .../Dcp/ResourceSnapshotBuilder.cs | 9 ++ .../ExecutableResourceBuilderExtensions.cs | 28 ++++ src/Aspire.Hosting/api/Aspire.Hosting.cs | 18 ++- .../Dcp/DcpExecutorTests.cs | 122 +++++++++++++++++- .../Dcp/ResourceSnapshotBuilderTests.cs | 60 +++++++++ ...ExecutableResourceBuilderExtensionTests.cs | 11 ++ 13 files changed, 367 insertions(+), 10 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs diff --git a/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs new file mode 100644 index 00000000000..27949e57150 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs @@ -0,0 +1,34 @@ +// 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; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Lifetime modes for executable resources. +/// +public enum ExecutableLifetime +{ + /// + /// Create the resource when the app host process starts and dispose of it when the app host process shuts down. + /// + Session, + + /// + /// Attempt to re-use a previously created resource if one exists. Do not destroy the executable on app host process shutdown. + /// + Persistent, +} + +/// +/// Annotation that controls the lifetime of an executable resource. +/// +[DebuggerDisplay("Type = {GetType().Name,nq}")] +public sealed class ExecutableLifetimeAnnotation : IResourceAnnotation +{ + /// + /// Gets or sets the lifetime type for the executable resource. + /// + public required ExecutableLifetime Lifetime { get; set; } +} diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 66fc4781786..f07910d7620 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1057,6 +1057,25 @@ internal static ContainerLifetime GetContainerLifetimeType(this IResource resour return ContainerLifetime.Session; } + /// + /// Gets the lifetime type of the executable for the specified resource. + /// Defaults to if no is found. + /// + /// The resource to get the ExecutableLifetimeType for. + /// + /// The from the for the resource (if the annotation exists). + /// Defaults to if the annotation is not set. + /// + internal static ExecutableLifetime GetExecutableLifetimeType(this IResource resource) + { + if (resource.TryGetLastAnnotation(out var lifetimeAnnotation)) + { + return lifetimeAnnotation.Lifetime; + } + + return ExecutableLifetime.Session; + } + /// /// Determines whether the specified resource has a pull policy annotation and retrieves the value if it does. /// diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index b91b6d03c47..db80ee7dc09 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -1099,6 +1099,9 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance case RenderedModelResource er: await EnsureResourceDeletedAsync(resourceReference.DcpResourceName, cancellationToken).ConfigureAwait(false); + // Ensure we explicitly start the executable even if original executable was created in "delay-start" mode. + er.DcpResource.Spec.Start = true; + await _executorEvents.PublishAsync(new OnConnectionStringAvailableContext(cancellationToken, resourceReference.ModelResource)).ConfigureAwait(false); await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, resourceReference.ModelResource, resourceReference.DcpResourceName)).ConfigureAwait(false); await _executableCreator.CreateObjectAsync(er, EmptyCreationContext.s_instance, resourceLogger, this, cancellationToken).ConfigureAwait(false); diff --git a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs index 742ce696b83..623555cbbe1 100644 --- a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs +++ b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs @@ -80,7 +80,12 @@ private static void AddInstancesAnnotation(IResource resource, ImmutableArray GetRandomNameSuffix(), + _ => GetProjectHashSuffix(), + }; + return (GetObjectNameForResource(project, _options.Value, nameSuffix), nameSuffix); } diff --git a/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs b/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs index ed5c15efbb4..b2c65523439 100644 --- a/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs +++ b/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs @@ -322,6 +322,12 @@ internal static ResourceStatus GetResourceStatus(CustomResource resource) } if (resource is Executable executable) { + if (executable.Spec.Start == false && IsNotStartedExecutableState(executable.Status?.State)) + { + // If the resource is set for delay start, treat not-yet-started states as NotStarted. + return new(KnownResourceStates.NotStarted, null, null); + } + return new(executable.Status?.State, executable.Status?.StartupTimestamp?.ToUniversalTime(), executable.Status?.FinishTimestamp?.ToUniversalTime()); } if (resource is ContainerExec containerExec) @@ -345,6 +351,11 @@ private void AddDcpResourceObservedEvent(CustomResource resource, IResource appM resource.Metadata.Annotations); } + private static bool IsNotStartedExecutableState(string? state) + { + return string.IsNullOrEmpty(state) || state == ExecutableState.Unknown; + } + public async IAsyncEnumerable> GetAllLogsAsync(string resourceName, [EnumeratorCancellation] CancellationToken cancellationToken) { IAsyncEnumerable>? enumerable = null; diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index 13dc2c0d714..39978262ce5 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -29,6 +29,7 @@ internal sealed class ExecutableCreator : IObjectCreator _logger; private readonly DcpAppResourceStore _appResources; @@ -39,6 +40,7 @@ public ExecutableCreator( DistributedApplicationOptions distributedApplicationOptions, DistributedApplicationExecutionContext executionContext, Locations locations, + IAspireStore aspireStore, ILogger logger, DcpAppResourceStore appResources) { @@ -48,6 +50,7 @@ public ExecutableCreator( _distributedApplicationOptions = distributedApplicationOptions; _executionContext = executionContext; _locations = locations; + _aspireStore = aspireStore; _logger = logger; _appResources = appResources; } @@ -61,8 +64,8 @@ public IEnumerable> PrepareObjects() public bool IsReadyToCreate(RenderedModelResource resource, EmptyCreationContext context) { - var explicitStartup = resource.ModelResource.TryGetAnnotationsOfType(out _); - return !explicitStartup; + // Executables are always created. When explicit startup is used, DCP receives Spec.Start = false. + return true; } public async Task CreateObjectAsync(RenderedModelResource er, EmptyCreationContext context, ILogger resourceLogger, IDcpObjectFactory factory, CancellationToken cancellationToken) @@ -247,6 +250,11 @@ private void PrepareProjectExecutables() exe.SetAnnotationAsObjectList(CustomResource.ResourceProjectArgsAnnotation, projectArgs); + if (project.TryGetLastAnnotation(out _)) + { + exe.Spec.Start = false; + } + var exeAppResource = new RenderedModelResource(project, exe); DcpModelUtilities.AddServicesProducedInfo(exeAppResource, _appResources.Get()); _appResources.Add(exeAppResource); @@ -272,7 +280,13 @@ private void PreparePlainExecutables() exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); exe.Annotate(CustomResource.ResourceNameAnnotation, executable.Name); - if (executable.SupportsDebugging(_configuration, out _)) + var persistent = executable.GetExecutableLifetimeType() == ExecutableLifetime.Persistent; + if (persistent) + { + exe.Spec.Persistent = true; + } + + if (!persistent && executable.SupportsDebugging(_configuration, out _)) { // Just mark as IDE execution here - the actual launch configuration callback // will be invoked in CreateExecutableAsync after endpoints are allocated. @@ -284,6 +298,11 @@ private void PreparePlainExecutables() exe.Spec.ExecutionType = ExecutionType.Process; } + if (executable.TryGetLastAnnotation(out _)) + { + exe.Spec.Start = false; + } + DcpExecutor.SetInitialResourceState(executable, exe); var exeAppResource = new RenderedModelResource(executable, exe); @@ -296,8 +315,7 @@ private async Task BuildExecutableConfiguration(Rendere { var exe = (Executable)er.DcpResource; - // Build the base paths for certificate output in the DCP session directory. - var certificatesRootDir = Path.Join(_locations.DcpSessionDir, exe.Metadata.Name); + var certificatesRootDir = GetCertificatesRootDirectory(er, exe); var bundleOutputPath = Path.Join(certificatesRootDir, "cert.pem"); var customBundleOutputPath = Path.Join(certificatesRootDir, "bundles"); var certificatesOutputPath = Path.Join(certificatesRootDir, "certs"); @@ -405,6 +423,16 @@ private async Task BuildExecutableConfiguration(Rendere return (configuration, pemCertificates); } + private string GetCertificatesRootDirectory(RenderedModelResource er, Executable exe) + { + if (er.ModelResource.GetExecutableLifetimeType() == ExecutableLifetime.Persistent) + { + return Path.Join(_aspireStore.BasePath, "dcp", "executables", exe.Metadata.Name, "certificates"); + } + + return Path.Join(_locations.DcpSessionDir, exe.Metadata.Name); + } + private static List BuildLaunchArgs(RenderedModelResource er, ExecutableSpec spec, IEnumerable<(string Value, bool IsSensitive)> appHostArgs, int executableArgumentStartIndex) { // Launch args is the final list of args that are displayed in the UI and possibly added to the executable spec. diff --git a/src/Aspire.Hosting/Dcp/Model/Executable.cs b/src/Aspire.Hosting/Dcp/Model/Executable.cs index e56e3f38217..4e6bcd470ad 100644 --- a/src/Aspire.Hosting/Dcp/Model/Executable.cs +++ b/src/Aspire.Hosting/Dcp/Model/Executable.cs @@ -58,8 +58,21 @@ internal sealed class ExecutableSpec public List? HealthProbes { get; set; } /// - /// Setting Stop property to true will stop the Executable if it is running. - /// Once the Executable is stopped, it cannot be started again. + /// Should this Executable be created and persisted between DCP runs? + /// Persistent executables are only compatible with the Process execution type. + /// + [JsonPropertyName("persistent")] + public bool? Persistent { get; set; } + + /// + /// Should this resource be started? If set to false, we will not attempt + /// to start the resource until Start is set to true (or null). + /// + [JsonPropertyName("start")] + public bool? Start { get; set; } + + /// + /// Should this resource be stopped? /// [JsonPropertyName("stop")] public bool? Stop { get; set; } diff --git a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs index f1337c7647b..39856c8d2ed 100644 --- a/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs +++ b/src/Aspire.Hosting/Dcp/ResourceSnapshotBuilder.cs @@ -144,6 +144,10 @@ public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSn } var state = executable.AppModelInitialState is "Hidden" ? "Hidden" : executable.Status?.State; + if (executable.Spec.Start is false && IsNotStartedExecutableState(state)) + { + state = KnownResourceStates.NotStarted; + } var urls = GetUrls(executable, executable.Status?.State); @@ -206,6 +210,11 @@ public CustomResourceSnapshot ToSnapshot(Executable executable, CustomResourceSn }; } + private static bool IsNotStartedExecutableState(string? state) + { + return string.IsNullOrEmpty(state) || state == ExecutableState.Unknown; + } + private static (ImmutableArray Args, ImmutableArray? ArgsAreSensitive, bool IsSensitive)? GetLaunchArgs(CustomResource resource, IReadOnlyList? effectiveArgs) { if (!resource.TryGetAnnotationAsObjectList(CustomResource.ResourceAppArgsAnnotation, out List? launchArgumentAnnotations)) diff --git a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs index 27217cf2db6..8c349536831 100644 --- a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs @@ -68,6 +68,34 @@ public static IResourceBuilder AddExecutable(this IDistribut }); } + /// + /// Sets the lifetime behavior of the executable resource. + /// + /// The resource type. + /// Builder for the executable resource. + /// The lifetime behavior of the executable resource. The default behavior is . + /// The . + /// + /// + /// Marking an executable resource to have a lifetime. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// builder.AddExecutable("myexecutable", "mycommand", ".") + /// .WithLifetime(ExecutableLifetime.Persistent); + /// + /// builder.Build().Run(); + /// + /// + /// + [AspireExport("withExecutableLifetime", Description = "Sets the lifetime behavior of the executable resource")] + public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ExecutableLifetime lifetime) where T : ExecutableResource + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithAnnotation(new ExecutableLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + } + /// /// Adds annotation to to support containerization during deployment. /// diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.cs b/src/Aspire.Hosting/api/Aspire.Hosting.cs index 9ab38d3821f..e8d1ca730bd 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.cs +++ b/src/Aspire.Hosting/api/Aspire.Hosting.cs @@ -510,6 +510,10 @@ public static ApplicationModel.IResourceBuilder PublishAsDockerFile(this A public static ApplicationModel.IResourceBuilder WithCommand(this ApplicationModel.IResourceBuilder builder, string command) where T : ApplicationModel.ExecutableResource { throw null; } + [AspireExport("withExecutableLifetime", Description = "Sets the lifetime behavior of the executable resource")] + public static ApplicationModel.IResourceBuilder WithLifetime(this ApplicationModel.IResourceBuilder builder, ApplicationModel.ExecutableLifetime lifetime) + where T : ApplicationModel.ExecutableResource { throw null; } + [AspireExport("withWorkingDirectory", Description = "Sets the executable working directory")] public static ApplicationModel.IResourceBuilder WithWorkingDirectory(this ApplicationModel.IResourceBuilder builder, string workingDirectory) where T : ApplicationModel.ExecutableResource { throw null; } @@ -2410,6 +2414,18 @@ public sealed partial class ExecutableAnnotation : IResourceAnnotation public required string WorkingDirectory { get { throw null; } set { } } } + public enum ExecutableLifetime + { + Session = 0, + Persistent = 1 + } + + [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}")] + public sealed partial class ExecutableLifetimeAnnotation : IResourceAnnotation + { + public required ExecutableLifetime Lifetime { get { throw null; } set { } } + } + [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}, Command = {Command}")] public partial class ExecutableResource : Resource, IResourceWithEnvironment, IResource, IResourceWithArgs, IResourceWithEndpoints, IResourceWithWaitSupport, IResourceWithProbes, IComputeResource { @@ -4548,4 +4564,4 @@ public partial class PublishingOptions public string? Publisher { get { throw null; } set { } } } -} \ No newline at end of file +} diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 93eb06dac23..20d31c86aef 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -6,6 +6,7 @@ using System.Collections.Concurrent; using System.Globalization; using System.IO.Pipelines; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; @@ -2014,6 +2015,100 @@ public async Task PlainExecutable_ExtensionMode_SupportedDebugMode_RunsInIde() Assert.False(nonDebuggableExe.TryGetAnnotationAsObjectList(Executable.LaunchConfigurationsAnnotation, out _)); } + [Fact] + public async Task PersistentPlainExecutable_ExtensionMode_RunsInProcess() + { + var builder = DistributedApplication.CreateBuilder(); + + var executable = new TestExecutableResource("test-working-directory"); + builder.AddResource(executable) + .WithDebugSupport(mode => new ExecutableLaunchConfiguration("test") { Mode = mode }, "test") + .WithLifetime(ExecutableLifetime.Persistent); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + [DcpExecutor.DebugSessionPortVar] = "12345", + [KnownConfigNames.DebugSessionInfo] = JsonSerializer.Serialize(new RunSessionInfo { ProtocolsSupported = ["test"], SupportedLaunchConfigurations = ["test"] }), + [KnownConfigNames.ExtensionEndpoint] = "http://localhost:1234", + [KnownConfigNames.DebugSessionRunMode] = "Debug" + }; + + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + + await appExecutor.RunApplicationAsync(); + + var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "TestExecutable"); + Assert.Equal("TestExecutable-12345678", exe.Metadata.Name); + Assert.True(exe.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); + Assert.Null(exe.Spec.FallbackExecutionTypes); + } + + [Fact] + public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath() + { + var builder = DistributedApplication.CreateBuilder(); + + using var certificate = CreateTestCertificate(); + var certificateAuthorities = builder.AddCertificateAuthorityCollection("certificates") + .WithCertificate(certificate); + + var executable = new TestExecutableResource("test-working-directory"); + builder.AddResource(executable) + .WithCertificateAuthorityCollection(certificateAuthorities) + .WithCertificateTrustScope(CertificateTrustScope.Override) + .WithLifetime(ExecutableLifetime.Persistent); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + + await appExecutor.RunApplicationAsync(); + + var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "TestExecutable"); + var sslCertDir = Assert.Single(exe.Spec.Env!, e => e.Name == "SSL_CERT_DIR").Value; + var sslCertFile = Assert.Single(exe.Spec.Env!, e => e.Name == "SSL_CERT_FILE").Value; + var expectedCertificatesRoot = Path.Join(".aspire", "dcp", "executables", "TestExecutable-12345678", "certificates"); + + Assert.EndsWith(Path.Join(expectedCertificatesRoot, "certs"), sslCertDir); + Assert.EndsWith(Path.Join(expectedCertificatesRoot, "cert.pem"), sslCertFile); + Assert.DoesNotContain("aspire-dcp", sslCertDir); + Assert.DoesNotContain("aspire-dcp", sslCertFile); + } + + [Fact] + public async Task ExplicitStartPlainExecutable_IsCreatedWithStartFalse() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") + .WithExplicitStart(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + await appExecutor.RunApplicationAsync(); + + var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "CoolProgram"); + Assert.False(exe.Spec.Start.GetValueOrDefault(true)); + } + [Fact] public async Task PlainExecutable_ExtensionMode_UnsupportedDebugMode_RunsInProcess() { @@ -3780,7 +3875,10 @@ private static DcpExecutor CreateAppExecutor( }); var ks = kubernetesService ?? new TestKubernetesService(); var dcpEvts = events ?? new DcpExecutorEvents(); - var locations = new Locations(new FileSystemService(configuration)); + var fileSystemService = new FileSystemService(configuration); + var locations = new Locations(fileSystemService); + var aspireStoreDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-store"); + var aspireStore = new AspireStore(Path.Join(aspireStoreDirectory.Path, ".aspire"), fileSystemService); var hostEnv = hostEnvironment ?? new TestHostEnvironment(); var dcpDependencyCheckService = new TestDcpDependencyCheckService(); @@ -3793,6 +3891,7 @@ private static DcpExecutor CreateAppExecutor( new DistributedApplicationOptions(), executionContext, locations, + aspireStore, NullLogger.Instance, appResources); @@ -3882,6 +3981,27 @@ private static IEnumerable GetEnumerablePropertyValue(CustomResourceSnapsh return Assert.IsAssignableFrom>(property.Value); } + private static X509Certificate2 CreateTestCertificate() + { + using var rsa = RSA.Create(2048); + var request = new CertificateRequest( + new X500DistinguishedName("CN=test"), + rsa, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + var serialNumber = new byte[16]; + RandomNumberGenerator.Fill(serialNumber); + var generator = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1); + + return request.Create( + request.SubjectName, + generator, + DateTimeOffset.Now, + DateTimeOffset.Now.AddYears(1), + serialNumber); + } + private sealed class TestExecutableResource(string directory) : ExecutableResource("TestExecutable", "test", directory); private sealed class TestOtherExecutableResource(string directory) : ExecutableResource("TestOtherExecutable", "test-other", directory); diff --git a/tests/Aspire.Hosting.Tests/Dcp/ResourceSnapshotBuilderTests.cs b/tests/Aspire.Hosting.Tests/Dcp/ResourceSnapshotBuilderTests.cs index 271f9f12ff2..c2ad5e9da6b 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/ResourceSnapshotBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/ResourceSnapshotBuilderTests.cs @@ -48,6 +48,66 @@ public void ExecutableSnapshotFallsBackToAnnotationValueWhenEffectiveArgMissing( Assert.Equal(["-port", DcpTemplateArgument], GetEnumerablePropertyValue(snapshot, KnownProperties.Resource.AppArgs).ToArray()); } + [Fact] + public void ExplicitStartExecutableSnapshotWithUnknownStateIsNotStarted() + { + var executable = Executable.Create("exe", "pwsh"); + executable.Spec.Start = false; + executable.Status = new ExecutableStatus + { + State = ExecutableState.Unknown + }; + + var snapshot = CreateSnapshotBuilder().ToSnapshot(executable, CreatePreviousSnapshot()); + + Assert.Equal(KnownResourceStates.NotStarted, snapshot.State?.Text); + } + + [Fact] + public void ExplicitStartExecutableStatusWithUnknownStateIsNotStarted() + { + var executable = Executable.Create("exe", "pwsh"); + executable.Spec.Start = false; + executable.Status = new ExecutableStatus + { + State = ExecutableState.Unknown + }; + + var status = DcpResourceWatcher.GetResourceStatus(executable); + + Assert.Equal(KnownResourceStates.NotStarted, status.State); + } + + [Fact] + public void ExplicitStartExecutableSnapshotWithEmptyStateIsNotStarted() + { + var executable = Executable.Create("exe", "pwsh"); + executable.Spec.Start = false; + executable.Status = new ExecutableStatus + { + State = "" + }; + + var snapshot = CreateSnapshotBuilder().ToSnapshot(executable, CreatePreviousSnapshot()); + + Assert.Equal(KnownResourceStates.NotStarted, snapshot.State?.Text); + } + + [Fact] + public void ExplicitStartExecutableStatusWithEmptyStateIsNotStarted() + { + var executable = Executable.Create("exe", "pwsh"); + executable.Spec.Start = false; + executable.Status = new ExecutableStatus + { + State = "" + }; + + var status = DcpResourceWatcher.GetResourceStatus(executable); + + Assert.Equal(KnownResourceStates.NotStarted, status.State); + } + private static Executable CreateExecutable(AppLaunchArgumentAnnotation[] launchArgumentAnnotations, IReadOnlyList effectiveArgs) { var executable = Executable.Create("exe", "pwsh"); diff --git a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs index e1fdcf3ae82..ccd14590deb 100644 --- a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs @@ -72,6 +72,17 @@ public void WithWorkingDirectoryAllowsEmptyString() Assert.Equal(builder.AppHostDirectory, annotation.WorkingDirectory); } + [Fact] + public void WithLifetimeAddsExecutableLifetimeAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + var executable = builder.AddExecutable("myexe", "command", "workingdirectory") + .WithLifetime(ExecutableLifetime.Persistent); + + var annotation = executable.Resource.Annotations.OfType().Single(); + Assert.Equal(ExecutableLifetime.Persistent, annotation.Lifetime); + } + [Fact] public void WithDebugSupportAddsAnnotationInRunMode() { From 0df49327477aed58dc168b1e6354eb4971b3f4bf Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 14 May 2026 18:30:34 -0700 Subject: [PATCH 02/38] Support persistent project endpoint allocation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DevTunnels/DevTunnels.AppHost/AppHost.cs | 6 +- .../DevTunnelHealthCheck.cs | 8 +- .../DevTunnelPortHealthCheck.cs | 26 ++- .../DevTunnelResource.cs | 26 +++ .../DevTunnelResourceBuilderExtensions.cs | 11 +- .../ApplicationModel/EndpointAnnotation.cs | 18 +- .../ApplicationModel/EndpointUpdateContext.cs | 2 +- .../ApplicationModel/ResourceExtensions.cs | 11 + .../ContainerResourceBuilderExtensions.cs | 24 -- src/Aspire.Hosting/Dcp/ContainerCreator.cs | 4 +- src/Aspire.Hosting/Dcp/DcpExecutor.cs | 85 ++++--- src/Aspire.Hosting/Dcp/DcpModelUtilities.cs | 213 +++++++++++++----- src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs | 9 + src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 7 +- src/Aspire.Hosting/DistributedApplication.cs | 10 +- .../DistributedApplicationEventing.cs | 5 + .../Orchestrator/ApplicationOrchestrator.cs | 14 +- .../ProjectResourceBuilderExtensions.cs | 29 +++ .../ResourceBuilderExtensions.cs | 50 ++-- src/Aspire.Hosting/api/Aspire.Hosting.cs | 26 ++- ...DevTunnelResourceBuilderExtensionsTests.cs | 76 +++++++ .../Aspire.Hosting.Tests.csproj | 1 + .../Dcp/DcpExecutorTests.cs | 211 ++++++++++++++--- .../Dcp/TestKubernetesService.cs | 42 +++- .../DistributedApplicationTests.cs | 33 +-- ...tributedApplicationBuilderEventingTests.cs | 29 ++- .../ProjectResourceBuilderExtensionTests.cs | 27 +++ 27 files changed, 759 insertions(+), 244 deletions(-) create mode 100644 tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs diff --git a/playground/DevTunnels/DevTunnels.AppHost/AppHost.cs b/playground/DevTunnels/DevTunnels.AppHost/AppHost.cs index e98400765d2..d63ea74eea1 100644 --- a/playground/DevTunnels/DevTunnels.AppHost/AppHost.cs +++ b/playground/DevTunnels/DevTunnels.AppHost/AppHost.cs @@ -3,8 +3,10 @@ var builder = DistributedApplication.CreateBuilder(args); -var api = builder.AddProject("api"); -var frontend = builder.AddProject("frontend"); +var api = builder.AddProject("api") + .WithEndpointProxySupport(false); +var frontend = builder.AddProject("frontend") + .WithEndpointProxySupport(false); var publicDevTunnel = builder.AddDevTunnel("devtunnel-public") .WithAnonymousAccess() // All ports on this tunnel default to allowing anonymous access diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs index baf42835038..f5fec38c837 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs @@ -34,11 +34,12 @@ public async Task CheckHealthAsync(HealthCheckContext context // Check that expected ports are active foreach (var portResource in _tunnelResource.Ports) { - var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == portResource.TargetEndpoint.Port); + var tunnelPort = await portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false); + var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == tunnelPort); portResource.LastKnownStatus = portStatus; if (portStatus?.PortUri is null) { - return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, _tunnelResource.TunnelId, portResource.TargetEndpoint.Port)); + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, _tunnelResource.TunnelId, tunnelPort)); } } @@ -49,7 +50,8 @@ public async Task CheckHealthAsync(HealthCheckContext context // Get access status for each port foreach (var portResource in _tunnelResource.Ports) { - var portAccessStatus = await _devTunnelClient.GetAccessAsync(_tunnelResource.TunnelId, portResource.TargetEndpoint.Port, logger, cancellationToken).ConfigureAwait(false); + var tunnelPort = await portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false); + var portAccessStatus = await _devTunnelClient.GetAccessAsync(_tunnelResource.TunnelId, tunnelPort, logger, cancellationToken).ConfigureAwait(false); portResource.LastKnownAccessStatus = portAccessStatus; } diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelPortHealthCheck.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelPortHealthCheck.cs index 4063d899b9a..025a6cf9c8e 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelPortHealthCheck.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelPortHealthCheck.cs @@ -6,40 +6,42 @@ namespace Aspire.Hosting.DevTunnels; -internal sealed class DevTunnelPortHealthCheck(DevTunnelResource tunnelResource, int port) : IHealthCheck +internal sealed class DevTunnelPortHealthCheck(DevTunnelPortResource portResource) : IHealthCheck { - private readonly DevTunnelResource _tunnelResource = tunnelResource ?? throw new ArgumentNullException(nameof(tunnelResource)); + private readonly DevTunnelPortResource _portResource = portResource ?? throw new ArgumentNullException(nameof(portResource)); - private readonly int _port = port; - - public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { try { + var tunnelResource = _portResource.DevTunnel; + var port = await _portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false); var tunnelStatus = tunnelResource.LastKnownStatus; if (tunnelStatus is null) { - return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_StatusUnknown, _port, _tunnelResource.TunnelId))); + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_StatusUnknown, port, tunnelResource.TunnelId)); } if (tunnelStatus.HostConnections == 0) { - return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_NoActiveHostConnections, _tunnelResource.TunnelId))); + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_NoActiveHostConnections, tunnelResource.TunnelId)); } - var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == _port); + var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == port); // Check that port is active if (portStatus?.PortUri is null) { - return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, _tunnelResource.TunnelId, _port))); + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, tunnelResource.TunnelId, port)); } - return Task.FromResult(HealthCheckResult.Healthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortHealthy, _port, _tunnelResource.TunnelId))); + return HealthCheckResult.Healthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortHealthy, port, tunnelResource.TunnelId)); } catch (Exception ex) { - return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_Error, _port, _tunnelResource.TunnelId, ex.Message), ex)); + var tunnelResource = _portResource.DevTunnel; + var port = _portResource.TargetEndpoint.TargetPort?.ToString(CultureInfo.InvariantCulture) ?? "unknown"; + return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_Error, port, tunnelResource.TunnelId, ex.Message), ex); } } -} \ No newline at end of file +} diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs index b83b95a789e..b03828b5beb 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Globalization; using Aspire.Hosting.ApplicationModel; namespace Aspire.Hosting.DevTunnels; @@ -85,4 +86,29 @@ public DevTunnelPortResource( internal EndpointReference TargetEndpoint { get; init; } internal DevTunnelPort? LastKnownStatus { get; set; } internal DevTunnelAccessStatus? LastKnownAccessStatus { get; set; } + + internal async ValueTask GetTunnelPortAsync(CancellationToken cancellationToken = default) + { + if (TargetEndpoint.TargetPort is int targetPort) + { + return targetPort; + } + + string? resolvedTargetPort = null; + try + { + resolvedTargetPort = await TargetEndpoint.Property(EndpointProperty.TargetPort).GetValueAsync(cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException) when (TargetEndpoint.IsAllocated) + { + // Endpoint references can only resolve targetPort dynamically when DCP reports a target-port expression. + } + + if (int.TryParse(resolvedTargetPort, NumberStyles.None, CultureInfo.InvariantCulture, out var port)) + { + return port; + } + + return TargetEndpoint.Port; + } } diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs index c84801f8380..6c321bf48c1 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs @@ -177,7 +177,7 @@ public static IResourceBuilder AddDevTunnel( async Task DeleteUnmodeledPortsAsync() { var existingPorts = await devTunnelClient.GetPortListAsync(tunnelResource.TunnelId, logger, ct).ConfigureAwait(false); - var modeledPortNumbers = tunnelResource.Ports.Select(p => p.TargetEndpoint.Port).ToHashSet(); + var modeledPortNumbers = (await Task.WhenAll(tunnelResource.Ports.Select(p => p.GetTunnelPortAsync(ct).AsTask())).ConfigureAwait(false)).ToHashSet(); var unmodeledPorts = existingPorts.Ports.Where(p => !modeledPortNumbers.Contains(p.PortNumber)).ToList(); if (unmodeledPorts.Count > 0) { @@ -189,6 +189,7 @@ async Task DeleteUnmodeledPortsAsync() async Task StartPortAsync(DevTunnelPortResource portResource) { var portLogger = e.Services.GetRequiredService().GetLogger(portResource); + var tunnelPort = await portResource.GetTunnelPortAsync(ct).ConfigureAwait(false); // Clear any prior port status portLogger.LogInformation("Tunnel starting"); @@ -202,17 +203,17 @@ await notifications.PublishUpdateAsync(portResource, snapshot => snapshot with { _ = await devTunnelClient.CreatePortAsync( portResource.DevTunnel.TunnelId, - portResource.TargetEndpoint.Port, + tunnelPort, portResource.Options, portLogger, ct) .ConfigureAwait(false); - portLogger.LogInformation("Created dev tunnel port '{Port}' on tunnel '{Tunnel}' targeting endpoint '{Endpoint}' on resource '{TargetResource}'", portResource.TargetEndpoint.Port, portResource.DevTunnel.TunnelId, portResource.TargetEndpoint.EndpointName, portResource.TargetEndpoint.Resource.Name); + portLogger.LogInformation("Created dev tunnel port '{Port}' on tunnel '{Tunnel}' targeting endpoint '{Endpoint}' on resource '{TargetResource}'", tunnelPort, portResource.DevTunnel.TunnelId, portResource.TargetEndpoint.EndpointName, portResource.TargetEndpoint.Resource.Name); } catch (Exception ex) { - portLogger.LogError(ex, "Error trying to create dev tunnel port '{Port}' on tunnel '{Tunnel}': {Error}", portResource.TargetEndpoint.Port, portResource.DevTunnel.TunnelId, ex.Message); + portLogger.LogError(ex, "Error trying to create dev tunnel port '{Port}' on tunnel '{Tunnel}': {Error}", tunnelPort, portResource.DevTunnel.TunnelId, ex.Message); #pragma warning disable CS0618 // Type or member is obsolete portResource.TunnelEndpointAnnotation.AllocatedEndpointSnapshot.SetException(ex); #pragma warning restore CS0618 // Type or member is obsolete @@ -589,7 +590,7 @@ private static void AddDevTunnelPort( var healtCheckKey = $"{portName}-check"; tunnelBuilder.ApplicationBuilder.Services.AddHealthChecks().Add(new HealthCheckRegistration( healtCheckKey, - services => new DevTunnelPortHealthCheck(tunnel, targetEndpoint.Port), + services => new DevTunnelPortHealthCheck(portResource), failureStatus: default, tags: default, timeout: default)); diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index e5afeaa80f5..71610c7558b 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -35,7 +35,7 @@ public sealed class EndpointAnnotation : IResourceAnnotation /// Desired port for the service. /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// Indicates that this endpoint should be exposed externally at publish time. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . public EndpointAnnotation( ProtocolType protocol, string? uriScheme = null, @@ -44,7 +44,7 @@ public EndpointAnnotation( int? port = null, int? targetPort = null, bool? isExternal = null, - bool isProxied = true + bool? isProxied = null ) : this( protocol, null, @@ -69,7 +69,7 @@ public EndpointAnnotation( /// Desired port for the service. /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// Indicates that this endpoint should be exposed externally at publish time. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . /// Clients connected to the same network can reach the endpoint without any routing or network address translation. public EndpointAnnotation( ProtocolType protocol, @@ -80,7 +80,7 @@ public EndpointAnnotation( int? port = null, int? targetPort = null, bool? isExternal = null, - bool isProxied = true + bool? isProxied = null ) { // If the URI scheme is null, we'll adopt either udp:// or tcp:// based on the @@ -129,7 +129,7 @@ public int? Port // It also depends on what the EndpointAnnotation is applied to. // In the Container case the TargetPort is the port that the process listens on inside the container, // and the Port is the host interface port, so it is fine for them to be different. - get => _port ?? (IsProxied || _portSetToNull ? null : _targetPort); + get => _port ?? (IsProxied.GetValueOrDefault(true) || _portSetToNull ? null : _targetPort); set { _port = value; @@ -137,6 +137,8 @@ public int? Port } } + internal int? SpecifiedPort => _port; + /// /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// @@ -146,7 +148,7 @@ public int? Port public int? TargetPort { // See comment on the Port setter, as this is the reciprocal logic - get => _targetPort ?? (IsProxied || _targetPortSetToNull ? null : _port); + get => _targetPort ?? (IsProxied.GetValueOrDefault(true) || _targetPortSetToNull ? null : _port); set { _targetPort = value; @@ -182,8 +184,8 @@ public string Transport /// Indicates that this endpoint should be managed by DCP. This means it can be replicated and use a different port internally than the one publicly exposed. /// Setting to false means the endpoint will be handled and exposed by the resource. /// - /// Defaults to true. - public bool IsProxied { get; set; } = true; + /// Defaults to . The effective default is computed from the resource that owns the endpoint. + public bool? IsProxied { get; set; } /// /// Gets or sets a value indicating whether this endpoint is excluded from the default set when referencing the resource's endpoints diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs b/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs index a5c28ee495e..ea6943f04c2 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs @@ -84,7 +84,7 @@ public bool IsExternal /// /// Gets or sets a value indicating whether the endpoint is proxied. /// - public bool IsProxied + public bool? IsProxied { get => _endpointAnnotation.IsProxied; set => _endpointAnnotation.IsProxied = value; diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index f07910d7620..afcf763064b 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1076,6 +1076,17 @@ internal static ExecutableLifetime GetExecutableLifetimeType(this IResource reso return ExecutableLifetime.Session; } + /// + /// Determines whether the specified resource has a persistent lifetime. + /// + /// The resource to get persistent lifetime behavior for. + /// if the resource has a persistent container or executable lifetime, otherwise . + internal static bool HasPersistentLifetime(this IResource resource) + { + return resource.GetContainerLifetimeType() == ContainerLifetime.Persistent || + resource.GetExecutableLifetimeType() == ExecutableLifetime.Persistent; + } + /// /// Determines whether the specified resource has a pull policy annotation and retrieves the value if it does. /// diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index e6b294e3d60..afbded55216 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -1462,30 +1462,6 @@ public static IResourceBuilder WithContainerFiles(this IResourceBuilder } } - /// - /// Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. - /// If set to false, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - /// - /// The type of container resource. - /// The resource builder for the container resource. - /// Should endpoints for the container resource support using a proxy? - /// The . - /// - /// This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - /// port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - /// The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - /// endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - /// - [AspireExport(Description = "Configures endpoint proxy support")] - public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource - { - ArgumentNullException.ThrowIfNull(builder); - - builder.WithAnnotation(new ProxySupportAnnotation { ProxyEnabled = proxyEnabled }, ResourceAnnotationMutationBehavior.Replace); - - return builder; - } - /// /// Builds the specified container image from a Dockerfile generated by a callback using the API. /// diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index 73474b4346a..e8f80cc59f5 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -941,9 +941,9 @@ private static List BuildContainerPorts(RenderedModelResource ContainerPort = ea.TargetPort, }; - if (!ea.IsProxied && ea.Port is int) + if (!ea.IsProxied.GetValueOrDefault() && ea.SpecifiedPort is int hostPort) { - portSpec.HostPort = ea.Port; + portSpec.HostPort = hostPort; } switch (sp.EndpointAnnotation.Protocol) diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index db80ee7dc09..7421a457904 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -112,7 +112,7 @@ public DcpExecutor(ILogger logger, _executionContext = executionContext; _appResources = appResources; - _resourceWatcher = new DcpResourceWatcher(logger, kubernetesService, loggerService, executorEvents, model, _appResources, _configuration, _shutdownCancellation.Token); + _resourceWatcher = new DcpResourceWatcher(logger, kubernetesService, loggerService, executorEvents, model, _appResources, _configuration, PublishEndpointsAllocatedEventAsync, _shutdownCancellation.Token); DeleteResourceRetryPipeline = DcpPipelineBuilder.BuildDeleteRetryPipeline(logger); @@ -206,11 +206,17 @@ public async Task RunApplicationAsync(CancellationToken ct = default) { await getProxyAddresses.ConfigureAwait(false); - DcpModelUtilities.AddWorkloadAllocatedEndpoints( - executables, - _options.Value.EnableAspireContainerTunnel, - ContainerHostName); - await PublishEndpointsAllocatedEventAsync(executables, ct).ConfigureAwait(false); + foreach (var executable in executables) + { + if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints( + executable, + _options.Value.EnableAspireContainerTunnel, + ContainerHostName, + allowPendingDynamicProxylessContainerEndpoints: false)) + { + await PublishEndpointsAllocatedEventAsync(executable.ModelResource, ct).ConfigureAwait(false); + } + } }, ct); var createExecutables = Task.Run(async () => @@ -228,20 +234,24 @@ public async Task RunApplicationAsync(CancellationToken ct = default) { await Task.WhenAll([getProxyAddresses, createContainerNetworks]).WaitAsync(ct).ConfigureAwait(false); - // Allocate container workload endpoints, then publish endpoint-allocated events. - DcpModelUtilities.AddWorkloadAllocatedEndpoints( - containers, - _options.Value.EnableAspireContainerTunnel, - ContainerHostName); - await PublishEndpointsAllocatedEventAsync(containers, ct).ConfigureAwait(false); + // Allocate container workload endpoints that are already known, then publish endpoint-allocated events. + foreach (var container in containers) + { + if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints( + container, + _options.Value.EnableAspireContainerTunnel, + ContainerHostName, + allowPendingDynamicProxylessContainerEndpoints: true)) + { + await PublishEndpointsAllocatedEventAsync(container.ModelResource, ct).ConfigureAwait(false); + } + } await CreateRenderedResourcesAsync(_containerCreator, containers, cctx, ct).ConfigureAwait(false); }, ct); // Now wait for all "leaf" creations to complete. await Task.WhenAll(createExecutables, createContainers).WaitAsync(ct).ConfigureAwait(false); - - await _executorEvents.PublishAsync(new OnEndpointsAllocatedContext(ct)).ConfigureAwait(false); } catch (Exception ex) { @@ -691,21 +701,19 @@ private void PrepareServices() var svc = Service.Create(serviceName); - if (!sp.ModelResource.SupportsProxy()) - { - // If the resource shouldn't be proxied, we need to enforce that on the annotation - endpoint.IsProxied = false; - } + endpoint.IsProxied = GetEffectiveIsProxied(sp.ModelResource, endpoint); int? port; - if (_options.Value.RandomizePorts && endpoint.IsProxied && endpoint.Port != null) + if (_options.Value.RandomizePorts && endpoint.IsProxied.Value && endpoint.Port != null) { port = null; _logger.LogDebug("Randomizing port for {ServiceName}. Original port: {OriginalPort}", serviceName, endpoint.Port); } else { - port = endpoint.Port; + port = sp.ModelResource.IsContainer() && !endpoint.IsProxied.Value + ? endpoint.SpecifiedPort + : endpoint.Port; } svc.Spec.Port = port; svc.Spec.Protocol = PortProtocol.FromProtocolType(endpoint.Protocol); @@ -718,7 +726,7 @@ private void PrepareServices() svc.Spec.Address = endpoint.TargetHost; } - if (!endpoint.IsProxied) + if (!endpoint.IsProxied.Value) { svc.Spec.AddressAllocationMode = AddressAllocationModes.Proxyless; } @@ -732,6 +740,21 @@ private void PrepareServices() } } + static bool GetEffectiveIsProxied(IResource resource, EndpointAnnotation endpoint) + { + if (!resource.SupportsProxy()) + { + return false; + } + + if (endpoint.IsProxied is bool isProxied) + { + return isProxied; + } + + return !resource.HasPersistentLifetime(); + } + var containers = _model.Resources.Where(r => r.IsContainer()); if (!containers.Any()) { @@ -1207,21 +1230,17 @@ private static void ForgetCachedCallbackResults(IResource resource) } } - private async Task PublishEndpointsAllocatedEventAsync(IEnumerable> resource, CancellationToken ct) - where TDcpResource : CustomResource, IKubernetesStaticMetadata + private async Task PublishEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) { - foreach (var r in resource) + lock (_endpointsAdvertised) { - lock (_endpointsAdvertised) + if (!_endpointsAdvertised.Add(resource.Name)) { - if (!_endpointsAdvertised.Add(r.ModelResource.Name)) - { - continue; // Already published for this resource - } + return; // Already published for this resource. } - - var ev = new ResourceEndpointsAllocatedEvent(r.ModelResource, _executionContext.ServiceProvider); - await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.NonBlockingConcurrent, ct).ConfigureAwait(false); } + + var ev = new ResourceEndpointsAllocatedEvent(resource, _executionContext.ServiceProvider); + await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.NonBlockingConcurrent, ct).ConfigureAwait(false); } } diff --git a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs index 992a9da8b76..aeba76b7967 100644 --- a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs +++ b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs @@ -38,7 +38,7 @@ internal static void AddServicesProducedInfo( throw new InvalidOperationException($"The endpoint '{ea.Name}' for container resource '{modelResourceName}' must specify the {nameof(EndpointAnnotation.TargetPort)} value"); } } - else if (!ea.IsProxied) + else if (!ea.IsProxied.GetValueOrDefault()) { if (HasMultipleReplicas(appResource.DcpResource)) { @@ -52,7 +52,7 @@ internal static void AddServicesProducedInfo( } else { - Debug.Assert(ea.IsProxied); + Debug.Assert(ea.IsProxied.GetValueOrDefault()); if (ea.TargetPort is int && ea.Port is int && ea.TargetPort == ea.Port) { @@ -92,70 +92,169 @@ internal static void AddWorkloadAllocatedEndpoints( { foreach (var res in resources) { - foreach (var sp in res.ServicesProduced) + TryAddWorkloadAllocatedEndpoints(res, enableAspireContainerTunnel, containerHostName, allowPendingDynamicProxylessContainerEndpoints: false); + } + } + + internal static bool TryAddWorkloadAllocatedEndpoints( + RenderedModelResource resource, + bool enableAspireContainerTunnel, + string containerHostName, + bool allowPendingDynamicProxylessContainerEndpoints) + where TDcpResource : CustomResource, IKubernetesStaticMetadata + { + foreach (var sp in resource.ServicesProduced) + { + if (TryAddLocalhostAllocatedEndpoint( + sp, + allowPending: allowPendingDynamicProxylessContainerEndpoints && IsDynamicProxylessContainerEndpoint(resource, sp))) { - var svc = sp.DcpResource; + AddContainerNetworkAllocatedEndpoint(resource, sp); + AddExecutableContainerNetworkAllocatedEndpoint(resource, sp, enableAspireContainerTunnel, containerHostName); + } + } - if (!svc.HasCompleteAddress && sp.EndpointAnnotation.IsProxied) - { - // This should never happen; if it does, we have a bug without a workaround for the user. - // We should have waited for the service to have a complete address before getting here. - throw new InvalidDataException($"Service {svc.Metadata.Name} should have valid address at this point"); - } + return AreResourceEndpointsAllocated(resource.ModelResource); + } - if (!sp.EndpointAnnotation.IsProxied && svc.AllocatedPort is null) - { - throw new InvalidOperationException($"Service '{svc.Metadata.Name}' needs to specify a port for endpoint '{sp.EndpointAnnotation.Name}' since it isn't using a proxy."); - } + internal static bool TryApplyServiceAddressToEndpoint(Service observedService, IEnumerable appResources, [NotNullWhen(true)] out IResource? modelResource) + { + var serviceResource = appResources.OfType() + .FirstOrDefault(swr => string.Equals(swr.DcpResource.Metadata.Name, observedService.Metadata.Name, StringComparison.Ordinal)); + + if (serviceResource is null) + { + modelResource = null; + return false; + } - var (targetHost, bindingMode) = NormalizeTargetHost(sp.EndpointAnnotation.TargetHost); + serviceResource.Service.ApplyAddressInfoFrom(observedService); + if (!TryAddLocalhostAllocatedEndpoint(serviceResource, allowPending: true)) + { + modelResource = null; + return false; + } - sp.EndpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint( - sp.EndpointAnnotation, - targetHost, - (int)svc.AllocatedPort!, - bindingMode, - targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", - KnownNetworkIdentifiers.LocalhostNetwork); + foreach (var containerResource in appResources.OfType>() + .Where(resource => ReferenceEquals(resource.ModelResource, serviceResource.ModelResource))) + { + AddContainerNetworkAllocatedEndpoint(containerResource, serviceResource); + } - if (res.DcpResource is Container ctr && ctr.Spec.Networks is not null) - { - // Once container networks are fully supported, this should allocate endpoints on those networks - var containerNetwork = ctr.Spec.Networks.FirstOrDefault(n => n.Name == KnownNetworkIdentifiers.DefaultAspireContainerNetwork.Value); - - if (containerNetwork is not null) - { - var port = sp.EndpointAnnotation.TargetPort!; - - var allocatedEndpoint = new AllocatedEndpoint( - sp.EndpointAnnotation, - $"{sp.ModelResource.Name}.dev.internal", - (int)port, - EndpointBindingMode.SingleAddress, - targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", - KnownNetworkIdentifiers.DefaultAspireContainerNetwork - ); - sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(allocatedEndpoint.NetworkID, allocatedEndpoint); - } - } + modelResource = serviceResource.ModelResource; + return AreResourceEndpointsAllocated(modelResource); + } - // If we are not using the tunnel, we can project Executable endpoints into container networks via ContainerHostName. - // This really only works for Docker Desktop, but it is useful for testing too. - if (res.DcpResource is Executable && !enableAspireContainerTunnel) - { - var port = sp.EndpointAnnotation.TargetPort!; - var allocatedEndpoint = new AllocatedEndpoint( - sp.EndpointAnnotation, - containerHostName, - (int)svc.AllocatedPort!, - EndpointBindingMode.SingleAddress, - targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", - KnownNetworkIdentifiers.DefaultAspireContainerNetwork - ); - sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, allocatedEndpoint); - } + private static bool TryAddLocalhostAllocatedEndpoint(ServiceWithModelResource sp, bool allowPending) + { + var svc = sp.DcpResource; + + if (sp.EndpointAnnotation.AllocatedEndpoint is not null) + { + return true; + } + + if (!svc.HasCompleteAddress && sp.EndpointAnnotation.IsProxied.GetValueOrDefault()) + { + // This should never happen; if it does, we have a bug without a workaround for the user. + // We should have waited for the service to have a complete address before getting here. + throw new InvalidDataException($"Service {svc.Metadata.Name} should have valid address at this point"); + } + + if (!sp.EndpointAnnotation.IsProxied.GetValueOrDefault() && svc.AllocatedPort is null) + { + if (allowPending) + { + return false; + } + + throw new InvalidOperationException($"Service '{svc.Metadata.Name}' needs to specify a port for endpoint '{sp.EndpointAnnotation.Name}' since it isn't using a proxy."); + } + + if (!svc.HasCompleteAddress) + { + if (allowPending) + { + return false; } + + throw new InvalidDataException($"Service {svc.Metadata.Name} should have valid address at this point"); + } + + var (targetHost, bindingMode) = NormalizeTargetHost(sp.EndpointAnnotation.TargetHost); + + sp.EndpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint( + sp.EndpointAnnotation, + targetHost, + (int)svc.AllocatedPort!, + bindingMode, + targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", + KnownNetworkIdentifiers.LocalhostNetwork); + + return true; + } + + private static void AddContainerNetworkAllocatedEndpoint(RenderedModelResource resource, ServiceWithModelResource sp) + where TDcpResource : CustomResource, IKubernetesStaticMetadata + { + if (resource.DcpResource is not Container ctr || ctr.Spec.Networks is null) + { + return; + } + + // Once container networks are fully supported, this should allocate endpoints on those networks. + var containerNetwork = ctr.Spec.Networks.FirstOrDefault(n => n.Name == KnownNetworkIdentifiers.DefaultAspireContainerNetwork.Value); + + if (containerNetwork is null) + { + return; } + + var port = sp.EndpointAnnotation.TargetPort!; + + var allocatedEndpoint = new AllocatedEndpoint( + sp.EndpointAnnotation, + $"{sp.ModelResource.Name}.dev.internal", + (int)port, + EndpointBindingMode.SingleAddress, + targetPortExpression: $$$"""{{- portForServing "{{{sp.DcpResource.Metadata.Name}}}" -}}""", + KnownNetworkIdentifiers.DefaultAspireContainerNetwork + ); + sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(allocatedEndpoint.NetworkID, allocatedEndpoint); + } + + private static void AddExecutableContainerNetworkAllocatedEndpoint(RenderedModelResource resource, ServiceWithModelResource sp, bool enableAspireContainerTunnel, string containerHostName) + where TDcpResource : CustomResource, IKubernetesStaticMetadata + { + if (resource.DcpResource is not Executable || enableAspireContainerTunnel) + { + return; + } + + // If we are not using the tunnel, we can project Executable endpoints into container networks via ContainerHostName. + // This really only works for Docker Desktop, but it is useful for testing too. + var allocatedEndpoint = new AllocatedEndpoint( + sp.EndpointAnnotation, + containerHostName, + (int)sp.DcpResource.AllocatedPort!, + EndpointBindingMode.SingleAddress, + targetPortExpression: $$$"""{{- portForServing "{{{sp.DcpResource.Metadata.Name}}}" -}}""", + KnownNetworkIdentifiers.DefaultAspireContainerNetwork + ); + sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, allocatedEndpoint); + } + + private static bool AreResourceEndpointsAllocated(IResource resource) + { + return !resource.TryGetEndpoints(out var endpoints) || endpoints.All(e => e.AllocatedEndpoint is not null); + } + + private static bool IsDynamicProxylessContainerEndpoint(RenderedModelResource resource, ServiceWithModelResource sp) + where TDcpResource : CustomResource, IKubernetesStaticMetadata + { + return resource.DcpResource is Container && + !sp.EndpointAnnotation.IsProxied.GetValueOrDefault() && + sp.EndpointAnnotation.SpecifiedPort is null; } internal static void AddContainerTunnelAllocatedEndpoints( diff --git a/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs b/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs index b2c65523439..52cc165a9d4 100644 --- a/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs +++ b/src/Aspire.Hosting/Dcp/DcpResourceWatcher.cs @@ -30,6 +30,7 @@ internal sealed class DcpResourceWatcher : IConsoleLogsService, IAsyncDisposable private readonly DcpExecutorEvents _executorEvents; private readonly ILogger _logger; private readonly IConfiguration _configuration; + private readonly Func _publishEndpointsAllocatedEventAsync; private readonly CancellationToken _shutdownToken; private readonly DcpResourceState _resourceState; @@ -55,6 +56,7 @@ public DcpResourceWatcher( DistributedApplicationModel model, DcpAppResourceStore appResources, IConfiguration configuration, + Func publishEndpointsAllocatedEventAsync, CancellationToken shutdownToken) { _kubernetesService = kubernetesService; @@ -62,6 +64,7 @@ public DcpResourceWatcher( _executorEvents = executorEvents; _logger = logger; _configuration = configuration; + _publishEndpointsAllocatedEventAsync = publishEndpointsAllocatedEventAsync; _shutdownToken = shutdownToken; _resourceState = new(model.Resources.ToDictionary(r => r.Name), appResources.Get()); @@ -492,6 +495,12 @@ private async Task ProcessServiceChange(WatchEventType watchEventType, Service s return; } + if (watchEventType is WatchEventType.Added or WatchEventType.Modified && + DcpModelUtilities.TryApplyServiceAddressToEndpoint(service, _resourceState.AppResources, out var allocatedResource)) + { + await _publishEndpointsAllocatedEventAsync(allocatedResource, _shutdownToken).ConfigureAwait(false); + } + foreach (var ((resourceKind, resourceName), _) in _resourceState.ResourceAssociatedServicesMap.Where(e => e.Value.Contains(service.Metadata.Name))) { await TryRefreshResource(resourceKind, resourceName).ConfigureAwait(false); diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index 39978262ce5..419b3fa3bc7 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -170,8 +170,11 @@ private void PrepareProjectExecutables() var projectArgs = new List(); var isInDebugSession = !string.IsNullOrEmpty(_configuration[DcpExecutor.DebugSessionPortVar]); + var persistent = project.GetExecutableLifetimeType() == ExecutableLifetime.Persistent; + exe.Spec.Persistent = persistent; - if (project.SupportsDebugging(_configuration, out var supportsDebuggingAnnotation)) + SupportsDebuggingAnnotation? supportsDebuggingAnnotation = null; + if (!persistent && project.SupportsDebugging(_configuration, out supportsDebuggingAnnotation)) { exe.Spec.ExecutionType = ExecutionType.IDE; exe.Spec.FallbackExecutionTypes = [ExecutionType.Process]; @@ -185,7 +188,7 @@ private void PrepareProjectExecutables() // applied later in CreateExecutableAsync() after endpoints are allocated, // unless the IDE didn't send DEBUG_SESSION_INFO (handled by the fallback branch below). } - else if (ShouldFallBackToIdeExecution(isInDebugSession, supportsDebuggingAnnotation)) + else if (!persistent && ShouldFallBackToIdeExecution(isInDebugSession, supportsDebuggingAnnotation)) { // Fall back to IDE execution with a standard ProjectLaunchConfiguration when: // 1. No SupportsDebuggingAnnotation exists (e.g. AddResource-based ProjectResource diff --git a/src/Aspire.Hosting/DistributedApplication.cs b/src/Aspire.Hosting/DistributedApplication.cs index 24d4db59d3b..c3b660a2b26 100644 --- a/src/Aspire.Hosting/DistributedApplication.cs +++ b/src/Aspire.Hosting/DistributedApplication.cs @@ -559,6 +559,14 @@ internal async Task ExecuteBeforeStartHooksAsync(CancellationToken cancellationT await subscriber.SubscribeAsync(eventing, execContext, cancellationToken).ConfigureAwait(false); } + var logger = _host.Services.GetRequiredService>(); +#pragma warning disable CS0618 // Type or member is obsolete + if (eventing is DistributedApplicationEventing { } eventingImpl && eventingImpl.HasSubscriptions()) + { + logger.LogWarning("{EventName} is obsolete and is no longer raised by the DCP executor. Use {ResourceEventName} to observe per-resource endpoint allocation.", nameof(AfterEndpointsAllocatedEvent), nameof(ResourceEndpointsAllocatedEvent)); + } +#pragma warning restore CS0618 // Type or member is obsolete + var beforeStartEvent = new BeforeStartEvent(_host.Services, _host.Services.GetRequiredService()); await eventing.PublishAsync(beforeStartEvent, cancellationToken).ConfigureAwait(false); @@ -577,8 +585,6 @@ internal async Task ExecuteBeforeStartHooksAsync(CancellationToken cancellationT #pragma warning disable ASPIREPIPELINES001 // Pipeline APIs are experimental // Execute the before-start pipeline step var pipeline = _host.Services.GetRequiredService(); - var logger = _host.Services.GetRequiredService>(); - // Cast to internal implementation to access ExecuteStepSequentiallyAsync if (pipeline is not DistributedApplicationPipeline pipelineImpl) { diff --git a/src/Aspire.Hosting/Eventing/DistributedApplicationEventing.cs b/src/Aspire.Hosting/Eventing/DistributedApplicationEventing.cs index 5333c276cc9..ce48a8e112b 100644 --- a/src/Aspire.Hosting/Eventing/DistributedApplicationEventing.cs +++ b/src/Aspire.Hosting/Eventing/DistributedApplicationEventing.cs @@ -102,6 +102,11 @@ public DistributedApplicationEventSubscription Subscribe(Func() where T : IDistributedApplicationEvent + { + return _eventSubscriptionListLookup.TryGetValue(typeof(T), out var subscriptions) && subscriptions.Count > 0; + } + /// public DistributedApplicationEventSubscription Subscribe(IResource resource, Func callback) where T : IDistributedApplicationResourceEvent { diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index c6679b4c520..a6d0372c73f 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -135,17 +135,11 @@ private async Task WaitForInBeforeResourceStartedEvent(BeforeResourceStartedEven } } - private async Task OnEndpointsAllocated(OnEndpointsAllocatedContext context) + private Task OnEndpointsAllocated(OnEndpointsAllocatedContext context) { -#pragma warning disable CS0618 // Type or member is obsolete - var afterEndpointsAllocatedEvent = new AfterEndpointsAllocatedEvent(_serviceProvider, _model); -#pragma warning restore CS0618 // Type or member is obsolete - await _eventing.PublishAsync(afterEndpointsAllocatedEvent, context.CancellationToken).ConfigureAwait(false); - - foreach (var lifecycleHook in _lifecycleHooks) - { - await lifecycleHook.AfterEndpointsAllocatedAsync(_model, context.CancellationToken).ConfigureAwait(false); - } + // Endpoint allocation can now complete after resource creation, so there is no longer a single + // app-wide point where all endpoints are guaranteed to be allocated. + return Task.CompletedTask; } private async Task PublishResourceEndpointUrls(IResource resource, CancellationToken cancellationToken) diff --git a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs index 140c6f681ec..62030268cf5 100644 --- a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs @@ -322,6 +322,35 @@ public static IResourceBuilder AddProject(this IDistributedAppl .WithProjectDefaults(options); } + /// + /// Sets the lifetime behavior of the project executable resource. + /// + /// The project resource type. + /// Builder for the project resource. + /// The lifetime behavior of the project executable resource. The default behavior is . + /// The . + /// + /// + /// Marking a project resource to have a lifetime. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// builder.AddProject<Projects.ApiService>("api") + /// .WithLifetime(ExecutableLifetime.Persistent); + /// + /// builder.Build().Run(); + /// + /// + /// + [AspireExport("withProjectExecutableLifetime", Description = "Sets the lifetime behavior of the project executable resource")] + public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ExecutableLifetime lifetime) + where TProjectResource : ProjectResource + { + ArgumentNullException.ThrowIfNull(builder); + + return builder.WithAnnotation(new ExecutableLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + } + /// /// Adds a C# project or file-based app to the application model. /// diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 0825f80a763..361cb754fee 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -1347,12 +1347,12 @@ private static IResourceBuilder WithWellKnownEndpointCallback(this IResour /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. /// Indicates that this endpoint should be exposed externally at publish time. /// Network protocol: TCP or UDP are supported today, others possibly in future. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. [AspireExport(Description = "Adds a network endpoint")] [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "")] - public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null, ProtocolType? protocol = null) where T : IResourceWithEndpoints + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, [EndpointName] string? name = null, string? env = null, bool? isProxied = null, bool? isExternal = null, ProtocolType? protocol = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); @@ -1380,11 +1380,9 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build existing.IsExternal = isExternal.Value; } - // Only apply isProxied when explicitly set to false — the default is true, - // so false is always intentional and safe to apply. - if (!isProxied) + if (isProxied is not null) { - existing.IsProxied = false; + existing.IsProxied = isProxied; } ConfigureEndpointEnvironmentVariable(builder, existing, env); @@ -1443,6 +1441,30 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder })); } + /// + /// Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + /// If set to false, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + /// + /// The resource type. + /// The resource builder. + /// Should endpoints for the resource support using a proxy? + /// The . + /// + /// This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + /// port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + /// The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + /// endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + /// + [AspireExport(Description = "Configures endpoint proxy support")] + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : IResourceWithEndpoints + { + ArgumentNullException.ThrowIfNull(builder); + + builder.WithAnnotation(new ProxySupportAnnotation { ProxyEnabled = proxyEnabled }, ResourceAnnotationMutationBehavior.Replace); + + return builder; + } + /// /// Exposes an endpoint on a resource. This endpoint reference can be retrieved using . /// The endpoint name will be the scheme name if not specified. @@ -1455,7 +1477,7 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder /// An optional name of the endpoint. Defaults to the scheme name if not specified. /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. /// Indicates that this endpoint should be exposed externally at publish time. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . /// The . /// Throws an exception if an endpoint with the same name already exists on the specified resource. /// @@ -1463,7 +1485,7 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder /// If an endpoint with the same name already exists, the existing endpoint is updated with any non-null parameter values. /// [AspireExportIgnore(Reason = "Subset of the full WithEndpoint overload which is already exported.")] - public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port, int? targetPort, string? scheme, [EndpointName] string? name, string? env, bool isProxied, bool? isExternal) where T : IResourceWithEndpoints + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port, int? targetPort, string? scheme, [EndpointName] string? name, string? env, bool? isProxied, bool? isExternal) where T : IResourceWithEndpoints { return WithEndpoint(builder, port, targetPort, scheme, name, env, isProxied, isExternal, protocol: null); } @@ -1479,14 +1501,14 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build /// An optional port. This is the port that will be given to other resource to communicate with this resource. /// An optional name of the endpoint. Defaults to "http" if not specified. /// An optional name of the environment variable to inject. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . /// The . /// /// If an endpoint with the same name already exists on the resource, the existing endpoint is updated /// with any non-null parameter values. Parameters left as will not modify the existing endpoint's values. /// [AspireExport(Description = "Adds an HTTP endpoint")] - public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool? isProxied = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); @@ -1504,14 +1526,14 @@ public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder b /// An optional host port. /// An optional name of the endpoint. Defaults to "https" if not specified. /// An optional name of the environment variable to inject. - /// Specifies if the endpoint will be proxied by DCP. Defaults to true. + /// Specifies if the endpoint will be proxied by DCP. Defaults to . /// The . /// /// If an endpoint with the same name already exists on the resource, the existing endpoint is updated /// with any non-null parameter values. Parameters left as will not modify the existing endpoint's values. /// [AspireExport(Description = "Adds an HTTPS endpoint")] - public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool isProxied = true) where T : IResourceWithEndpoints + public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port = null, int? targetPort = null, [EndpointName] string? name = null, string? env = null, bool? isProxied = null) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); @@ -1546,7 +1568,7 @@ public static IResourceBuilder WithExternalHttpEndpoints(this IResourceBui } /// - /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). + /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). /// The can be used to resolve the address of the endpoint in . /// /// The resource type. @@ -1564,7 +1586,7 @@ public static EndpointReference GetEndpoint(this IResourceBuilder builder, } /// - /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). + /// Gets an by name from the resource. These endpoints are declared either using or by launch settings (for project resources). /// The can be used to resolve the address of the endpoint in . /// /// The resource type. diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.cs b/src/Aspire.Hosting/api/Aspire.Hosting.cs index e8d1ca730bd..42fa50f02ad 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.cs +++ b/src/Aspire.Hosting/api/Aspire.Hosting.cs @@ -202,10 +202,6 @@ public static ApplicationModel.IResourceBuilder WithDockerfileFactory(this public static ApplicationModel.IResourceBuilder WithDockerfileFactory(this ApplicationModel.IResourceBuilder builder, string contextPath, System.Func> dockerfileFactory, string? stage = null) where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport("withEndpointProxySupport", Description = "Configures endpoint proxy support")] - public static ApplicationModel.IResourceBuilder WithEndpointProxySupport(this ApplicationModel.IResourceBuilder builder, bool proxyEnabled) - where T : ApplicationModel.ContainerResource { throw null; } - [AspireExport("withEntrypoint", Description = "Sets the container entrypoint")] public static ApplicationModel.IResourceBuilder WithEntrypoint(this ApplicationModel.IResourceBuilder builder, string entrypoint) where T : ApplicationModel.ContainerResource { throw null; } @@ -983,6 +979,10 @@ public static partial class ProjectResourceBuilderExtensions [AspireExport("disableForwardedHeaders", Description = "Disables forwarded headers for the project")] public static ApplicationModel.IResourceBuilder DisableForwardedHeaders(this ApplicationModel.IResourceBuilder builder) { throw null; } + [AspireExport("withProjectExecutableLifetime", Description = "Sets the lifetime behavior of the project executable resource")] + public static ApplicationModel.IResourceBuilder WithLifetime(this ApplicationModel.IResourceBuilder builder, ApplicationModel.ExecutableLifetime lifetime) + where TProjectResource : ApplicationModel.ProjectResource { throw null; } + [AspireExport("publishProjectAsDockerFileWithConfigure", MethodName = "publishAsDockerFile", Description = "Publishes a project as a Docker file with optional container configuration", RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder PublishAsDockerFile(this ApplicationModel.IResourceBuilder builder, System.Action>? configure = null) where T : ApplicationModel.ProjectResource { throw null; } @@ -1147,12 +1147,16 @@ public static ApplicationModel.IResourceBuilder WithDebugSupport WithDeveloperCertificateTrust(this ApplicationModel.IResourceBuilder builder, bool trust) where TResource : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithArgs { throw null; } + [AspireExport("withEndpointProxySupport", Description = "Configures endpoint proxy support")] + public static ApplicationModel.IResourceBuilder WithEndpointProxySupport(this ApplicationModel.IResourceBuilder builder, bool proxyEnabled) + where T : ApplicationModel.IResourceWithEndpoints { throw null; } + [AspireExport("withEndpoint", Description = "Adds a network endpoint")] - public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null, System.Net.Sockets.ProtocolType? protocol = null) + public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, string? name = null, string? env = null, bool? isProxied = null, bool? isExternal = null, System.Net.Sockets.ProtocolType? protocol = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [AspireExportIgnore(Reason = "Subset of the full WithEndpoint overload which is already exported.")] - public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port, int? targetPort, string? scheme, string? name, string? env, bool isProxied, bool? isExternal) + public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port, int? targetPort, string? scheme, string? name, string? env, bool? isProxied, bool? isExternal) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [AspireExportIgnore(Reason = "EndpointAnnotation has read-only properties AllocatedEndpointSnapshot and AllAllocatedEndpoints that are not ATS-compatible. Callback-free variant is exported.")] @@ -1224,7 +1228,7 @@ public static ApplicationModel.IResourceBuilder WithHttpCommand WithHttpEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool isProxied = true) + public static ApplicationModel.IResourceBuilder WithHttpEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool? isProxied = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [AspireExportIgnore(Reason = "Func delegate — not ATS-compatible.")] @@ -1261,7 +1265,7 @@ public static ApplicationModel.IResourceBuilder WithHttpsDeveloperCer where TResource : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithArgs { throw null; } [AspireExport("withHttpsEndpoint", Description = "Adds an HTTPS endpoint")] - public static ApplicationModel.IResourceBuilder WithHttpsEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool isProxied = true) + public static ApplicationModel.IResourceBuilder WithHttpsEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool? isProxied = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [System.Obsolete("This method is obsolete and will be removed in a future version. Use the WithHttpHealthCheck method instead.")] @@ -2197,9 +2201,9 @@ public sealed partial class EmulatorResourceAnnotation : IResourceAnnotation [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}")] public sealed partial class EndpointAnnotation : IResourceAnnotation { - public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, NetworkIdentifier? networkID, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true) { } + public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, NetworkIdentifier? networkID, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool? isProxied = null) { } - public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true) { } + public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool? isProxied = null) { } public NetworkEndpointSnapshotList AllAllocatedEndpoints { get { throw null; } } @@ -2214,7 +2218,7 @@ public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriS public bool IsExternal { get { throw null; } set { } } - public bool IsProxied { get { throw null; } set { } } + public bool? IsProxied { get { throw null; } set { } } public string Name { get { throw null; } set { } } diff --git a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs index 7a66284836a..43a5846ab51 100644 --- a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs +++ b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs @@ -55,6 +55,28 @@ public void AddDevTunnel_WithSpecificTunnelId_SetsTunnelIdProperty() Assert.Equal("custom-id", tunnel.Resource.TunnelId); } + [Fact] + public void AddDevTunnel_WithPersistentExecutableLifetime_AddsExecutableLifetimeAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var tunnel = builder.AddDevTunnel("tunnel", "custom-id") + .WithLifetime(ExecutableLifetime.Persistent); + + Assert.True(tunnel.Resource.TryGetLastAnnotation(out var annotation)); + Assert.Equal(ExecutableLifetime.Persistent, annotation.Lifetime); + } + + [Fact] + public void AddDevTunnel_DefaultLifetimeDoesNotAddExecutableLifetimeAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var tunnel = builder.AddDevTunnel("tunnel", "custom-id"); + + Assert.False(tunnel.Resource.TryGetLastAnnotation(out _)); + } + [Fact] public void WithReference_WithAnonymousAccess_SetsPortAllowAnonymousOption() { @@ -70,6 +92,60 @@ public void WithReference_WithAnonymousAccess_SetsPortAllowAnonymousOption() Assert.True(port.Options.AllowAnonymous); } + [Fact] + public async Task WithReference_UsesTargetPortForDevTunnelPortWhenAvailable() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var target = builder.AddProject("target") + .WithHttpEndpoint(port: 5000, targetPort: 5001, name: "http"); + var tunnel = builder.AddDevTunnel("tunnel") + .WithReference(target); + + var tunnelPort = Assert.Single(tunnel.Resource.Ports); + + Assert.Equal(5001, await tunnelPort.GetTunnelPortAsync()); + } + + [Fact] + public async Task WithReference_ResolvesDynamicTargetPortForDevTunnelPortWhenAvailable() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var target = builder.AddProject("target") + .WithHttpEndpoint(port: 5000, name: "http"); + var tunnel = builder.AddDevTunnel("tunnel") + .WithReference(target); + + var tunnelPort = Assert.Single(tunnel.Resource.Ports); + tunnelPort.TargetEndpoint.EndpointAnnotation.AllocatedEndpoint = new( + tunnelPort.TargetEndpoint.EndpointAnnotation, + "localhost", + 5000, + targetPortExpression: "5001"); + + Assert.Equal(5001, await tunnelPort.GetTunnelPortAsync()); + } + + [Fact] + public async Task WithReference_UsesAllocatedPortForDevTunnelPortWhenTargetPortIsUnavailable() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var target = builder.AddProject("target") + .WithHttpEndpoint(port: 5000, name: "http"); + var tunnel = builder.AddDevTunnel("tunnel") + .WithReference(target); + + var tunnelPort = Assert.Single(tunnel.Resource.Ports); + tunnelPort.TargetEndpoint.EndpointAnnotation.AllocatedEndpoint = new( + tunnelPort.TargetEndpoint.EndpointAnnotation, + "localhost", + 5000); + + Assert.Equal(5000, await tunnelPort.GetTunnelPortAsync()); + } + [Fact] public void GetEndpoint_WithResourceAndEndpointName_ReturnsTunnelEndpoint() { diff --git a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj index ef4d47e9d71..e8e803885f9 100644 --- a/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj +++ b/tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 20d31c86aef..33aa8d18f6f 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -486,7 +486,7 @@ public async Task EndpointPortsExecutableNotReplicatedProxiedPortSetNoTargetPort const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1000; var exe = builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") - .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT", isProxied: true); + .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT"); var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); @@ -598,6 +598,70 @@ public async Task UnsupportedEndpointPortsExecutableNotReplicatedProxied() Assert.Contains("cannot be proxied when both TargetPort and Port are specified with the same value", exception.Message); } + [Fact] + public async Task EndpointPortsExecutableWithEndpointProxySupportUsesProxylessEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1001; + builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") + .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT") + .WithEndpointProxySupport(false); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + await appExecutor.RunApplicationAsync(); + + var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "CoolProgram"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "CoolProgram").Port); + + var envVarVal = dcpExe.Spec.Env?.Single(v => v.Name == "PORT_SET_NO_TARGET_PORT").Value; + Assert.False(string.IsNullOrWhiteSpace(envVarVal)); + Assert.Equal(desiredPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); + } + + [Fact] + public async Task EndpointPortsPersistentExecutableDefaultsToProxylessEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1002; + builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") + .WithLifetime(ExecutableLifetime.Persistent) + .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT"); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + await appExecutor.RunApplicationAsync(); + + var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "CoolProgram"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "CoolProgram").Port); + + var envVarVal = dcpExe.Spec.Env?.Single(v => v.Name == "PORT_SET_NO_TARGET_PORT").Value; + Assert.False(string.IsNullOrWhiteSpace(envVarVal)); + Assert.Equal(desiredPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); + } + [Fact] public async Task EndpointPortsExecutableNotReplicatedProxylessPortSetNoTargetPort() { @@ -607,9 +671,6 @@ public async Task EndpointPortsExecutableNotReplicatedProxylessPortSetNoTargetPo builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -641,9 +702,6 @@ public async Task EndpointPortsExecutableNotReplicatedProxylessNoPortTargetPortS builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") .WithEndpoint(name: "NoPortTargetPortSet", targetPort: desiredPort, env: "NO_PORT_TARGET_PORT_SET", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -675,9 +733,6 @@ public async Task EndpointPortsExecutableNotReplicatedProxylessPortAndTargetPort builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") .WithEndpoint(name: "PortAndTargetPortSet", port: desiredPort, targetPort: desiredPort, env: "PORT_AND_TARGET_PORT_SET", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -1209,7 +1264,7 @@ public async Task EndpointPortsProjectNoPortNoTargetPort() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); await appExecutor.RunApplicationAsync(); - var exes = kubernetesService.CreatedResources.OfType().ToList(); + var exes = kubernetesService.CreatedResources.OfType().Where(e => e.AppModelResourceName == "ServiceA").ToList(); Assert.Equal(3, exes.Count); foreach (var dcpExe in exes) @@ -1245,7 +1300,7 @@ public async Task EndpointPortsProjectPortSetNoTargetPort() const int desiredPortOne = TestKubernetesService.StartOfAutoPortRange - 1000; builder.AddProject("ServiceA") - .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPortOne, env: "PORT_SET_NO_TARGET_PORT", isProxied: true) + .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPortOne, env: "PORT_SET_NO_TARGET_PORT") .WithReplicas(3); var kubernetesService = new TestKubernetesService(); @@ -1254,7 +1309,7 @@ public async Task EndpointPortsProjectPortSetNoTargetPort() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); await appExecutor.RunApplicationAsync(); - var exes = kubernetesService.CreatedResources.OfType().ToList(); + var exes = kubernetesService.CreatedResources.OfType().Where(e => e.AppModelResourceName == "ServiceA").ToList(); Assert.Equal(3, exes.Count); foreach (var dcpExe in exes) @@ -1276,6 +1331,74 @@ public async Task EndpointPortsProjectPortSetNoTargetPort() } } + [Fact] + public async Task EndpointPortsProjectWithEndpointProxySupportUsesProxylessEndpoint() + { + var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions + { + AssemblyName = typeof(DistributedApplicationTests).Assembly.FullName + }); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1001; + builder.AddProject("ServiceA", launchProfileName: null) + .WithHttpEndpoint(name: "stable", port: desiredPort) + .WithEndpointProxySupport(false); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + await appExecutor.RunApplicationAsync(); + + var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "ServiceA"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "ServiceA").Port); + + var aspnetCoreUrls = dcpExe.Spec.Env?.Single(v => v.Name == "ASPNETCORE_URLS").Value; + Assert.Equal($"http://localhost:{desiredPort}", aspnetCoreUrls); + } + + [Fact] + public async Task EndpointPortsPersistentProjectDefaultsToProxylessEndpoint() + { + var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions + { + AssemblyName = typeof(DistributedApplicationTests).Assembly.FullName + }); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1002; + builder.AddProject("ServiceA", launchProfileName: null) + .WithLifetime(ExecutableLifetime.Persistent) + .WithHttpEndpoint(name: "stable", port: desiredPort); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); + await appExecutor.RunApplicationAsync(); + + var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "ServiceA"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "ServiceA").Port); + + var aspnetCoreUrls = dcpExe.Spec.Env?.Single(v => v.Name == "ASPNETCORE_URLS").Value; + Assert.Equal($"http://localhost:{desiredPort}", aspnetCoreUrls); + } + [Fact] public async Task EndpointPortsConainerProxiedNoPortTargetPortSet() { @@ -1398,9 +1521,6 @@ public async Task EndpointPortsContainerProxylessPortSetNoTargetPort() builder.AddContainer("database", "image") .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -1434,9 +1554,6 @@ public async Task EndpointPortsContainerProxylessNoPortTargetPortSet() builder.AddContainer("database", "image") .WithEndpoint(name: "NoPortTargetPortSet", targetPort: desiredTargetPort, env: "NO_PORT_TARGET_PORT_SET", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -1446,14 +1563,15 @@ public async Task EndpointPortsContainerProxylessNoPortTargetPortSet() var dcpCtr = Assert.Single(kubernetesService.CreatedResources.OfType()); Assert.True(dcpCtr.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); - // Port is empty, TargetPort is set + // Port is empty, TargetPort is set. // Clients connect directly to the container host port, MAY have the container host port injected. - // Container is using TargetPort for BOTH listening inside the container and as a host port. + // DCP allocates the container host port after the container is created. var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "database"); Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); - Assert.Equal(desiredTargetPort, svc.Status?.EffectivePort); + Assert.Null(svc.Spec.Port); + Assert.True(svc.Status?.EffectivePort >= TestKubernetesService.StartOfAutoPortRange); Assert.NotNull(dcpCtr.Spec.Ports); - Assert.Contains(dcpCtr.Spec.Ports!, p => p.HostPort == desiredTargetPort && p.ContainerPort == desiredTargetPort); + Assert.Contains(dcpCtr.Spec.Ports!, p => p.HostPort is null && p.ContainerPort == desiredTargetPort); // Desired port should be part of the service producer annotation. Assert.Equal(desiredTargetPort, spAnnList.Single(ann => ann.ServiceName == "database").Port); var envVarVal = dcpCtr.Spec.Env?.Single(v => v.Name == "NO_PORT_TARGET_PORT_SET").Value; @@ -1461,6 +1579,42 @@ public async Task EndpointPortsContainerProxylessNoPortTargetPortSet() Assert.Equal(desiredTargetPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); } + [Fact] + public async Task EndpointPortsContainerProxylessNoPortTargetPortSetPublishesAllocatedEndpointAfterServiceUpdate() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredTargetPort = TestKubernetesService.StartOfAutoPortRange - 999; + builder.AddContainer("database", "image") + .WithEndpoint(name: "NoPortTargetPortSet", targetPort: desiredTargetPort, isProxied: false); + + var allocatedPortChannel = Channel.CreateUnbounded(); + var eventing = new Hosting.Eventing.DistributedApplicationEventing(); + eventing.Subscribe((@event, ct) => + { + if (@event.Resource.Name == "database") + { + var endpoint = ((IResourceWithEndpoints)@event.Resource).GetEndpoint("NoPortTargetPortSet"); + if (endpoint.AllocatedEndpoint is { } allocatedEndpoint) + { + allocatedPortChannel.Writer.TryWrite(allocatedEndpoint.Port); + } + } + + return Task.CompletedTask; + }); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, distributedApplicationEventing: eventing); + await appExecutor.RunApplicationAsync(); + + var allocatedPort = await allocatedPortChannel.Reader.ReadAsync().AsTask().DefaultTimeout(); + + Assert.True(allocatedPort >= TestKubernetesService.StartOfAutoPortRange); + } + [Fact] public async Task EndpointPortsContainerProxylessPortAndTargetPortSet() { @@ -1471,9 +1625,6 @@ public async Task EndpointPortsContainerProxylessPortAndTargetPortSet() builder.AddContainer("database", "image") .WithEndpoint(name: "PortAndTargetPortSet", port: desiredPort, targetPort: desiredTargetPort, env: "PORT_AND_TARGET_PORT_SET", isProxied: false); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -1508,9 +1659,6 @@ public async Task EndpointPortsContainerProxylessProtocolSet() builder.AddContainer("database", "image") .WithEndpoint(name: "PortAndProtocolSet", port: desiredPort, targetPort: desiredTargetPort, env: "PORT_AND_PROTOCOL_SET", isProxied: false, protocol: System.Net.Sockets.ProtocolType.Udp); - // All these configurations are effectively the same because EndpointAnnotation constructor for proxy-less endpoints - // will make sure Port and TargetPort have the same value if one is specified but the other is not. - var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); var distributedAppModel = app.Services.GetRequiredService(); @@ -3844,7 +3992,8 @@ private static DcpExecutor CreateAppExecutor( IKubernetesService? kubernetesService = null, DcpOptions? dcpOptions = null, ResourceLoggerService? resourceLoggerService = null, - DcpExecutorEvents? events = null) + DcpExecutorEvents? events = null, + Hosting.Eventing.IDistributedApplicationEventing? distributedApplicationEventing = null) { if (configuration == null) { @@ -3913,7 +4062,7 @@ private static DcpExecutor CreateAppExecutor( distributedAppModel, ks, configuration, - new Hosting.Eventing.DistributedApplicationEventing(), + distributedApplicationEventing ?? new Hosting.Eventing.DistributedApplicationEventing(), Options.Create(dcpOptions), executionContext, resourceLoggerService, diff --git a/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs b/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs index 949e127a3a6..6e0416331f6 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs @@ -70,12 +70,15 @@ static T Clone(T r) // "Allocate" port for a service. if (res is Service svc) { - if (svc.Status is null) + if (svc.Spec.AddressAllocationMode != AddressAllocationModes.Proxyless || svc.Spec.Port is not null) { - svc.Status = new ServiceStatus(); + if (svc.Status is null) + { + svc.Status = new ServiceStatus(); + } + svc.Status.EffectiveAddress = svc.Spec.Address ?? "localhost"; + svc.Status.EffectivePort = svc.Spec.Port ?? Interlocked.Increment(ref _nextPort); } - svc.Status.EffectiveAddress = svc.Spec.Address ?? "localhost"; - svc.Status.EffectivePort = svc.Spec.Port ?? Interlocked.Increment(ref _nextPort); } // Simulate proxy startup by marking it as running immediately. @@ -92,10 +95,15 @@ static T Clone(T r) lock (CreatedResources) { + var modifiedResources = AllocateProxylessContainerServicePorts(res); CreatedResources.Enqueue(res); foreach (var c in _watchChannels) { c.Writer.TryWrite((WatchEventType.Added, res)); + foreach (var modifiedResource in modifiedResources) + { + c.Writer.TryWrite((WatchEventType.Modified, modifiedResource)); + } } } @@ -113,6 +121,32 @@ public void PushResourceModified(CustomResource resource) } } + private List AllocateProxylessContainerServicePorts(CustomResource resource) + { + if (resource is not Container container || + container.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var servicesProduced) is not true) + { + return []; + } + + var modifiedResources = new List(); + foreach (var serviceProduced in servicesProduced) + { + var service = CreatedResources.OfType().FirstOrDefault(s => s.Metadata.Name == serviceProduced.ServiceName); + if (service?.Spec.AddressAllocationMode != AddressAllocationModes.Proxyless || service.Spec.Port is not null || service.Status?.EffectivePort is not null) + { + continue; + } + + service.Status ??= new ServiceStatus(); + service.Status.EffectiveAddress = service.Spec.Address ?? "localhost"; + service.Status.EffectivePort = Interlocked.Increment(ref _nextPort); + modifiedResources.Add(service); + } + + return modifiedResources; + } + public async Task DeleteAsync(string name, string? namespaceParameter = null, CancellationToken cancellationToken = default) where T : CustomResource, IKubernetesStaticMetadata { try diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index d0a6cf44da9..cd5e112a0ef 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -670,9 +670,9 @@ public void TryAddWillNotAddTheSameLifecycleHook() } [Fact] - public async Task AllocatedPortsAssignedAfterHookRuns() + public async Task AfterEndpointsAllocatedLifecycleHookIsNotCalled() { - using var testProgram = CreateTestProgram("ports-assigned-after-hook-runs"); + using var testProgram = CreateTestProgram("after-endpoints-allocated-hook-not-called"); 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)); @@ -682,15 +682,7 @@ public async Task AllocatedPortsAssignedAfterHookRuns() await app.StartAsync().DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); - var appModel = await tcs.Task.DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout); - - foreach (var item in appModel.Resources) - { - if (item is IResourceWithEndpoints resourceWithEndpoints) - { - Assert.True(resourceWithEndpoints.GetEndpoints().All(e => e.IsAllocated)); - } - } + Assert.False(tcs.Task.IsCompleted); } #pragma warning disable CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. @@ -1914,6 +1906,7 @@ public async Task AfterResourcesCreatedLifecycleHookWorks() await app.StartAsync().DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); await kubernetesLifecycle.HooksCompleted.DefaultTimeout(TestConstants.DefaultOrchestratorTestTimeout); + Assert.False(kubernetesLifecycle.AfterEndpointsAllocatedCalled); } [Fact] @@ -2048,26 +2041,22 @@ private sealed class KubernetesTestLifecycleHook : IDistributedApplicationLifecy #pragma warning restore CS0618 // Lifecycle hooks are obsolete, but still need to be tested until removed. { private readonly TaskCompletionSource _tcs = new(); - private readonly CountdownEvent _cevent = new(2); // AfterResourcesCreated and AfterEndpointsAllocated public IKubernetesService? KubernetesService { get; set; } + public bool AfterEndpointsAllocatedCalled { get; private set; } public Task HooksCompleted => _tcs.Task; - public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) { - if (_cevent.Signal()) - { - _tcs.TrySetResult(); - } + AfterEndpointsAllocatedCalled = true; + return Task.CompletedTask; } - public async Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + public Task AfterResourcesCreatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) { - if (_cevent.Signal()) - { - _tcs.TrySetResult(); - } + _tcs.TrySetResult(); + return Task.CompletedTask; } } diff --git a/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs b/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs index acbba2a8808..3aa55fd2d02 100644 --- a/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs +++ b/tests/Aspire.Hosting.Tests/Eventing/DistributedApplicationBuilderEventingTests.cs @@ -7,6 +7,8 @@ using Aspire.Hosting.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; namespace Aspire.Hosting.Tests.Eventing; @@ -259,11 +261,36 @@ public async Task LifeycleHookAnalogousEventsFire() await app.StartAsync(); var allFired = ManualResetEvent.WaitAll( - [beforeStartEventFired.WaitHandle, afterEndpointsAllocatedEventFired.WaitHandle, afterResourcesCreatedEventFired.WaitHandle], + [beforeStartEventFired.WaitHandle, afterResourcesCreatedEventFired.WaitHandle], TimeSpan.FromSeconds(10) ); Assert.True(allFired); + Assert.False(afterEndpointsAllocatedEventFired.IsSet); + await app.StopAsync(); + } + + [Fact] + public async Task ObsoleteAfterEndpointsAllocatedEventSubscriptionLogsWarning() + { + var testSink = new TestSink(); + + using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper); + builder.Services.AddLogging(logging => logging.AddProvider(new TestLoggerProvider(testSink))); +#pragma warning disable CS0618 // Type or member is obsolete + builder.Eventing.Subscribe((e, ct) => Task.CompletedTask); +#pragma warning restore CS0618 // Type or member is obsolete + + using var app = builder.Build(); + await app.StartAsync(); + +#pragma warning disable CS0618 // Type or member is obsolete + Assert.Contains(testSink.Writes, w => + w.LogLevel == LogLevel.Warning && + w.Message?.Contains(nameof(AfterEndpointsAllocatedEvent), StringComparison.Ordinal) == true && + w.Message?.Contains(nameof(ResourceEndpointsAllocatedEvent), StringComparison.Ordinal) == true); +#pragma warning restore CS0618 // Type or member is obsolete + await app.StopAsync(); } diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs new file mode 100644 index 00000000000..93af6725bb4 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs @@ -0,0 +1,27 @@ +// 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.Utils; + +namespace Aspire.Hosting.Tests; + +[Trait("Partition", "2")] +public class ProjectResourceBuilderExtensionTests +{ + [Fact] + public void WithLifetimeAddsExecutableLifetimeAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var project = builder.AddProject("project", options => options.ExcludeLaunchProfile = true) + .WithLifetime(ExecutableLifetime.Persistent); + + var annotation = project.Resource.Annotations.OfType().Single(); + Assert.Equal(ExecutableLifetime.Persistent, annotation.Lifetime); + } + + private sealed class TestProject : IProjectMetadata + { + public string ProjectPath => "test.csproj"; + } +} From 54180cc1174de655f1dda30dbf30061ca4bd8bbe Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 14 May 2026 18:41:59 -0700 Subject: [PATCH 03/38] Unify resource lifetime API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ExecutableLifetimeAnnotation.cs | 2 +- .../ApplicationModel/Lifetime.cs | 20 ++++++++ .../ApplicationModel/ResourceExtensions.cs | 12 ++--- .../ContainerResourceBuilderExtensions.cs | 4 +- src/Aspire.Hosting/Dcp/DcpNameGenerator.cs | 2 +- src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 6 +-- .../ExecutableResourceBuilderExtensions.cs | 19 ++++++-- .../ProjectResourceBuilderExtensions.cs | 19 ++++++-- .../ResourceBuilderExtensions.cs | 47 +++++++++++++++++++ src/Aspire.Hosting/api/Aspire.Hosting.cs | 44 +++++------------ ...DevTunnelResourceBuilderExtensionsTests.cs | 4 +- .../Dcp/DcpExecutorTests.cs | 8 ++-- ...ExecutableResourceBuilderExtensionTests.cs | 4 +- .../ProjectResourceBuilderExtensionTests.cs | 4 +- .../ResourceBuilderLifetimeTests.cs | 35 ++++++++++++++ 15 files changed, 165 insertions(+), 65 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/Lifetime.cs create mode 100644 tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs diff --git a/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs index 27949e57150..858277d3a3d 100644 --- a/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs @@ -30,5 +30,5 @@ public sealed class ExecutableLifetimeAnnotation : IResourceAnnotation /// /// Gets or sets the lifetime type for the executable resource. /// - public required ExecutableLifetime Lifetime { get; set; } + public required Lifetime Lifetime { get; set; } } diff --git a/src/Aspire.Hosting/ApplicationModel/Lifetime.cs b/src/Aspire.Hosting/ApplicationModel/Lifetime.cs new file mode 100644 index 00000000000..851ab02a2bc --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/Lifetime.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Lifetime modes for resources that can outlive the app host process. +/// +public enum Lifetime +{ + /// + /// Create the resource when the app host process starts and dispose of it when the app host process shuts down. + /// + Session, + + /// + /// Attempt to re-use a previously created resource if one exists. Do not destroy the resource on app host process shutdown. + /// + Persistent, +} diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index afcf763064b..dad313cab0d 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1059,21 +1059,21 @@ internal static ContainerLifetime GetContainerLifetimeType(this IResource resour /// /// Gets the lifetime type of the executable for the specified resource. - /// Defaults to if no is found. + /// Defaults to if no is found. /// /// The resource to get the ExecutableLifetimeType for. /// - /// The from the for the resource (if the annotation exists). - /// Defaults to if the annotation is not set. + /// The from the for the resource (if the annotation exists). + /// Defaults to if the annotation is not set. /// - internal static ExecutableLifetime GetExecutableLifetimeType(this IResource resource) + internal static Lifetime GetExecutableLifetimeType(this IResource resource) { if (resource.TryGetLastAnnotation(out var lifetimeAnnotation)) { return lifetimeAnnotation.Lifetime; } - return ExecutableLifetime.Session; + return Lifetime.Session; } /// @@ -1084,7 +1084,7 @@ internal static ExecutableLifetime GetExecutableLifetimeType(this IResource reso internal static bool HasPersistentLifetime(this IResource resource) { return resource.GetContainerLifetimeType() == ContainerLifetime.Persistent || - resource.GetExecutableLifetimeType() == ExecutableLifetime.Persistent; + resource.GetExecutableLifetimeType() == Lifetime.Persistent; } /// diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index afbded55216..c89070b3c7a 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -532,7 +532,7 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// /// The resource type. /// Builder for the container resource. - /// The lifetime behavior of the container resource. The defaults behavior is . + /// The lifetime behavior of the container resource. The default behavior is . /// The . /// /// @@ -547,7 +547,7 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// /// /// - [AspireExport(Description = "Sets the lifetime behavior of the container resource")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the resource-level overload that accepts Lifetime.")] public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ContainerLifetime lifetime) where T : ContainerResource { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs index 623555cbbe1..83e887b8a79 100644 --- a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs +++ b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs @@ -82,7 +82,7 @@ private static void AddInstancesAnnotation(IResource resource, ImmutableArray GetRandomNameSuffix(), + Lifetime.Session => GetRandomNameSuffix(), _ => GetProjectHashSuffix(), }; diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index 419b3fa3bc7..75afca05f89 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -170,7 +170,7 @@ private void PrepareProjectExecutables() var projectArgs = new List(); var isInDebugSession = !string.IsNullOrEmpty(_configuration[DcpExecutor.DebugSessionPortVar]); - var persistent = project.GetExecutableLifetimeType() == ExecutableLifetime.Persistent; + var persistent = project.GetExecutableLifetimeType() == Lifetime.Persistent; exe.Spec.Persistent = persistent; SupportsDebuggingAnnotation? supportsDebuggingAnnotation = null; @@ -283,7 +283,7 @@ private void PreparePlainExecutables() exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); exe.Annotate(CustomResource.ResourceNameAnnotation, executable.Name); - var persistent = executable.GetExecutableLifetimeType() == ExecutableLifetime.Persistent; + var persistent = executable.GetExecutableLifetimeType() == Lifetime.Persistent; if (persistent) { exe.Spec.Persistent = true; @@ -428,7 +428,7 @@ private async Task BuildExecutableConfiguration(Rendere private string GetCertificatesRootDirectory(RenderedModelResource er, Executable exe) { - if (er.ModelResource.GetExecutableLifetimeType() == ExecutableLifetime.Persistent) + if (er.ModelResource.GetExecutableLifetimeType() == Lifetime.Persistent) { return Path.Join(_aspireStore.BasePath, "dcp", "executables", exe.Metadata.Name, "certificates"); } diff --git a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs index 8c349536831..3c7e92001ff 100644 --- a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs @@ -73,29 +73,38 @@ public static IResourceBuilder AddExecutable(this IDistribut /// /// The resource type. /// Builder for the executable resource. - /// The lifetime behavior of the executable resource. The default behavior is . + /// The lifetime behavior of the executable resource. The default behavior is . /// The . /// /// - /// Marking an executable resource to have a lifetime. + /// Marking an executable resource to have a lifetime. /// /// var builder = DistributedApplication.CreateBuilder(args); /// /// builder.AddExecutable("myexecutable", "mycommand", ".") - /// .WithLifetime(ExecutableLifetime.Persistent); + /// .WithLifetime(Lifetime.Persistent); /// /// builder.Build().Run(); /// /// /// - [AspireExport("withExecutableLifetime", Description = "Sets the lifetime behavior of the executable resource")] + [Obsolete("Use ResourceBuilderExtensions.WithLifetime with Lifetime instead.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the resource-level overload that accepts Lifetime.")] public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ExecutableLifetime lifetime) where T : ExecutableResource { ArgumentNullException.ThrowIfNull(builder); - return builder.WithAnnotation(new ExecutableLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + return builder.WithLifetime(ToLifetime(lifetime)); } + private static Lifetime ToLifetime(ExecutableLifetime lifetime) + => lifetime switch + { + ExecutableLifetime.Session => Lifetime.Session, + ExecutableLifetime.Persistent => Lifetime.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + }; + /// /// Adds annotation to to support containerization during deployment. /// diff --git a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs index 62030268cf5..9a988de7072 100644 --- a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs @@ -327,30 +327,39 @@ public static IResourceBuilder AddProject(this IDistributedAppl /// /// The project resource type. /// Builder for the project resource. - /// The lifetime behavior of the project executable resource. The default behavior is . + /// The lifetime behavior of the project executable resource. The default behavior is . /// The . /// /// - /// Marking a project resource to have a lifetime. + /// Marking a project resource to have a lifetime. /// /// var builder = DistributedApplication.CreateBuilder(args); /// /// builder.AddProject<Projects.ApiService>("api") - /// .WithLifetime(ExecutableLifetime.Persistent); + /// .WithLifetime(Lifetime.Persistent); /// /// builder.Build().Run(); /// /// /// - [AspireExport("withProjectExecutableLifetime", Description = "Sets the lifetime behavior of the project executable resource")] + [Obsolete("Use ResourceBuilderExtensions.WithLifetime with Lifetime instead.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the resource-level overload that accepts Lifetime.")] public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ExecutableLifetime lifetime) where TProjectResource : ProjectResource { ArgumentNullException.ThrowIfNull(builder); - return builder.WithAnnotation(new ExecutableLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + return builder.WithLifetime(ToLifetime(lifetime)); } + private static Lifetime ToLifetime(ExecutableLifetime lifetime) + => lifetime switch + { + ExecutableLifetime.Session => Lifetime.Session, + ExecutableLifetime.Persistent => Lifetime.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + }; + /// /// Adds a C# project or file-based app to the application model. /// diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 361cb754fee..2f55573e1bf 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -27,6 +27,45 @@ public static class ResourceBuilderExtensions private const string ConnectionStringEnvironmentName = "ConnectionStrings__"; private static readonly MethodInfo s_dispatchCustomWithReferenceMethod = typeof(ResourceBuilderExtensions).GetMethod(nameof(DispatchCustomWithReference), BindingFlags.NonPublic | BindingFlags.Static)!; + /// + /// Sets the lifetime behavior for a resource that supports lifetime configuration. + /// + /// The resource type. + /// The resource builder. + /// The lifetime behavior for the resource. The default behavior is . + /// The . + /// + /// + /// Marking a resource to have a lifetime. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// builder.AddProject<Projects.ApiService>("api") + /// .WithLifetime(Lifetime.Persistent); + /// + /// builder.Build().Run(); + /// + /// + /// + [AspireExport(Description = "Sets the lifetime behavior of the resource")] + public static IResourceBuilder WithLifetime(this IResourceBuilder builder, Lifetime lifetime) + where T : IResource + { + ArgumentNullException.ThrowIfNull(builder); + + if (builder.Resource is ContainerResource) + { + return builder.WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = ToContainerLifetime(lifetime) }, ResourceAnnotationMutationBehavior.Replace); + } + + if (builder.Resource is ExecutableResource or ProjectResource) + { + return builder.WithAnnotation(new ExecutableLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + } + + throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); + } + /// /// Adds an environment variable to the resource. /// @@ -1465,6 +1504,14 @@ public static IResourceBuilder WithEndpointProxySupport(this IResourceBuil return builder; } + private static ContainerLifetime ToContainerLifetime(Lifetime lifetime) + => lifetime switch + { + Lifetime.Session => ContainerLifetime.Session, + Lifetime.Persistent => ContainerLifetime.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + }; + /// /// Exposes an endpoint on a resource. This endpoint reference can be retrieved using . /// The endpoint name will be the scheme name if not specified. diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.cs b/src/Aspire.Hosting/api/Aspire.Hosting.cs index 42fa50f02ad..9ab38d3821f 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.cs +++ b/src/Aspire.Hosting/api/Aspire.Hosting.cs @@ -202,6 +202,10 @@ public static ApplicationModel.IResourceBuilder WithDockerfileFactory(this public static ApplicationModel.IResourceBuilder WithDockerfileFactory(this ApplicationModel.IResourceBuilder builder, string contextPath, System.Func> dockerfileFactory, string? stage = null) where T : ApplicationModel.ContainerResource { throw null; } + [AspireExport("withEndpointProxySupport", Description = "Configures endpoint proxy support")] + public static ApplicationModel.IResourceBuilder WithEndpointProxySupport(this ApplicationModel.IResourceBuilder builder, bool proxyEnabled) + where T : ApplicationModel.ContainerResource { throw null; } + [AspireExport("withEntrypoint", Description = "Sets the container entrypoint")] public static ApplicationModel.IResourceBuilder WithEntrypoint(this ApplicationModel.IResourceBuilder builder, string entrypoint) where T : ApplicationModel.ContainerResource { throw null; } @@ -506,10 +510,6 @@ public static ApplicationModel.IResourceBuilder PublishAsDockerFile(this A public static ApplicationModel.IResourceBuilder WithCommand(this ApplicationModel.IResourceBuilder builder, string command) where T : ApplicationModel.ExecutableResource { throw null; } - [AspireExport("withExecutableLifetime", Description = "Sets the lifetime behavior of the executable resource")] - public static ApplicationModel.IResourceBuilder WithLifetime(this ApplicationModel.IResourceBuilder builder, ApplicationModel.ExecutableLifetime lifetime) - where T : ApplicationModel.ExecutableResource { throw null; } - [AspireExport("withWorkingDirectory", Description = "Sets the executable working directory")] public static ApplicationModel.IResourceBuilder WithWorkingDirectory(this ApplicationModel.IResourceBuilder builder, string workingDirectory) where T : ApplicationModel.ExecutableResource { throw null; } @@ -979,10 +979,6 @@ public static partial class ProjectResourceBuilderExtensions [AspireExport("disableForwardedHeaders", Description = "Disables forwarded headers for the project")] public static ApplicationModel.IResourceBuilder DisableForwardedHeaders(this ApplicationModel.IResourceBuilder builder) { throw null; } - [AspireExport("withProjectExecutableLifetime", Description = "Sets the lifetime behavior of the project executable resource")] - public static ApplicationModel.IResourceBuilder WithLifetime(this ApplicationModel.IResourceBuilder builder, ApplicationModel.ExecutableLifetime lifetime) - where TProjectResource : ApplicationModel.ProjectResource { throw null; } - [AspireExport("publishProjectAsDockerFileWithConfigure", MethodName = "publishAsDockerFile", Description = "Publishes a project as a Docker file with optional container configuration", RunSyncOnBackgroundThread = true)] public static ApplicationModel.IResourceBuilder PublishAsDockerFile(this ApplicationModel.IResourceBuilder builder, System.Action>? configure = null) where T : ApplicationModel.ProjectResource { throw null; } @@ -1147,16 +1143,12 @@ public static ApplicationModel.IResourceBuilder WithDebugSupport WithDeveloperCertificateTrust(this ApplicationModel.IResourceBuilder builder, bool trust) where TResource : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithArgs { throw null; } - [AspireExport("withEndpointProxySupport", Description = "Configures endpoint proxy support")] - public static ApplicationModel.IResourceBuilder WithEndpointProxySupport(this ApplicationModel.IResourceBuilder builder, bool proxyEnabled) - where T : ApplicationModel.IResourceWithEndpoints { throw null; } - [AspireExport("withEndpoint", Description = "Adds a network endpoint")] - public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, string? name = null, string? env = null, bool? isProxied = null, bool? isExternal = null, System.Net.Sockets.ProtocolType? protocol = null) + public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? scheme = null, string? name = null, string? env = null, bool isProxied = true, bool? isExternal = null, System.Net.Sockets.ProtocolType? protocol = null) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [AspireExportIgnore(Reason = "Subset of the full WithEndpoint overload which is already exported.")] - public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port, int? targetPort, string? scheme, string? name, string? env, bool? isProxied, bool? isExternal) + public static ApplicationModel.IResourceBuilder WithEndpoint(this ApplicationModel.IResourceBuilder builder, int? port, int? targetPort, string? scheme, string? name, string? env, bool isProxied, bool? isExternal) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [AspireExportIgnore(Reason = "EndpointAnnotation has read-only properties AllocatedEndpointSnapshot and AllAllocatedEndpoints that are not ATS-compatible. Callback-free variant is exported.")] @@ -1228,7 +1220,7 @@ public static ApplicationModel.IResourceBuilder WithHttpCommand WithHttpEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool? isProxied = null) + public static ApplicationModel.IResourceBuilder WithHttpEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool isProxied = true) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [AspireExportIgnore(Reason = "Func delegate — not ATS-compatible.")] @@ -1265,7 +1257,7 @@ public static ApplicationModel.IResourceBuilder WithHttpsDeveloperCer where TResource : ApplicationModel.IResourceWithEnvironment, ApplicationModel.IResourceWithArgs { throw null; } [AspireExport("withHttpsEndpoint", Description = "Adds an HTTPS endpoint")] - public static ApplicationModel.IResourceBuilder WithHttpsEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool? isProxied = null) + public static ApplicationModel.IResourceBuilder WithHttpsEndpoint(this ApplicationModel.IResourceBuilder builder, int? port = null, int? targetPort = null, string? name = null, string? env = null, bool isProxied = true) where T : ApplicationModel.IResourceWithEndpoints { throw null; } [System.Obsolete("This method is obsolete and will be removed in a future version. Use the WithHttpHealthCheck method instead.")] @@ -2201,9 +2193,9 @@ public sealed partial class EmulatorResourceAnnotation : IResourceAnnotation [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}")] public sealed partial class EndpointAnnotation : IResourceAnnotation { - public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, NetworkIdentifier? networkID, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool? isProxied = null) { } + public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, NetworkIdentifier? networkID, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true) { } - public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool? isProxied = null) { } + public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriScheme = null, string? transport = null, string? name = null, int? port = null, int? targetPort = null, bool? isExternal = null, bool isProxied = true) { } public NetworkEndpointSnapshotList AllAllocatedEndpoints { get { throw null; } } @@ -2218,7 +2210,7 @@ public EndpointAnnotation(System.Net.Sockets.ProtocolType protocol, string? uriS public bool IsExternal { get { throw null; } set { } } - public bool? IsProxied { get { throw null; } set { } } + public bool IsProxied { get { throw null; } set { } } public string Name { get { throw null; } set { } } @@ -2418,18 +2410,6 @@ public sealed partial class ExecutableAnnotation : IResourceAnnotation public required string WorkingDirectory { get { throw null; } set { } } } - public enum ExecutableLifetime - { - Session = 0, - Persistent = 1 - } - - [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}")] - public sealed partial class ExecutableLifetimeAnnotation : IResourceAnnotation - { - public required ExecutableLifetime Lifetime { get { throw null; } set { } } - } - [System.Diagnostics.DebuggerDisplay("Type = {GetType().Name,nq}, Name = {Name}, Command = {Command}")] public partial class ExecutableResource : Resource, IResourceWithEnvironment, IResource, IResourceWithArgs, IResourceWithEndpoints, IResourceWithWaitSupport, IResourceWithProbes, IComputeResource { @@ -4568,4 +4548,4 @@ public partial class PublishingOptions public string? Publisher { get { throw null; } set { } } } -} +} \ No newline at end of file diff --git a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs index 43a5846ab51..30b5fe0b8e8 100644 --- a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs +++ b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs @@ -61,10 +61,10 @@ public void AddDevTunnel_WithPersistentExecutableLifetime_AddsExecutableLifetime using var builder = TestDistributedApplicationBuilder.Create(); var tunnel = builder.AddDevTunnel("tunnel", "custom-id") - .WithLifetime(ExecutableLifetime.Persistent); + .WithLifetime(Lifetime.Persistent); Assert.True(tunnel.Resource.TryGetLastAnnotation(out var annotation)); - Assert.Equal(ExecutableLifetime.Persistent, annotation.Lifetime); + Assert.Equal(Lifetime.Persistent, annotation.Lifetime); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 33aa8d18f6f..eb317e47534 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -634,7 +634,7 @@ public async Task EndpointPortsPersistentExecutableDefaultsToProxylessEndpoint() const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1002; builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") - .WithLifetime(ExecutableLifetime.Persistent) + .WithLifetime(Lifetime.Persistent) .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT"); var configDict = new Dictionary @@ -1372,7 +1372,7 @@ public async Task EndpointPortsPersistentProjectDefaultsToProxylessEndpoint() const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1002; builder.AddProject("ServiceA", launchProfileName: null) - .WithLifetime(ExecutableLifetime.Persistent) + .WithLifetime(Lifetime.Persistent) .WithHttpEndpoint(name: "stable", port: desiredPort); var configDict = new Dictionary @@ -2171,7 +2171,7 @@ public async Task PersistentPlainExecutable_ExtensionMode_RunsInProcess() var executable = new TestExecutableResource("test-working-directory"); builder.AddResource(executable) .WithDebugSupport(mode => new ExecutableLaunchConfiguration("test") { Mode = mode }, "test") - .WithLifetime(ExecutableLifetime.Persistent); + .WithLifetime(Lifetime.Persistent); var configDict = new Dictionary { @@ -2211,7 +2211,7 @@ public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath() builder.AddResource(executable) .WithCertificateAuthorityCollection(certificateAuthorities) .WithCertificateTrustScope(CertificateTrustScope.Override) - .WithLifetime(ExecutableLifetime.Persistent); + .WithLifetime(Lifetime.Persistent); var configDict = new Dictionary { diff --git a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs index ccd14590deb..34ee2c7798a 100644 --- a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs @@ -77,10 +77,10 @@ public void WithLifetimeAddsExecutableLifetimeAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var executable = builder.AddExecutable("myexe", "command", "workingdirectory") - .WithLifetime(ExecutableLifetime.Persistent); + .WithLifetime(Lifetime.Persistent); var annotation = executable.Resource.Annotations.OfType().Single(); - Assert.Equal(ExecutableLifetime.Persistent, annotation.Lifetime); + Assert.Equal(Lifetime.Persistent, annotation.Lifetime); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs index 93af6725bb4..ae1d5068634 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs @@ -14,10 +14,10 @@ public void WithLifetimeAddsExecutableLifetimeAnnotation() using var builder = TestDistributedApplicationBuilder.Create(); var project = builder.AddProject("project", options => options.ExcludeLaunchProfile = true) - .WithLifetime(ExecutableLifetime.Persistent); + .WithLifetime(Lifetime.Persistent); var annotation = project.Resource.Annotations.OfType().Single(); - Assert.Equal(ExecutableLifetime.Persistent, annotation.Lifetime); + Assert.Equal(Lifetime.Persistent, annotation.Lifetime); } private sealed class TestProject : IProjectMetadata diff --git a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs new file mode 100644 index 00000000000..bd041a4024b --- /dev/null +++ b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs @@ -0,0 +1,35 @@ +// 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.Utils; + +namespace Aspire.Hosting.Tests; + +[Trait("Partition", "2")] +public class ResourceBuilderLifetimeTests +{ + [Fact] + public void WithLifetimeAddsContainerLifetimeAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("container", "image") + .WithLifetime(Lifetime.Persistent); + + var annotation = container.Resource.Annotations.OfType().Single(); + Assert.Equal(ContainerLifetime.Persistent, annotation.Lifetime); + } + + [Fact] + public void WithLifetimeRejectsUnsupportedResourceTypes() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var parameter = builder.AddParameter("parameter"); + + void ConfigureLifetime() => parameter.WithLifetime(Lifetime.Persistent); + + var exception = Assert.Throws((Action)ConfigureLifetime); + Assert.Contains("does not support lifetime configuration", exception.Message); + } +} From 8c49d78a7e3f606a826bc587f0263d1914a1172b Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 14 May 2026 19:10:13 -0700 Subject: [PATCH 04/38] Add DCP monitor process fields Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Dcp/ContainerCreator.cs | 13 ++ src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs | 202 ++++++++++++++++++ src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 17 ++ src/Aspire.Hosting/Dcp/Model/Container.cs | 10 +- src/Aspire.Hosting/Dcp/Model/Executable.cs | 13 ++ .../DistributedApplicationBuilder.cs | 1 + .../Dcp/DcpExecutorTests.cs | 90 +++++++- 7 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index e8f80cc59f5..dc536602964 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -54,6 +54,7 @@ internal sealed class ContainerCreator : IObjectCreator _logger; private readonly string _normalizedApplicationName; private readonly DcpAppResourceStore _appResources; @@ -69,6 +70,7 @@ public ContainerCreator( DistributedApplicationExecutionContext executionContext, ResourceLoggerService loggerService, IDcpDependencyCheckService dcpDependencyCheckService, + IDcpProcessMonitor processMonitor, IHostEnvironment hostEnvironment, ILogger logger, DcpAppResourceStore appResources) @@ -80,6 +82,7 @@ public ContainerCreator( _executionContext = executionContext; _loggerService = loggerService; _dcpDependencyCheckService = dcpDependencyCheckService; + _processMonitor = processMonitor; _logger = logger; _normalizedApplicationName = DcpExecutor.NormalizeApplicationName(hostEnvironment.ApplicationName); _appResources = appResources; @@ -155,6 +158,7 @@ public IEnumerable> PrepareObjects() if (container.GetContainerLifetimeType() == ContainerLifetime.Persistent) { ctr.Spec.Persistent = true; + ApplyMonitorProcess(ctr.Spec); } if (container.TryGetContainerImagePullPolicy(out var pullPolicy)) @@ -206,6 +210,15 @@ public IEnumerable> PrepareObjects() return result; } + private void ApplyMonitorProcess(ContainerSpec spec) + { + if (_processMonitor.GetMonitorProcess() is { } monitorProcess) + { + spec.MonitorPid = monitorProcess.ProcessId; + spec.MonitorTimestamp = monitorProcess.Timestamp; + } + } + private void ValidateContainerTunnelContainerNameConflicts(IEnumerable modelContainerResources) { if (!_options.Value.EnableAspireContainerTunnel) diff --git a/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs b/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs new file mode 100644 index 00000000000..216d34155a0 --- /dev/null +++ b/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs @@ -0,0 +1,202 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using SystemProcess = System.Diagnostics.Process; + +namespace Aspire.Hosting.Dcp; + +internal sealed record DcpProcessIdentity(int ProcessId, DateTime Timestamp); + +internal interface IDcpProcessMonitor +{ + DcpProcessIdentity? GetMonitorProcess(); +} + +internal sealed partial class DcpProcessMonitor(IConfiguration configuration, ILogger logger) : IDcpProcessMonitor +{ + private const int DefaultLinuxClockTicksPerSecond = 100; + private const int LinuxClockTicksPerSecondConfigName = 2; // _SC_CLK_TCK + + private bool _initialized; + private DcpProcessIdentity? _monitorProcess; + + public DcpProcessIdentity? GetMonitorProcess() + { + if (!_initialized) + { + _monitorProcess = CreateMonitorProcess(); + _initialized = true; + } + + return _monitorProcess; + } + + private DcpProcessIdentity? CreateMonitorProcess() + { + var monitorProcessId = GetConfiguredCliProcessId() ?? GetParentProcessId(); + if (monitorProcessId is null) + { + logger.LogDebug("No monitor process ID could be determined for persistent resources."); + return null; + } + + var timestamp = GetProcessIdentityTimestamp(monitorProcessId.Value); + if (timestamp is null) + { + logger.LogDebug("No monitor process timestamp could be determined for process {MonitorProcessId}.", monitorProcessId); + return null; + } + + return new(monitorProcessId.Value, timestamp.Value); + } + + private int? GetConfiguredCliProcessId() + { + if (configuration[KnownConfigNames.CliProcessId] is not { } value) + { + return null; + } + + if (int.TryParse(value, CultureInfo.InvariantCulture, out var processId) && processId > 0) + { + return processId; + } + + logger.LogDebug("Configured CLI process ID '{ConfiguredCliProcessId}' is invalid.", value); + return null; + } + + private static int? GetParentProcessId() + { + if (!OperatingSystem.IsWindows()) + { + var parentProcessId = getppid(); + return parentProcessId > 0 ? parentProcessId : null; + } + + return GetWindowsParentProcessId(); + } + + private DateTime? GetProcessIdentityTimestamp(int processId) + { + if (OperatingSystem.IsLinux()) + { + return GetLinuxProcessIdentityTimestamp(processId); + } + + try + { + using var process = SystemProcess.GetProcessById(processId); + return process.StartTime.ToUniversalTime(); + } + catch (ArgumentException) + { + logger.LogDebug("Monitor process {MonitorProcessId} no longer exists.", processId); + return null; + } + catch (InvalidOperationException) + { + logger.LogDebug("Monitor process {MonitorProcessId} exited before its timestamp could be read.", processId); + return null; + } + } + + private DateTime? GetLinuxProcessIdentityTimestamp(int processId) + { + var statPath = Path.Combine( + Environment.GetEnvironmentVariable("HOST_PROC") ?? "/proc", + processId.ToString(CultureInfo.InvariantCulture), + "stat"); + + string contents; + try + { + contents = File.ReadAllText(statPath); + } + catch (IOException ex) + { + logger.LogDebug(ex, "Could not read monitor process stat file '{StatPath}'.", statPath); + return null; + } + catch (UnauthorizedAccessException ex) + { + logger.LogDebug(ex, "Could not read monitor process stat file '{StatPath}'.", statPath); + return null; + } + + // /proc//stat fields start as: + // 12345 (process name may contain spaces or parentheses) S 1 2 3 ... + // The process start time is field 22, in clock ticks since boot. Match DCP's + // Linux identity time by converting that monotonic value into a DateTime + // offset from DateTime.MinValue instead of estimating a wall-clock time. + var closeParenIndex = contents.LastIndexOf(')'); + if (closeParenIndex < 0 || closeParenIndex + 2 >= contents.Length) + { + logger.LogDebug("Monitor process stat file '{StatPath}' was malformed.", statPath); + return null; + } + + var fields = contents[(closeParenIndex + 2)..].Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (fields.Length < 20 || !ulong.TryParse(fields[19], CultureInfo.InvariantCulture, out var startTicks)) + { + logger.LogDebug("Monitor process stat file '{StatPath}' did not contain a valid start time.", statPath); + return null; + } + + var startTimeMilliseconds = (startTicks * 1000) / (ulong)GetLinuxClockTicksPerSecond(); + return DateTime.SpecifyKind(DateTime.MinValue, DateTimeKind.Utc).AddMilliseconds(startTimeMilliseconds); + } + + private static int GetLinuxClockTicksPerSecond() + { + var result = sysconf(LinuxClockTicksPerSecondConfigName); + return result > 0 ? (int)result : DefaultLinuxClockTicksPerSecond; + } + + private static int? GetWindowsParentProcessId() + { + var basicInformation = new ProcessBasicInformation(); + var status = NtQueryInformationProcess( + SystemProcess.GetCurrentProcess().Handle, + processInformationClass: 0, + ref basicInformation, + Marshal.SizeOf(), + returnLength: IntPtr.Zero); + + if (status != 0 || basicInformation.InheritedFromUniqueProcessId <= 0 || basicInformation.InheritedFromUniqueProcessId > int.MaxValue) + { + return null; + } + + return (int)basicInformation.InheritedFromUniqueProcessId; + } + + [LibraryImport("libc", SetLastError = true, EntryPoint = "getppid")] + private static partial int getppid(); + + [LibraryImport("libc", SetLastError = true, EntryPoint = "sysconf")] + private static partial long sysconf(int name); + + [LibraryImport("ntdll", EntryPoint = "NtQueryInformationProcess")] + private static partial int NtQueryInformationProcess( + IntPtr processHandle, + int processInformationClass, + ref ProcessBasicInformation processInformation, + int processInformationLength, + IntPtr returnLength); + + [StructLayout(LayoutKind.Sequential)] + private struct ProcessBasicInformation + { + public IntPtr Reserved1; + public IntPtr PebBaseAddress; + public IntPtr Reserved2; + public IntPtr Reserved3; + public nint UniqueProcessId; + public nint InheritedFromUniqueProcessId; + } +} diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index 75afca05f89..0bc2808a994 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -30,6 +30,7 @@ internal sealed class ExecutableCreator : IObjectCreator _logger; private readonly DcpAppResourceStore _appResources; @@ -41,6 +42,7 @@ public ExecutableCreator( DistributedApplicationExecutionContext executionContext, Locations locations, IAspireStore aspireStore, + IDcpProcessMonitor processMonitor, ILogger logger, DcpAppResourceStore appResources) { @@ -51,6 +53,7 @@ public ExecutableCreator( _executionContext = executionContext; _locations = locations; _aspireStore = aspireStore; + _processMonitor = processMonitor; _logger = logger; _appResources = appResources; } @@ -172,6 +175,10 @@ private void PrepareProjectExecutables() var isInDebugSession = !string.IsNullOrEmpty(_configuration[DcpExecutor.DebugSessionPortVar]); var persistent = project.GetExecutableLifetimeType() == Lifetime.Persistent; exe.Spec.Persistent = persistent; + if (persistent) + { + ApplyMonitorProcess(exe.Spec); + } SupportsDebuggingAnnotation? supportsDebuggingAnnotation = null; if (!persistent && project.SupportsDebugging(_configuration, out supportsDebuggingAnnotation)) @@ -287,6 +294,7 @@ private void PreparePlainExecutables() if (persistent) { exe.Spec.Persistent = true; + ApplyMonitorProcess(exe.Spec); } if (!persistent && executable.SupportsDebugging(_configuration, out _)) @@ -314,6 +322,15 @@ private void PreparePlainExecutables() } } + private void ApplyMonitorProcess(ExecutableSpec spec) + { + if (_processMonitor.GetMonitorProcess() is { } monitorProcess) + { + spec.MonitorPid = monitorProcess.ProcessId; + spec.MonitorTimestamp = monitorProcess.Timestamp; + } + } + private async Task BuildExecutableConfiguration(RenderedModelResource er, ILogger resourceLogger, CancellationToken cancellationToken) { var exe = (Executable)er.DcpResource; diff --git a/src/Aspire.Hosting/Dcp/Model/Container.cs b/src/Aspire.Hosting/Dcp/Model/Container.cs index 7a9279632f7..470817d3230 100644 --- a/src/Aspire.Hosting/Dcp/Model/Container.cs +++ b/src/Aspire.Hosting/Dcp/Model/Container.cs @@ -62,6 +62,15 @@ internal sealed class ContainerSpec [JsonPropertyName("persistent")] public bool? Persistent { get; set; } + // Optional parent process PID used to scope persistent container cleanup to a process lifecycle. + // When set, MonitorTimestamp must also be set and Persistent must be true. + [JsonPropertyName("monitorPid")] + public int? MonitorPid { get; set; } + + // Optional parent process identity timestamp used with MonitorPid to guard against PID reuse. + [JsonPropertyName("monitorTimestamp")] + public DateTime? MonitorTimestamp { get; set; } + [JsonPropertyName("networks")] public List? Networks { get; set; } @@ -582,4 +591,3 @@ public static Container Create(string name, string image) public static string ObjectKind => Dcp.ContainerKind; } - diff --git a/src/Aspire.Hosting/Dcp/Model/Executable.cs b/src/Aspire.Hosting/Dcp/Model/Executable.cs index 4e6bcd470ad..7491d1be005 100644 --- a/src/Aspire.Hosting/Dcp/Model/Executable.cs +++ b/src/Aspire.Hosting/Dcp/Model/Executable.cs @@ -64,6 +64,19 @@ internal sealed class ExecutableSpec [JsonPropertyName("persistent")] public bool? Persistent { get; set; } + /// + /// Optional parent process PID used to scope persistent Executable cleanup to a process lifecycle. + /// When set, must also be set and must be true. + /// + [JsonPropertyName("monitorPid")] + public int? MonitorPid { get; set; } + + /// + /// Optional parent process identity timestamp used with to guard against PID reuse. + /// + [JsonPropertyName("monitorTimestamp")] + public DateTime? MonitorTimestamp { get; set; } + /// /// Should this resource be started? If set to false, we will not attempt /// to start the resource until Start is set to true (or null). diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 387686dc6ef..8792b272974 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -511,6 +511,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) // DCP stuff _innerBuilder.Services.AddSingleton(); + _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index eb317e47534..6860f4b1a23 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -2198,6 +2198,85 @@ public async Task PersistentPlainExecutable_ExtensionMode_RunsInProcess() Assert.Null(exe.Spec.FallbackExecutionTypes); } + [Fact] + public async Task PersistentDcpResourcesIncludeMonitorProcess() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddContainer("database", "image") + .WithLifetime(Lifetime.Persistent); + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithLifetime(Lifetime.Persistent); + builder.AddProject("project", launchProfileName: null) + .WithLifetime(Lifetime.Persistent); + + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + var monitorProcess = new DcpProcessIdentity(1234, new DateTime(2026, 5, 14, 1, 2, 3, DateTimeKind.Utc)); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: kubernetesService, + configuration: configuration, + processMonitor: new TestDcpProcessMonitor(monitorProcess)); + + await appExecutor.RunApplicationAsync(); + + var container = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(container.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(monitorProcess.ProcessId, container.Spec.MonitorPid); + Assert.Equal(monitorProcess.Timestamp, container.Spec.MonitorTimestamp); + + var executables = kubernetesService.CreatedResources.OfType() + .Where(e => e.AppModelResourceName is "worker" or "project") + .ToArray(); + Assert.Equal(2, executables.Length); + Assert.All(executables, exe => + { + Assert.True(exe.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(monitorProcess.ProcessId, exe.Spec.MonitorPid); + Assert.Equal(monitorProcess.Timestamp, exe.Spec.MonitorTimestamp); + Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); + }); + } + + [Fact] + public async Task NonPersistentDcpResourcesDoNotIncludeMonitorProcess() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddContainer("database", "image"); + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory); + + var monitorProcess = new DcpProcessIdentity(1234, new DateTime(2026, 5, 14, 1, 2, 3, DateTimeKind.Utc)); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: kubernetesService, + processMonitor: new TestDcpProcessMonitor(monitorProcess)); + + await appExecutor.RunApplicationAsync(); + + var container = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.Null(container.Spec.Persistent); + Assert.Null(container.Spec.MonitorPid); + Assert.Null(container.Spec.MonitorTimestamp); + + var executable = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.Null(executable.Spec.Persistent); + Assert.Null(executable.Spec.MonitorPid); + Assert.Null(executable.Spec.MonitorTimestamp); + } + [Fact] public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath() { @@ -3993,7 +4072,8 @@ private static DcpExecutor CreateAppExecutor( DcpOptions? dcpOptions = null, ResourceLoggerService? resourceLoggerService = null, DcpExecutorEvents? events = null, - Hosting.Eventing.IDistributedApplicationEventing? distributedApplicationEventing = null) + Hosting.Eventing.IDistributedApplicationEventing? distributedApplicationEventing = null, + IDcpProcessMonitor? processMonitor = null) { if (configuration == null) { @@ -4030,6 +4110,7 @@ private static DcpExecutor CreateAppExecutor( var aspireStore = new AspireStore(Path.Join(aspireStoreDirectory.Path, ".aspire"), fileSystemService); var hostEnv = hostEnvironment ?? new TestHostEnvironment(); var dcpDependencyCheckService = new TestDcpDependencyCheckService(); + processMonitor ??= new TestDcpProcessMonitor(null); var appResources = new DcpAppResourceStore(); @@ -4041,6 +4122,7 @@ private static DcpExecutor CreateAppExecutor( executionContext, locations, aspireStore, + processMonitor, NullLogger.Instance, appResources); @@ -4052,6 +4134,7 @@ private static DcpExecutor CreateAppExecutor( executionContext, resourceLoggerService, dcpDependencyCheckService, + processMonitor, hostEnv, NullLogger.Instance, appResources); @@ -4105,6 +4188,11 @@ private static void AssertEffectiveArgumentIndexesMatchSpecArgs(IReadOnlyList monitorProcess; + } + private static Aspire.Hosting.Dcp.ResourceSnapshotBuilder CreateSnapshotBuilder(DistributedApplicationModel model) { return new(new DcpResourceState(model.Resources.ToDictionary(r => r.Name), [])); From 034ef79db3e8c0173729b5ab79cbb76379b5b7fb Mon Sep 17 00:00:00 2001 From: David Negstad Date: Fri, 15 May 2026 09:52:34 -0700 Subject: [PATCH 05/38] Use explicit parent process lifetime API Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ParentProcessLifetimeAnnotation.cs | 17 +++ src/Aspire.Hosting/Dcp/ContainerCreator.cs | 7 +- src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs | 126 +++--------------- src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 9 +- .../ResourceBuilderExtensions.cs | 38 ++++++ .../Dcp/DcpExecutorTests.cs | 68 +++++++--- 6 files changed, 127 insertions(+), 138 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs diff --git a/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs new file mode 100644 index 00000000000..159468c02c5 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +using SystemProcess = System.Diagnostics.Process; + +/// +/// Configures a persistent resource to be monitored by a parent process identity. +/// +internal sealed class ParentProcessLifetimeAnnotation(SystemProcess parentProcess) : IResourceAnnotation +{ + /// + /// Gets the parent process to monitor. + /// + public SystemProcess ParentProcess { get; } = parentProcess ?? throw new ArgumentNullException(nameof(parentProcess)); +} diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index dc536602964..9ded8dc664d 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -158,7 +158,7 @@ public IEnumerable> PrepareObjects() if (container.GetContainerLifetimeType() == ContainerLifetime.Persistent) { ctr.Spec.Persistent = true; - ApplyMonitorProcess(ctr.Spec); + ApplyMonitorProcess(container, ctr.Spec); } if (container.TryGetContainerImagePullPolicy(out var pullPolicy)) @@ -210,10 +210,11 @@ public IEnumerable> PrepareObjects() return result; } - private void ApplyMonitorProcess(ContainerSpec spec) + private void ApplyMonitorProcess(IResource resource, ContainerSpec spec) { - if (_processMonitor.GetMonitorProcess() is { } monitorProcess) + if (resource.TryGetLastAnnotation(out var annotation)) { + var monitorProcess = _processMonitor.GetMonitorProcess(annotation.ParentProcess); spec.MonitorPid = monitorProcess.ProcessId; spec.MonitorTimestamp = monitorProcess.Timestamp; } diff --git a/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs b/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs index 216d34155a0..14b07ee25e6 100644 --- a/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs +++ b/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs @@ -3,8 +3,6 @@ using System.Globalization; using System.Runtime.InteropServices; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using SystemProcess = System.Diagnostics.Process; namespace Aspire.Hosting.Dcp; @@ -13,99 +11,51 @@ internal sealed record DcpProcessIdentity(int ProcessId, DateTime Timestamp); internal interface IDcpProcessMonitor { - DcpProcessIdentity? GetMonitorProcess(); + DcpProcessIdentity GetMonitorProcess(SystemProcess parentProcess); } -internal sealed partial class DcpProcessMonitor(IConfiguration configuration, ILogger logger) : IDcpProcessMonitor +internal sealed partial class DcpProcessMonitor : IDcpProcessMonitor { private const int DefaultLinuxClockTicksPerSecond = 100; private const int LinuxClockTicksPerSecondConfigName = 2; // _SC_CLK_TCK - private bool _initialized; - private DcpProcessIdentity? _monitorProcess; - - public DcpProcessIdentity? GetMonitorProcess() + public DcpProcessIdentity GetMonitorProcess(SystemProcess parentProcess) { - if (!_initialized) - { - _monitorProcess = CreateMonitorProcess(); - _initialized = true; - } + ArgumentNullException.ThrowIfNull(parentProcess); - return _monitorProcess; - } + var monitorProcessId = parentProcess.Id; + var timestamp = GetProcessIdentityTimestamp(parentProcess); - private DcpProcessIdentity? CreateMonitorProcess() - { - var monitorProcessId = GetConfiguredCliProcessId() ?? GetParentProcessId(); - if (monitorProcessId is null) - { - logger.LogDebug("No monitor process ID could be determined for persistent resources."); - return null; - } - - var timestamp = GetProcessIdentityTimestamp(monitorProcessId.Value); if (timestamp is null) { - logger.LogDebug("No monitor process timestamp could be determined for process {MonitorProcessId}.", monitorProcessId); - return null; - } - - return new(monitorProcessId.Value, timestamp.Value); - } - - private int? GetConfiguredCliProcessId() - { - if (configuration[KnownConfigNames.CliProcessId] is not { } value) - { - return null; - } - - if (int.TryParse(value, CultureInfo.InvariantCulture, out var processId) && processId > 0) - { - return processId; - } - - logger.LogDebug("Configured CLI process ID '{ConfiguredCliProcessId}' is invalid.", value); - return null; - } - - private static int? GetParentProcessId() - { - if (!OperatingSystem.IsWindows()) - { - var parentProcessId = getppid(); - return parentProcessId > 0 ? parentProcessId : null; + throw new InvalidOperationException($"Could not determine the identity timestamp for monitor process {monitorProcessId}."); } - return GetWindowsParentProcessId(); + return new(monitorProcessId, timestamp.Value); } - private DateTime? GetProcessIdentityTimestamp(int processId) + private static DateTime? GetProcessIdentityTimestamp(SystemProcess parentProcess) { if (OperatingSystem.IsLinux()) { - return GetLinuxProcessIdentityTimestamp(processId); + return GetLinuxProcessIdentityTimestamp(parentProcess.Id); } try { - using var process = SystemProcess.GetProcessById(processId); - return process.StartTime.ToUniversalTime(); + return parentProcess.StartTime.ToUniversalTime(); } catch (ArgumentException) { - logger.LogDebug("Monitor process {MonitorProcessId} no longer exists.", processId); return null; } catch (InvalidOperationException) { - logger.LogDebug("Monitor process {MonitorProcessId} exited before its timestamp could be read.", processId); return null; } } - private DateTime? GetLinuxProcessIdentityTimestamp(int processId) + private static DateTime? GetLinuxProcessIdentityTimestamp(int processId) { var statPath = Path.Combine( Environment.GetEnvironmentVariable("HOST_PROC") ?? "/proc", @@ -119,13 +69,11 @@ internal sealed partial class DcpProcessMonitor(IConfiguration configuration, IL } catch (IOException ex) { - logger.LogDebug(ex, "Could not read monitor process stat file '{StatPath}'.", statPath); - return null; + throw new InvalidOperationException($"Could not read monitor process stat file '{statPath}'.", ex); } catch (UnauthorizedAccessException ex) { - logger.LogDebug(ex, "Could not read monitor process stat file '{StatPath}'.", statPath); - return null; + throw new InvalidOperationException($"Could not read monitor process stat file '{statPath}'.", ex); } // /proc//stat fields start as: @@ -136,15 +84,13 @@ internal sealed partial class DcpProcessMonitor(IConfiguration configuration, IL var closeParenIndex = contents.LastIndexOf(')'); if (closeParenIndex < 0 || closeParenIndex + 2 >= contents.Length) { - logger.LogDebug("Monitor process stat file '{StatPath}' was malformed.", statPath); - return null; + throw new InvalidOperationException($"Monitor process stat file '{statPath}' was malformed."); } var fields = contents[(closeParenIndex + 2)..].Split(' ', StringSplitOptions.RemoveEmptyEntries); if (fields.Length < 20 || !ulong.TryParse(fields[19], CultureInfo.InvariantCulture, out var startTicks)) { - logger.LogDebug("Monitor process stat file '{StatPath}' did not contain a valid start time.", statPath); - return null; + throw new InvalidOperationException($"Monitor process stat file '{statPath}' did not contain a valid start time."); } var startTimeMilliseconds = (startTicks * 1000) / (ulong)GetLinuxClockTicksPerSecond(); @@ -157,46 +103,6 @@ private static int GetLinuxClockTicksPerSecond() return result > 0 ? (int)result : DefaultLinuxClockTicksPerSecond; } - private static int? GetWindowsParentProcessId() - { - var basicInformation = new ProcessBasicInformation(); - var status = NtQueryInformationProcess( - SystemProcess.GetCurrentProcess().Handle, - processInformationClass: 0, - ref basicInformation, - Marshal.SizeOf(), - returnLength: IntPtr.Zero); - - if (status != 0 || basicInformation.InheritedFromUniqueProcessId <= 0 || basicInformation.InheritedFromUniqueProcessId > int.MaxValue) - { - return null; - } - - return (int)basicInformation.InheritedFromUniqueProcessId; - } - - [LibraryImport("libc", SetLastError = true, EntryPoint = "getppid")] - private static partial int getppid(); - [LibraryImport("libc", SetLastError = true, EntryPoint = "sysconf")] private static partial long sysconf(int name); - - [LibraryImport("ntdll", EntryPoint = "NtQueryInformationProcess")] - private static partial int NtQueryInformationProcess( - IntPtr processHandle, - int processInformationClass, - ref ProcessBasicInformation processInformation, - int processInformationLength, - IntPtr returnLength); - - [StructLayout(LayoutKind.Sequential)] - private struct ProcessBasicInformation - { - public IntPtr Reserved1; - public IntPtr PebBaseAddress; - public IntPtr Reserved2; - public IntPtr Reserved3; - public nint UniqueProcessId; - public nint InheritedFromUniqueProcessId; - } } diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index 0bc2808a994..41dedfa8e99 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -177,7 +177,7 @@ private void PrepareProjectExecutables() exe.Spec.Persistent = persistent; if (persistent) { - ApplyMonitorProcess(exe.Spec); + ApplyMonitorProcess(project, exe.Spec); } SupportsDebuggingAnnotation? supportsDebuggingAnnotation = null; @@ -294,7 +294,7 @@ private void PreparePlainExecutables() if (persistent) { exe.Spec.Persistent = true; - ApplyMonitorProcess(exe.Spec); + ApplyMonitorProcess(executable, exe.Spec); } if (!persistent && executable.SupportsDebugging(_configuration, out _)) @@ -322,10 +322,11 @@ private void PreparePlainExecutables() } } - private void ApplyMonitorProcess(ExecutableSpec spec) + private void ApplyMonitorProcess(IResource resource, ExecutableSpec spec) { - if (_processMonitor.GetMonitorProcess() is { } monitorProcess) + if (resource.TryGetLastAnnotation(out var annotation)) { + var monitorProcess = _processMonitor.GetMonitorProcess(annotation.ParentProcess); spec.MonitorPid = monitorProcess.ProcessId; spec.MonitorTimestamp = monitorProcess.Timestamp; } diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 2f55573e1bf..31d38566c91 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using SystemProcess = System.Diagnostics.Process; namespace Aspire.Hosting; @@ -66,6 +67,43 @@ public static IResourceBuilder WithLifetime(this IResourceBuilder build throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); } + /// + /// Configures a resource to use a persistent lifetime that ends when a parent process exits. + /// + /// The resource type. + /// The resource builder. + /// The parent process to monitor. + /// The . + /// + /// This method also sets the resource lifetime to . The resource is tied to both + /// the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + /// + /// Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// using var parentProcess = Process.GetProcessById(1234); + /// builder.AddProject<Projects.ApiService>("api") + /// .WithParentProcessLifetime(parentProcess); + /// + /// builder.Build().Run(); + /// + /// + /// + /// Thrown when is . + /// Thrown when the resource does not support lifetime configuration. + [AspireExportIgnore(Reason = "System.Diagnostics.Process is a .NET runtime type not usable from polyglot hosts.")] + public static IResourceBuilder WithParentProcessLifetime(this IResourceBuilder builder, SystemProcess parentProcess) + where T : IResource + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(parentProcess); + + return builder + .WithLifetime(Lifetime.Persistent) + .WithAnnotation(new ParentProcessLifetimeAnnotation(parentProcess), ResourceAnnotationMutationBehavior.Replace); + } + /// /// Adds an environment variable to the resource. /// diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 6860f4b1a23..97fb9fe1abc 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -4,6 +4,7 @@ #pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. using System.Collections.Concurrent; +using System.Diagnostics; using System.Globalization; using System.IO.Pipelines; using System.Security.Cryptography; @@ -2199,7 +2200,7 @@ public async Task PersistentPlainExecutable_ExtensionMode_RunsInProcess() } [Fact] - public async Task PersistentDcpResourcesIncludeMonitorProcess() + public async Task PersistentDcpResourcesDoNotIncludeMonitorProcessByDefault() { var builder = DistributedApplication.CreateBuilder(); @@ -2215,7 +2216,6 @@ public async Task PersistentDcpResourcesIncludeMonitorProcess() ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" }; var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); - var monitorProcess = new DcpProcessIdentity(1234, new DateTime(2026, 5, 14, 1, 2, 3, DateTimeKind.Utc)); var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); @@ -2223,15 +2223,14 @@ public async Task PersistentDcpResourcesIncludeMonitorProcess() var appExecutor = CreateAppExecutor( distributedAppModel, kubernetesService: kubernetesService, - configuration: configuration, - processMonitor: new TestDcpProcessMonitor(monitorProcess)); + configuration: configuration); await appExecutor.RunApplicationAsync(); var container = Assert.Single(kubernetesService.CreatedResources.OfType()); Assert.True(container.Spec.Persistent.GetValueOrDefault()); - Assert.Equal(monitorProcess.ProcessId, container.Spec.MonitorPid); - Assert.Equal(monitorProcess.Timestamp, container.Spec.MonitorTimestamp); + Assert.Null(container.Spec.MonitorPid); + Assert.Null(container.Spec.MonitorTimestamp); var executables = kubernetesService.CreatedResources.OfType() .Where(e => e.AppModelResourceName is "worker" or "project") @@ -2240,21 +2239,32 @@ public async Task PersistentDcpResourcesIncludeMonitorProcess() Assert.All(executables, exe => { Assert.True(exe.Spec.Persistent.GetValueOrDefault()); - Assert.Equal(monitorProcess.ProcessId, exe.Spec.MonitorPid); - Assert.Equal(monitorProcess.Timestamp, exe.Spec.MonitorTimestamp); + Assert.Null(exe.Spec.MonitorPid); + Assert.Null(exe.Spec.MonitorTimestamp); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); }); } [Fact] - public async Task NonPersistentDcpResourcesDoNotIncludeMonitorProcess() + public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() { var builder = DistributedApplication.CreateBuilder(); + var parentProcess = Process.GetCurrentProcess(); - builder.AddContainer("database", "image"); - builder.AddExecutable("worker", "worker", Environment.CurrentDirectory); + builder.AddContainer("database", "image") + .WithParentProcessLifetime(parentProcess); + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithParentProcessLifetime(parentProcess); + builder.AddProject("project", launchProfileName: null) + .WithParentProcessLifetime(parentProcess); - var monitorProcess = new DcpProcessIdentity(1234, new DateTime(2026, 5, 14, 1, 2, 3, DateTimeKind.Utc)); + var configDict = new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); + var monitorProcess = new DcpProcessIdentity(parentProcess.Id, new DateTime(2026, 5, 14, 1, 2, 3, DateTimeKind.Utc)); + var processMonitor = new TestDcpProcessMonitor(monitorProcess); var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); @@ -2262,19 +2272,29 @@ public async Task NonPersistentDcpResourcesDoNotIncludeMonitorProcess() var appExecutor = CreateAppExecutor( distributedAppModel, kubernetesService: kubernetesService, - processMonitor: new TestDcpProcessMonitor(monitorProcess)); + configuration: configuration, + processMonitor: processMonitor); await appExecutor.RunApplicationAsync(); var container = Assert.Single(kubernetesService.CreatedResources.OfType()); - Assert.Null(container.Spec.Persistent); - Assert.Null(container.Spec.MonitorPid); - Assert.Null(container.Spec.MonitorTimestamp); + Assert.True(container.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(monitorProcess.ProcessId, container.Spec.MonitorPid); + Assert.Equal(monitorProcess.Timestamp, container.Spec.MonitorTimestamp); - var executable = Assert.Single(kubernetesService.CreatedResources.OfType()); - Assert.Null(executable.Spec.Persistent); - Assert.Null(executable.Spec.MonitorPid); - Assert.Null(executable.Spec.MonitorTimestamp); + var executables = kubernetesService.CreatedResources.OfType() + .Where(e => e.AppModelResourceName is "worker" or "project") + .ToArray(); + Assert.Equal(2, executables.Length); + Assert.All(executables, exe => + { + Assert.True(exe.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(monitorProcess.ProcessId, exe.Spec.MonitorPid); + Assert.Equal(monitorProcess.Timestamp, exe.Spec.MonitorTimestamp); + Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); + }); + Assert.Equal(3, processMonitor.MonitoredProcesses.Count); + Assert.All(processMonitor.MonitoredProcesses, process => Assert.Same(parentProcess, process)); } [Fact] @@ -4190,7 +4210,13 @@ private static void AssertEffectiveArgumentIndexesMatchSpecArgs(IReadOnlyList monitorProcess; + public List MonitoredProcesses { get; } = []; + + public DcpProcessIdentity GetMonitorProcess(Process process) + { + MonitoredProcesses.Add(process); + return monitorProcess ?? throw new InvalidOperationException("No test monitor process identity was configured."); + } } private static Aspire.Hosting.Dcp.ResourceSnapshotBuilder CreateSnapshotBuilder(DistributedApplicationModel model) From 4e9dea8133121fe2113e2cbffc80ff8103a63a6d Mon Sep 17 00:00:00 2001 From: David Negstad Date: Fri, 15 May 2026 12:02:50 -0700 Subject: [PATCH 06/38] Add named resource lifetime APIs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureAppService.AppHost/AppHost.cs | 4 +- .../AzureContainerApps.AppHost/AppHost.cs | 6 +- .../ServiceBus.AppHost/AppHost.cs | 2 +- .../AppHost.cs | 2 +- .../DevTunnels/DevTunnels.AppHost/AppHost.cs | 6 +- .../TestShop/TestShop.AppHost/AppHost.cs | 2 +- playground/TypeScriptAppHost/apphost.ts | 3 +- playground/TypeScriptApps/RpsArena/apphost.ts | 4 +- .../AzureEventHubsExtensions.cs | 10 +- .../AzureServiceBusExtensions.cs | 7 +- .../ExecutableLifetimeAnnotation.cs | 16 --- .../ContainerResourceBuilderExtensions.cs | 10 +- .../ExecutableResourceBuilderExtensions.cs | 37 ------- .../ProjectResourceBuilderExtensions.cs | 38 ------- .../ResourceBuilderExtensions.cs | 101 +++++++++++++----- ...eScriptSqlServerNativeAssetsBundleTests.cs | 4 +- .../AddAzureKustoTests.cs | 2 +- .../AzureEventHubsExtensionsTests.cs | 7 +- .../AzureResourceOptionsTests.cs | 3 +- .../AzureServiceBusExtensionsTests.cs | 7 +- ...DevTunnelResourceBuilderExtensionsTests.cs | 2 +- .../MySqlFunctionalTests.cs | 6 +- .../PostgresFunctionalTests.cs | 6 +- .../Dcp/DcpExecutorTests.cs | 22 ++-- .../DistributedApplicationTests.cs | 4 +- ...ExecutableResourceBuilderExtensionTests.cs | 4 +- .../ProjectResourceBuilderExtensionTests.cs | 4 +- .../ResourceBuilderLifetimeTests.cs | 32 +++++- .../Go/apphost.go | 2 +- .../Java/AppHost.java | 2 +- .../TypeScript/apphost.ts | 4 +- .../Aspire.Hosting.Milvus/Go/apphost.go | 2 +- .../Aspire.Hosting.Milvus/Java/AppHost.java | 2 +- .../TypeScript/apphost.ts | 4 +- .../Aspire.Hosting.MongoDB/Go/apphost.go | 2 +- .../Aspire.Hosting.MongoDB/Java/AppHost.java | 2 +- .../TypeScript/apphost.ts | 6 +- .../Aspire.Hosting.Nats/Go/apphost.go | 2 +- .../Aspire.Hosting.Nats/Java/AppHost.java | 2 +- .../Aspire.Hosting.Nats/TypeScript/apphost.ts | 4 +- .../Aspire.Hosting.Oracle/Go/apphost.go | 2 +- .../Aspire.Hosting.Oracle/Java/AppHost.java | 2 +- .../TypeScript/apphost.ts | 4 +- .../Aspire.Hosting.RabbitMQ/Go/apphost.go | 2 +- .../Aspire.Hosting.RabbitMQ/Java/AppHost.java | 2 +- .../TypeScript/apphost.ts | 4 +- .../Aspire.Hosting.SqlServer/Go/apphost.go | 2 +- .../Java/AppHost.java | 2 +- .../TypeScript/apphost.ts | 4 +- 49 files changed, 208 insertions(+), 201 deletions(-) diff --git a/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs b/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs index 36096e351dc..5efb805a14e 100644 --- a/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs +++ b/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs @@ -13,7 +13,7 @@ // Testing kv secret refs var cosmosDb = builder.AddAzureCosmosDB("account") - .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsEmulator(c => c.WithPersistentLifetime()); cosmosDb.AddCosmosDatabase("db"); @@ -24,7 +24,7 @@ var storage = infra.GetProvisionableResources().OfType().Single(); storage.AllowBlobPublicAccess = false; }) - .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsEmulator(c => c.WithPersistentLifetime()); var blobs = storage.AddBlobs("blobs"); // Testing projects diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs b/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs index 86876845d09..b7585e41e4e 100644 --- a/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs +++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs @@ -18,19 +18,19 @@ // Testing volumes var redis = builder.AddRedis("cache") - .WithLifetime(ContainerLifetime.Persistent) + .WithPersistentLifetime() .WithDataVolume(); // Testing secret outputs var cosmosDb = builder.AddAzureCosmosDB("account") .WithAccessKeyAuthentication() - .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsEmulator(c => c.WithPersistentLifetime()); cosmosDb.AddCosmosDatabase("db"); // Testing a connection string var storage = builder.AddAzureStorage("storage") - .RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsEmulator(c => c.WithPersistentLifetime()); var blobs = storage.AddBlobs("blobs"); // Testing docker files diff --git a/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs b/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs index 7ce1556a856..04d6441382b 100644 --- a/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs +++ b/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs @@ -34,7 +34,7 @@ serviceBus.RunAsEmulator(configure => configure.WithConfiguration(document => { document["UserConfig"]!["Logging"] = new JsonObject { ["Type"] = "Console" }; -}).WithLifetime(ContainerLifetime.Persistent)); +}).WithPersistentLifetime()); builder.AddProject("worker") .WithReference(queue).WaitFor(queue) diff --git a/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs b/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs index 70f955c925c..2779f382c31 100644 --- a/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs +++ b/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs @@ -49,7 +49,7 @@ privateEndpointsSubnet.AddPrivateEndpoint(queues); var sqlServer = builder.AddAzureSqlServer("sql") - .RunAsContainer(c => c.WithLifetime(ContainerLifetime.Persistent)); + .RunAsContainer(c => c.WithPersistentLifetime()); privateEndpointsSubnet.AddPrivateEndpoint(sqlServer); var db = sqlServer.AddDatabase("sqldb"); diff --git a/playground/DevTunnels/DevTunnels.AppHost/AppHost.cs b/playground/DevTunnels/DevTunnels.AppHost/AppHost.cs index d63ea74eea1..e98400765d2 100644 --- a/playground/DevTunnels/DevTunnels.AppHost/AppHost.cs +++ b/playground/DevTunnels/DevTunnels.AppHost/AppHost.cs @@ -3,10 +3,8 @@ var builder = DistributedApplication.CreateBuilder(args); -var api = builder.AddProject("api") - .WithEndpointProxySupport(false); -var frontend = builder.AddProject("frontend") - .WithEndpointProxySupport(false); +var api = builder.AddProject("api"); +var frontend = builder.AddProject("frontend"); var publicDevTunnel = builder.AddDevTunnel("devtunnel-public") .WithAnonymousAccess() // All ports on this tunnel default to allowing anonymous access diff --git a/playground/TestShop/TestShop.AppHost/AppHost.cs b/playground/TestShop/TestShop.AppHost/AppHost.cs index 7e5a16ccb15..939b2ef3de8 100644 --- a/playground/TestShop/TestShop.AppHost/AppHost.cs +++ b/playground/TestShop/TestShop.AppHost/AppHost.cs @@ -69,7 +69,7 @@ var messaging = builder.AddRabbitMQ("messaging") .WithDataVolume() - .WithLifetime(ContainerLifetime.Persistent) + .WithPersistentLifetime() .WithManagementPlugin() .PublishAsContainer(); diff --git a/playground/TypeScriptAppHost/apphost.ts b/playground/TypeScriptAppHost/apphost.ts index 38f24d7f6f9..fe03d4b03d1 100644 --- a/playground/TypeScriptAppHost/apphost.ts +++ b/playground/TypeScriptAppHost/apphost.ts @@ -8,7 +8,6 @@ import { createBuilder, refExpr, EnvironmentCallbackContext, - ContainerLifetime, ExecuteCommandContext, InputsDialogValidationContext, InputType @@ -50,7 +49,7 @@ console.log("Added Express API with reference to PostgreSQL database"); // Redis const cache = await builder.addRedis("cache"); -await cache.withLifetime(ContainerLifetime.Persistent); +await cache.withPersistentLifetime(); await cache.withCommand( "set-prefix", "Set prefix", diff --git a/playground/TypeScriptApps/RpsArena/apphost.ts b/playground/TypeScriptApps/RpsArena/apphost.ts index b0eb4a5695d..c01ae0cb9e6 100644 --- a/playground/TypeScriptApps/RpsArena/apphost.ts +++ b/playground/TypeScriptApps/RpsArena/apphost.ts @@ -1,7 +1,7 @@ // Rock Paper Scissors Arena — Aspire TypeScript AppHost // A polyglot game: C# Game Master, Python & Node.js players, React frontend, PostgreSQL -import { createBuilder, ContainerLifetime, type ExecuteCommandContext, type ExecuteCommandResult } from './.modules/aspire.js'; +import { createBuilder, type ExecuteCommandContext, type ExecuteCommandResult } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -10,7 +10,7 @@ const builder = await createBuilder(); // Persistent lifetime so data survives restarts during development. const postgres = await builder .addPostgres("postgres") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withPgAdmin() .withDataVolume(); diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs index 124c88b511c..f2334a6a8fe 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs @@ -288,7 +288,15 @@ public static IResourceBuilder RunAsEmulator(this IResou } } - storageResource = storageResource.RunAsEmulator(c => c.WithLifetime(lifetime)); + storageResource = storageResource.RunAsEmulator(c => + { + _ = lifetime switch + { + ContainerLifetime.Session => c.WithSessionLifetime(), + ContainerLifetime.Persistent => c.WithPersistentLifetime(), + _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), lifetime)}'.") + }; + }); var storage = storageResource.Resource; diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs index 13f2d48f272..902eef4b0e5 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs @@ -448,7 +448,12 @@ public static IResourceBuilder RunAsEmulator(this IReso } } - sqlServerResource = sqlServerResource.WithLifetime(lifetime); + sqlServerResource = lifetime switch + { + ContainerLifetime.Session => sqlServerResource.WithSessionLifetime(), + ContainerLifetime.Persistent => sqlServerResource.WithPersistentLifetime(), + _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), lifetime)}'.") + }; // RunAsEmulator() can be followed by custom model configuration so we need to delay the creation of the Config.json file // until all resources are about to be prepared and annotations can't be updated anymore. diff --git a/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs index 858277d3a3d..fc5eb016df3 100644 --- a/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs @@ -5,22 +5,6 @@ namespace Aspire.Hosting.ApplicationModel; -/// -/// Lifetime modes for executable resources. -/// -public enum ExecutableLifetime -{ - /// - /// Create the resource when the app host process starts and dispose of it when the app host process shuts down. - /// - Session, - - /// - /// Attempt to re-use a previously created resource if one exists. Do not destroy the executable on app host process shutdown. - /// - Persistent, -} - /// /// Annotation that controls the lifetime of an executable resource. /// diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index c89070b3c7a..f7325aaac53 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -541,17 +541,23 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// var builder = DistributedApplication.CreateBuilder(args); /// /// builder.AddContainer("mycontainer", "myimage") - /// .WithLifetime(ContainerLifetime.Persistent); + /// .WithPersistentLifetime(); /// /// builder.Build().Run(); /// /// /// - [AspireExportIgnore(Reason = "Polyglot app hosts use the resource-level overload that accepts Lifetime.")] + [Obsolete("Use WithPersistentLifetime or WithSessionLifetime instead.")] + [AspireExportIgnore(Reason = "Polyglot app hosts use WithPersistentLifetime or WithSessionLifetime instead.")] public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ContainerLifetime lifetime) where T : ContainerResource { ArgumentNullException.ThrowIfNull(builder); + foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) + { + builder.Resource.Annotations.Remove(annotation); + } + return builder.WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); } diff --git a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs index 3c7e92001ff..27217cf2db6 100644 --- a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs @@ -68,43 +68,6 @@ public static IResourceBuilder AddExecutable(this IDistribut }); } - /// - /// Sets the lifetime behavior of the executable resource. - /// - /// The resource type. - /// Builder for the executable resource. - /// The lifetime behavior of the executable resource. The default behavior is . - /// The . - /// - /// - /// Marking an executable resource to have a lifetime. - /// - /// var builder = DistributedApplication.CreateBuilder(args); - /// - /// builder.AddExecutable("myexecutable", "mycommand", ".") - /// .WithLifetime(Lifetime.Persistent); - /// - /// builder.Build().Run(); - /// - /// - /// - [Obsolete("Use ResourceBuilderExtensions.WithLifetime with Lifetime instead.")] - [AspireExportIgnore(Reason = "Polyglot app hosts use the resource-level overload that accepts Lifetime.")] - public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ExecutableLifetime lifetime) where T : ExecutableResource - { - ArgumentNullException.ThrowIfNull(builder); - - return builder.WithLifetime(ToLifetime(lifetime)); - } - - private static Lifetime ToLifetime(ExecutableLifetime lifetime) - => lifetime switch - { - ExecutableLifetime.Session => Lifetime.Session, - ExecutableLifetime.Persistent => Lifetime.Persistent, - _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) - }; - /// /// Adds annotation to to support containerization during deployment. /// diff --git a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs index 9a988de7072..140c6f681ec 100644 --- a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs @@ -322,44 +322,6 @@ public static IResourceBuilder AddProject(this IDistributedAppl .WithProjectDefaults(options); } - /// - /// Sets the lifetime behavior of the project executable resource. - /// - /// The project resource type. - /// Builder for the project resource. - /// The lifetime behavior of the project executable resource. The default behavior is . - /// The . - /// - /// - /// Marking a project resource to have a lifetime. - /// - /// var builder = DistributedApplication.CreateBuilder(args); - /// - /// builder.AddProject<Projects.ApiService>("api") - /// .WithLifetime(Lifetime.Persistent); - /// - /// builder.Build().Run(); - /// - /// - /// - [Obsolete("Use ResourceBuilderExtensions.WithLifetime with Lifetime instead.")] - [AspireExportIgnore(Reason = "Polyglot app hosts use the resource-level overload that accepts Lifetime.")] - public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ExecutableLifetime lifetime) - where TProjectResource : ProjectResource - { - ArgumentNullException.ThrowIfNull(builder); - - return builder.WithLifetime(ToLifetime(lifetime)); - } - - private static Lifetime ToLifetime(ExecutableLifetime lifetime) - => lifetime switch - { - ExecutableLifetime.Session => Lifetime.Session, - ExecutableLifetime.Persistent => Lifetime.Persistent, - _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) - }; - /// /// Adds a C# project or file-based app to the application model. /// diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 31d38566c91..ac475792a85 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -29,42 +29,61 @@ public static class ResourceBuilderExtensions private static readonly MethodInfo s_dispatchCustomWithReferenceMethod = typeof(ResourceBuilderExtensions).GetMethod(nameof(DispatchCustomWithReference), BindingFlags.NonPublic | BindingFlags.Static)!; /// - /// Sets the lifetime behavior for a resource that supports lifetime configuration. + /// Configures a resource to use a session lifetime. /// /// The resource type. /// The resource builder. - /// The lifetime behavior for the resource. The default behavior is . /// The . /// /// - /// Marking a resource to have a lifetime. + /// Marking a resource to have a session lifetime. /// /// var builder = DistributedApplication.CreateBuilder(args); /// /// builder.AddProject<Projects.ApiService>("api") - /// .WithLifetime(Lifetime.Persistent); + /// .WithSessionLifetime(); /// /// builder.Build().Run(); /// /// /// - [AspireExport(Description = "Sets the lifetime behavior of the resource")] - public static IResourceBuilder WithLifetime(this IResourceBuilder builder, Lifetime lifetime) + /// Thrown when the resource does not support lifetime configuration. + [AspireExport(Description = "Sets session lifetime behavior for the resource")] + public static IResourceBuilder WithSessionLifetime(this IResourceBuilder builder) where T : IResource { ArgumentNullException.ThrowIfNull(builder); - if (builder.Resource is ContainerResource) - { - return builder.WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = ToContainerLifetime(lifetime) }, ResourceAnnotationMutationBehavior.Replace); - } + return ApplyLifetime(builder, Lifetime.Session); + } - if (builder.Resource is ExecutableResource or ProjectResource) - { - return builder.WithAnnotation(new ExecutableLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); - } + /// + /// Configures a resource to use a persistent lifetime. + /// + /// The resource type. + /// The resource builder. + /// The . + /// + /// + /// Marking a resource to have a persistent lifetime. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// + /// builder.AddProject<Projects.ApiService>("api") + /// .WithPersistentLifetime(); + /// + /// builder.Build().Run(); + /// + /// + /// + /// Thrown when the resource does not support lifetime configuration. + [AspireExport(Description = "Sets persistent lifetime behavior for the resource")] + public static IResourceBuilder WithPersistentLifetime(this IResourceBuilder builder) + where T : IResource + { + ArgumentNullException.ThrowIfNull(builder); - throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); + return ApplyLifetime(builder, Lifetime.Persistent); } /// @@ -72,36 +91,66 @@ public static IResourceBuilder WithLifetime(this IResourceBuilder build /// /// The resource type. /// The resource builder. - /// The parent process to monitor. + /// The ID of the parent process to monitor. /// The . /// - /// This method also sets the resource lifetime to . The resource is tied to both - /// the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + /// The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. /// /// Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. /// /// var builder = DistributedApplication.CreateBuilder(args); /// - /// using var parentProcess = Process.GetProcessById(1234); /// builder.AddProject<Projects.ApiService>("api") - /// .WithParentProcessLifetime(parentProcess); + /// .WithParentProcessLifetime(parentProcessId: 1234); /// /// builder.Build().Run(); /// /// /// - /// Thrown when is . + /// Thrown when is less than or equal to zero. + /// Thrown when does not identify a running process. /// Thrown when the resource does not support lifetime configuration. - [AspireExportIgnore(Reason = "System.Diagnostics.Process is a .NET runtime type not usable from polyglot hosts.")] - public static IResourceBuilder WithParentProcessLifetime(this IResourceBuilder builder, SystemProcess parentProcess) + [AspireExport(Description = "Sets persistent lifetime behavior tied to a parent process")] + public static IResourceBuilder WithParentProcessLifetime(this IResourceBuilder builder, int parentProcessId) where T : IResource { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(parentProcess); + + if (parentProcessId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(parentProcessId), "The parent process ID must be greater than zero."); + } return builder - .WithLifetime(Lifetime.Persistent) - .WithAnnotation(new ParentProcessLifetimeAnnotation(parentProcess), ResourceAnnotationMutationBehavior.Replace); + .WithPersistentLifetime() + .WithAnnotation(new ParentProcessLifetimeAnnotation(SystemProcess.GetProcessById(parentProcessId)), ResourceAnnotationMutationBehavior.Replace); + } + + private static IResourceBuilder ApplyLifetime(IResourceBuilder builder, Lifetime lifetime) + where T : IResource + { + RemoveParentProcessLifetime(builder); + + if (builder.Resource is ContainerResource) + { + return builder.WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = ToContainerLifetime(lifetime) }, ResourceAnnotationMutationBehavior.Replace); + } + + if (builder.Resource is ExecutableResource or ProjectResource) + { + return builder.WithAnnotation(new ExecutableLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + } + + throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); + } + + private static void RemoveParentProcessLifetime(IResourceBuilder builder) + where T : IResource + { + foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) + { + builder.Resource.Annotations.Remove(annotation); + } } /// diff --git a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs index 0492cd3a386..d3ba76ec212 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/TypeScriptSqlServerNativeAssetsBundleTests.cs @@ -56,11 +56,11 @@ public async Task StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets() var appHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "apphost.ts"); File.WriteAllText(appHostPath, """ - import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; + import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); const sql = await builder.addSqlServer('sql') - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume(); await sql.addDatabase('mydb'); diff --git a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs index b029174d695..86f54f7d06b 100644 --- a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs +++ b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs @@ -303,7 +303,7 @@ public void RunAsEmulator_WithCustomLifetime_ShouldConfigureLifetimeAnnotation() // Act var resourceBuilder = builder.AddAzureKustoCluster("test-kusto").RunAsEmulator(containerBuilder => { - containerBuilder.WithLifetime(ContainerLifetime.Persistent); + containerBuilder.WithPersistentLifetime(); }); // Assert diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs index e974b387335..0dfdeb02e95 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs @@ -511,7 +511,12 @@ public void AddAzureEventHubsWithEmulator_SetsStorageLifetime(bool isPersistent) var serviceBus = builder.AddAzureEventHubs("eh").RunAsEmulator(configureContainer: builder => { - builder.WithLifetime(lifetime); + _ = lifetime switch + { + ContainerLifetime.Session => builder.WithSessionLifetime(), + ContainerLifetime.Persistent => builder.WithPersistentLifetime(), + _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), lifetime)}'.") + }; }); var azurite = builder.Resources.FirstOrDefault(x => x.Name == "eh-storage"); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs index f172fc6014c..fb9d4bde14e 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs @@ -1,7 +1,6 @@ // 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.ApplicationModel; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -34,7 +33,7 @@ public async Task AzureResourceOptionsCanBeConfigured() // ensure that resources with a hyphen still have a hyphen in the bicep name var sqlDatabase = builder.AddAzureSqlServer("sql-server") - .RunAsContainer(x => x.WithLifetime(ContainerLifetime.Persistent)) + .RunAsContainer(x => x.WithPersistentLifetime()) .AddDatabase("evadexdb").WithDefaultAzureSku(); using var app = builder.Build(); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs index 20320a5eb5d..30d078d2395 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs @@ -608,7 +608,12 @@ public void AddAzureServiceBusWithEmulator_SetsSqlLifetime(bool isPersistent) var serviceBus = builder.AddAzureServiceBus("sb").RunAsEmulator(configureContainer: builder => { - builder.WithLifetime(lifetime); + _ = lifetime switch + { + ContainerLifetime.Session => builder.WithSessionLifetime(), + ContainerLifetime.Persistent => builder.WithPersistentLifetime(), + _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), lifetime)}'.") + }; }); var sql = builder.Resources.FirstOrDefault(x => x.Name == "sb-mssql"); diff --git a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs index 30b5fe0b8e8..6ce6fd08bb1 100644 --- a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs +++ b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs @@ -61,7 +61,7 @@ public void AddDevTunnel_WithPersistentExecutableLifetime_AddsExecutableLifetime using var builder = TestDistributedApplicationBuilder.Create(); var tunnel = builder.AddDevTunnel("tunnel", "custom-id") - .WithLifetime(Lifetime.Persistent); + .WithPersistentLifetime(); Assert.True(tunnel.Resource.TryGetLastAnnotation(out var annotation)); Assert.Equal(Lifetime.Persistent, annotation.Lifetime); diff --git a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs index 172e5720341..7368b7c386d 100644 --- a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs +++ b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs @@ -572,14 +572,14 @@ public async Task MySql_WithPersistentLifetime_ReusesContainers(bool useMultiple var passwordParameter = builder.AddParameter("pwd", "p@ssw0rd1", secret: true); var mysql = builder - .AddMySql("resource", password: passwordParameter).WithLifetime(ContainerLifetime.Persistent) - .WithPhpMyAdmin(c => c.WithLifetime(ContainerLifetime.Persistent)) + .AddMySql("resource", password: passwordParameter).WithPersistentLifetime() + .WithPhpMyAdmin(c => c.WithPersistentLifetime()) .AddDatabase("db"); if (useMultipleInstances) { var passwordParameter2 = builder.AddParameter("pwd2", "p@ssw0rd2", secret: true); - builder.AddMySql("resource2", password: passwordParameter2).WithLifetime(ContainerLifetime.Persistent); + builder.AddMySql("resource2", password: passwordParameter2).WithPersistentLifetime(); } var app = builder.Build(); diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs index 37dd7f65051..7a4551e7b65 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs @@ -559,9 +559,9 @@ public async Task Postgres_WithPersistentLifetime_ReusesContainers() var passwordParameter = builder.AddParameter("pwd", "p@ssword1", secret: true); builder - .AddPostgres("resource", password: passwordParameter).WithLifetime(ContainerLifetime.Persistent) - .WithPgWeb(c => c.WithLifetime(ContainerLifetime.Persistent)) - .WithPgAdmin(c => c.WithLifetime(ContainerLifetime.Persistent)) + .AddPostgres("resource", password: passwordParameter).WithPersistentLifetime() + .WithPgWeb(c => c.WithPersistentLifetime()) + .WithPgAdmin(c => c.WithPersistentLifetime()) .AddDatabase("mydb"); var app = builder.Build(); diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 97fb9fe1abc..f8f945db4d5 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -635,7 +635,7 @@ public async Task EndpointPortsPersistentExecutableDefaultsToProxylessEndpoint() const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1002; builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") - .WithLifetime(Lifetime.Persistent) + .WithPersistentLifetime() .WithEndpoint(name: "PortSetNoTargetPort", port: desiredPort, env: "PORT_SET_NO_TARGET_PORT"); var configDict = new Dictionary @@ -1373,7 +1373,7 @@ public async Task EndpointPortsPersistentProjectDefaultsToProxylessEndpoint() const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1002; builder.AddProject("ServiceA", launchProfileName: null) - .WithLifetime(Lifetime.Persistent) + .WithPersistentLifetime() .WithHttpEndpoint(name: "stable", port: desiredPort); var configDict = new Dictionary @@ -2172,7 +2172,7 @@ public async Task PersistentPlainExecutable_ExtensionMode_RunsInProcess() var executable = new TestExecutableResource("test-working-directory"); builder.AddResource(executable) .WithDebugSupport(mode => new ExecutableLaunchConfiguration("test") { Mode = mode }, "test") - .WithLifetime(Lifetime.Persistent); + .WithPersistentLifetime(); var configDict = new Dictionary { @@ -2205,11 +2205,11 @@ public async Task PersistentDcpResourcesDoNotIncludeMonitorProcessByDefault() var builder = DistributedApplication.CreateBuilder(); builder.AddContainer("database", "image") - .WithLifetime(Lifetime.Persistent); + .WithPersistentLifetime(); builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) - .WithLifetime(Lifetime.Persistent); + .WithPersistentLifetime(); builder.AddProject("project", launchProfileName: null) - .WithLifetime(Lifetime.Persistent); + .WithPersistentLifetime(); var configDict = new Dictionary { @@ -2252,11 +2252,11 @@ public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() var parentProcess = Process.GetCurrentProcess(); builder.AddContainer("database", "image") - .WithParentProcessLifetime(parentProcess); + .WithParentProcessLifetime(parentProcess.Id); builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) - .WithParentProcessLifetime(parentProcess); + .WithParentProcessLifetime(parentProcess.Id); builder.AddProject("project", launchProfileName: null) - .WithParentProcessLifetime(parentProcess); + .WithParentProcessLifetime(parentProcess.Id); var configDict = new Dictionary { @@ -2294,7 +2294,7 @@ public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); }); Assert.Equal(3, processMonitor.MonitoredProcesses.Count); - Assert.All(processMonitor.MonitoredProcesses, process => Assert.Same(parentProcess, process)); + Assert.All(processMonitor.MonitoredProcesses, process => Assert.Equal(parentProcess.Id, process.Id)); } [Fact] @@ -2310,7 +2310,7 @@ public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath() builder.AddResource(executable) .WithCertificateAuthorityCollection(certificateAuthorities) .WithCertificateTrustScope(CertificateTrustScope.Override) - .WithLifetime(Lifetime.Persistent); + .WithPersistentLifetime(); var configDict = new Dictionary { diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index cd5e112a0ef..f8831868251 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -547,7 +547,7 @@ public async Task ExplicitStart_StartPersistentContainer() var containerBuilder = AddRedisContainer(testProgram.AppBuilder, notStartedResourceName) .WithContainerName(notStartedResourceName) - .WithLifetime(ContainerLifetime.Persistent) + .WithPersistentLifetime() .WithEndpoint(port: 6379, targetPort: 6379, name: "tcp", env: "REDIS_PORT") .WithExplicitStart(); @@ -1860,7 +1860,7 @@ public async Task PersistentNetworkCreatedIfPersistentContainers(bool createPers if (createPersistentContainer) { builder.AddContainer($"{testName}-persistent", RedisContainerImageTags.Image, RedisContainerImageTags.Tag) - .WithLifetime(ContainerLifetime.Persistent); + .WithPersistentLifetime(); } builder.AddContainer($"{testName}-nonpersistent", RedisContainerImageTags.Image, RedisContainerImageTags.Tag); diff --git a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs index 34ee2c7798a..b09d747e66d 100644 --- a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs @@ -73,11 +73,11 @@ public void WithWorkingDirectoryAllowsEmptyString() } [Fact] - public void WithLifetimeAddsExecutableLifetimeAnnotation() + public void WithPersistentLifetimeAddsExecutableLifetimeAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var executable = builder.AddExecutable("myexe", "command", "workingdirectory") - .WithLifetime(Lifetime.Persistent); + .WithPersistentLifetime(); var annotation = executable.Resource.Annotations.OfType().Single(); Assert.Equal(Lifetime.Persistent, annotation.Lifetime); diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs index ae1d5068634..5ec652f5275 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs @@ -9,12 +9,12 @@ namespace Aspire.Hosting.Tests; public class ProjectResourceBuilderExtensionTests { [Fact] - public void WithLifetimeAddsExecutableLifetimeAnnotation() + public void WithPersistentLifetimeAddsExecutableLifetimeAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var project = builder.AddProject("project", options => options.ExcludeLaunchProfile = true) - .WithLifetime(Lifetime.Persistent); + .WithPersistentLifetime(); var annotation = project.Resource.Annotations.OfType().Single(); Assert.Equal(Lifetime.Persistent, annotation.Lifetime); diff --git a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs index bd041a4024b..290cb7fb434 100644 --- a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs @@ -9,27 +9,51 @@ namespace Aspire.Hosting.Tests; public class ResourceBuilderLifetimeTests { [Fact] - public void WithLifetimeAddsContainerLifetimeAnnotation() + public void WithPersistentLifetimeAddsContainerLifetimeAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var container = builder.AddContainer("container", "image") - .WithLifetime(Lifetime.Persistent); + .WithPersistentLifetime(); var annotation = container.Resource.Annotations.OfType().Single(); Assert.Equal(ContainerLifetime.Persistent, annotation.Lifetime); } [Fact] - public void WithLifetimeRejectsUnsupportedResourceTypes() + public void WithPersistentLifetimeRejectsUnsupportedResourceTypes() { using var builder = TestDistributedApplicationBuilder.Create(); var parameter = builder.AddParameter("parameter"); - void ConfigureLifetime() => parameter.WithLifetime(Lifetime.Persistent); + void ConfigureLifetime() => parameter.WithPersistentLifetime(); var exception = Assert.Throws((Action)ConfigureLifetime); Assert.Contains("does not support lifetime configuration", exception.Message); } + + [Fact] + public void WithPersistentLifetimeRemovesParentProcessLifetimeAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("container", "image") + .WithParentProcessLifetime(Environment.ProcessId) + .WithPersistentLifetime(); + + Assert.False(container.Resource.TryGetLastAnnotation(out _)); + } + + [Fact] + public void WithSessionLifetimeRemovesParentProcessLifetimeAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("container", "image") + .WithParentProcessLifetime(Environment.ProcessId) + .WithSessionLifetime(); + + Assert.False(container.Resource.TryGetLastAnnotation(out _)); + } } diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Go/apphost.go index f619af99784..9ae0b14a099 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Go/apphost.go @@ -40,7 +40,7 @@ func main() { } pgContainer.RunAsContainer(&aspire.RunAsContainerOptions{ ConfigureContainer: func(container aspire.PostgresServerResource) { - container.WithLifetime(aspire.ContainerLifetimePersistent) + container.WithPersistentLifetime() }, }) if pgContainer.Err() != nil { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Java/AppHost.java index ab3c5358286..578af44dba5 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/Java/AppHost.java @@ -13,7 +13,7 @@ void main() throws Exception { var pgContainer = builder.addAzurePostgresFlexibleServer("pg-container"); pgContainer.runAsContainer((container) -> { // Exercise PostgresServerResource builder methods within the callback - container.withLifetime(ContainerLifetime.PERSISTENT); + container.withPersistentLifetime(); }); // 5) addDatabase on container-mode server var dbContainer = pgContainer.addDatabase("containerdb"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/TypeScript/apphost.ts index 127c865566b..9f3667ac408 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Azure.PostgreSQL/TypeScript/apphost.ts @@ -1,4 +1,4 @@ -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -17,7 +17,7 @@ const pgContainer = await builder.addAzurePostgresFlexibleServer("pg-container") await pgContainer.runAsContainer({ configureContainer: async (container) => { // Exercise PostgresServerResource builder methods within the callback - await container.withLifetime(ContainerLifetime.Persistent); + await container.withPersistentLifetime(); }, }); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Go/apphost.go index bc8bf5318ce..9efff9e9aae 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Go/apphost.go @@ -47,7 +47,7 @@ func main() { builder.AddMilvus("milvus-cfg").WithConfigurationFile("./milvus.yaml") milvusChained := builder.AddMilvus("milvus-chained") - milvusChained.WithLifetime(aspire.ContainerLifetimePersistent) + milvusChained.WithPersistentLifetime() milvusChained.WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("milvus-chained-data")}) milvusChained.WithAttu() diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Java/AppHost.java index ddc8bf2596f..bb79d67dcc6 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/Java/AppHost.java @@ -39,7 +39,7 @@ void main() throws Exception { .withConfigurationFile("./milvus.yaml"); // ── 14. Fluent chaining: multiple With* methods ──────────────────────────── var milvusChained = builder.addMilvus("milvus-chained"); - milvusChained.withLifetime(ContainerLifetime.PERSISTENT); + milvusChained.withPersistentLifetime(); milvusChained.withDataVolume(new WithDataVolumeOptions().name("milvus-chained-data")); milvusChained.withAttu(); // ── 15. withReference: use Milvus database from a container resource ─────── diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/TypeScript/apphost.ts index a0b4880fe21..cebc4531b1b 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Milvus/TypeScript/apphost.ts @@ -1,7 +1,7 @@ // Aspire TypeScript AppHost — Milvus integration validation // Exercises every exported member of Aspire.Hosting.Milvus -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -55,7 +55,7 @@ await builder.addMilvus("milvus-cfg") // ── 14. Fluent chaining: multiple With* methods ──────────────────────────── await builder.addMilvus("milvus-chained") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume({ name: "milvus-chained-data" }) .withAttu(); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Go/apphost.go index cfd8d6c6afa..c0b0ef9df21 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Go/apphost.go @@ -40,7 +40,7 @@ func main() { builder.AddMongoDB("mongo-custom-pass", &aspire.AddMongoDBOptions{Password: &customPassword}) mongoChained := builder.AddMongoDB("mongo-chained") - mongoChained.WithLifetime(aspire.ContainerLifetimePersistent) + mongoChained.WithPersistentLifetime() mongoChained.WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("mongo-chained-data")}) mongoChained.AddDatabase("app-db") diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Java/AppHost.java index a23f0ac5539..26a9b08222f 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/Java/AppHost.java @@ -29,7 +29,7 @@ void main() throws Exception { builder.addMongoDB("mongo-custom-pass", new AddMongoDBOptions().password(customPassword)); // Test 9: Chained configuration - multiple With* methods var mongoChained = builder.addMongoDB("mongo-chained"); - mongoChained.withLifetime(ContainerLifetime.PERSISTENT); + mongoChained.withPersistentLifetime(); mongoChained.withDataVolume(new WithDataVolumeOptions().name("mongo-chained-data")); // Test 10: Add multiple databases to same server mongoChained.addDatabase("app-db"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/TypeScript/apphost.ts index 6d9ad0ec02a..cd4da41014f 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.MongoDB/TypeScript/apphost.ts @@ -1,7 +1,7 @@ // Aspire TypeScript AppHost // For more information, see: https://aspire.dev -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -40,7 +40,7 @@ await builder.addMongoDB("mongo-custom-pass", { password: customPassword }); // Test 9: Chained configuration - multiple With* methods const mongoChained = await builder.addMongoDB("mongo-chained") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume({ name: "mongo-chained-data" }); // Test 10: Add multiple databases to same server @@ -57,4 +57,4 @@ const _userName = await mongo.userNameReference(); // Build and run the app const _cstr = await mongo.connectionStringExpression(); const _databases = mongo.databases; -await builder.build().run(); \ No newline at end of file +await builder.build().run(); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Go/apphost.go index c9242b7af5e..8a0d815c82f 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Go/apphost.go @@ -25,7 +25,7 @@ func main() { Name: aspire.StringPtr("nats-data"), IsReadOnly: aspire.BoolPtr(false), }) - nats2.WithLifetime(aspire.ContainerLifetimePersistent) + nats2.WithPersistentLifetime() if err = nats2.Err(); err != nil { log.Fatalf(aspire.FormatError(err)) } diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Java/AppHost.java index 446ff018365..f7286037d66 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/Java/AppHost.java @@ -14,7 +14,7 @@ void main() throws Exception { var nats2 = builder.addNats("messaging2", new AddNatsOptions().port(4223.0)) .withJetStream() .withDataVolume(new WithDataVolumeOptions().name("nats-data").isReadOnly(false)) - .withLifetime(ContainerLifetime.PERSISTENT); + .withPersistentLifetime(); // withDataBindMount - bind mount a host directory var nats3 = builder.addNats("messaging3"); nats3.withDataBindMount("/tmp/nats-data"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/TypeScript/apphost.ts index aa220a1856c..965d5286151 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Nats/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Nats/TypeScript/apphost.ts @@ -1,7 +1,7 @@ // Aspire TypeScript AppHost — NATS integration validation // Exercises all [AspireExport] methods for Aspire.Hosting.Nats -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -18,7 +18,7 @@ await nats.withDataVolume(); const nats2 = await builder.addNats("messaging2", { port: 4223 }) .withJetStream() .withDataVolume({ name: "nats-data", isReadOnly: false }) - .withLifetime(ContainerLifetime.Persistent); + .withPersistentLifetime(); // withDataBindMount — bind mount a host directory const nats3 = await builder.addNats("messaging3"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Go/apphost.go index e77ebdcef51..7cfdd623207 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Go/apphost.go @@ -54,7 +54,7 @@ func main() { oracle.WithReference(otherOracle) oracle3 := builder.AddOracle("oracledb3") - oracle3.WithLifetime(aspire.ContainerLifetimePersistent) + oracle3.WithPersistentLifetime() oracle3.WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("oracle3-data")}) oracle3.AddDatabase("chaineddb") if err = oracle3.Err(); err != nil { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Java/AppHost.java index 4bc9727c505..d7e067afc2b 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/Java/AppHost.java @@ -33,7 +33,7 @@ void main() throws Exception { oracle.withReference(otherOracle, new WithReferenceOptions()); // ---- Fluent chaining: multiple methods chained ---- var oracle3 = builder.addOracle("oracledb3"); - oracle3.withLifetime(ContainerLifetime.PERSISTENT); + oracle3.withPersistentLifetime(); oracle3.withDataVolume("oracle3-data"); oracle3.addDatabase("chaineddb"); // ---- Property access on OracleDatabaseServerResource ---- diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/TypeScript/apphost.ts index 72bbcec782f..e59d9a79297 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.Oracle/TypeScript/apphost.ts @@ -1,7 +1,7 @@ // Aspire TypeScript AppHost - Oracle Integration Validation // Validates all [AspireExport] methods for Aspire.Hosting.Oracle -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -46,7 +46,7 @@ await oracle.withReference(otherOracle); // ---- Fluent chaining: multiple methods chained ---- const oracle3 = await builder.addOracle("oracledb3") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume({ name: "oracle3-data" }); await oracle3.addDatabase("chaineddb"); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Go/apphost.go index e878a87404e..ce839881957 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Go/apphost.go @@ -20,7 +20,7 @@ func main() { } rabbitmq2 := builder.AddRabbitMQ("messaging2") - rabbitmq2.WithLifetime(aspire.ContainerLifetimePersistent) + rabbitmq2.WithPersistentLifetime() rabbitmq2.WithDataVolume() rabbitmq2.WithManagementPlugin(&aspire.WithManagementPluginOptions{Port: aspire.Float64Ptr(15673)}) if err = rabbitmq2.Err(); err != nil { diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Java/AppHost.java index 703bd992de5..9b44c72f885 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/Java/AppHost.java @@ -6,7 +6,7 @@ void main() throws Exception { rabbitmq.withDataVolume(); rabbitmq.withManagementPlugin(); var rabbitmq2 = builder.addRabbitMQ("messaging2"); - rabbitmq2.withLifetime(ContainerLifetime.PERSISTENT); + rabbitmq2.withPersistentLifetime(); rabbitmq2.withDataVolume(); rabbitmq2.withManagementPlugin(15673.0); // ---- Property access on RabbitMQServerResource ---- diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/TypeScript/apphost.ts index abf93afcf6d..edce80695dc 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.RabbitMQ/TypeScript/apphost.ts @@ -1,4 +1,4 @@ -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -8,7 +8,7 @@ await rabbitmq.withManagementPlugin(); const rabbitmq2 = await builder .addRabbitMQ("messaging2") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume() .withManagementPlugin({ port: 15673 }); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Go/apphost.go b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Go/apphost.go index 31aa32841e2..8d06e4b6f63 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Go/apphost.go +++ b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Go/apphost.go @@ -27,7 +27,7 @@ func main() { builder.AddSqlServer("sql-custom-pass", &aspire.AddSqlServerOptions{Password: &customPassword}) sqlChained := builder.AddSqlServer("sql-chained") - sqlChained.WithLifetime(aspire.ContainerLifetimePersistent) + sqlChained.WithPersistentLifetime() sqlChained.WithDataVolume(&aspire.WithDataVolumeOptions{Name: aspire.StringPtr("sql-chained-data")}) sqlChained.WithHostPort(12433) diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Java/AppHost.java index 13241434091..c44e77df036 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/Java/AppHost.java @@ -17,7 +17,7 @@ void main() throws Exception { builder.addSqlServer("sql-custom-pass", new AddSqlServerOptions().password(customPassword)); // Test 6: Chained configuration - multiple With* methods var sqlChained = builder.addSqlServer("sql-chained"); - sqlChained.withLifetime(ContainerLifetime.PERSISTENT); + sqlChained.withPersistentLifetime(); sqlChained.withDataVolume(new WithDataVolumeOptions().name("sql-chained-data")); sqlChained.withHostPort(12433.0); // Test 7: Add multiple databases to same server diff --git a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/TypeScript/apphost.ts index a931df9c4b0..1ceacc413e2 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting.SqlServer/TypeScript/apphost.ts @@ -1,4 +1,4 @@ -import { createBuilder, ContainerLifetime } from './.modules/aspire.js'; +import { createBuilder } from './.modules/aspire.js'; const builder = await createBuilder(); @@ -22,7 +22,7 @@ await builder.addSqlServer("sql-custom-pass", { password: customPassword }); // Test 6: Chained configuration - multiple With* methods const sqlChained = await builder.addSqlServer("sql-chained") - .withLifetime(ContainerLifetime.Persistent) + .withPersistentLifetime() .withDataVolume({ name: "sql-chained-data" }) .withHostPort({ port: 12433 }); From 5ea0446f6a4af5b9fb102f59b4fc9f1810d65a5f Mon Sep 17 00:00:00 2001 From: David Negstad Date: Fri, 15 May 2026 14:13:18 -0700 Subject: [PATCH 07/38] Update API compatibility suppressions Regenerate Aspire.Hosting API compatibility suppressions for endpoint API changes and remove the obsolete marker from the container-specific lifetime API while documenting the preferred named lifetime methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CompatibilitySuppressions.xml | 56 +++++++++++++++++++ .../ContainerResourceBuilderExtensions.cs | 5 +- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/CompatibilitySuppressions.xml b/src/Aspire.Hosting/CompatibilitySuppressions.xml index 1200e513ce8..27cc2519e8f 100644 --- a/src/Aspire.Hosting/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting/CompatibilitySuppressions.xml @@ -1,6 +1,62 @@  + + CP0002 + M:Aspire.Hosting.ApplicationModel.EndpointAnnotation.#ctor(System.Net.Sockets.ProtocolType,Aspire.Hosting.ApplicationModel.NetworkIdentifier,System.String,System.String,System.String,System.Nullable{System.Int32},System.Nullable{System.Int32},System.Nullable{System.Boolean},System.Boolean) + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0002 + M:Aspire.Hosting.ApplicationModel.EndpointAnnotation.#ctor(System.Net.Sockets.ProtocolType,System.String,System.String,System.String,System.Nullable{System.Int32},System.Nullable{System.Int32},System.Nullable{System.Boolean},System.Boolean) + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0002 + M:Aspire.Hosting.ApplicationModel.EndpointAnnotation.get_IsProxied + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0002 + M:Aspire.Hosting.ContainerResourceBuilderExtensions.WithEndpointProxySupport``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Boolean) + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0002 + M:Aspire.Hosting.ResourceBuilderExtensions.WithEndpoint``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.String,System.String,System.Boolean,System.Nullable{System.Boolean},System.Nullable{System.Net.Sockets.ProtocolType}) + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0002 + M:Aspire.Hosting.ResourceBuilderExtensions.WithEndpoint``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.String,System.String,System.Boolean,System.Nullable{System.Boolean}) + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0002 + M:Aspire.Hosting.ResourceBuilderExtensions.WithHttpEndpoint``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.String,System.Boolean) + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0002 + M:Aspire.Hosting.ResourceBuilderExtensions.WithHttpsEndpoint``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.String,System.Boolean) + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + CP0006 M:Aspire.Hosting.Pipelines.IDeploymentStateManager.ClearAllStateAsync(System.Threading.CancellationToken) diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index f7325aaac53..9b076e8349a 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -535,6 +535,10 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// The lifetime behavior of the container resource. The default behavior is . /// The . /// + /// + /// Prefer or + /// for new code. + /// /// /// Marking a container resource to have a lifetime. /// @@ -547,7 +551,6 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// /// /// - [Obsolete("Use WithPersistentLifetime or WithSessionLifetime instead.")] [AspireExportIgnore(Reason = "Polyglot app hosts use WithPersistentLifetime or WithSessionLifetime instead.")] public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ContainerLifetime lifetime) where T : ContainerResource { From b433333329226f0b5d44d6a78f9126bd431fb1c6 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Sat, 16 May 2026 12:35:10 -0700 Subject: [PATCH 08/38] Fix persistent resource CI regressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../HostedAgentBuilderExtension.cs | 2 +- src/Aspire.Hosting/Dcp/DcpExecutor.cs | 134 +++++++++++++----- src/Aspire.Hosting/Dcp/DcpModelUtilities.cs | 7 +- .../DistributedApplicationBuilder.cs | 17 ++- .../ResourceBuilderExtensions.cs | 7 +- .../Aspire.Hosting.Tests/AspireStoreTests.cs | 16 +++ .../Dcp/DcpExecutorTests.cs | 8 +- 7 files changed, 143 insertions(+), 48 deletions(-) diff --git a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs index 8d9f7b1308f..18048c6fe64 100644 --- a/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs +++ b/src/Aspire.Hosting.Foundry/HostedAgent/HostedAgentBuilderExtension.cs @@ -111,7 +111,7 @@ public static IResourceBuilder PublishAsHostedAgent( var targetPort = existingHttpEndpoint?.TargetPort ?? 8088; builder - .WithHttpEndpoint(name: "http", env: "DEFAULT_AD_PORT", targetPort: targetPort) + .WithHttpEndpoint(name: "http", env: "DEFAULT_AD_PORT", targetPort: targetPort, isProxied: true) .WithUrls((ctx) => { var http = ctx.Urls.FirstOrDefault(u => u.Endpoint?.EndpointName == "http" || u.Endpoint?.EndpointName == "https"); diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index 7421a457904..acc6fbb66c7 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -112,7 +112,7 @@ public DcpExecutor(ILogger logger, _executionContext = executionContext; _appResources = appResources; - _resourceWatcher = new DcpResourceWatcher(logger, kubernetesService, loggerService, executorEvents, model, _appResources, _configuration, PublishEndpointsAllocatedEventAsync, _shutdownCancellation.Token); + _resourceWatcher = new DcpResourceWatcher(logger, kubernetesService, loggerService, executorEvents, model, _appResources, _configuration, PublishLateEndpointsAllocatedEventAsync, _shutdownCancellation.Token); DeleteResourceRetryPipeline = DcpPipelineBuilder.BuildDeleteRetryPipeline(logger); @@ -892,7 +892,33 @@ await _executorEvents.PublishAsync(new OnResourceChangedContext( return; } - await _executorEvents.PublishAsync(new OnConnectionStringAvailableContext(cancellationToken, modelResource)).ConfigureAwait(false); + if (replicas.All(r => IsDelayedStart(r.DcpResource))) + { + // DCP resources with Spec.Start=false are created now so they are visible to DCP and the dashboard, + // but their process/container is not actually started until StartResourceAsync flips Spec.Start to true. + // Keep BeforeResourceStartedEvent tied to the actual start operation rather than object creation. + foreach (var r in replicas) + { + await _executorEvents.PublishAsync(new OnResourceChangedContext( + cancellationToken, resourceType, modelResource, + r.DcpResource.Metadata.Name, + new ResourceStatus(KnownResourceStates.NotStarted, null, null), + s => s with + { + State = new ResourceStateSnapshot(KnownResourceStates.NotStarted, null) + }) + ).ConfigureAwait(false); + } + + foreach (var er in replicas) + { + await CreateReplicaAsync(er).ConfigureAwait(false); + } + + return; + } + + await PublishConnectionStringAvailableEventAsync(modelResource, cancellationToken).ConfigureAwait(false); // For single-replica resources (e.g. containers), include the DCP resource name in the starting event. // For multi-replica resources (e.g. projects with replicas), the starting event applies to the group, so DcpResourceName is null. @@ -901,37 +927,7 @@ await _executorEvents.PublishAsync(new OnResourceChangedContext( foreach (var er in replicas) { - try - { - using var replicaActivity = ProfilingTelemetry.StartDcpCreateResourceReplica(_configuration, er.ModelResource, er.DcpResourceKind, er.DcpResourceName); - AspireEventSource.Instance.DcpObjectCreationStart(er.DcpResourceKind, er.DcpResourceName); - try - { - await creator.CreateObjectAsync(er, context, resourceLogger, this, cancellationToken).ConfigureAwait(false); - } - catch (Exception ex) - { - replicaActivity.SetError(ex); - throw; - } - finally - { - AspireEventSource.Instance.DcpObjectCreationStop(er.DcpResourceKind, er.DcpResourceName); - } - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - throw; - } - catch (FailedToApplyEnvironmentException) - { - await _executorEvents.PublishAsync(new OnResourceFailedToStartContext(cancellationToken, resourceType, er.ModelResource, er.DcpResource.Metadata.Name)).ConfigureAwait(false); - } - catch (Exception ex) - { - resourceLogger.LogError(ex, "Failed to create resource {ResourceName}", er.ModelResource.Name); - await _executorEvents.PublishAsync(new OnResourceFailedToStartContext(cancellationToken, resourceType, er.ModelResource, er.DcpResource.Metadata.Name)).ConfigureAwait(false); - } + await CreateReplicaAsync(er).ConfigureAwait(false); } } catch (Exception ex) @@ -958,6 +954,51 @@ Func BuildSnapshotFunc(CustomRes _ => throw new NotImplementedException($"Does not support snapshots for resources of type '{dcpResource.Kind}'") }; } + + async Task CreateReplicaAsync(RenderedModelResource er) + { + try + { + using var replicaActivity = ProfilingTelemetry.StartDcpCreateResourceReplica(_configuration, er.ModelResource, er.DcpResourceKind, er.DcpResourceName); + AspireEventSource.Instance.DcpObjectCreationStart(er.DcpResourceKind, er.DcpResourceName); + try + { + await creator.CreateObjectAsync(er, context, resourceLogger, this, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + replicaActivity.SetError(ex); + throw; + } + finally + { + AspireEventSource.Instance.DcpObjectCreationStop(er.DcpResourceKind, er.DcpResourceName); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (FailedToApplyEnvironmentException) + { + await _executorEvents.PublishAsync(new OnResourceFailedToStartContext(cancellationToken, resourceType, er.ModelResource, er.DcpResource.Metadata.Name)).ConfigureAwait(false); + } + catch (Exception ex) + { + resourceLogger.LogError(ex, "Failed to create resource {ResourceName}", er.ModelResource.Name); + await _executorEvents.PublishAsync(new OnResourceFailedToStartContext(cancellationToken, resourceType, er.ModelResource, er.DcpResource.Metadata.Name)).ConfigureAwait(false); + } + } + + static bool IsDelayedStart(CustomResource resource) + { + return resource switch + { + Container { Spec.Start: false } => true, + Executable { Spec.Start: false } => true, + _ => false + }; + } } /// @@ -1114,7 +1155,7 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance // Ensure we explicitly start the container even if original container was created in "delay-start" mode. cr.DcpResource.Spec.Start = true; - await _executorEvents.PublishAsync(new OnConnectionStringAvailableContext(cancellationToken, resourceReference.ModelResource)).ConfigureAwait(false); + await PublishConnectionStringAvailableEventAsync(resourceReference.ModelResource, cancellationToken).ConfigureAwait(false); await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, resourceReference.ModelResource, resourceReference.DcpResourceName)).ConfigureAwait(false); var cctx = await _containerContextSource.Task.ConfigureAwait(false); await _containerCreator.CreateObjectAsync(cr, cctx, resourceLogger, this, cancellationToken).ConfigureAwait(false); @@ -1125,7 +1166,7 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance // Ensure we explicitly start the executable even if original executable was created in "delay-start" mode. er.DcpResource.Spec.Start = true; - await _executorEvents.PublishAsync(new OnConnectionStringAvailableContext(cancellationToken, resourceReference.ModelResource)).ConfigureAwait(false); + await PublishConnectionStringAvailableEventAsync(resourceReference.ModelResource, cancellationToken).ConfigureAwait(false); await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, resourceReference.ModelResource, resourceReference.DcpResourceName)).ConfigureAwait(false); await _executableCreator.CreateObjectAsync(er, EmptyCreationContext.s_instance, resourceLogger, this, cancellationToken).ConfigureAwait(false); break; @@ -1230,17 +1271,36 @@ private static void ForgetCachedCallbackResults(IResource resource) } } - private async Task PublishEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) + private async Task PublishEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) { lock (_endpointsAdvertised) { if (!_endpointsAdvertised.Add(resource.Name)) { - return; // Already published for this resource. + return false; // Already published for this resource. } } var ev = new ResourceEndpointsAllocatedEvent(resource, _executionContext.ServiceProvider); await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.NonBlockingConcurrent, ct).ConfigureAwait(false); + return true; + } + + private async Task PublishLateEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) + { + if (await PublishEndpointsAllocatedEventAsync(resource, ct).ConfigureAwait(false)) + { + await PublishConnectionStringAvailableEventAsync(resource, ct).ConfigureAwait(false); + } + } + + private async Task PublishConnectionStringAvailableEventAsync(IResource resource, CancellationToken ct) + { + if (!DcpModelUtilities.AreResourceEndpointsAllocated(resource)) + { + return; + } + + await _executorEvents.PublishAsync(new OnConnectionStringAvailableContext(ct, resource)).ConfigureAwait(false); } } diff --git a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs index aeba76b7967..d5fd305e84c 100644 --- a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs +++ b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs @@ -156,6 +156,11 @@ private static bool TryAddLocalhostAllocatedEndpoint(ServiceWithModelResource sp if (!svc.HasCompleteAddress && sp.EndpointAnnotation.IsProxied.GetValueOrDefault()) { + if (allowPending) + { + return false; + } + // This should never happen; if it does, we have a bug without a workaround for the user. // We should have waited for the service to have a complete address before getting here. throw new InvalidDataException($"Service {svc.Metadata.Name} should have valid address at this point"); @@ -244,7 +249,7 @@ private static void AddExecutableContainerNetworkAllocatedEndpoint sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, allocatedEndpoint); } - private static bool AreResourceEndpointsAllocated(IResource resource) + internal static bool AreResourceEndpointsAllocated(IResource resource) { return !resource.TryGetEndpoints(out var endpoints) || endpoints.All(e => e.AllocatedEndpoint is not null); } diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 8792b272974..38b2400f005 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -253,7 +253,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) var appHostFilePath = options.AppHostFilePath; var assemblyMetadata = AppHostAssembly?.GetCustomAttributes(); - var aspireDir = GetMetadataValue(assemblyMetadata, "AppHostProjectBaseIntermediateOutputPath"); + var aspireDir = ResolveAspireStorePath(assemblyMetadata, AppHostDirectory); ConfigurePipelineOptions(options); @@ -997,4 +997,19 @@ private void LoadDeploymentState(string appHostSha) /// The metadata value if found; otherwise, null. private static string? GetMetadataValue(IEnumerable? assemblyMetadata, string key) => assemblyMetadata?.FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase))?.Value; + + private static string ResolveAspireStorePath(IEnumerable? assemblyMetadata, string appHostDirectory) + { + var baseIntermediateOutputPath = GetMetadataValue(assemblyMetadata, "AppHostProjectBaseIntermediateOutputPath"); + if (!string.IsNullOrEmpty(baseIntermediateOutputPath)) + { + return baseIntermediateOutputPath; + } + + // File-based and dynamically loaded AppHosts do not have the MSBuild intermediate output + // metadata that normal project AppHosts get. Use the AppHost directory as the root so + // IAspireStore resolves to the workspace-local .aspire folder instead of creating a + // .NET-style obj directory for non-.NET AppHosts. + return Path.GetFullPath(appHostDirectory); + } } diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index ac475792a85..862954bc947 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -2618,6 +2618,7 @@ public static IResourceBuilder WithHttpHealthCheck(this IResourceBuilder { if (!endpoint.Exists) @@ -2625,12 +2626,6 @@ public static IResourceBuilder WithHttpHealthCheck(this IResourceBuilder - { var baseUri = new Uri(endpoint.Url, UriKind.Absolute); uri = new Uri(baseUri, path); return Task.CompletedTask; diff --git a/tests/Aspire.Hosting.Tests/AspireStoreTests.cs b/tests/Aspire.Hosting.Tests/AspireStoreTests.cs index beb1ab00730..79982b5475e 100644 --- a/tests/Aspire.Hosting.Tests/AspireStoreTests.cs +++ b/tests/Aspire.Hosting.Tests/AspireStoreTests.cs @@ -53,6 +53,22 @@ public void BasePath_ShouldBePrefixed_WhenUsingConfiguration() Assert.Contains(".aspire", path); } + [Fact] + public void BasePath_ShouldFallbackToAppHostAspireDirectory_WhenIntermediateOutputMetadataIsUnavailable() + { + using var projectDirectory = new TestTempDirectory(); + var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions + { + AssemblyName = typeof(string).Assembly.GetName().Name, + ProjectDirectory = projectDirectory.Path + }); + + using var app = builder.Build(); + var store = app.Services.GetRequiredService(); + + Assert.Equal(Path.Combine(projectDirectory.Path, ".aspire"), store.BasePath); + } + [Fact] public void GetOrCreateFileWithContent_ShouldCreateFile_WithStreamContent() { diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index f8f945db4d5..b0bdfc9fb5f 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -92,7 +92,9 @@ public async Task ResourceStarted_ProjectHasReplicas_EventRaisedOnce() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions, events: events); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType().ToList(); + var executables = kubernetesService.CreatedResources.OfType() + .Where(executable => string.Equals(executable.AppModelResourceName, "ServiceA", StringComparison.Ordinal)) + .ToList(); Assert.Equal(2, executables.Count); var e = Assert.Single(startingEvents); @@ -827,7 +829,9 @@ public async Task EndpointOtelServiceName(int replicaCount, string expectedName) var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType().ToList(); + var executables = kubernetesService.CreatedResources.OfType() + .Where(executable => string.Equals(executable.AppModelResourceName, "ServiceA", StringComparison.Ordinal)) + .ToList(); Assert.Equal(replicaCount, executables.Count); foreach (var exe in executables) From 74912d6d5367c112f3730c8430fce4df801de7a9 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Sat, 16 May 2026 15:48:07 -0700 Subject: [PATCH 09/38] Fix DCP endpoint allocation ordering Keep public resource endpoint events non-blocking while DCP waits for internal URL processing before creating workloads. Harden annotation access and update regression tests for endpoint allocation ordering. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ApplicationModel/EndpointReference.cs | 8 +- .../ApplicationModel/ResourceExtensions.cs | 18 +++- src/Aspire.Hosting/Dcp/DcpExecutor.cs | 95 +++++++++++++------ src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs | 1 + src/Aspire.Hosting/Dcp/DcpModelUtilities.cs | 5 +- .../Orchestrator/ApplicationOrchestrator.cs | 30 +++++- .../Dcp/DcpExecutorTests.cs | 61 ++++++------ .../Aspire.Hosting.Tests/WithEndpointTests.cs | 6 +- tests/Aspire.Hosting.Tests/WithUrlsTests.cs | 4 +- 9 files changed, 156 insertions(+), 72 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index be08e8d2c96..e5749d138f3 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -209,8 +209,12 @@ public ReferenceExpression GetTlsValue(ReferenceExpression enabledValue, Referen return _endpointAnnotation; } - _endpointAnnotation ??= Resource.Annotations.OfType() - .SingleOrDefault(a => string.Equals(a.Name, EndpointName, StringComparisons.EndpointAnnotationName)); + lock (Resource.Annotations) + { + _endpointAnnotation ??= Resource.Annotations.OfType() + .SingleOrDefault(a => string.Equals(a.Name, EndpointName, StringComparisons.EndpointAnnotationName)); + } + return _endpointAnnotation; } diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index dad313cab0d..962d015dd74 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -27,7 +27,13 @@ public static class ResourceExtensions [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool TryGetLastAnnotation(this IResource resource, [NotNullWhen(true)] out T? annotation) where T : IResourceAnnotation { - if (resource.Annotations.OfType().LastOrDefault() is { } lastAnnotation) + T? lastAnnotation; + lock (resource.Annotations) + { + lastAnnotation = resource.Annotations.OfType().LastOrDefault(); + } + + if (lastAnnotation is not null) { annotation = lastAnnotation; return true; @@ -49,11 +55,15 @@ public static bool TryGetLastAnnotation(this IResource resource, [NotNullWhen [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool TryGetAnnotationsOfType(this IResource resource, [NotNullWhen(true)] out IEnumerable? result) where T : IResourceAnnotation { - var matchingTypeAnnotations = resource.Annotations.OfType(); + T[] matchingTypeAnnotations; + lock (resource.Annotations) + { + matchingTypeAnnotations = resource.Annotations.OfType().ToArray(); + } - if (matchingTypeAnnotations.Any()) + if (matchingTypeAnnotations.Length > 0) { - result = matchingTypeAnnotations.ToArray(); + result = matchingTypeAnnotations; return true; } else diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index acc6fbb66c7..e66979676fc 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -59,11 +59,12 @@ internal sealed partial class DcpExecutor : IDcpExecutor, IDcpObjectFactory, IAs private readonly DistributedApplicationExecutionContext _executionContext; private readonly DcpAppResourceStore _appResources; - // Has an entry if we raised ResourceEndpointsAllocatedEvent for a resource with a given name. - // We want to ensure we raise the event only once for each app model resource. + // Has an entry if we raised, or are raising, ResourceEndpointsAllocatedEvent for a resource with a given name. + // We want to ensure we raise the event only once for each app model resource, while also letting concurrent + // callers wait for in-flight URL callbacks before they continue to resource creation. // There may be multiple physical replicas of the same app model resource // which can result in the event being raised multiple times if we are not careful. - private readonly HashSet _endpointsAdvertised = new(StringComparers.ResourceName); + private readonly Dictionary _endpointsAdvertised = new(StringComparers.ResourceName); private readonly CancellationTokenSource _shutdownCancellation = new(); private readonly DcpExecutorEvents _executorEvents; @@ -202,10 +203,11 @@ public async Task RunApplicationAsync(CancellationToken ct = default) var createContainerNetworks = Task.Run(() => CreateAllDcpObjectsAsync(ct), ct); - var createExecutableEndpoints = Task.Run(async () => + var createWorkloadEndpoints = Task.Run(async () => { - await getProxyAddresses.ConfigureAwait(false); + await Task.WhenAll([getProxyAddresses, createContainerNetworks]).WaitAsync(ct).ConfigureAwait(false); + List endpointAllocatedResources = []; foreach (var executable in executables) { if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints( @@ -214,38 +216,46 @@ public async Task RunApplicationAsync(CancellationToken ct = default) ContainerHostName, allowPendingDynamicProxylessContainerEndpoints: false)) { - await PublishEndpointsAllocatedEventAsync(executable.ModelResource, ct).ConfigureAwait(false); + endpointAllocatedResources.Add(executable.ModelResource); + } + } + + foreach (var container in containers) + { + if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints( + container, + _options.Value.EnableAspireContainerTunnel, + ContainerHostName, + allowPendingDynamicProxylessContainerEndpoints: true)) + { + endpointAllocatedResources.Add(container.ModelResource); } } + + // Allocate every endpoint that is known before workload creation before publishing any + // resource-specific endpoint events. URL callbacks can reference endpoints on other + // resources, so publishing per-resource while another resource is still allocating can + // make a valid cross-resource callback observe an unallocated endpoint. + foreach (var resource in endpointAllocatedResources.Distinct()) + { + await PublishEndpointsAllocatedEventAsync(resource, ct).ConfigureAwait(false); + } }, ct); var createExecutables = Task.Run(async () => { - await createExecutableEndpoints.ConfigureAwait(false); + await createWorkloadEndpoints.ConfigureAwait(false); await CreateRenderedResourcesAsync(_executableCreator, executables, EmptyCreationContext.s_instance, ct).ConfigureAwait(false); }, ct); // Configuring containers that use the tunnel require these host network-side endpoints for Executables to be ready. - var cctx = new ContainerCreationContext(createContainerNetworks, createExecutableEndpoints, ct); + var cctx = new ContainerCreationContext(createContainerNetworks, createWorkloadEndpoints, ct); _containerContextSource.SetResult(cctx); var createContainers = Task.Run(async () => { - await Task.WhenAll([getProxyAddresses, createContainerNetworks]).WaitAsync(ct).ConfigureAwait(false); - - // Allocate container workload endpoints that are already known, then publish endpoint-allocated events. - foreach (var container in containers) - { - if (DcpModelUtilities.TryAddWorkloadAllocatedEndpoints( - container, - _options.Value.EnableAspireContainerTunnel, - ContainerHostName, - allowPendingDynamicProxylessContainerEndpoints: true)) - { - await PublishEndpointsAllocatedEventAsync(container.ModelResource, ct).ConfigureAwait(false); - } - } + await createWorkloadEndpoints.ConfigureAwait(false); await CreateRenderedResourcesAsync(_containerCreator, containers, cctx, ct).ConfigureAwait(false); }, ct); @@ -1273,17 +1283,48 @@ private static void ForgetCachedCallbackResults(IResource resource) private async Task PublishEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) { + TaskCompletionSource? publishCompletion = null; + Task? existingPublish; + lock (_endpointsAdvertised) { - if (!_endpointsAdvertised.Add(resource.Name)) + if (_endpointsAdvertised.TryGetValue(resource.Name, out existingPublish)) { - return false; // Already published for this resource. + publishCompletion = null; + } + else + { + publishCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously); + _endpointsAdvertised.Add(resource.Name, publishCompletion.Task); + existingPublish = null; } } - var ev = new ResourceEndpointsAllocatedEvent(resource, _executionContext.ServiceProvider); - await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.NonBlockingConcurrent, ct).ConfigureAwait(false); - return true; + if (existingPublish is not null) + { + await existingPublish.ConfigureAwait(false); + return false; // Already published for this resource. + } + + try + { + await _executorEvents.PublishAsync(new OnResourceEndpointsAllocatedContext(ct, resource)).ConfigureAwait(false); + + var ev = new ResourceEndpointsAllocatedEvent(resource, _executionContext.ServiceProvider); + await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.NonBlockingConcurrent, ct).ConfigureAwait(false); + publishCompletion!.SetResult(); + return true; + } + catch (OperationCanceledException ex) + { + publishCompletion!.SetException(ex); + throw; + } + catch (Exception ex) + { + publishCompletion!.SetException(ex); + throw; + } } private async Task PublishLateEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) diff --git a/src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs b/src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs index d5f45145841..2a660757ac9 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs @@ -8,6 +8,7 @@ namespace Aspire.Hosting.Dcp; internal record ResourceStatus(string? State, DateTime? StartupTimestamp, DateTime? FinishedTimestamp); internal record OnEndpointsAllocatedContext(CancellationToken CancellationToken); +internal record OnResourceEndpointsAllocatedContext(CancellationToken CancellationToken, IResource Resource); internal record OnResourceStartingContext(CancellationToken CancellationToken, string ResourceType, IResource Resource, string? DcpResourceName); internal record OnConnectionStringAvailableContext(CancellationToken CancellationToken, IResource Resource); internal record OnResourcesPreparedContext(CancellationToken CancellationToken); diff --git a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs index d5fd305e84c..fd37460c0c4 100644 --- a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs +++ b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs @@ -129,6 +129,9 @@ internal static bool TryApplyServiceAddressToEndpoint(Service observedService, I } serviceResource.Service.ApplyAddressInfoFrom(observedService); + var isDynamicProxylessContainerEndpoint = appResources.OfType>() + .Any(resource => ReferenceEquals(resource.ModelResource, serviceResource.ModelResource) && + IsDynamicProxylessContainerEndpoint(resource, serviceResource)); if (!TryAddLocalhostAllocatedEndpoint(serviceResource, allowPending: true)) { modelResource = null; @@ -142,7 +145,7 @@ internal static bool TryApplyServiceAddressToEndpoint(Service observedService, I } modelResource = serviceResource.ModelResource; - return AreResourceEndpointsAllocated(modelResource); + return isDynamicProxylessContainerEndpoint && AreResourceEndpointsAllocated(modelResource); } private static bool TryAddLocalhostAllocatedEndpoint(ServiceWithModelResource sp, bool allowPending) diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index a6d0372c73f..de852586d1b 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -3,6 +3,7 @@ #pragma warning disable ASPIREINTERACTION001 +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Data; using System.Diagnostics; @@ -37,6 +38,7 @@ internal sealed class ApplicationOrchestrator private readonly DistributedApplicationExecutionContext _executionContext; private readonly ParameterProcessor _parameterProcessor; private readonly CancellationTokenSource _shutdownCancellation = new(); + private readonly ConcurrentDictionary _skipNextPublicEndpointUrlProcessing = new(StringComparers.ResourceName); private IConfiguration? Configuration => _serviceProvider.GetService(); public ApplicationOrchestrator(DistributedApplicationModel model, @@ -71,11 +73,12 @@ public ApplicationOrchestrator(DistributedApplicationModel model, dcpExecutorEvents.Subscribe(OnResourcesPrepared); dcpExecutorEvents.Subscribe(OnResourceChanged); dcpExecutorEvents.Subscribe(OnEndpointsAllocated); + dcpExecutorEvents.Subscribe(OnResourceEndpointsAllocated); dcpExecutorEvents.Subscribe(OnResourceStarting); dcpExecutorEvents.Subscribe(OnConnectionStringAvailable); dcpExecutorEvents.Subscribe(OnResourceFailedToStart); - _eventing.Subscribe(OnResourceEndpointsAllocated); + _eventing.Subscribe(OnPublicResourceEndpointsAllocated); _eventing.Subscribe(PublishConnectionStringValue); // Implement WaitFor functionality using BeforeResourceStartedEvent. _eventing.Subscribe(WaitForInBeforeResourceStartedEvent); @@ -232,7 +235,7 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT foreach (var endpoint in endpoints) { // Create a URL for each endpoint - Debug.Assert(endpoint.AllocatedEndpoint is not null, "Endpoint should be allocated at this point as we're calling this from ResourceEndpointsAllocatedEvent handler."); + Debug.Assert(endpoint.AllocatedEndpoint is not null, "Endpoint should be allocated at this point as we're processing resource endpoint allocation."); if (endpoint.AllocatedEndpoint is not { } allocatedEndpoint) { continue; @@ -359,7 +362,10 @@ static string TrimSuffix(string value, string suffix) } // Remove it from the resource here, we'll add it back later to avoid duplicates. - resource.Annotations.Remove(staticUrl); + lock (resource.Annotations) + { + resource.Annotations.Remove(staticUrl); + } } } @@ -472,7 +478,10 @@ static string TrimSuffix(string value, string suffix) var count = 0; foreach (var url in urls) { - resource.Annotations.Add(url); + lock (resource.Annotations) + { + resource.Annotations.Add(url); + } count++; if (_logger.IsEnabled(LogLevel.Trace)) { @@ -491,8 +500,19 @@ static string TrimSuffix(string value, string suffix) } } - private async Task OnResourceEndpointsAllocated(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken) + private async Task OnResourceEndpointsAllocated(OnResourceEndpointsAllocatedContext context) { + await PublishResourceEndpointUrls(context.Resource, context.CancellationToken).ConfigureAwait(false); + _skipNextPublicEndpointUrlProcessing.TryAdd(context.Resource.Name, 0); + } + + private async Task OnPublicResourceEndpointsAllocated(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken) + { + if (_skipNextPublicEndpointUrlProcessing.TryRemove(@event.Resource.Name, out _)) + { + return; + } + await PublishResourceEndpointUrls(@event.Resource, cancellationToken).ConfigureAwait(false); } diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index b0bdfc9fb5f..24356f609d4 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -1620,6 +1620,37 @@ public async Task EndpointPortsContainerProxylessNoPortTargetPortSetPublishesAll Assert.True(allocatedPort >= TestKubernetesService.StartOfAutoPortRange); } + [Fact] + public async Task ResourceEndpointsAllocatedEventSubscribersDoNotBlockDcpStartup() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddContainer("database", "image") + .WithHttpEndpoint(targetPort: 8080); + + var subscriberEntered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseSubscriber = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var eventing = new Hosting.Eventing.DistributedApplicationEventing(); + eventing.Subscribe(async (@event, ct) => + { + if (@event.Resource.Name == "database") + { + subscriberEntered.TrySetResult(); + await releaseSubscriber.Task.WaitAsync(ct).ConfigureAwait(false); + } + }); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, distributedApplicationEventing: eventing); + + await appExecutor.RunApplicationAsync().DefaultTimeout(); + await subscriberEntered.Task.DefaultTimeout(); + + releaseSubscriber.SetResult(); + } + [Fact] public async Task EndpointPortsContainerProxylessPortAndTargetPortSet() { @@ -3971,15 +4002,7 @@ public async Task Project_NonProjectLaunchConfig_ExtensionMode_RunsInIde() await appExecutor.RunApplicationAsync(); // Assert - List dcpExes = []; - var haveExes = RetryTillTrueOrTimeout(() => - { - dcpExes.Clear(); - dcpExes.AddRange(kubernetesService.CreatedResources.OfType()); - return dcpExes.Count == 1; - }, TestConstants.DefaultOrchestratorTestTimeout); - Assert.True(haveExes, $"Expected one executable but instead got {dcpExes.Count}"); - + var dcpExes = kubernetesService.CreatedResources.OfType(); var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "proj"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); @@ -4026,15 +4049,7 @@ public async Task Project_NonProjectLaunchConfig_AnnotatorThrows_FallsBackToProc await appExecutor.RunApplicationAsync(); // Assert - List dcpExes = []; - var haveExes = RetryTillTrueOrTimeout(() => - { - dcpExes.Clear(); - dcpExes.AddRange(kubernetesService.CreatedResources.OfType()); - return dcpExes.Count == 1; - }, TestConstants.DefaultOrchestratorTestTimeout); - Assert.True(haveExes, $"Expected one executable but instead got {dcpExes.Count}"); - + var dcpExes = kubernetesService.CreatedResources.OfType(); var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "proj"); // Should fall back to Process execution when the launch configuration producer throws Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); @@ -4075,15 +4090,7 @@ public async Task Project_NonProjectLaunchConfig_UnsupportedByExtension_RunsInPr await appExecutor.RunApplicationAsync(); // Assert - List dcpExes = []; - var haveExes = RetryTillTrueOrTimeout(() => - { - dcpExes.Clear(); - dcpExes.AddRange(kubernetesService.CreatedResources.OfType()); - return dcpExes.Count == 1; - }, TestConstants.DefaultOrchestratorTestTimeout); - Assert.True(haveExes, $"Expected one executable but instead got {dcpExes.Count}"); - + var dcpExes = kubernetesService.CreatedResources.OfType(); var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "proj"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } diff --git a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs index 055e4676a7a..f421fadb565 100644 --- a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs @@ -881,12 +881,10 @@ public void WithEndpointUpdateDoesNotChangeScheme() } [Fact] - public void WithEndpointUpdateDoesNotChangeIsProxiedBackToTrue() + public void WithEndpointUpdateCanSetIsProxiedToTrue() { var builder = DistributedApplication.CreateBuilder(); - // isProxied defaults to true in the method signature, so passing true - // on update can't be distinguished from the default — it's a no-op. builder.AddContainer("mycontainer", "myimage") .WithHttpEndpoint(port: 8080, isProxied: false) .WithHttpEndpoint(port: 9090, isProxied: true); @@ -896,7 +894,7 @@ public void WithEndpointUpdateDoesNotChangeIsProxiedBackToTrue() var resource = Assert.Single(builder.Resources.OfType()); var endpoint = Assert.Single(resource.Annotations.OfType(), e => e.Name == "http"); Assert.Equal(9090, endpoint.Port); - Assert.False(endpoint.IsProxied); + Assert.True(endpoint.IsProxied); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/WithUrlsTests.cs b/tests/Aspire.Hosting.Tests/WithUrlsTests.cs index cf0affb3bc8..686a3ac06df 100644 --- a/tests/Aspire.Hosting.Tests/WithUrlsTests.cs +++ b/tests/Aspire.Hosting.Tests/WithUrlsTests.cs @@ -473,7 +473,7 @@ static string FormatUrls(IEnumerable urls) => var watchTask = Task.Run(async () => { - await foreach (var notification in rns.WatchAsync().DefaultTimeout()) + await foreach (var notification in rns.WatchAsync().DefaultTimeout(TestConstants.LongTimeoutDuration)) { if (notification.Resource == servicea.Resource && notification.Snapshot.Urls.Length > 0) { @@ -589,7 +589,7 @@ static string FormatUrls(IEnumerable urls) => var watchTask = Task.Run(async () => { - await foreach (var notification in rns.WatchAsync().DefaultTimeout()) + await foreach (var notification in rns.WatchAsync().DefaultTimeout(TestConstants.LongTimeoutDuration)) { if (notification.Resource == custom.Resource && notification.Snapshot.Urls.Length > 0) { From d6369a68a538604cee386468ee1851b150072790 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Sat, 16 May 2026 20:34:19 -0700 Subject: [PATCH 10/38] Harden DCP executable test assertions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Dcp/DcpExecutorTests.cs | 115 ++++++++---------- 1 file changed, 51 insertions(+), 64 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 24356f609d4..c619ede1f22 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -92,9 +92,7 @@ public async Task ResourceStarted_ProjectHasReplicas_EventRaisedOnce() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions, events: events); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType() - .Where(executable => string.Equals(executable.AppModelResourceName, "ServiceA", StringComparison.Ordinal)) - .ToList(); + var executables = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); Assert.Equal(2, executables.Count); var e = Assert.Single(startingEvents); @@ -162,7 +160,7 @@ public async Task CreateExecutable_LaunchProfileHasCommandLineArgs_AnnotationsAd var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions, events: events, configuration: configuration); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType().ToList(); + var executables = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); var exe = Assert.Single(executables); // Ignore dotnet specific args for .NET project in process execution. @@ -233,7 +231,7 @@ public async Task CreateExecutable_ProjectArgsResolvedInSnapshot_UsesEffectiveAr var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.True(exe.TryGetAnnotationAsObjectList(CustomResource.ResourceAppArgsAnnotation, out var argAnnotations)); Assert.Equal(2, argAnnotations.Count); AssertEffectiveArgumentIndexesMatchSpecArgs(argAnnotations, exe.Spec.Args); @@ -419,7 +417,7 @@ public async Task ResourceRestarted_EnvironmentCallbacksApplied() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions, events: events); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType().ToList(); + var executables = GetCreatedExecutablesForResource(kubernetesService, resource.Name); var exe1 = Assert.Single(executables); var callCount1 = exe1.Spec.Env!.Single(e => e.Name == "CALL_COUNT"); @@ -437,7 +435,7 @@ public async Task ResourceRestarted_EnvironmentCallbacksApplied() await appExecutor.StartResourceAsync(reference, CancellationToken.None); - executables = kubernetesService.CreatedResources.OfType().ToList(); + executables = GetCreatedExecutablesForResource(kubernetesService, resource.Name); Assert.Equal(2, executables.Count); var exe2 = executables[1]; @@ -829,9 +827,7 @@ public async Task EndpointOtelServiceName(int replicaCount, string expectedName) var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, dcpOptions: dcpOptions); await appExecutor.RunApplicationAsync(); - var executables = kubernetesService.CreatedResources.OfType() - .Where(executable => string.Equals(executable.AppModelResourceName, "ServiceA", StringComparison.Ordinal)) - .ToList(); + var executables = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); Assert.Equal(replicaCount, executables.Count); foreach (var exe in executables) @@ -1269,7 +1265,7 @@ public async Task EndpointPortsProjectNoPortNoTargetPort() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); await appExecutor.RunApplicationAsync(); - var exes = kubernetesService.CreatedResources.OfType().Where(e => e.AppModelResourceName == "ServiceA").ToList(); + var exes = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); Assert.Equal(3, exes.Count); foreach (var dcpExe in exes) @@ -1314,7 +1310,7 @@ public async Task EndpointPortsProjectPortSetNoTargetPort() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); await appExecutor.RunApplicationAsync(); - var exes = kubernetesService.CreatedResources.OfType().Where(e => e.AppModelResourceName == "ServiceA").ToList(); + var exes = GetCreatedExecutablesForResource(kubernetesService, "ServiceA"); Assert.Equal(3, exes.Count); foreach (var dcpExe in exes) @@ -1355,7 +1351,7 @@ public async Task EndpointPortsProjectWithEndpointProxySupportUsesProxylessEndpo var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); await appExecutor.RunApplicationAsync(); - var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + var dcpExe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "ServiceA"); @@ -1392,7 +1388,7 @@ public async Task EndpointPortsPersistentProjectDefaultsToProxylessEndpoint() var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, configuration: configuration); await appExecutor.RunApplicationAsync(); - var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + var dcpExe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "ServiceA"); @@ -1877,7 +1873,7 @@ public async Task ProjectLaunchConfiguration_Populated_WhenLaunchProfileSpecifie await executor.RunApplicationAsync(); // Assert - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.NotNull(plc); Assert.False(plc!.DisableLaunchProfile); @@ -1911,7 +1907,7 @@ public async Task ProjectLaunchConfiguration_RespectsDebugSessionRunMode(string await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.NotNull(plc); Assert.Equal(expectedMode, plc!.Mode); @@ -1945,7 +1941,7 @@ public async Task ProjectLaunchConfiguration_Disabled_WhenLaunchProfileExcluded_ await executor.RunApplicationAsync(); // Assert - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.NotNull(plc); Assert.True(plc!.DisableLaunchProfile); @@ -1981,7 +1977,7 @@ public async Task ProjectLaunchConfiguration_DefaultLaunchProfileAnnotationFalls await executor.RunApplicationAsync(); // Assert - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.NotNull(plc); // Should have fallen back to the first available profile (in insertion order) which is Foo, not the missing one. @@ -2017,7 +2013,7 @@ public async Task ProjectLaunchConfiguration_DefaultLaunchProfileAnnotationSelec var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.False(plc!.DisableLaunchProfile); Assert.Equal("http", plc.LaunchProfile); @@ -2048,7 +2044,7 @@ public async Task ProjectLaunchConfiguration_ExplicitLaunchProfileOverridesDefau var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.False(plc!.DisableLaunchProfile); Assert.Equal("http", plc.LaunchProfile); // explicit wins @@ -2079,7 +2075,7 @@ public async Task ProjectLaunchConfiguration_DefaultIgnoredWhenExcluded_InDebugS var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.True(plc!.DisableLaunchProfile); Assert.Equal(string.Empty, plc.LaunchProfile); @@ -2109,7 +2105,7 @@ public async Task ProjectLaunchConfiguration_NoProfiles_NoLaunchProfileSelected_ var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.False(plc!.DisableLaunchProfile); // not excluded Assert.Equal(string.Empty, plc.LaunchProfile); // nothing selected @@ -2138,7 +2134,7 @@ public async Task ProjectLaunchConfiguration_FallbackToFirstProfileInsertionOrde var executor = CreateAppExecutor(model, configuration: configuration, kubernetesService: kubernetes); await executor.RunApplicationAsync(); - var exe = Assert.Single(kubernetes.CreatedResources.OfType()); + var exe = GetCreatedExecutableForResource(kubernetes, "proj"); Assert.True(exe.TryGetProjectLaunchConfiguration(out var plc)); Assert.False(plc!.DisableLaunchProfile); Assert.Equal("Zed", plc.LaunchProfile); // first inserted wins @@ -2696,11 +2692,7 @@ public async Task ProjectExecutable_NoDebugSessionInfo_DefaultsToProjectSupport( // Act await appExecutor.RunApplicationAsync(); - // Assert - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Single(dcpExes); - - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); } @@ -2733,11 +2725,7 @@ public async Task ProjectExecutable_InvalidDebugSessionInfo_DefaultsToProjectSup // Act await appExecutor.RunApplicationAsync(); - // Assert - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Single(dcpExes); - - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); } @@ -2776,11 +2764,7 @@ public async Task ProjectExecutable_DebugSessionInfoWithNullSupportedLaunchConfi // Act await appExecutor.RunApplicationAsync(); - // Assert - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Single(dcpExes); - - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); } @@ -2821,7 +2805,7 @@ public async Task ProjectExecutable_DebugSessionInfoWithoutProjectFallsBackToPro await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -2856,7 +2840,7 @@ public async Task ProjectWithNonProjectAnnotation_DebugSessionWithoutInfo_FallsB await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); Assert.Equal(ExecutionType.Process, Assert.Single(exe.Spec.FallbackExecutionTypes)); @@ -2904,7 +2888,7 @@ public async Task ProjectWithNonProjectAnnotation_VSCodeExplicitlyUnsupported_Ru await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -2930,7 +2914,7 @@ public async Task ProjectWithNonProjectAnnotation_NoDebugSession_RunsInProcess() await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -2972,7 +2956,7 @@ public async Task ProjectWithNonProjectAnnotation_VSCodeWithMatchingSupport_Runs await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); } @@ -3011,13 +2995,10 @@ public async Task StandardAndCustomProjects_VSScenario_BothRunInIde() await appExecutor.RunApplicationAsync(); - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Equal(2, dcpExes.Count); - - var standardExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "standard-project"); + var standardExe = GetCreatedExecutableForResource(kubernetesService, "standard-project"); Assert.Equal(ExecutionType.IDE, standardExe.Spec.ExecutionType); - var customExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "custom-project"); + var customExe = GetCreatedExecutableForResource(kubernetesService, "custom-project"); Assert.Equal(ExecutionType.IDE, customExe.Spec.ExecutionType); Assert.NotNull(customExe.Spec.FallbackExecutionTypes); Assert.Equal(ExecutionType.Process, Assert.Single(customExe.Spec.FallbackExecutionTypes)); @@ -3072,15 +3053,12 @@ public async Task StandardAndCustomProjects_VSCodeScenario_BothRunInIde() await appExecutor.RunApplicationAsync(); - var dcpExes = kubernetesService.CreatedResources.OfType().ToList(); - Assert.Equal(2, dcpExes.Count); - // Standard project: Process execution because the IDE did not advertise "project" support. - var standardExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "standard-project"); + var standardExe = GetCreatedExecutableForResource(kubernetesService, "standard-project"); Assert.Equal(ExecutionType.Process, standardExe.Spec.ExecutionType); // Azure Functions project: IDE via explicit "azure-functions" support. - var functionsExe = Assert.Single(dcpExes, e => e.AppModelResourceName == "functions-project"); + var functionsExe = GetCreatedExecutableForResource(kubernetesService, "functions-project"); Assert.Equal(ExecutionType.IDE, functionsExe.Spec.ExecutionType); } @@ -3113,7 +3091,7 @@ public async Task ProjectWithNonProjectAnnotation_VSFallback_HasProcessFallbackE await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); Assert.Single(exe.Spec.FallbackExecutionTypes); @@ -3195,7 +3173,7 @@ public async Task ProjectExecutable_NoSupportsDebuggingAnnotation_InDebugSession await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); Assert.Equal(ExecutionType.Process, Assert.Single(exe.Spec.FallbackExecutionTypes)); @@ -3229,7 +3207,7 @@ public async Task ProjectExecutable_NoSupportsDebuggingAnnotation_NoDebugSession await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "ServiceA"); + var exe = GetCreatedExecutableForResource(kubernetesService, "ServiceA"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -3264,7 +3242,7 @@ public async Task ProjectExecutable_NoAnnotation_ExecutableLaunchProfile_InDebug await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "TestFunction"); + var exe = GetCreatedExecutableForResource(kubernetesService, "TestFunction"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); Assert.Equal(ExecutionType.Process, Assert.Single(exe.Spec.FallbackExecutionTypes)); @@ -3307,7 +3285,7 @@ public async Task ProjectExecutable_NoAnnotation_ProjectLaunchProfile_InDebugSes await appExecutor.RunApplicationAsync(); - var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); // Should be IDE, because it's a normal Project profile Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); Assert.NotNull(exe.Spec.FallbackExecutionTypes); @@ -4002,8 +3980,7 @@ public async Task Project_NonProjectLaunchConfig_ExtensionMode_RunsInIde() await appExecutor.RunApplicationAsync(); // Assert - var dcpExes = kubernetesService.CreatedResources.OfType(); - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.IDE, exe.Spec.ExecutionType); // The launch config should have been applied in CreateExecutableAsync (not PrepareProjectExecutables) @@ -4049,8 +4026,7 @@ public async Task Project_NonProjectLaunchConfig_AnnotatorThrows_FallsBackToProc await appExecutor.RunApplicationAsync(); // Assert - var dcpExes = kubernetesService.CreatedResources.OfType(); - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); // Should fall back to Process execution when the launch configuration producer throws Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } @@ -4090,11 +4066,22 @@ public async Task Project_NonProjectLaunchConfig_UnsupportedByExtension_RunsInPr await appExecutor.RunApplicationAsync(); // Assert - var dcpExes = kubernetesService.CreatedResources.OfType(); - var exe = Assert.Single(dcpExes, e => e.AppModelResourceName == "proj"); + var exe = GetCreatedExecutableForResource(kubernetesService, "proj"); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); } + private static Executable GetCreatedExecutableForResource(TestKubernetesService kubernetesService, string appModelResourceName) + { + return Assert.Single(GetCreatedExecutablesForResource(kubernetesService, appModelResourceName)); + } + + private static List GetCreatedExecutablesForResource(TestKubernetesService kubernetesService, string appModelResourceName) + { + return [.. kubernetesService.CreatedResources + .OfType() + .Where(e => e.AppModelResourceName == appModelResourceName)]; + } + private static DcpExecutor CreateAppExecutor( DistributedApplicationModel distributedAppModel, IHostEnvironment? hostEnvironment = null, From ffd554e678826c45efae54c54ecc8cdf9e96009c Mon Sep 17 00:00:00 2001 From: David Negstad Date: Sat, 16 May 2026 21:21:52 -0700 Subject: [PATCH 11/38] Add parent-scoped lifetime E2E coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DistributedApplicationTests.cs | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index f8831868251..1e73ec65988 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -3,9 +3,11 @@ #pragma warning disable ASPIRECERTIFICATES001 +using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; using System.Threading.Channels; +using Aspire.Dashboard.Model; using Aspire.TestUtilities; using Aspire.Hosting.Dcp; using Aspire.Hosting.Dcp.Model; @@ -1882,6 +1884,172 @@ public async Task PersistentNetworkCreatedIfPersistentContainers(bool createPers } } + [Fact] + [RequiresFeature(TestFeature.Docker)] + public async Task ParentProcessLifetimeScopesExecutableAndContainerToParentProcess() + { + const string testName = "parent-process-lifetime-scope"; + using var builder = TestDistributedApplicationBuilder.Create(_testOutputHelper); + var parentProcess = Process.GetCurrentProcess(); + var parentProcessIdentity = new DcpProcessMonitor().GetMonitorProcess(parentProcess); + + var container = AddRedisContainer(builder, $"{testName}-container") + .WithParentProcessLifetime(parentProcess.Id) + .WithExplicitStart(); + + var executable = builder.AddExecutable($"{testName}-executable", "dotnet", Environment.CurrentDirectory, "--info") + .WithParentProcessLifetime(parentProcess.Id) + .WithExplicitStart(); + + using var app = builder.Build(); + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.DefaultOrchestratorTestLongTimeout); + var token = cts.Token; + + await app.StartAsync(token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + + await app.ResourceNotifications.WaitForResourceAsync(container.Resource.Name, KnownResourceStates.NotStarted, token) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + await app.ResourceNotifications.WaitForResourceAsync(executable.Resource.Name, KnownResourceStates.NotStarted, token) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + + var kubernetes = app.Services.GetRequiredService(); + var containers = await kubernetes.ListAsync(cancellationToken: token) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + var dcpContainer = Assert.Single(containers, c => c.AppModelResourceName == container.Resource.Name); + Assert.True(dcpContainer.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(parentProcessIdentity.ProcessId, dcpContainer.Spec.MonitorPid); + Assert.NotNull(dcpContainer.Spec.MonitorTimestamp); + Assert.InRange((dcpContainer.Spec.MonitorTimestamp.Value - parentProcessIdentity.Timestamp).Duration(), TimeSpan.Zero, TimeSpan.FromMilliseconds(1)); + Assert.False(dcpContainer.Spec.Start.GetValueOrDefault(true)); + + var executables = await kubernetes.ListAsync(cancellationToken: token) + .DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout); + var dcpExecutable = Assert.Single(executables, e => e.AppModelResourceName == executable.Resource.Name); + Assert.True(dcpExecutable.Spec.Persistent.GetValueOrDefault()); + Assert.Equal(parentProcessIdentity.ProcessId, dcpExecutable.Spec.MonitorPid); + Assert.NotNull(dcpExecutable.Spec.MonitorTimestamp); + Assert.InRange((dcpExecutable.Spec.MonitorTimestamp.Value - parentProcessIdentity.Timestamp).Duration(), TimeSpan.Zero, TimeSpan.FromMilliseconds(1)); + Assert.False(dcpExecutable.Spec.Start.GetValueOrDefault(true)); + Assert.Equal(ExecutionType.Process, dcpExecutable.Spec.ExecutionType); + } + + [Fact] + [RequiresFeature(TestFeature.Docker)] + public async Task ParentProcessLifetimeReusesResourcesAcrossAppRestartsAndStopsWhenParentExits() + { + const string testName = "parent-process-lifetime-reuse"; + var containerResourceName = $"{testName}-redis"; + var executableResourceName = $"{testName}-worker"; + + using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.ExtraLongTimeoutDuration); + var token = cts.Token; + + using var aspireStore = new TestTempDirectory(); + using var executableDirectory = new TestTempDirectory(); + var executableAppPath = DotnetFileAppProcess.WriteApp(executableDirectory, "worker.cs", """ + using System.Threading; + using System.Threading.Tasks; + + await Task.Delay(Timeout.InfiniteTimeSpan); + """); + + using var parentProcess = StartLongRunningProcess(); + try + { + var firstRun = await StartParentScopedResourcesAsync(parentProcess.Id, token); + await StopAndDisposeAppAsync(firstRun.App, token); + + var secondRun = await StartParentScopedResourcesAsync(parentProcess.Id, token); + try + { + Assert.Equal(firstRun.ContainerId, secondRun.ContainerId); + Assert.Equal(firstRun.ExecutablePid, secondRun.ExecutablePid); + + await KillProcessAsync(parentProcess, token); + + var rns = secondRun.App.Services.GetRequiredService(); + await Task.WhenAll( + rns.WaitForResourceAsync(containerResourceName, e => + { + var state = e.Snapshot.State?.Text; + // DCP stops parent-scoped containers and removes the DCP resource object, + // which the app observes as Unknown rather than an Exited status update. + // Tighten this to require Exited after DCP stops the container without + // removing the resource object. + return state == KnownResourceStates.Exited || state == ContainerState.Unknown; + }, token), + rns.WaitForResourceAsync(executableResourceName, e => + { + var state = e.Snapshot.State?.Text; + return state == KnownResourceStates.Finished || state == ExecutableState.Terminated; + }, token)) + .DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + } + finally + { + await StopAndDisposeAppAsync(secondRun.App, token); + } + } + finally + { + await KillProcessAsync(parentProcess, token); + } + + async Task StartParentScopedResourcesAsync(int parentProcessId, CancellationToken cancellationToken) + { + var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(_testOutputHelper) + .WithTempAspireStore(aspireStore.Path) + .WithResourceCleanUp(false); + + AddRedisContainer(builder, containerResourceName) + .WithContainerName(containerResourceName) + .WithParentProcessLifetime(parentProcessId); + + builder.AddExecutable(executableResourceName, DotnetFileAppProcess.ExecutablePath, executableDirectory.Path, DotnetFileAppProcess.CreateArguments(executableAppPath)) + .WithParentProcessLifetime(parentProcessId); + + var app = builder.Build(); + try + { + await app.StartAsync(cancellationToken).DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + + var rns = app.Services.GetRequiredService(); + var containerEvent = await rns.WaitForResourceAsync( + containerResourceName, + e => e.Snapshot.State?.Text == KnownResourceStates.Running && + GetResourcePropertyValue(e, KnownProperties.Container.Id) is string, + cancellationToken).DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + var executableEvent = await rns.WaitForResourceAsync( + executableResourceName, + e => e.Snapshot.State?.Text == KnownResourceStates.Running && + GetResourcePropertyValue(e, KnownProperties.Executable.Pid) is int, + cancellationToken).DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + + var containerId = Assert.IsType(GetResourcePropertyValue(containerEvent, KnownProperties.Container.Id)); + var executablePid = Assert.IsType(GetResourcePropertyValue(executableEvent, KnownProperties.Executable.Pid)); + + return new ParentScopedResourcesRun(app, containerId, executablePid); + } + catch + { + await app.DisposeAsync().AsTask().DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + throw; + } + } + + static async Task StopAndDisposeAppAsync(DistributedApplication app, CancellationToken cancellationToken) + { + try + { + await app.StopAsync(cancellationToken).DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + } + finally + { + await app.DisposeAsync().AsTask().DefaultTimeout(TestConstants.ExtraLongTimeoutTimeSpan); + } + } + } + [Fact] [RequiresFeature(TestFeature.Docker)] public async Task AfterResourcesCreatedLifecycleHookWorks() @@ -2036,6 +2204,38 @@ private static IResourceBuilder AddRedisContainer(IDistribute .WithImageRegistry(AspireTestContainerRegistry); } + private static object? GetResourcePropertyValue(ResourceEvent resourceEvent, string propertyName) + { + return resourceEvent.Snapshot.Properties.FirstOrDefault(p => p.Name == propertyName)?.Value; + } + + private static Process StartLongRunningProcess() + { + var startInfo = OperatingSystem.IsWindows() + ? new ProcessStartInfo("ping", "-t localhost") { CreateNoWindow = true } + : new ProcessStartInfo("tail", "-f /dev/null"); + + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + + var process = Process.Start(startInfo); + Assert.NotNull(process); + + return process; + } + + private static async Task KillProcessAsync(Process process, CancellationToken cancellationToken) + { + if (!process.HasExited) + { + process.Kill(entireProcessTree: true); + } + + await process.WaitForExitAsync(cancellationToken); + } + + private sealed record ParentScopedResourcesRun(DistributedApplication App, string ContainerId, int ExecutablePid); + #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. From 76cdb1d76cae0131680ed96ccc6c8c643d1b585b Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 10:34:13 -0700 Subject: [PATCH 12/38] Fix container tunnel service allocation in tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs b/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs index 6e0416331f6..a2f72f8b3f6 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/TestKubernetesService.cs @@ -70,7 +70,11 @@ static T Clone(T r) // "Allocate" port for a service. if (res is Service svc) { - if (svc.Spec.AddressAllocationMode != AddressAllocationModes.Proxyless || svc.Spec.Port is not null) + // Container tunnel client services are proxyless, but unlike dynamic + // container endpoints they must be ready before the dependent container starts. + if (svc.Spec.AddressAllocationMode != AddressAllocationModes.Proxyless || + svc.Spec.Port is not null || + svc.Metadata.Annotations?.ContainsKey(CustomResource.ContainerTunnelInstanceName) is true) { if (svc.Status is null) { From 209019369a5c607663db056626b461ba2b8ff980 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 11:37:19 -0700 Subject: [PATCH 13/38] Add shared resource lifetime annotations Introduce shared lifetime annotations and WithLifetimeOf so child resources can mirror container, executable, or project lifetime behavior. Update DCP lifetime resolution and emulator child resource propagation to use the shared model while preserving default emulator behavior unless the container callback is supplied. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureEventHubsExtensions.cs | 21 +---- .../AzureServiceBusExtensions.cs | 15 +-- .../ApplicationModel/Lifetime.cs | 26 +++++ .../ApplicationModel/ResourceExtensions.cs | 94 ++++++++++++++----- ...tedApplicationEventSubscriptionHandlers.cs | 2 +- .../ContainerResourceBuilderExtensions.cs | 16 +++- src/Aspire.Hosting/Dcp/ContainerCreator.cs | 6 +- src/Aspire.Hosting/Dcp/DcpNameGenerator.cs | 6 +- src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 8 +- .../ResourceBuilderExtensions.cs | 72 ++++++++++---- .../AddAzureKustoTests.cs | 4 +- .../AzureEventHubsExtensionsTests.cs | 40 ++++++-- .../AzureServiceBusExtensionsTests.cs | 37 ++++++-- ...DevTunnelResourceBuilderExtensionsTests.cs | 8 +- ...ExecutableResourceBuilderExtensionTests.cs | 4 +- .../PersistentContainerWarningTests.cs | 4 +- .../ProjectResourceBuilderExtensionTests.cs | 4 +- .../ResourceBuilderLifetimeTests.cs | 87 ++++++++++++++++- 18 files changed, 336 insertions(+), 118 deletions(-) diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs index f2334a6a8fe..77c5eb11dfc 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs @@ -273,30 +273,17 @@ public static IResourceBuilder RunAsEmulator(this IResou .AddAzureStorage($"{builder.Resource.Name}-storage") .WithParentRelationship(builder); - var lifetime = ContainerLifetime.Session; - - // Copy the lifetime from the main resource to the storage resource var surrogate = new AzureEventHubsEmulatorResource(builder.Resource); var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(surrogate); if (configureContainer != null) { configureContainer(surrogateBuilder); - - if (surrogate.TryGetLastAnnotation(out var lifetimeAnnotation)) - { - lifetime = lifetimeAnnotation.Lifetime; - } + storageResource = storageResource.RunAsEmulator(c => c.WithLifetimeOf(surrogateBuilder)); } - - storageResource = storageResource.RunAsEmulator(c => + else { - _ = lifetime switch - { - ContainerLifetime.Session => c.WithSessionLifetime(), - ContainerLifetime.Persistent => c.WithPersistentLifetime(), - _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), lifetime)}'.") - }; - }); + storageResource = storageResource.RunAsEmulator(); + } var storage = storageResource.Resource; diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs index 902eef4b0e5..7b0aaf7bcbb 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs @@ -433,28 +433,15 @@ public static IResourceBuilder RunAsEmulator(this IReso context.EnvironmentVariables["MSSQL_SA_PASSWORD"] = passwordParameter; })); - var lifetime = ContainerLifetime.Session; - var surrogate = new AzureServiceBusEmulatorResource(builder.Resource); var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(surrogate); if (configureContainer != null) { configureContainer(surrogateBuilder); - - if (surrogate.TryGetLastAnnotation(out var lifetimeAnnotation)) - { - lifetime = lifetimeAnnotation.Lifetime; - } + sqlServerResource = sqlServerResource.WithLifetimeOf(surrogateBuilder); } - sqlServerResource = lifetime switch - { - ContainerLifetime.Session => sqlServerResource.WithSessionLifetime(), - ContainerLifetime.Persistent => sqlServerResource.WithPersistentLifetime(), - _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), lifetime)}'.") - }; - // RunAsEmulator() can be followed by custom model configuration so we need to delay the creation of the Config.json file // until all resources are about to be prepared and annotations can't be updated anymore. surrogateBuilder.WithContainerFiles( diff --git a/src/Aspire.Hosting/ApplicationModel/Lifetime.cs b/src/Aspire.Hosting/ApplicationModel/Lifetime.cs index 851ab02a2bc..1fa30b28552 100644 --- a/src/Aspire.Hosting/ApplicationModel/Lifetime.cs +++ b/src/Aspire.Hosting/ApplicationModel/Lifetime.cs @@ -1,6 +1,8 @@ // 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; + namespace Aspire.Hosting.ApplicationModel; /// @@ -18,3 +20,27 @@ public enum Lifetime /// Persistent, } + +/// +/// Annotation that controls the lifetime of a resource. +/// +[DebuggerDisplay("Type = {GetType().Name,nq}")] +public sealed class LifetimeAnnotation : IResourceAnnotation +{ + /// + /// Gets or sets the lifetime type for the resource. + /// + public required Lifetime Lifetime { get; set; } +} + +/// +/// Annotation that configures a resource to match the lifetime of another resource. +/// +[DebuggerDisplay("Type = {GetType().Name,nq}, Source = {SourceResource.Name,nq}")] +internal sealed class LifetimeReferenceAnnotation(IResource sourceResource) : IResourceAnnotation +{ + /// + /// Gets the resource whose lifetime should be used. + /// + public IResource SourceResource { get; } = sourceResource ?? throw new ArgumentNullException(nameof(sourceResource)); +} diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 962d015dd74..589cb9b5582 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1049,38 +1049,44 @@ internal static bool IsBuildOnlyContainer(this IResource resource) } /// - /// Gets the lifetime type of the container for the specified resource. - /// Defaults to if no is found. + /// Gets the lifetime type for the specified resource. + /// Defaults to if no lifetime annotation is found. /// - /// The resource to get the ContainerLifetimeType for. + /// The resource to get the lifetime type for. /// - /// The from the for the resource (if the annotation exists). - /// Defaults to if the annotation is not set. + /// The from the for the resource (if the annotation exists). + /// Defaults to if the annotation is not set. /// - internal static ContainerLifetime GetContainerLifetimeType(this IResource resource) + internal static Lifetime GetLifetimeType(this IResource resource) { - if (resource.TryGetLastAnnotation(out var lifetimeAnnotation)) - { - return lifetimeAnnotation.Lifetime; - } - - return ContainerLifetime.Session; + return GetLifetimeType(resource, []); } - /// - /// Gets the lifetime type of the executable for the specified resource. - /// Defaults to if no is found. - /// - /// The resource to get the ExecutableLifetimeType for. - /// - /// The from the for the resource (if the annotation exists). - /// Defaults to if the annotation is not set. - /// - internal static Lifetime GetExecutableLifetimeType(this IResource resource) + private static Lifetime GetLifetimeType(IResource resource, HashSet visitedResources) { - if (resource.TryGetLastAnnotation(out var lifetimeAnnotation)) + if (!visitedResources.Add(resource)) { - return lifetimeAnnotation.Lifetime; + throw new InvalidOperationException($"A circular lifetime reference was detected for resource '{resource.Name}'."); + } + + foreach (var annotation in resource.Annotations.Reverse()) + { + switch (annotation) + { + case LifetimeAnnotation lifetimeAnnotation: + return lifetimeAnnotation.Lifetime; + case LifetimeReferenceAnnotation lifetimeReferenceAnnotation: + return GetLifetimeType(lifetimeReferenceAnnotation.SourceResource, visitedResources); + case ExecutableLifetimeAnnotation executableLifetimeAnnotation: + return executableLifetimeAnnotation.Lifetime; + case ContainerLifetimeAnnotation containerLifetimeAnnotation: + return containerLifetimeAnnotation.Lifetime switch + { + ContainerLifetime.Session => Lifetime.Session, + ContainerLifetime.Persistent => Lifetime.Persistent, + _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), containerLifetimeAnnotation.Lifetime)}'.") + }; + } } return Lifetime.Session; @@ -1093,8 +1099,44 @@ internal static Lifetime GetExecutableLifetimeType(this IResource resource) /// if the resource has a persistent container or executable lifetime, otherwise . internal static bool HasPersistentLifetime(this IResource resource) { - return resource.GetContainerLifetimeType() == ContainerLifetime.Persistent || - resource.GetExecutableLifetimeType() == Lifetime.Persistent; + return resource.GetLifetimeType() == Lifetime.Persistent; + } + + /// + /// Determines whether the specified resource has a parent process lifetime. + /// + /// The resource to get parent process lifetime behavior for. + /// The parent process lifetime annotation if one exists. + /// if the resource has a parent process lifetime, otherwise . + internal static bool TryGetParentProcessLifetime(this IResource resource, [NotNullWhen(true)] out ParentProcessLifetimeAnnotation? annotation) + { + return TryGetParentProcessLifetime(resource, [], out annotation); + } + + private static bool TryGetParentProcessLifetime(IResource resource, HashSet visitedResources, [NotNullWhen(true)] out ParentProcessLifetimeAnnotation? annotation) + { + if (!visitedResources.Add(resource)) + { + throw new InvalidOperationException($"A circular lifetime reference was detected for resource '{resource.Name}'."); + } + + foreach (var resourceAnnotation in resource.Annotations.Reverse()) + { + switch (resourceAnnotation) + { + case ParentProcessLifetimeAnnotation parentProcessLifetimeAnnotation: + annotation = parentProcessLifetimeAnnotation; + return true; + case LifetimeReferenceAnnotation lifetimeReferenceAnnotation: + return TryGetParentProcessLifetime(lifetimeReferenceAnnotation.SourceResource, visitedResources, out annotation); + case LifetimeAnnotation or ExecutableLifetimeAnnotation or ContainerLifetimeAnnotation: + annotation = null; + return false; + } + } + + annotation = null; + return false; } /// diff --git a/src/Aspire.Hosting/BuiltInDistributedApplicationEventSubscriptionHandlers.cs b/src/Aspire.Hosting/BuiltInDistributedApplicationEventSubscriptionHandlers.cs index f9cdb126112..7a2ece308aa 100644 --- a/src/Aspire.Hosting/BuiltInDistributedApplicationEventSubscriptionHandlers.cs +++ b/src/Aspire.Hosting/BuiltInDistributedApplicationEventSubscriptionHandlers.cs @@ -96,7 +96,7 @@ public static Task WarnPersistentContainersWithoutUserSecrets(BeforeStartEvent b foreach (var resource in beforeStartEvent.Model.Resources) { - if (resource.GetContainerLifetimeType() == ContainerLifetime.Persistent) + if (resource is ContainerResource && resource.GetLifetimeType() == Lifetime.Persistent) { if (logger.IsEnabled(LogLevel.Warning)) { diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 9b076e8349a..e4aadcb3cac 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -561,7 +561,21 @@ public static IResourceBuilder WithLifetime(this IResourceBuilder build builder.Resource.Annotations.Remove(annotation); } - return builder.WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) + { + builder.Resource.Annotations.Remove(annotation); + } + + var resourceLifetime = lifetime switch + { + ContainerLifetime.Session => Lifetime.Session, + ContainerLifetime.Persistent => Lifetime.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + }; + + return builder + .WithAnnotation(new LifetimeAnnotation { Lifetime = resourceLifetime }, ResourceAnnotationMutationBehavior.Replace) + .WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); } /// diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index 9ded8dc664d..3e388fe6ac1 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -115,7 +115,7 @@ internal void PrepareContainerNetworks() if (!containerResources.Any()) { return; } var network = ContainerNetwork.Create(KnownNetworkIdentifiers.DefaultAspireContainerNetwork.Value); - if (containerResources.Any(cr => cr.GetContainerLifetimeType() == ContainerLifetime.Persistent)) + if (containerResources.Any(cr => cr.GetLifetimeType() == Lifetime.Persistent)) { network.Spec.Persistent = true; network.Spec.NetworkName = $"{DcpExecutor.DefaultAspirePersistentNetworkName}-{_nameGenerator.GetProjectHashSuffix()}"; @@ -155,7 +155,7 @@ public IEnumerable> PrepareObjects() ctr.Spec.ContainerName = containerObjectInstance.Name; - if (container.GetContainerLifetimeType() == ContainerLifetime.Persistent) + if (container.GetLifetimeType() == Lifetime.Persistent) { ctr.Spec.Persistent = true; ApplyMonitorProcess(container, ctr.Spec); @@ -212,7 +212,7 @@ public IEnumerable> PrepareObjects() private void ApplyMonitorProcess(IResource resource, ContainerSpec spec) { - if (resource.TryGetLastAnnotation(out var annotation)) + if (resource.TryGetParentProcessLifetime(out var annotation)) { var monitorProcess = _processMonitor.GetMonitorProcess(annotation.ParentProcess); spec.MonitorPid = monitorProcess.ProcessId; diff --git a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs index 83e887b8a79..8ca47b45e09 100644 --- a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs +++ b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs @@ -69,9 +69,9 @@ private static void AddInstancesAnnotation(IResource resource, ImmutableArray GetRandomNameSuffix(), + Lifetime.Session => GetRandomNameSuffix(), _ => GetProjectHashSuffix(), }; @@ -80,7 +80,7 @@ private static void AddInstancesAnnotation(IResource resource, ImmutableArray GetRandomNameSuffix(), _ => GetProjectHashSuffix(), diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index 41dedfa8e99..007e461550c 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -173,7 +173,7 @@ private void PrepareProjectExecutables() var projectArgs = new List(); var isInDebugSession = !string.IsNullOrEmpty(_configuration[DcpExecutor.DebugSessionPortVar]); - var persistent = project.GetExecutableLifetimeType() == Lifetime.Persistent; + var persistent = project.GetLifetimeType() == Lifetime.Persistent; exe.Spec.Persistent = persistent; if (persistent) { @@ -290,7 +290,7 @@ private void PreparePlainExecutables() exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); exe.Annotate(CustomResource.ResourceNameAnnotation, executable.Name); - var persistent = executable.GetExecutableLifetimeType() == Lifetime.Persistent; + var persistent = executable.GetLifetimeType() == Lifetime.Persistent; if (persistent) { exe.Spec.Persistent = true; @@ -324,7 +324,7 @@ private void PreparePlainExecutables() private void ApplyMonitorProcess(IResource resource, ExecutableSpec spec) { - if (resource.TryGetLastAnnotation(out var annotation)) + if (resource.TryGetParentProcessLifetime(out var annotation)) { var monitorProcess = _processMonitor.GetMonitorProcess(annotation.ParentProcess); spec.MonitorPid = monitorProcess.ProcessId; @@ -446,7 +446,7 @@ private async Task BuildExecutableConfiguration(Rendere private string GetCertificatesRootDirectory(RenderedModelResource er, Executable exe) { - if (er.ModelResource.GetExecutableLifetimeType() == Lifetime.Persistent) + if (er.ModelResource.GetLifetimeType() == Lifetime.Persistent) { return Path.Join(_aspireStore.BasePath, "dcp", "executables", exe.Metadata.Name, "certificates"); } diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 862954bc947..b490140b97a 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -86,6 +86,37 @@ public static IResourceBuilder WithPersistentLifetime(this IResourceBuilde return ApplyLifetime(builder, Lifetime.Persistent); } + /// + /// Configures a resource to match the lifetime of another resource. + /// + /// The resource type. + /// The source resource type. + /// The resource builder. + /// The resource builder whose lifetime should be used. + /// The . + /// + /// The resource lifetime is evaluated from when the application model is prepared, so later lifetime + /// changes to the source resource are reflected by this resource. + /// + /// Thrown when the resource does not support lifetime configuration. + [AspireExport(Description = "Sets resource lifetime behavior to match another resource")] + public static IResourceBuilder WithLifetimeOf(this IResourceBuilder builder, IResourceBuilder sourceBuilder) + where T : IResource + where TSource : IResource + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(sourceBuilder); + + RemoveLifetimeAnnotations(builder); + + if (builder.Resource is ContainerResource or ExecutableResource or ProjectResource) + { + return builder.WithAnnotation(new LifetimeReferenceAnnotation(sourceBuilder.Resource), ResourceAnnotationMutationBehavior.Replace); + } + + throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); + } + /// /// Configures a resource to use a persistent lifetime that ends when a parent process exits. /// @@ -129,28 +160,43 @@ public static IResourceBuilder WithParentProcessLifetime(this IResourceBui private static IResourceBuilder ApplyLifetime(IResourceBuilder builder, Lifetime lifetime) where T : IResource { - RemoveParentProcessLifetime(builder); + RemoveLifetimeAnnotations(builder); - if (builder.Resource is ContainerResource) + if (builder.Resource is ContainerResource or ExecutableResource or ProjectResource) { - return builder.WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = ToContainerLifetime(lifetime) }, ResourceAnnotationMutationBehavior.Replace); - } - - if (builder.Resource is ExecutableResource or ProjectResource) - { - return builder.WithAnnotation(new ExecutableLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + return builder.WithAnnotation(new LifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); } throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); } - private static void RemoveParentProcessLifetime(IResourceBuilder builder) + private static void RemoveLifetimeAnnotations(IResourceBuilder builder) where T : IResource { foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) { builder.Resource.Annotations.Remove(annotation); } + + foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) + { + builder.Resource.Annotations.Remove(annotation); + } + + foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) + { + builder.Resource.Annotations.Remove(annotation); + } + + foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) + { + builder.Resource.Annotations.Remove(annotation); + } + + foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) + { + builder.Resource.Annotations.Remove(annotation); + } } /// @@ -1591,14 +1637,6 @@ public static IResourceBuilder WithEndpointProxySupport(this IResourceBuil return builder; } - private static ContainerLifetime ToContainerLifetime(Lifetime lifetime) - => lifetime switch - { - Lifetime.Session => ContainerLifetime.Session, - Lifetime.Persistent => ContainerLifetime.Persistent, - _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) - }; - /// /// Exposes an endpoint on a resource. This endpoint reference can be retrieved using . /// The endpoint name will be the scheme name if not specified. diff --git a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs index 86f54f7d06b..26280a47431 100644 --- a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs +++ b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs @@ -307,9 +307,9 @@ public void RunAsEmulator_WithCustomLifetime_ShouldConfigureLifetimeAnnotation() }); // Assert - var lifetimeAnnotation = resourceBuilder.Resource.Annotations.OfType().SingleOrDefault(); + var lifetimeAnnotation = resourceBuilder.Resource.Annotations.OfType().SingleOrDefault(); Assert.NotNull(lifetimeAnnotation); - Assert.Equal(ContainerLifetime.Persistent, lifetimeAnnotation.Lifetime); + Assert.Equal(Lifetime.Persistent, lifetimeAnnotation.Lifetime); } [Fact] diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs index 0dfdeb02e95..a7638c2e9de 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs @@ -3,6 +3,7 @@ using System.Text; using System.Text.Json.Nodes; +using System.Reflection; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure.EventHubs; using Aspire.Hosting.Utils; @@ -507,15 +508,15 @@ public async Task AzureEventHubsEmulator_WithConfigurationFile() public void AddAzureEventHubsWithEmulator_SetsStorageLifetime(bool isPersistent) { using var builder = TestDistributedApplicationBuilder.Create(); - var lifetime = isPersistent ? ContainerLifetime.Persistent : ContainerLifetime.Session; + var lifetime = isPersistent ? Lifetime.Persistent : Lifetime.Session; - var serviceBus = builder.AddAzureEventHubs("eh").RunAsEmulator(configureContainer: builder => + var eventHubs = builder.AddAzureEventHubs("eh").RunAsEmulator(configureContainer: builder => { _ = lifetime switch { - ContainerLifetime.Session => builder.WithSessionLifetime(), - ContainerLifetime.Persistent => builder.WithPersistentLifetime(), - _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), lifetime)}'.") + Lifetime.Session => builder.WithSessionLifetime(), + Lifetime.Persistent => builder.WithPersistentLifetime(), + _ => throw new InvalidOperationException($"Unknown resource lifetime '{Enum.GetName(typeof(Lifetime), lifetime)}'.") }; }); @@ -523,11 +524,24 @@ public void AddAzureEventHubsWithEmulator_SetsStorageLifetime(bool isPersistent) Assert.NotNull(azurite); - serviceBus.Resource.TryGetLastAnnotation(out var sbLifetimeAnnotation); - azurite.TryGetLastAnnotation(out var sqlLifetimeAnnotation); + var sourceResource = GetLifetimeReferenceSource(azurite); + Assert.Same(eventHubs.Resource.Annotations, sourceResource.Annotations); - Assert.Equal(lifetime, sbLifetimeAnnotation?.Lifetime); - Assert.Equal(lifetime, sqlLifetimeAnnotation?.Lifetime); + eventHubs.Resource.TryGetLastAnnotation(out var lifetimeAnnotation); + Assert.Equal(lifetime, lifetimeAnnotation?.Lifetime); + } + + [Fact] + public void AddAzureEventHubsWithEmulator_DoesNotSetStorageLifetimeWithoutContainerConfiguration() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddAzureEventHubs("eh").RunAsEmulator(); + + var azurite = builder.Resources.FirstOrDefault(x => x.Name == "eh-storage"); + + Assert.NotNull(azurite); + Assert.DoesNotContain(azurite.Annotations, a => a.GetType().Name == "LifetimeReferenceAnnotation"); } [Fact] @@ -539,6 +553,14 @@ public void RunAsEmulator_CalledTwice_Throws() Assert.Throws(() => eventHubs.RunAsEmulator()); } + private static IResource GetLifetimeReferenceSource(IResource resource) + { + var annotation = Assert.Single(resource.Annotations, a => a.GetType().Name == "LifetimeReferenceAnnotation"); + var sourceResource = annotation.GetType().GetProperty("SourceResource", BindingFlags.Instance | BindingFlags.Public)!.GetValue(annotation); + + return Assert.IsAssignableFrom(sourceResource); + } + [Fact] public void AzureEventHubsHasCorrectConnectionStrings() { diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs index 30d078d2395..734a2e29dba 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs @@ -604,15 +604,15 @@ public async Task AzureServiceBusEmulator_WithConfigurationFile() public void AddAzureServiceBusWithEmulator_SetsSqlLifetime(bool isPersistent) { using var builder = TestDistributedApplicationBuilder.Create(); - var lifetime = isPersistent ? ContainerLifetime.Persistent : ContainerLifetime.Session; + var lifetime = isPersistent ? Lifetime.Persistent : Lifetime.Session; var serviceBus = builder.AddAzureServiceBus("sb").RunAsEmulator(configureContainer: builder => { _ = lifetime switch { - ContainerLifetime.Session => builder.WithSessionLifetime(), - ContainerLifetime.Persistent => builder.WithPersistentLifetime(), - _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), lifetime)}'.") + Lifetime.Session => builder.WithSessionLifetime(), + Lifetime.Persistent => builder.WithPersistentLifetime(), + _ => throw new InvalidOperationException($"Unknown resource lifetime '{Enum.GetName(typeof(Lifetime), lifetime)}'.") }; }); @@ -620,11 +620,24 @@ public void AddAzureServiceBusWithEmulator_SetsSqlLifetime(bool isPersistent) Assert.NotNull(sql); - serviceBus.Resource.TryGetLastAnnotation(out var sbLifetimeAnnotation); - sql.TryGetLastAnnotation(out var sqlLifetimeAnnotation); + var sourceResource = GetLifetimeReferenceSource(sql); + Assert.Same(serviceBus.Resource.Annotations, sourceResource.Annotations); - Assert.Equal(lifetime, sbLifetimeAnnotation?.Lifetime); - Assert.Equal(lifetime, sqlLifetimeAnnotation?.Lifetime); + serviceBus.Resource.TryGetLastAnnotation(out var lifetimeAnnotation); + Assert.Equal(lifetime, lifetimeAnnotation?.Lifetime); + } + + [Fact] + public void AddAzureServiceBusWithEmulator_DoesNotSetSqlLifetimeWithoutContainerConfiguration() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + builder.AddAzureServiceBus("sb").RunAsEmulator(); + + var sql = builder.Resources.FirstOrDefault(x => x.Name == "sb-mssql"); + + Assert.NotNull(sql); + Assert.DoesNotContain(sql.Annotations, a => a.GetType().Name == "LifetimeReferenceAnnotation"); } [Fact] @@ -636,6 +649,14 @@ public void RunAsEmulator_CalledTwice_Throws() Assert.Throws(() => serviceBus.RunAsEmulator()); } + private static IResource GetLifetimeReferenceSource(IResource resource) + { + var annotation = Assert.Single(resource.Annotations, a => a.GetType().Name == "LifetimeReferenceAnnotation"); + var sourceResource = annotation.GetType().GetProperty("SourceResource", BindingFlags.Instance | BindingFlags.Public)!.GetValue(annotation); + + return Assert.IsAssignableFrom(sourceResource); + } + [Fact] public void AzureServiceBusHasCorrectConnectionStrings() { diff --git a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs index 6ce6fd08bb1..ac51a098913 100644 --- a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs +++ b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs @@ -56,25 +56,25 @@ public void AddDevTunnel_WithSpecificTunnelId_SetsTunnelIdProperty() } [Fact] - public void AddDevTunnel_WithPersistentExecutableLifetime_AddsExecutableLifetimeAnnotation() + public void AddDevTunnel_WithPersistentLifetime_AddsLifetimeAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var tunnel = builder.AddDevTunnel("tunnel", "custom-id") .WithPersistentLifetime(); - Assert.True(tunnel.Resource.TryGetLastAnnotation(out var annotation)); + Assert.True(tunnel.Resource.TryGetLastAnnotation(out var annotation)); Assert.Equal(Lifetime.Persistent, annotation.Lifetime); } [Fact] - public void AddDevTunnel_DefaultLifetimeDoesNotAddExecutableLifetimeAnnotation() + public void AddDevTunnel_DefaultLifetimeDoesNotAddLifetimeAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var tunnel = builder.AddDevTunnel("tunnel", "custom-id"); - Assert.False(tunnel.Resource.TryGetLastAnnotation(out _)); + Assert.False(tunnel.Resource.TryGetLastAnnotation(out _)); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs index b09d747e66d..7e33bc8d8c8 100644 --- a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs @@ -73,13 +73,13 @@ public void WithWorkingDirectoryAllowsEmptyString() } [Fact] - public void WithPersistentLifetimeAddsExecutableLifetimeAnnotation() + public void WithPersistentLifetimeAddsLifetimeAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var executable = builder.AddExecutable("myexe", "command", "workingdirectory") .WithPersistentLifetime(); - var annotation = executable.Resource.Annotations.OfType().Single(); + var annotation = executable.Resource.Annotations.OfType().Single(); Assert.Equal(Lifetime.Persistent, annotation.Lifetime); } diff --git a/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs b/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs index b96873bcc8a..5eb50f7cec2 100644 --- a/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs +++ b/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs @@ -26,7 +26,7 @@ public async Task PersistentContainerWithoutUserSecrets_LogsWarning() var resources = new ResourceCollection(); var container = new ContainerResource("my-container"); - container.Annotations.Add(new ContainerLifetimeAnnotation { Lifetime = ContainerLifetime.Persistent }); + container.Annotations.Add(new LifetimeAnnotation { Lifetime = Lifetime.Persistent }); resources.Add(container); var model = new DistributedApplicationModel(resources); @@ -50,7 +50,7 @@ public async Task PersistentContainerWithUserSecrets_DoesNotLogWarning() var resources = new ResourceCollection(); var container = new ContainerResource("my-container"); - container.Annotations.Add(new ContainerLifetimeAnnotation { Lifetime = ContainerLifetime.Persistent }); + container.Annotations.Add(new LifetimeAnnotation { Lifetime = Lifetime.Persistent }); resources.Add(container); var model = new DistributedApplicationModel(resources); diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs index 5ec652f5275..c341b81ef6f 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs @@ -9,14 +9,14 @@ namespace Aspire.Hosting.Tests; public class ProjectResourceBuilderExtensionTests { [Fact] - public void WithPersistentLifetimeAddsExecutableLifetimeAnnotation() + public void WithPersistentLifetimeAddsLifetimeAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var project = builder.AddProject("project", options => options.ExcludeLaunchProfile = true) .WithPersistentLifetime(); - var annotation = project.Resource.Annotations.OfType().Single(); + var annotation = project.Resource.Annotations.OfType().Single(); Assert.Equal(Lifetime.Persistent, annotation.Lifetime); } diff --git a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs index 290cb7fb434..d4ed396fc84 100644 --- a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs @@ -9,15 +9,15 @@ namespace Aspire.Hosting.Tests; public class ResourceBuilderLifetimeTests { [Fact] - public void WithPersistentLifetimeAddsContainerLifetimeAnnotation() + public void WithPersistentLifetimeAddsLifetimeAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var container = builder.AddContainer("container", "image") .WithPersistentLifetime(); - var annotation = container.Resource.Annotations.OfType().Single(); - Assert.Equal(ContainerLifetime.Persistent, annotation.Lifetime); + var annotation = container.Resource.Annotations.OfType().Single(); + Assert.Equal(Lifetime.Persistent, annotation.Lifetime); } [Fact] @@ -45,6 +45,87 @@ public void WithPersistentLifetimeRemovesParentProcessLifetimeAnnotation() Assert.False(container.Resource.TryGetLastAnnotation(out _)); } + [Fact] + public void WithLifetimeOfMatchesSourceResourceLifetime() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var source = builder.AddContainer("source", "image") + .WithPersistentLifetime(); + var container = builder.AddContainer("container", "image") + .WithSessionLifetime() + .WithLifetimeOf(source); + + Assert.Equal(Lifetime.Persistent, container.Resource.GetLifetimeType()); + Assert.Empty(container.Resource.Annotations.OfType()); + + source.WithSessionLifetime(); + + Assert.Equal(Lifetime.Session, container.Resource.GetLifetimeType()); + } + + [Fact] + public void WithLifetimeOfMatchesSourceParentProcessLifetime() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var source = builder.AddContainer("source", "image") + .WithParentProcessLifetime(Environment.ProcessId); + var container = builder.AddContainer("container", "image") + .WithLifetimeOf(source); + + Assert.True(container.Resource.TryGetParentProcessLifetime(out var parentProcessLifetimeAnnotation)); + Assert.Equal(Environment.ProcessId, parentProcessLifetimeAnnotation.ParentProcess.Id); + + source.WithSessionLifetime(); + + Assert.False(container.Resource.TryGetParentProcessLifetime(out _)); + } + + [Fact] + public void ExplicitLifetimeOverridesWithLifetimeOf() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var source = builder.AddContainer("source", "image") + .WithSessionLifetime(); + var container = builder.AddContainer("container", "image") + .WithLifetimeOf(source) + .WithPersistentLifetime(); + + source.WithSessionLifetime(); + + Assert.Equal(Lifetime.Persistent, container.Resource.GetLifetimeType()); + } + + [Fact] + public void WithLifetimeOfRejectsUnsupportedResourceTypes() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var parameter = builder.AddParameter("parameter"); + var container = builder.AddContainer("container", "image"); + + void ConfigureLifetime() => parameter.WithLifetimeOf(container); + + var exception = Assert.Throws((Action)ConfigureLifetime); + Assert.Contains("does not support lifetime configuration", exception.Message); + } + + [Fact] + public void WithLifetimeOfDetectsCircularReferences() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var containerA = builder.AddContainer("container-a", "image"); + var containerB = builder.AddContainer("container-b", "image") + .WithLifetimeOf(containerA); + containerA.WithLifetimeOf(containerB); + + var exception = Assert.Throws(() => containerA.Resource.GetLifetimeType()); + Assert.Contains("circular lifetime reference", exception.Message); + } + [Fact] public void WithSessionLifetimeRemovesParentProcessLifetimeAnnotation() { From de0264a2223182696d658d386d8f5eecbd880109 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 12:17:06 -0700 Subject: [PATCH 14/38] Stabilize OTLP instance IDs for persistent resources Use the stable DCP object name as the generated OTLP service instance ID for persistent resources while preserving the random suffix for session resources. Add regression coverage for persistent containers and executables using WithOtlpExporter across app host runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ApplicationModel/ResourceExtensions.cs | 5 ++ src/Aspire.Hosting/Dcp/ContainerCreator.cs | 4 +- src/Aspire.Hosting/Dcp/DcpNameGenerator.cs | 2 +- src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 4 +- .../Dcp/DcpExecutorTests.cs | 69 +++++++++++++++++++ 5 files changed, 79 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 589cb9b5582..10accf9fd56 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1102,6 +1102,11 @@ internal static bool HasPersistentLifetime(this IResource resource) return resource.GetLifetimeType() == Lifetime.Persistent; } + internal static string GetOtelServiceInstanceId(this IResource resource, DcpInstance instance) + { + return resource.GetLifetimeType() == Lifetime.Persistent ? instance.Name : instance.Suffix; + } + /// /// Determines whether the specified resource has a parent process lifetime. /// diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index 3e388fe6ac1..daba932b1ef 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -175,7 +175,7 @@ public IEnumerable> PrepareObjects() ctr.Annotate(CustomResource.ResourceNameAnnotation, container.Name); ctr.Annotate(CustomResource.OtelServiceNameAnnotation, container.Name); - ctr.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, containerObjectInstance.Suffix); + ctr.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, container.GetOtelServiceInstanceId(containerObjectInstance)); DcpExecutor.SetInitialResourceState(container, ctr); var aanns = container.Annotations.OfType().ToImmutableArray(); @@ -292,7 +292,7 @@ internal void PrepareContainerExecutables() workingDirectory: containerExecutable.WorkingDirectory); containerExec.Annotate(CustomResource.OtelServiceNameAnnotation, containerExecutable.Name); - containerExec.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); + containerExec.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, containerExecutable.GetOtelServiceInstanceId(exeInstance)); containerExec.Annotate(CustomResource.ResourceNameAnnotation, containerExecutable.Name); DcpExecutor.SetInitialResourceState(containerExecutable, containerExec); diff --git a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs index 8ca47b45e09..567c4019e63 100644 --- a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs +++ b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs @@ -14,7 +14,7 @@ internal sealed class DcpNameGenerator // A random suffix added to every DCP object name ensures that those names (and derived object names, for example container names) // are unique machine-wide with a high level of probability. // The length of 8 achieves that while keeping the names relatively short and readable. - // The second purpose of the suffix is to play a role of a unique OpenTelemetry service instance ID. + // The second purpose of the suffix is to play the role of a unique OpenTelemetry service instance ID for session resources. private const int RandomNameSuffixLength = 8; private readonly IConfiguration _configuration; private readonly IOptions _options; diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index 007e461550c..3b2f8296a37 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -163,7 +163,7 @@ private void PrepareProjectExecutables() exe.Spec.WorkingDirectory = Path.GetDirectoryName(projectMetadata.ProjectPath); exe.Annotate(CustomResource.OtelServiceNameAnnotation, project.Name); - exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); + exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, project.GetOtelServiceInstanceId(exeInstance)); exe.Annotate(CustomResource.ResourceNameAnnotation, project.Name); exe.Annotate(CustomResource.ResourceReplicaCount, replicas.ToString(CultureInfo.InvariantCulture)); exe.Annotate(CustomResource.ResourceReplicaIndex, i.ToString(CultureInfo.InvariantCulture)); @@ -287,7 +287,7 @@ private void PreparePlainExecutables() // The working directory is always relative to the app host project directory (if it exists). exe.Spec.WorkingDirectory = executable.WorkingDirectory; exe.Annotate(CustomResource.OtelServiceNameAnnotation, executable.Name); - exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, exeInstance.Suffix); + exe.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, executable.GetOtelServiceInstanceId(exeInstance)); exe.Annotate(CustomResource.ResourceNameAnnotation, executable.Name); var persistent = executable.GetLifetimeType() == Lifetime.Persistent; diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index c619ede1f22..6716fd53d3a 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -2276,6 +2276,75 @@ public async Task PersistentDcpResourcesDoNotIncludeMonitorProcessByDefault() }); } + [Fact] + public async Task PersistentContainerWithOtlpExporterUsesStableServiceInstanceId() + { + var first = await CreateOtlpServiceInstanceIdAsync(builder => + { + builder.AddContainer("database", "image") + .WithPersistentLifetime() + .WithOtlpExporter(); + }); + var second = await CreateOtlpServiceInstanceIdAsync(builder => + { + builder.AddContainer("database", "image") + .WithPersistentLifetime() + .WithOtlpExporter(); + }); + + Assert.Equal("database-12345678", first); + Assert.Equal(first, second); + } + + [Fact] + public async Task PersistentExecutableWithOtlpExporterUsesStableServiceInstanceId() + { + var first = await CreateOtlpServiceInstanceIdAsync(builder => + { + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithPersistentLifetime() + .WithOtlpExporter(); + }); + var second = await CreateOtlpServiceInstanceIdAsync(builder => + { + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithPersistentLifetime() + .WithOtlpExporter(); + }); + + Assert.Equal("worker-12345678", first); + Assert.Equal(first, second); + } + + private static async Task CreateOtlpServiceInstanceIdAsync(Action configureBuilder) + { + var builder = DistributedApplication.CreateBuilder(); + configureBuilder(builder); + + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }) + .Build(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor( + distributedAppModel, + kubernetesService: kubernetesService, + configuration: configuration); + + await appExecutor.RunApplicationAsync(); + + var resource = Assert.Single(kubernetesService.CreatedResources, r => + r.Metadata.Annotations is not null && + r.Metadata.Annotations.ContainsKey(CustomResource.OtelServiceInstanceIdAnnotation)); + + return resource.Metadata.Annotations![CustomResource.OtelServiceInstanceIdAnnotation]; + } + [Fact] public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() { From 3f77fa6bc2fb2f8b5e7caaef1056a7714e8d2448 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 12:25:58 -0700 Subject: [PATCH 15/38] Regenerate CodeGeneration snapshots Refresh polyglot CodeGeneration Verify baselines after rebasing onto latest main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...TwoPassScanningGeneratedAspire.verified.go | 609 ++- ...oPassScanningGeneratedAspire.verified.java | 592 ++- ...TwoPassScanningGeneratedAspire.verified.py | 219 +- ...TwoPassScanningGeneratedAspire.verified.rs | 597 ++- ...ContainerResourceCapabilities.verified.txt | 54 +- ...TwoPassScanningGeneratedAspire.verified.ts | 3547 ++++++++++++++--- 6 files changed, 4867 insertions(+), 751 deletions(-) diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 5409e596b64..716b69c7685 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -31,14 +31,6 @@ const ( ContainerMountTypeVolume ContainerMountType = "Volume" ) -// ContainerLifetime represents ContainerLifetime. -type ContainerLifetime string - -const ( - ContainerLifetimeSession ContainerLifetime = "Session" - ContainerLifetimePersistent ContainerLifetime = "Persistent" -) - // ImagePullPolicy represents ImagePullPolicy. type ImagePullPolicy string @@ -1075,7 +1067,7 @@ type Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource interface { WithImageRegistry(registry string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithImageSHA256(sha256 string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithImageTag(tag string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource - WithLifetime(lifetime ContainerLifetime) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource + WithLifetimeOf(sourceBuilder Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithMcpServer(options ...*WithMcpServerOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithMergeEndpoint(endpointName string, port float64) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource @@ -1090,7 +1082,9 @@ type Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithOptionalString(options ...*WithOptionalStringOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithOtlpExporter(options ...*WithOtlpExporterOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource + WithParentProcessLifetime(parentProcessId float64) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithParentRelationship(parent Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource + WithPersistentLifetime() Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource @@ -1101,6 +1095,7 @@ type Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource interface { WithRemoteImageName(remoteImageName string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithRemoteImageTag(remoteImageTag string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource + WithSessionLifetime() Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithStatus(status TestResourceStatus) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithUnionDependency(dependency any) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithUrl(url any, options ...*WithUrlOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource @@ -2214,15 +2209,16 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithImageTag(t return s } -// WithLifetime sets the lifetime behavior of the container resource -func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithLifetime(lifetime ContainerLifetime) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithLifetimeOf(sourceBuilder Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } ctx := context.Background() reqArgs := map[string]any{ "builder": s.handle.ToJSON(), } - reqArgs["lifetime"] = serializeValue(lifetime) - if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetime", reqArgs); err != nil { s.setErr(err) } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } return s } @@ -2452,6 +2448,18 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithOtlpExport return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithParentProcessLifetime(parentProcessId float64) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithParentRelationship(parent Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { if s.err != nil { return s } @@ -2465,6 +2473,17 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithParentRela return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithPersistentLifetime() Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { if s.err != nil { return s } @@ -2647,6 +2666,17 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithRequiredCo return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithSessionLifetime() Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithStatus(status TestResourceStatus) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { if s.err != nil { return s } @@ -3010,6 +3040,7 @@ type CSharpAppResource interface { WithDockerfileBaseImage(options ...*WithDockerfileBaseImageOptions) CSharpAppResource WithEndpoint(options ...*WithEndpointOptions) CSharpAppResource WithEndpointCallback(endpointName string, callback func(obj EndpointUpdateContext), options ...*WithEndpointCallbackOptions) CSharpAppResource + WithEndpointProxySupport(proxyEnabled bool) CSharpAppResource WithEndpoints(endpoints []string) CSharpAppResource WithEnvironment(name string, value any) CSharpAppResource WithEnvironmentCallback(callback func(arg EnvironmentCallbackContext)) CSharpAppResource @@ -3027,6 +3058,7 @@ type CSharpAppResource interface { WithHttpsEndpointCallback(callback func(obj EndpointUpdateContext), options ...*WithHttpsEndpointCallbackOptions) CSharpAppResource WithIconName(iconName string, options ...*WithIconNameOptions) CSharpAppResource WithImagePushOptions(callback func(arg ContainerImagePushOptionsCallbackContext)) CSharpAppResource + WithLifetimeOf(sourceBuilder Resource) CSharpAppResource WithMcpServer(options ...*WithMcpServerOptions) CSharpAppResource WithMergeEndpoint(endpointName string, port float64) CSharpAppResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) CSharpAppResource @@ -3041,7 +3073,9 @@ type CSharpAppResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) CSharpAppResource WithOptionalString(options ...*WithOptionalStringOptions) CSharpAppResource WithOtlpExporter(options ...*WithOtlpExporterOptions) CSharpAppResource + WithParentProcessLifetime(parentProcessId float64) CSharpAppResource WithParentRelationship(parent Resource) CSharpAppResource + WithPersistentLifetime() CSharpAppResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) CSharpAppResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) CSharpAppResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) CSharpAppResource @@ -3053,6 +3087,7 @@ type CSharpAppResource interface { WithRemoteImageTag(remoteImageTag string) CSharpAppResource WithReplicas(replicas float64) CSharpAppResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) CSharpAppResource + WithSessionLifetime() CSharpAppResource WithStatus(status TestResourceStatus) CSharpAppResource WithUnionDependency(dependency any) CSharpAppResource WithUrl(url any, options ...*WithUrlOptions) CSharpAppResource @@ -3650,6 +3685,18 @@ func (s *cSharpAppResource) WithEndpointCallback(endpointName string, callback f return s } +// WithEndpointProxySupport configures endpoint proxy support +func (s *cSharpAppResource) WithEndpointProxySupport(proxyEnabled bool) CSharpAppResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["proxyEnabled"] = serializeValue(proxyEnabled) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withEndpointProxySupport", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithEndpoints sets the endpoints func (s *cSharpAppResource) WithEndpoints(endpoints []string) CSharpAppResource { if s.err != nil { return s } @@ -3948,6 +3995,19 @@ func (s *cSharpAppResource) WithImagePushOptions(callback func(arg ContainerImag return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *cSharpAppResource) WithLifetimeOf(sourceBuilder Resource) CSharpAppResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *cSharpAppResource) WithMcpServer(options ...*WithMcpServerOptions) CSharpAppResource { if s.err != nil { return s } @@ -4174,6 +4234,18 @@ func (s *cSharpAppResource) WithOtlpExporter(options ...*WithOtlpExporterOptions return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *cSharpAppResource) WithParentProcessLifetime(parentProcessId float64) CSharpAppResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *cSharpAppResource) WithParentRelationship(parent Resource) CSharpAppResource { if s.err != nil { return s } @@ -4187,6 +4259,17 @@ func (s *cSharpAppResource) WithParentRelationship(parent Resource) CSharpAppRes return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *cSharpAppResource) WithPersistentLifetime() CSharpAppResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *cSharpAppResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) CSharpAppResource { if s.err != nil { return s } @@ -4381,6 +4464,17 @@ func (s *cSharpAppResource) WithRequiredCommand(command string, options ...*With return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *cSharpAppResource) WithSessionLifetime() CSharpAppResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *cSharpAppResource) WithStatus(status TestResourceStatus) CSharpAppResource { if s.err != nil { return s } @@ -5223,6 +5317,7 @@ type ContainerRegistryResource interface { WithExplicitStart() ContainerRegistryResource WithHealthCheck(key string) ContainerRegistryResource WithIconName(iconName string, options ...*WithIconNameOptions) ContainerRegistryResource + WithLifetimeOf(sourceBuilder Resource) ContainerRegistryResource WithMergeEndpoint(endpointName string, port float64) ContainerRegistryResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ContainerRegistryResource WithMergeLabel(label string) ContainerRegistryResource @@ -5235,13 +5330,16 @@ type ContainerRegistryResource interface { WithNestedConfig(config *TestNestedDto) ContainerRegistryResource WithOptionalCallback(options ...*WithOptionalCallbackOptions) ContainerRegistryResource WithOptionalString(options ...*WithOptionalStringOptions) ContainerRegistryResource + WithParentProcessLifetime(parentProcessId float64) ContainerRegistryResource WithParentRelationship(parent Resource) ContainerRegistryResource + WithPersistentLifetime() ContainerRegistryResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ContainerRegistryResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ContainerRegistryResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ContainerRegistryResource WithProcessCommandFactory(commandName string, displayName string, createProcessSpec func(arg ExecuteCommandContext) *ProcessCommandSpecExportData, options ...*WithProcessCommandFactoryOptions) ContainerRegistryResource WithRelationship(resourceBuilder Resource, type_ string) ContainerRegistryResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ContainerRegistryResource + WithSessionLifetime() ContainerRegistryResource WithStatus(status TestResourceStatus) ContainerRegistryResource WithUnionDependency(dependency any) ContainerRegistryResource WithUrl(url any, options ...*WithUrlOptions) ContainerRegistryResource @@ -5599,6 +5697,19 @@ func (s *containerRegistryResource) WithIconName(iconName string, options ...*Wi return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *containerRegistryResource) WithLifetimeOf(sourceBuilder Resource) ContainerRegistryResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMergeEndpoint configures a named endpoint func (s *containerRegistryResource) WithMergeEndpoint(endpointName string, port float64) ContainerRegistryResource { if s.err != nil { return s } @@ -5789,6 +5900,18 @@ func (s *containerRegistryResource) WithOptionalString(options ...*WithOptionalS return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *containerRegistryResource) WithParentProcessLifetime(parentProcessId float64) ContainerRegistryResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *containerRegistryResource) WithParentRelationship(parent Resource) ContainerRegistryResource { if s.err != nil { return s } @@ -5802,6 +5925,17 @@ func (s *containerRegistryResource) WithParentRelationship(parent Resource) Cont return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *containerRegistryResource) WithPersistentLifetime() ContainerRegistryResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *containerRegistryResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ContainerRegistryResource { if s.err != nil { return s } @@ -5922,6 +6056,17 @@ func (s *containerRegistryResource) WithRequiredCommand(command string, options return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *containerRegistryResource) WithSessionLifetime() ContainerRegistryResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *containerRegistryResource) WithStatus(status TestResourceStatus) ContainerRegistryResource { if s.err != nil { return s } @@ -6109,7 +6254,7 @@ type ContainerResource interface { WithImageRegistry(registry string) ContainerResource WithImageSHA256(sha256 string) ContainerResource WithImageTag(tag string) ContainerResource - WithLifetime(lifetime ContainerLifetime) ContainerResource + WithLifetimeOf(sourceBuilder Resource) ContainerResource WithMcpServer(options ...*WithMcpServerOptions) ContainerResource WithMergeEndpoint(endpointName string, port float64) ContainerResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ContainerResource @@ -6124,7 +6269,9 @@ type ContainerResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) ContainerResource WithOptionalString(options ...*WithOptionalStringOptions) ContainerResource WithOtlpExporter(options ...*WithOtlpExporterOptions) ContainerResource + WithParentProcessLifetime(parentProcessId float64) ContainerResource WithParentRelationship(parent Resource) ContainerResource + WithPersistentLifetime() ContainerResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ContainerResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ContainerResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ContainerResource @@ -6135,6 +6282,7 @@ type ContainerResource interface { WithRemoteImageName(remoteImageName string) ContainerResource WithRemoteImageTag(remoteImageTag string) ContainerResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ContainerResource + WithSessionLifetime() ContainerResource WithStatus(status TestResourceStatus) ContainerResource WithUnionDependency(dependency any) ContainerResource WithUrl(url any, options ...*WithUrlOptions) ContainerResource @@ -7247,15 +7395,16 @@ func (s *containerResource) WithImageTag(tag string) ContainerResource { return s } -// WithLifetime sets the lifetime behavior of the container resource -func (s *containerResource) WithLifetime(lifetime ContainerLifetime) ContainerResource { +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *containerResource) WithLifetimeOf(sourceBuilder Resource) ContainerResource { if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } ctx := context.Background() reqArgs := map[string]any{ "builder": s.handle.ToJSON(), } - reqArgs["lifetime"] = serializeValue(lifetime) - if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetime", reqArgs); err != nil { s.setErr(err) } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } return s } @@ -7485,6 +7634,18 @@ func (s *containerResource) WithOtlpExporter(options ...*WithOtlpExporterOptions return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *containerResource) WithParentProcessLifetime(parentProcessId float64) ContainerResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *containerResource) WithParentRelationship(parent Resource) ContainerResource { if s.err != nil { return s } @@ -7498,6 +7659,17 @@ func (s *containerResource) WithParentRelationship(parent Resource) ContainerRes return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *containerResource) WithPersistentLifetime() ContainerResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *containerResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ContainerResource { if s.err != nil { return s } @@ -7680,6 +7852,17 @@ func (s *containerResource) WithRequiredCommand(command string, options ...*With return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *containerResource) WithSessionLifetime() ContainerResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *containerResource) WithStatus(status TestResourceStatus) ContainerResource { if s.err != nil { return s } @@ -9359,6 +9542,7 @@ type DotnetToolResource interface { WithDockerfileBaseImage(options ...*WithDockerfileBaseImageOptions) DotnetToolResource WithEndpoint(options ...*WithEndpointOptions) DotnetToolResource WithEndpointCallback(endpointName string, callback func(obj EndpointUpdateContext), options ...*WithEndpointCallbackOptions) DotnetToolResource + WithEndpointProxySupport(proxyEnabled bool) DotnetToolResource WithEndpoints(endpoints []string) DotnetToolResource WithEnvironment(name string, value any) DotnetToolResource WithEnvironmentCallback(callback func(arg EnvironmentCallbackContext)) DotnetToolResource @@ -9377,6 +9561,7 @@ type DotnetToolResource interface { WithHttpsEndpointCallback(callback func(obj EndpointUpdateContext), options ...*WithHttpsEndpointCallbackOptions) DotnetToolResource WithIconName(iconName string, options ...*WithIconNameOptions) DotnetToolResource WithImagePushOptions(callback func(arg ContainerImagePushOptionsCallbackContext)) DotnetToolResource + WithLifetimeOf(sourceBuilder Resource) DotnetToolResource WithMcpServer(options ...*WithMcpServerOptions) DotnetToolResource WithMergeEndpoint(endpointName string, port float64) DotnetToolResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) DotnetToolResource @@ -9391,7 +9576,9 @@ type DotnetToolResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) DotnetToolResource WithOptionalString(options ...*WithOptionalStringOptions) DotnetToolResource WithOtlpExporter(options ...*WithOtlpExporterOptions) DotnetToolResource + WithParentProcessLifetime(parentProcessId float64) DotnetToolResource WithParentRelationship(parent Resource) DotnetToolResource + WithPersistentLifetime() DotnetToolResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) DotnetToolResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) DotnetToolResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) DotnetToolResource @@ -9402,6 +9589,7 @@ type DotnetToolResource interface { WithRemoteImageName(remoteImageName string) DotnetToolResource WithRemoteImageTag(remoteImageTag string) DotnetToolResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) DotnetToolResource + WithSessionLifetime() DotnetToolResource WithStatus(status TestResourceStatus) DotnetToolResource WithToolIgnoreExistingFeeds() DotnetToolResource WithToolIgnoreFailedSources() DotnetToolResource @@ -9974,6 +10162,18 @@ func (s *dotnetToolResource) WithEndpointCallback(endpointName string, callback return s } +// WithEndpointProxySupport configures endpoint proxy support +func (s *dotnetToolResource) WithEndpointProxySupport(proxyEnabled bool) DotnetToolResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["proxyEnabled"] = serializeValue(proxyEnabled) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withEndpointProxySupport", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithEndpoints sets the endpoints func (s *dotnetToolResource) WithEndpoints(endpoints []string) DotnetToolResource { if s.err != nil { return s } @@ -10284,6 +10484,19 @@ func (s *dotnetToolResource) WithImagePushOptions(callback func(arg ContainerIma return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *dotnetToolResource) WithLifetimeOf(sourceBuilder Resource) DotnetToolResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *dotnetToolResource) WithMcpServer(options ...*WithMcpServerOptions) DotnetToolResource { if s.err != nil { return s } @@ -10510,6 +10723,18 @@ func (s *dotnetToolResource) WithOtlpExporter(options ...*WithOtlpExporterOption return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *dotnetToolResource) WithParentProcessLifetime(parentProcessId float64) DotnetToolResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *dotnetToolResource) WithParentRelationship(parent Resource) DotnetToolResource { if s.err != nil { return s } @@ -10523,6 +10748,17 @@ func (s *dotnetToolResource) WithParentRelationship(parent Resource) DotnetToolR return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *dotnetToolResource) WithPersistentLifetime() DotnetToolResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *dotnetToolResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) DotnetToolResource { if s.err != nil { return s } @@ -10705,6 +10941,17 @@ func (s *dotnetToolResource) WithRequiredCommand(command string, options ...*Wit return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *dotnetToolResource) WithSessionLifetime() DotnetToolResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *dotnetToolResource) WithStatus(status TestResourceStatus) DotnetToolResource { if s.err != nil { return s } @@ -11965,6 +12212,7 @@ type ExecutableResource interface { WithDockerfileBaseImage(options ...*WithDockerfileBaseImageOptions) ExecutableResource WithEndpoint(options ...*WithEndpointOptions) ExecutableResource WithEndpointCallback(endpointName string, callback func(obj EndpointUpdateContext), options ...*WithEndpointCallbackOptions) ExecutableResource + WithEndpointProxySupport(proxyEnabled bool) ExecutableResource WithEndpoints(endpoints []string) ExecutableResource WithEnvironment(name string, value any) ExecutableResource WithEnvironmentCallback(callback func(arg EnvironmentCallbackContext)) ExecutableResource @@ -11983,6 +12231,7 @@ type ExecutableResource interface { WithHttpsEndpointCallback(callback func(obj EndpointUpdateContext), options ...*WithHttpsEndpointCallbackOptions) ExecutableResource WithIconName(iconName string, options ...*WithIconNameOptions) ExecutableResource WithImagePushOptions(callback func(arg ContainerImagePushOptionsCallbackContext)) ExecutableResource + WithLifetimeOf(sourceBuilder Resource) ExecutableResource WithMcpServer(options ...*WithMcpServerOptions) ExecutableResource WithMergeEndpoint(endpointName string, port float64) ExecutableResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ExecutableResource @@ -11997,7 +12246,9 @@ type ExecutableResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) ExecutableResource WithOptionalString(options ...*WithOptionalStringOptions) ExecutableResource WithOtlpExporter(options ...*WithOtlpExporterOptions) ExecutableResource + WithParentProcessLifetime(parentProcessId float64) ExecutableResource WithParentRelationship(parent Resource) ExecutableResource + WithPersistentLifetime() ExecutableResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ExecutableResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ExecutableResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ExecutableResource @@ -12008,6 +12259,7 @@ type ExecutableResource interface { WithRemoteImageName(remoteImageName string) ExecutableResource WithRemoteImageTag(remoteImageTag string) ExecutableResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ExecutableResource + WithSessionLifetime() ExecutableResource WithStatus(status TestResourceStatus) ExecutableResource WithUnionDependency(dependency any) ExecutableResource WithUrl(url any, options ...*WithUrlOptions) ExecutableResource @@ -12574,6 +12826,18 @@ func (s *executableResource) WithEndpointCallback(endpointName string, callback return s } +// WithEndpointProxySupport configures endpoint proxy support +func (s *executableResource) WithEndpointProxySupport(proxyEnabled bool) ExecutableResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["proxyEnabled"] = serializeValue(proxyEnabled) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withEndpointProxySupport", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithEndpoints sets the endpoints func (s *executableResource) WithEndpoints(endpoints []string) ExecutableResource { if s.err != nil { return s } @@ -12884,6 +13148,19 @@ func (s *executableResource) WithImagePushOptions(callback func(arg ContainerIma return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *executableResource) WithLifetimeOf(sourceBuilder Resource) ExecutableResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *executableResource) WithMcpServer(options ...*WithMcpServerOptions) ExecutableResource { if s.err != nil { return s } @@ -13110,6 +13387,18 @@ func (s *executableResource) WithOtlpExporter(options ...*WithOtlpExporterOption return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *executableResource) WithParentProcessLifetime(parentProcessId float64) ExecutableResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *executableResource) WithParentRelationship(parent Resource) ExecutableResource { if s.err != nil { return s } @@ -13123,6 +13412,17 @@ func (s *executableResource) WithParentRelationship(parent Resource) ExecutableR return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *executableResource) WithPersistentLifetime() ExecutableResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *executableResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ExecutableResource { if s.err != nil { return s } @@ -13305,6 +13605,17 @@ func (s *executableResource) WithRequiredCommand(command string, options ...*Wit return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *executableResource) WithSessionLifetime() ExecutableResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *executableResource) WithStatus(status TestResourceStatus) ExecutableResource { if s.err != nil { return s } @@ -13740,6 +14051,7 @@ type ExternalServiceResource interface { WithHealthCheck(key string) ExternalServiceResource WithHttpHealthCheck(options ...*WithHttpHealthCheckOptions) ExternalServiceResource WithIconName(iconName string, options ...*WithIconNameOptions) ExternalServiceResource + WithLifetimeOf(sourceBuilder Resource) ExternalServiceResource WithMergeEndpoint(endpointName string, port float64) ExternalServiceResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ExternalServiceResource WithMergeLabel(label string) ExternalServiceResource @@ -13752,13 +14064,16 @@ type ExternalServiceResource interface { WithNestedConfig(config *TestNestedDto) ExternalServiceResource WithOptionalCallback(options ...*WithOptionalCallbackOptions) ExternalServiceResource WithOptionalString(options ...*WithOptionalStringOptions) ExternalServiceResource + WithParentProcessLifetime(parentProcessId float64) ExternalServiceResource WithParentRelationship(parent Resource) ExternalServiceResource + WithPersistentLifetime() ExternalServiceResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ExternalServiceResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ExternalServiceResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ExternalServiceResource WithProcessCommandFactory(commandName string, displayName string, createProcessSpec func(arg ExecuteCommandContext) *ProcessCommandSpecExportData, options ...*WithProcessCommandFactoryOptions) ExternalServiceResource WithRelationship(resourceBuilder Resource, type_ string) ExternalServiceResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ExternalServiceResource + WithSessionLifetime() ExternalServiceResource WithStatus(status TestResourceStatus) ExternalServiceResource WithUnionDependency(dependency any) ExternalServiceResource WithUrl(url any, options ...*WithUrlOptions) ExternalServiceResource @@ -14134,6 +14449,19 @@ func (s *externalServiceResource) WithIconName(iconName string, options ...*With return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *externalServiceResource) WithLifetimeOf(sourceBuilder Resource) ExternalServiceResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMergeEndpoint configures a named endpoint func (s *externalServiceResource) WithMergeEndpoint(endpointName string, port float64) ExternalServiceResource { if s.err != nil { return s } @@ -14324,6 +14652,18 @@ func (s *externalServiceResource) WithOptionalString(options ...*WithOptionalStr return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *externalServiceResource) WithParentProcessLifetime(parentProcessId float64) ExternalServiceResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *externalServiceResource) WithParentRelationship(parent Resource) ExternalServiceResource { if s.err != nil { return s } @@ -14337,6 +14677,17 @@ func (s *externalServiceResource) WithParentRelationship(parent Resource) Extern return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *externalServiceResource) WithPersistentLifetime() ExternalServiceResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *externalServiceResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ExternalServiceResource { if s.err != nil { return s } @@ -14457,6 +14808,17 @@ func (s *externalServiceResource) WithRequiredCommand(command string, options .. return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *externalServiceResource) WithSessionLifetime() ExternalServiceResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *externalServiceResource) WithStatus(status TestResourceStatus) ExternalServiceResource { if s.err != nil { return s } @@ -15199,6 +15561,7 @@ type ParameterResource interface { WithExplicitStart() ParameterResource WithHealthCheck(key string) ParameterResource WithIconName(iconName string, options ...*WithIconNameOptions) ParameterResource + WithLifetimeOf(sourceBuilder Resource) ParameterResource WithMergeEndpoint(endpointName string, port float64) ParameterResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ParameterResource WithMergeLabel(label string) ParameterResource @@ -15211,13 +15574,16 @@ type ParameterResource interface { WithNestedConfig(config *TestNestedDto) ParameterResource WithOptionalCallback(options ...*WithOptionalCallbackOptions) ParameterResource WithOptionalString(options ...*WithOptionalStringOptions) ParameterResource + WithParentProcessLifetime(parentProcessId float64) ParameterResource WithParentRelationship(parent Resource) ParameterResource + WithPersistentLifetime() ParameterResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ParameterResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ParameterResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ParameterResource WithProcessCommandFactory(commandName string, displayName string, createProcessSpec func(arg ExecuteCommandContext) *ProcessCommandSpecExportData, options ...*WithProcessCommandFactoryOptions) ParameterResource WithRelationship(resourceBuilder Resource, type_ string) ParameterResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ParameterResource + WithSessionLifetime() ParameterResource WithStatus(status TestResourceStatus) ParameterResource WithUnionDependency(dependency any) ParameterResource WithUrl(url any, options ...*WithUrlOptions) ParameterResource @@ -15594,6 +15960,19 @@ func (s *parameterResource) WithIconName(iconName string, options ...*WithIconNa return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *parameterResource) WithLifetimeOf(sourceBuilder Resource) ParameterResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMergeEndpoint configures a named endpoint func (s *parameterResource) WithMergeEndpoint(endpointName string, port float64) ParameterResource { if s.err != nil { return s } @@ -15784,6 +16163,18 @@ func (s *parameterResource) WithOptionalString(options ...*WithOptionalStringOpt return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *parameterResource) WithParentProcessLifetime(parentProcessId float64) ParameterResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *parameterResource) WithParentRelationship(parent Resource) ParameterResource { if s.err != nil { return s } @@ -15797,6 +16188,17 @@ func (s *parameterResource) WithParentRelationship(parent Resource) ParameterRes return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *parameterResource) WithPersistentLifetime() ParameterResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *parameterResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ParameterResource { if s.err != nil { return s } @@ -15917,6 +16319,17 @@ func (s *parameterResource) WithRequiredCommand(command string, options ...*With return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *parameterResource) WithSessionLifetime() ParameterResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *parameterResource) WithStatus(status TestResourceStatus) ParameterResource { if s.err != nil { return s } @@ -16708,6 +17121,7 @@ type ProjectResource interface { WithDockerfileBaseImage(options ...*WithDockerfileBaseImageOptions) ProjectResource WithEndpoint(options ...*WithEndpointOptions) ProjectResource WithEndpointCallback(endpointName string, callback func(obj EndpointUpdateContext), options ...*WithEndpointCallbackOptions) ProjectResource + WithEndpointProxySupport(proxyEnabled bool) ProjectResource WithEndpoints(endpoints []string) ProjectResource WithEnvironment(name string, value any) ProjectResource WithEnvironmentCallback(callback func(arg EnvironmentCallbackContext)) ProjectResource @@ -16725,6 +17139,7 @@ type ProjectResource interface { WithHttpsEndpointCallback(callback func(obj EndpointUpdateContext), options ...*WithHttpsEndpointCallbackOptions) ProjectResource WithIconName(iconName string, options ...*WithIconNameOptions) ProjectResource WithImagePushOptions(callback func(arg ContainerImagePushOptionsCallbackContext)) ProjectResource + WithLifetimeOf(sourceBuilder Resource) ProjectResource WithMcpServer(options ...*WithMcpServerOptions) ProjectResource WithMergeEndpoint(endpointName string, port float64) ProjectResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) ProjectResource @@ -16739,7 +17154,9 @@ type ProjectResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) ProjectResource WithOptionalString(options ...*WithOptionalStringOptions) ProjectResource WithOtlpExporter(options ...*WithOtlpExporterOptions) ProjectResource + WithParentProcessLifetime(parentProcessId float64) ProjectResource WithParentRelationship(parent Resource) ProjectResource + WithPersistentLifetime() ProjectResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ProjectResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) ProjectResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) ProjectResource @@ -16751,6 +17168,7 @@ type ProjectResource interface { WithRemoteImageTag(remoteImageTag string) ProjectResource WithReplicas(replicas float64) ProjectResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) ProjectResource + WithSessionLifetime() ProjectResource WithStatus(status TestResourceStatus) ProjectResource WithUnionDependency(dependency any) ProjectResource WithUrl(url any, options ...*WithUrlOptions) ProjectResource @@ -17348,6 +17766,18 @@ func (s *projectResource) WithEndpointCallback(endpointName string, callback fun return s } +// WithEndpointProxySupport configures endpoint proxy support +func (s *projectResource) WithEndpointProxySupport(proxyEnabled bool) ProjectResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["proxyEnabled"] = serializeValue(proxyEnabled) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withEndpointProxySupport", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithEndpoints sets the endpoints func (s *projectResource) WithEndpoints(endpoints []string) ProjectResource { if s.err != nil { return s } @@ -17646,6 +18076,19 @@ func (s *projectResource) WithImagePushOptions(callback func(arg ContainerImageP return s } +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *projectResource) WithLifetimeOf(sourceBuilder Resource) ProjectResource { + if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithMcpServer configures an MCP server endpoint on the resource func (s *projectResource) WithMcpServer(options ...*WithMcpServerOptions) ProjectResource { if s.err != nil { return s } @@ -17872,6 +18315,18 @@ func (s *projectResource) WithOtlpExporter(options ...*WithOtlpExporterOptions) return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *projectResource) WithParentProcessLifetime(parentProcessId float64) ProjectResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *projectResource) WithParentRelationship(parent Resource) ProjectResource { if s.err != nil { return s } @@ -17885,6 +18340,17 @@ func (s *projectResource) WithParentRelationship(parent Resource) ProjectResourc return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *projectResource) WithPersistentLifetime() ProjectResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *projectResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) ProjectResource { if s.err != nil { return s } @@ -18079,6 +18545,17 @@ func (s *projectResource) WithRequiredCommand(command string, options ...*WithRe return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *projectResource) WithSessionLifetime() ProjectResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *projectResource) WithStatus(status TestResourceStatus) ProjectResource { if s.err != nil { return s } @@ -19649,7 +20126,7 @@ type TestDatabaseResource interface { WithImageRegistry(registry string) TestDatabaseResource WithImageSHA256(sha256 string) TestDatabaseResource WithImageTag(tag string) TestDatabaseResource - WithLifetime(lifetime ContainerLifetime) TestDatabaseResource + WithLifetimeOf(sourceBuilder Resource) TestDatabaseResource WithMcpServer(options ...*WithMcpServerOptions) TestDatabaseResource WithMergeEndpoint(endpointName string, port float64) TestDatabaseResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) TestDatabaseResource @@ -19664,7 +20141,9 @@ type TestDatabaseResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) TestDatabaseResource WithOptionalString(options ...*WithOptionalStringOptions) TestDatabaseResource WithOtlpExporter(options ...*WithOtlpExporterOptions) TestDatabaseResource + WithParentProcessLifetime(parentProcessId float64) TestDatabaseResource WithParentRelationship(parent Resource) TestDatabaseResource + WithPersistentLifetime() TestDatabaseResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) TestDatabaseResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) TestDatabaseResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) TestDatabaseResource @@ -19675,6 +20154,7 @@ type TestDatabaseResource interface { WithRemoteImageName(remoteImageName string) TestDatabaseResource WithRemoteImageTag(remoteImageTag string) TestDatabaseResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) TestDatabaseResource + WithSessionLifetime() TestDatabaseResource WithStatus(status TestResourceStatus) TestDatabaseResource WithUnionDependency(dependency any) TestDatabaseResource WithUrl(url any, options ...*WithUrlOptions) TestDatabaseResource @@ -20787,15 +21267,16 @@ func (s *testDatabaseResource) WithImageTag(tag string) TestDatabaseResource { return s } -// WithLifetime sets the lifetime behavior of the container resource -func (s *testDatabaseResource) WithLifetime(lifetime ContainerLifetime) TestDatabaseResource { +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *testDatabaseResource) WithLifetimeOf(sourceBuilder Resource) TestDatabaseResource { if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } ctx := context.Background() reqArgs := map[string]any{ "builder": s.handle.ToJSON(), } - reqArgs["lifetime"] = serializeValue(lifetime) - if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetime", reqArgs); err != nil { s.setErr(err) } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } return s } @@ -21025,6 +21506,18 @@ func (s *testDatabaseResource) WithOtlpExporter(options ...*WithOtlpExporterOpti return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *testDatabaseResource) WithParentProcessLifetime(parentProcessId float64) TestDatabaseResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *testDatabaseResource) WithParentRelationship(parent Resource) TestDatabaseResource { if s.err != nil { return s } @@ -21038,6 +21531,17 @@ func (s *testDatabaseResource) WithParentRelationship(parent Resource) TestDatab return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *testDatabaseResource) WithPersistentLifetime() TestDatabaseResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *testDatabaseResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) TestDatabaseResource { if s.err != nil { return s } @@ -21220,6 +21724,17 @@ func (s *testDatabaseResource) WithRequiredCommand(command string, options ...*W return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *testDatabaseResource) WithSessionLifetime() TestDatabaseResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *testDatabaseResource) WithStatus(status TestResourceStatus) TestDatabaseResource { if s.err != nil { return s } @@ -21612,7 +22127,7 @@ type TestRedisResource interface { WithImageRegistry(registry string) TestRedisResource WithImageSHA256(sha256 string) TestRedisResource WithImageTag(tag string) TestRedisResource - WithLifetime(lifetime ContainerLifetime) TestRedisResource + WithLifetimeOf(sourceBuilder Resource) TestRedisResource WithMcpServer(options ...*WithMcpServerOptions) TestRedisResource WithMergeEndpoint(endpointName string, port float64) TestRedisResource WithMergeEndpointScheme(endpointName string, port float64, scheme string) TestRedisResource @@ -21628,8 +22143,10 @@ type TestRedisResource interface { WithOptionalCallback(options ...*WithOptionalCallbackOptions) TestRedisResource WithOptionalString(options ...*WithOptionalStringOptions) TestRedisResource WithOtlpExporter(options ...*WithOtlpExporterOptions) TestRedisResource + WithParentProcessLifetime(parentProcessId float64) TestRedisResource WithParentRelationship(parent Resource) TestRedisResource WithPersistence(options ...*WithPersistenceOptions) TestRedisResource + WithPersistentLifetime() TestRedisResource WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) TestRedisResource WithPipelineStepFactory(stepName string, callback func(arg PipelineStepContext), options ...*WithPipelineStepFactoryOptions) TestRedisResource WithProcessCommand(commandName string, displayName string, options *ProcessCommandExportOptions) TestRedisResource @@ -21641,6 +22158,7 @@ type TestRedisResource interface { WithRemoteImageName(remoteImageName string) TestRedisResource WithRemoteImageTag(remoteImageTag string) TestRedisResource WithRequiredCommand(command string, options ...*WithRequiredCommandOptions) TestRedisResource + WithSessionLifetime() TestRedisResource WithStatus(status TestResourceStatus) TestRedisResource WithUnionDependency(dependency any) TestRedisResource WithUrl(url any, options ...*WithUrlOptions) TestRedisResource @@ -22972,15 +23490,16 @@ func (s *testRedisResource) WithImageTag(tag string) TestRedisResource { return s } -// WithLifetime sets the lifetime behavior of the container resource -func (s *testRedisResource) WithLifetime(lifetime ContainerLifetime) TestRedisResource { +// WithLifetimeOf sets resource lifetime behavior to match another resource +func (s *testRedisResource) WithLifetimeOf(sourceBuilder Resource) TestRedisResource { if s.err != nil { return s } + if sourceBuilder != nil { if err := sourceBuilder.Err(); err != nil { s.setErr(err); return s } } ctx := context.Background() reqArgs := map[string]any{ "builder": s.handle.ToJSON(), } - reqArgs["lifetime"] = serializeValue(lifetime) - if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetime", reqArgs); err != nil { s.setErr(err) } + reqArgs["sourceBuilder"] = serializeValue(sourceBuilder) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetimeOf", reqArgs); err != nil { s.setErr(err) } return s } @@ -23229,6 +23748,18 @@ func (s *testRedisResource) WithOtlpExporter(options ...*WithOtlpExporterOptions return s } +// WithParentProcessLifetime sets persistent lifetime behavior tied to a parent process +func (s *testRedisResource) WithParentProcessLifetime(parentProcessId float64) TestRedisResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["parentProcessId"] = serializeValue(parentProcessId) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withParentProcessLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithParentRelationship sets the parent relationship func (s *testRedisResource) WithParentRelationship(parent Resource) TestRedisResource { if s.err != nil { return s } @@ -23260,6 +23791,17 @@ func (s *testRedisResource) WithPersistence(options ...*WithPersistenceOptions) return s } +// WithPersistentLifetime sets persistent lifetime behavior for the resource +func (s *testRedisResource) WithPersistentLifetime() TestRedisResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withPersistentLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithPipelineConfiguration configures pipeline step dependencies via a callback func (s *testRedisResource) WithPipelineConfiguration(callback func(obj PipelineConfigurationContext)) TestRedisResource { if s.err != nil { return s } @@ -23454,6 +23996,17 @@ func (s *testRedisResource) WithRequiredCommand(command string, options ...*With return s } +// WithSessionLifetime sets session lifetime behavior for the resource +func (s *testRedisResource) WithSessionLifetime() TestRedisResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withSessionLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithStatus sets the resource status func (s *testRedisResource) WithStatus(status TestResourceStatus) TestRedisResource { if s.err != nil { return s } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index fdf8f623a48..59190b6f0f9 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1702,6 +1702,44 @@ public CSharpAppResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public CSharpAppResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public CSharpAppResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public CSharpAppResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public CSharpAppResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public CSharpAppResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public CSharpAppResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -1991,6 +2029,15 @@ private CSharpAppResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public CSharpAppResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public CSharpAppResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -3681,35 +3728,6 @@ public String valueExpression() { } -// ===== ContainerLifetime.java ===== -// ContainerLifetime.java - GENERATED CODE - DO NOT EDIT - -package aspire; - -import java.util.*; -import java.util.function.*; - -/** ContainerLifetime enum. */ -public enum ContainerLifetime implements WireValueEnum { - SESSION("Session"), - PERSISTENT("Persistent"); - - private final String value; - - ContainerLifetime(String value) { - this.value = value; - } - - public String getValue() { return value; } - - public static ContainerLifetime fromValue(String value) { - for (ContainerLifetime e : values()) { - if (e.value.equals(value)) return e; - } - throw new IllegalArgumentException("Unknown value: " + value); - } -} - // ===== ContainerMountAnnotation.java ===== // ContainerMountAnnotation.java - GENERATED CODE - DO NOT EDIT @@ -3881,6 +3899,44 @@ public ContainerRegistryResource withRequiredCommand(String command, String help return this; } + /** Sets session lifetime behavior for the resource */ + public ContainerRegistryResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ContainerRegistryResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ContainerRegistryResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ContainerRegistryResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ContainerRegistryResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + /** Customizes displayed URLs via callback */ public ContainerRegistryResource withUrls(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -4633,15 +4689,6 @@ public ContainerResource withContainerRuntimeArgs(String[] args) { return this; } - /** Sets the lifetime behavior of the container resource */ - public ContainerResource withLifetime(ContainerLifetime lifetime) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("lifetime", AspireClient.serializeValue(lifetime)); - getClient().invokeCapability("Aspire.Hosting/withLifetime", reqArgs); - return this; - } - /** Sets the container image pull policy */ public ContainerResource withImagePullPolicy(ImagePullPolicy pullPolicy) { Map reqArgs = new HashMap<>(); @@ -4751,15 +4798,6 @@ private ContainerResource withContainerCertificatePathsImpl(String customCertifi return this; } - /** Configures endpoint proxy support */ - public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public ContainerResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { return withDockerfileBuilder(contextPath, callback, null); } @@ -4882,6 +4920,44 @@ public ContainerResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public ContainerResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ContainerResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ContainerResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ContainerResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ContainerResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public ContainerResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -5171,6 +5247,15 @@ private ContainerResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public ContainerResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -6992,6 +7077,44 @@ public DotnetToolResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public DotnetToolResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public DotnetToolResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public DotnetToolResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public DotnetToolResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public DotnetToolResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public DotnetToolResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -7281,6 +7404,15 @@ private DotnetToolResource withEndpointImpl(Double port, Double targetPort, Stri return this; } + /** Configures endpoint proxy support */ + public DotnetToolResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public DotnetToolResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -9049,6 +9181,44 @@ public ExecutableResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public ExecutableResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ExecutableResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ExecutableResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ExecutableResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ExecutableResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public ExecutableResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -9338,6 +9508,15 @@ private ExecutableResource withEndpointImpl(Double port, Double targetPort, Stri return this; } + /** Configures endpoint proxy support */ + public ExecutableResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public ExecutableResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -10562,6 +10741,44 @@ public ExternalServiceResource withRequiredCommand(String command, String helpLi return this; } + /** Sets session lifetime behavior for the resource */ + public ExternalServiceResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ExternalServiceResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ExternalServiceResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ExternalServiceResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ExternalServiceResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + /** Customizes displayed URLs via callback */ public ExternalServiceResource withUrls(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -13554,6 +13771,44 @@ public ParameterResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public ParameterResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ParameterResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ParameterResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ParameterResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ParameterResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + /** Customizes displayed URLs via callback */ public ParameterResource withUrls(AspireAction1 callback) { Map reqArgs = new HashMap<>(); @@ -14845,6 +15100,44 @@ public ProjectResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public ProjectResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public ProjectResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public ProjectResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public ProjectResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public ProjectResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public ProjectResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -15134,6 +15427,15 @@ private ProjectResource withEndpointImpl(Double port, Double targetPort, String return this; } + /** Configures endpoint proxy support */ + public ProjectResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public ProjectResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -17319,15 +17621,6 @@ public TestDatabaseResource withContainerRuntimeArgs(String[] args) { return this; } - /** Sets the lifetime behavior of the container resource */ - public TestDatabaseResource withLifetime(ContainerLifetime lifetime) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("lifetime", AspireClient.serializeValue(lifetime)); - getClient().invokeCapability("Aspire.Hosting/withLifetime", reqArgs); - return this; - } - /** Sets the container image pull policy */ public TestDatabaseResource withImagePullPolicy(ImagePullPolicy pullPolicy) { Map reqArgs = new HashMap<>(); @@ -17437,15 +17730,6 @@ private TestDatabaseResource withContainerCertificatePathsImpl(String customCert return this; } - /** Configures endpoint proxy support */ - public TestDatabaseResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public TestDatabaseResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { return withDockerfileBuilder(contextPath, callback, null); } @@ -17568,6 +17852,44 @@ public TestDatabaseResource withRequiredCommand(String command, String helpLink) return this; } + /** Sets session lifetime behavior for the resource */ + public TestDatabaseResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public TestDatabaseResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public TestDatabaseResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public TestDatabaseResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public TestDatabaseResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public TestDatabaseResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -17857,6 +18179,15 @@ private TestDatabaseResource withEndpointImpl(Double port, Double targetPort, St return this; } + /** Configures endpoint proxy support */ + public TestDatabaseResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public TestDatabaseResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -19227,15 +19558,6 @@ public TestRedisResource withContainerRuntimeArgs(String[] args) { return this; } - /** Sets the lifetime behavior of the container resource */ - public TestRedisResource withLifetime(ContainerLifetime lifetime) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("lifetime", AspireClient.serializeValue(lifetime)); - getClient().invokeCapability("Aspire.Hosting/withLifetime", reqArgs); - return this; - } - /** Sets the container image pull policy */ public TestRedisResource withImagePullPolicy(ImagePullPolicy pullPolicy) { Map reqArgs = new HashMap<>(); @@ -19345,15 +19667,6 @@ private TestRedisResource withContainerCertificatePathsImpl(String customCertifi return this; } - /** Configures endpoint proxy support */ - public TestRedisResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public TestRedisResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { return withDockerfileBuilder(contextPath, callback, null); } @@ -19476,6 +19789,44 @@ public TestRedisResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public TestRedisResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public TestRedisResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public TestRedisResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public TestRedisResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public TestRedisResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public TestRedisResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -19791,6 +20142,15 @@ private TestRedisResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public TestRedisResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public TestRedisResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -21228,15 +21588,6 @@ public TestVaultResource withContainerRuntimeArgs(String[] args) { return this; } - /** Sets the lifetime behavior of the container resource */ - public TestVaultResource withLifetime(ContainerLifetime lifetime) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("lifetime", AspireClient.serializeValue(lifetime)); - getClient().invokeCapability("Aspire.Hosting/withLifetime", reqArgs); - return this; - } - /** Sets the container image pull policy */ public TestVaultResource withImagePullPolicy(ImagePullPolicy pullPolicy) { Map reqArgs = new HashMap<>(); @@ -21346,15 +21697,6 @@ private TestVaultResource withContainerCertificatePathsImpl(String customCertifi return this; } - /** Configures endpoint proxy support */ - public TestVaultResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public TestVaultResource withDockerfileBuilder(String contextPath, AspireAction1 callback) { return withDockerfileBuilder(contextPath, callback, null); } @@ -21477,6 +21819,44 @@ public TestVaultResource withRequiredCommand(String command, String helpLink) { return this; } + /** Sets session lifetime behavior for the resource */ + public TestVaultResource withSessionLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withSessionLifetime", reqArgs); + return this; + } + + /** Sets persistent lifetime behavior for the resource */ + public TestVaultResource withPersistentLifetime() { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + getClient().invokeCapability("Aspire.Hosting/withPersistentLifetime", reqArgs); + return this; + } + + /** Sets resource lifetime behavior to match another resource */ + public TestVaultResource withLifetimeOf(IResource sourceBuilder) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("sourceBuilder", AspireClient.serializeValue(sourceBuilder)); + getClient().invokeCapability("Aspire.Hosting/withLifetimeOf", reqArgs); + return this; + } + + public TestVaultResource withLifetimeOf(ResourceBuilderBase sourceBuilder) { + return withLifetimeOf(new IResource(sourceBuilder.getHandle(), sourceBuilder.getClient())); + } + + /** Sets persistent lifetime behavior tied to a parent process */ + public TestVaultResource withParentProcessLifetime(double parentProcessId) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("parentProcessId", AspireClient.serializeValue(parentProcessId)); + getClient().invokeCapability("Aspire.Hosting/withParentProcessLifetime", reqArgs); + return this; + } + public TestVaultResource withEnvironment(String name, String value) { return withEnvironment(name, AspireUnion.of(value)); } @@ -21766,6 +22146,15 @@ private TestVaultResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public TestVaultResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public TestVaultResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -23705,7 +24094,6 @@ public WithVolumeOptions isReadOnly(Boolean value) { .modules/ContainerImagePushOptions.java .modules/ContainerImagePushOptionsCallbackContext.java .modules/ContainerImageReference.java -.modules/ContainerLifetime.java .modules/ContainerMountAnnotation.java .modules/ContainerMountType.java .modules/ContainerPortReference.java diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 1d5269cc904..d4a86dd3c0a 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1499,8 +1499,6 @@ def _validate_dict_types(args: typing.Any, arg_types: typing.Any) -> bool: CommandResultFormat = typing.Literal["Text", "Json", "Markdown"] -ContainerLifetime = typing.Literal["Session", "Persistent"] - ContainerMountType = typing.Literal["BindMount", "Volume"] DistributedApplicationOperation = typing.Literal["Run", "Publish"] @@ -4449,16 +4447,16 @@ def is_external(self, value: bool) -> None: ) @_uncached_property - def is_proxied(self) -> bool: + def is_proxied(self) -> bool | None: """Gets the IsProxied property""" result = self._client.invoke_capability( 'Aspire.Hosting.ApplicationModel/EndpointUpdateContext.isProxied', {'context': self._handle} ) - return typing.cast(bool, result) + return typing.cast(bool | None, result) @is_proxied.setter - def is_proxied(self, value: bool) -> None: + def is_proxied(self, value: bool | None) -> None: """Sets the IsProxied property""" self._client.invoke_capability( 'Aspire.Hosting.ApplicationModel/EndpointUpdateContext.setIsProxied', @@ -6046,6 +6044,22 @@ def with_dockerfile_base_image(self, *, build_image: str | None = None, runtime_ def with_required_command(self, command: str, *, help_link: str | None = None) -> typing.Self: """Adds a required command dependency""" + @abc.abstractmethod + def with_session_lifetime(self) -> typing.Self: + """Sets session lifetime behavior for the resource""" + + @abc.abstractmethod + def with_persistent_lifetime(self) -> typing.Self: + """Sets persistent lifetime behavior for the resource""" + + @abc.abstractmethod + def with_lifetime_of(self, source_builder: AbstractResource) -> typing.Self: + """Sets resource lifetime behavior to match another resource""" + + @abc.abstractmethod + def with_parent_process_lifetime(self, parent_process_id: int) -> typing.Self: + """Sets persistent lifetime behavior tied to a parent process""" + @abc.abstractmethod def with_urls(self, callback: typing.Callable[[ResourceUrlsCallbackContext], None]) -> typing.Self: """Customizes displayed URLs via callback""" @@ -6307,15 +6321,19 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate """Updates an HTTPS endpoint via callback""" @abc.abstractmethod - def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: + def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" @abc.abstractmethod - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + + @abc.abstractmethod + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" @abc.abstractmethod - def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTPS endpoint""" @abc.abstractmethod @@ -6437,6 +6455,10 @@ class _BaseResourceKwargs(typing.TypedDict, total=False): container_registry: AbstractResource dockerfile_base_image: DockerfileBaseImageParameters | typing.Literal[True] required_command: str | tuple[str, str] + session_lifetime: typing.Literal[True] + persistent_lifetime: typing.Literal[True] + lifetime_of: AbstractResource + parent_process_lifetime: int urls: typing.Callable[[ResourceUrlsCallbackContext], None] url: str | ReferenceExpression | tuple[str | ReferenceExpression, str] url_for_endpoint: tuple[str, typing.Callable[[ResourceUrlAnnotation], None]] @@ -6527,6 +6549,48 @@ def with_required_command(self, command: str, *, help_link: str | None = None) - self._handle = self._wrap_builder(result) return self + def with_session_lifetime(self) -> typing.Self: + """Sets session lifetime behavior for the resource""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting/withSessionLifetime', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_persistent_lifetime(self) -> typing.Self: + """Sets persistent lifetime behavior for the resource""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + result = self._client.invoke_capability( + 'Aspire.Hosting/withPersistentLifetime', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_lifetime_of(self, source_builder: AbstractResource) -> typing.Self: + """Sets resource lifetime behavior to match another resource""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['sourceBuilder'] = source_builder + result = self._client.invoke_capability( + 'Aspire.Hosting/withLifetimeOf', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_parent_process_lifetime(self, parent_process_id: int) -> typing.Self: + """Sets persistent lifetime behavior tied to a parent process""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['parentProcessId'] = parent_process_id + result = self._client.invoke_capability( + 'Aspire.Hosting/withParentProcessLifetime', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def with_urls(self, callback: typing.Callable[[ResourceUrlsCallbackContext], None]) -> typing.Self: """Customizes displayed URLs via callback""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -7040,6 +7104,32 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withRequiredCommand', rpc_args)) else: raise TypeError("Invalid type for option 'required_command'. Expected: str or (str, str)") + if _session_lifetime := kwargs.pop("session_lifetime", None): + if _session_lifetime is True: + rpc_args: dict[str, typing.Any] = {"builder": handle} + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withSessionLifetime', rpc_args)) + else: + raise TypeError("Invalid type for option 'session_lifetime'. Expected: Literal[True]") + if _persistent_lifetime := kwargs.pop("persistent_lifetime", None): + if _persistent_lifetime is True: + rpc_args: dict[str, typing.Any] = {"builder": handle} + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withPersistentLifetime', rpc_args)) + else: + raise TypeError("Invalid type for option 'persistent_lifetime'. Expected: Literal[True]") + if _lifetime_of := kwargs.pop("lifetime_of", None): + if _validate_type(_lifetime_of, AbstractResource): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["sourceBuilder"] = typing.cast(AbstractResource, _lifetime_of) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withLifetimeOf', rpc_args)) + else: + raise TypeError("Invalid type for option 'lifetime_of'. Expected: AbstractResource") + if _parent_process_lifetime := kwargs.pop("parent_process_lifetime", None): + if _validate_type(_parent_process_lifetime, int): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["parentProcessId"] = typing.cast(int, _parent_process_lifetime) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withParentProcessLifetime', rpc_args)) + else: + raise TypeError("Invalid type for option 'parent_process_lifetime'. Expected: int") if _urls := kwargs.pop("urls", None): if _validate_type(_urls, typing.Callable[[ResourceUrlsCallbackContext], None]): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -7408,7 +7498,6 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): image: str | tuple[str, str] image_sha256: str container_runtime_args: typing.Iterable[str] - lifetime: ContainerLifetime image_pull_policy: ImagePullPolicy publish_as_container: typing.Literal[True] dockerfile: str | DockerfileParameters @@ -7416,7 +7505,6 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): build_arg: tuple[str, str | ParameterResource] build_secret: tuple[str, ParameterResource] container_certificate_paths: ContainerCertificatePathsParameters | typing.Literal[True] - endpoint_proxy_support: bool dockerfile_builder: tuple[str, typing.Callable[[DockerfileBuilderCallbackContext], None]] | DockerfileBuilderParameters container_network_alias: str mcp_server: McpServerParameters | typing.Literal[True] @@ -7432,6 +7520,7 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] + endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -7543,17 +7632,6 @@ def with_container_runtime_args(self, args: typing.Iterable[str]) -> typing.Self self._handle = self._wrap_builder(result) return self - def with_lifetime(self, lifetime: ContainerLifetime) -> typing.Self: - """Sets the lifetime behavior of the container resource""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['lifetime'] = lifetime - result = self._client.invoke_capability( - 'Aspire.Hosting/withLifetime', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_image_pull_policy(self, pull_policy: ImagePullPolicy) -> typing.Self: """Sets the container image pull policy""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -7641,17 +7719,6 @@ def with_container_certificate_paths(self, *, custom_certificates_destination: s self._handle = self._wrap_builder(result) return self - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['proxyEnabled'] = proxy_enabled - result = self._client.invoke_capability( - 'Aspire.Hosting/withEndpointProxySupport', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_dockerfile_builder(self, context_path: str, callback: typing.Callable[[DockerfileBuilderCallbackContext], None], *, stage: str | None = None) -> typing.Self: """Configures the resource to use a programmatically generated Dockerfile""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -7830,7 +7897,7 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate self._handle = self._wrap_builder(result) return self - def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: + def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -7856,7 +7923,18 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -7876,7 +7954,7 @@ def with_http_endpoint(self, *, port: int | None = None, target_port: int | None self._handle = self._wrap_builder(result) return self - def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTPS endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -8218,13 +8296,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withContainerRuntimeArgs', rpc_args)) else: raise TypeError("Invalid type for option 'container_runtime_args'. Expected: Iterable[str]") - if _lifetime := kwargs.pop("lifetime", None): - if _validate_type(_lifetime, ContainerLifetime): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["lifetime"] = typing.cast(ContainerLifetime, _lifetime) - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withLifetime', rpc_args)) - else: - raise TypeError("Invalid type for option 'lifetime'. Expected: ContainerLifetime") if _image_pull_policy := kwargs.pop("image_pull_policy", None): if _validate_type(_image_pull_policy, ImagePullPolicy): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8286,13 +8357,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withContainerCertificatePaths', rpc_args)) else: raise TypeError("Invalid type for option 'container_certificate_paths'. Expected: ContainerCertificatePathsParameters or Literal[True]") - if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): - if _validate_type(_endpoint_proxy_support, bool): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) - else: - raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _dockerfile_builder := kwargs.pop("dockerfile_builder", None): if _validate_tuple_types(_dockerfile_builder, (str, typing.Callable[[DockerfileBuilderCallbackContext], None])): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8448,6 +8512,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8681,6 +8752,7 @@ class ProjectResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] + endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -8886,7 +8958,7 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate self._handle = self._wrap_builder(result) return self - def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: + def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -8912,7 +8984,18 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -8932,7 +9015,7 @@ def with_http_endpoint(self, *, port: int | None = None, target_port: int | None self._handle = self._wrap_builder(result) return self - def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTPS endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -9361,6 +9444,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -9603,6 +9693,7 @@ class ExecutableResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] + endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -9807,7 +9898,7 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate self._handle = self._wrap_builder(result) return self - def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: + def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -9833,7 +9924,18 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -9853,7 +9955,7 @@ def with_http_endpoint(self, *, port: int | None = None, target_port: int | None self._handle = self._wrap_builder(result) return self - def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool = True) -> typing.Self: + def with_https_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTPS endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} if port is not None: @@ -10268,6 +10370,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 12302d00cba..f671925e34d 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -39,25 +39,6 @@ impl std::fmt::Display for ContainerMountType { } } -/// ContainerLifetime -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum ContainerLifetime { - #[default] - #[serde(rename = "Session")] - Session, - #[serde(rename = "Persistent")] - Persistent, -} - -impl std::fmt::Display for ContainerLifetime { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Session => write!(f, "Session"), - Self::Persistent => write!(f, "Persistent"), - } - } -} - /// ImagePullPolicy #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ImagePullPolicy { @@ -1664,6 +1645,44 @@ impl CSharpAppResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -1818,6 +1837,16 @@ impl CSharpAppResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3094,6 +3123,44 @@ impl ContainerRegistryResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Customizes displayed URLs via callback pub fn with_urls(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3716,16 +3783,6 @@ impl ContainerResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Sets the lifetime behavior of the container resource - pub fn with_lifetime(&self, lifetime: ContainerLifetime) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("lifetime".to_string(), serde_json::to_value(&lifetime).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withLifetime", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Sets the container image pull policy pub fn with_image_pull_policy(&self, pull_policy: ImagePullPolicy) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3811,16 +3868,6 @@ impl ContainerResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Configures the resource to use a programmatically generated Dockerfile pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3910,6 +3957,44 @@ impl ContainerResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -4064,6 +4149,16 @@ impl ContainerResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -5608,6 +5703,44 @@ impl DotnetToolResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -5762,6 +5895,16 @@ impl DotnetToolResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7315,6 +7458,44 @@ impl ExecutableResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7469,6 +7650,16 @@ impl ExecutableResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -8422,6 +8613,44 @@ impl ExternalServiceResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Customizes displayed URLs via callback pub fn with_urls(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -10991,6 +11220,44 @@ impl ParameterResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Customizes displayed URLs via callback pub fn with_urls(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12070,6 +12337,44 @@ impl ProjectResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12224,6 +12529,16 @@ impl ProjectResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -13822,16 +14137,6 @@ impl TestDatabaseResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Sets the lifetime behavior of the container resource - pub fn with_lifetime(&self, lifetime: ContainerLifetime) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("lifetime".to_string(), serde_json::to_value(&lifetime).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withLifetime", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Sets the container image pull policy pub fn with_image_pull_policy(&self, pull_policy: ImagePullPolicy) -> Result> { let mut args: HashMap = HashMap::new(); @@ -13917,16 +14222,6 @@ impl TestDatabaseResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Configures the resource to use a programmatically generated Dockerfile pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14016,6 +14311,44 @@ impl TestDatabaseResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14170,6 +14503,16 @@ impl TestDatabaseResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15236,16 +15579,6 @@ impl TestRedisResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Sets the lifetime behavior of the container resource - pub fn with_lifetime(&self, lifetime: ContainerLifetime) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("lifetime".to_string(), serde_json::to_value(&lifetime).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withLifetime", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Sets the container image pull policy pub fn with_image_pull_policy(&self, pull_policy: ImagePullPolicy) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15331,16 +15664,6 @@ impl TestRedisResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Configures the resource to use a programmatically generated Dockerfile pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15430,6 +15753,44 @@ impl TestRedisResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15604,6 +15965,16 @@ impl TestRedisResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -16756,16 +17127,6 @@ impl TestVaultResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Sets the lifetime behavior of the container resource - pub fn with_lifetime(&self, lifetime: ContainerLifetime) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("lifetime".to_string(), serde_json::to_value(&lifetime).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withLifetime", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Sets the container image pull policy pub fn with_image_pull_policy(&self, pull_policy: ImagePullPolicy) -> Result> { let mut args: HashMap = HashMap::new(); @@ -16851,16 +17212,6 @@ impl TestVaultResource { Ok(ContainerResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(ContainerResource::new(handle, self.client.clone())) - } - /// Configures the resource to use a programmatically generated Dockerfile pub fn with_dockerfile_builder(&self, context_path: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, stage: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -16950,6 +17301,44 @@ impl TestVaultResource { Ok(IResource::new(handle, self.client.clone())) } + /// Sets session lifetime behavior for the resource + pub fn with_session_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withSessionLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior for the resource + pub fn with_persistent_lifetime(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withPersistentLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets resource lifetime behavior to match another resource + pub fn with_lifetime_of(&self, source_builder: &IResource) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("sourceBuilder".to_string(), source_builder.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetimeOf", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + + /// Sets persistent lifetime behavior tied to a parent process + pub fn with_parent_process_lifetime(&self, parent_process_id: f64) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("parentProcessId".to_string(), serde_json::to_value(&parent_process_id).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withParentProcessLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResource::new(handle, self.client.clone())) + } + /// Sets an environment variable pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); @@ -17104,6 +17493,16 @@ impl TestVaultResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt index ca6a6b7ff27..a3dabf5f316 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt @@ -521,8 +521,8 @@ CapabilityId: Aspire.Hosting/withEndpointProxySupport, MethodName: withEndpointProxySupport, TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints, + IsInterface: true }, ExpandedTargetTypes: [ { @@ -812,11 +812,11 @@ ] }, { - CapabilityId: Aspire.Hosting/withLifetime, - MethodName: withLifetime, + CapabilityId: Aspire.Hosting/withLifetimeOf, + MethodName: withLifetimeOf, TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource, + IsInterface: true }, ExpandedTargetTypes: [ { @@ -895,6 +895,34 @@ } ] }, + { + CapabilityId: Aspire.Hosting/withParentProcessLifetime, + MethodName: withParentProcessLifetime, + TargetType: { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource, + IsInterface: true + }, + ExpandedTargetTypes: [ + { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + } + ] + }, + { + CapabilityId: Aspire.Hosting/withPersistentLifetime, + MethodName: withPersistentLifetime, + TargetType: { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource, + IsInterface: true + }, + ExpandedTargetTypes: [ + { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + } + ] + }, { CapabilityId: Aspire.Hosting/withPipelineConfiguration, MethodName: withPipelineConfiguration, @@ -1021,6 +1049,20 @@ } ] }, + { + CapabilityId: Aspire.Hosting/withSessionLifetime, + MethodName: withSessionLifetime, + TargetType: { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource, + IsInterface: true + }, + ExpandedTargetTypes: [ + { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + } + ] + }, { CapabilityId: Aspire.Hosting/withUrl, MethodName: withUrl, diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 01079141f0a..52dae48c5c9 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -490,23 +490,6 @@ export enum CommandResultFormat { Markdown = "Markdown", } -/** Lifetime modes for container resources. */ -export enum ContainerLifetime { - /** Create the resource when the app host process starts and dispose of it when the app host process shuts down. */ - Session = "Session", - /** - * Attempt to re-use a previously created resource (based on the container name) if one exists. Do not destroy the container on app host process shutdown. - * - * In the event that a container with the given name does not exist, a new container will always be created based on the - * current `ContainerResource` configuration. - * When an existing container IS found, Aspire MAY re-use it based on the following criteria: - * - - * - - * - - */ - Persistent = "Persistent", -} - /** Represents the type of a container mount. */ export enum ContainerMountType { /** A local directory or file that is mounted into the container. */ @@ -1380,7 +1363,7 @@ export interface WithEndpointOptions { name?: string; /** An optional name of the environment variable that will be used to inject the `targetPort`. If the target port is null one will be dynamically generated and assigned to the environment variable. */ env?: string; - /** Specifies if the endpoint will be proxied by DCP. Defaults to true. */ + /** Specifies if the endpoint will be proxied by DCP. Defaults to `null`. */ isProxied?: boolean; /** Indicates that this endpoint should be exposed externally at publish time. */ isExternal?: boolean; @@ -1402,7 +1385,7 @@ export interface WithHttpEndpointOptions { name?: string; /** An optional name of the environment variable to inject. */ env?: string; - /** Specifies if the endpoint will be proxied by DCP. Defaults to true. */ + /** Specifies if the endpoint will be proxied by DCP. Defaults to `null`. */ isProxied?: boolean; } @@ -1444,7 +1427,7 @@ export interface WithHttpsEndpointOptions { name?: string; /** An optional name of the environment variable to inject. */ env?: string; - /** Specifies if the endpoint will be proxied by DCP. Defaults to true. */ + /** Specifies if the endpoint will be proxied by DCP. Defaults to `null`. */ isProxied?: boolean; } @@ -3748,8 +3731,8 @@ export interface EndpointUpdateContext { }; /** Gets or sets a value indicating whether the endpoint is proxied. */ isProxied: { - get: () => Promise; - set: (value: boolean) => Promise; + get: () => Promise; + set: (value: boolean | null) => Promise; }; /** Gets or sets a value indicating whether the endpoint is excluded from the default reference set. */ excludeReferenceEndpoint: { @@ -3887,13 +3870,13 @@ class EndpointUpdateContextImpl implements EndpointUpdateContext { }; isProxied = { - get: async (): Promise => { - return await this._client.invokeCapability( + get: async (): Promise => { + return await this._client.invokeCapability( 'Aspire.Hosting.ApplicationModel/EndpointUpdateContext.isProxied', { context: this._handle } ); }, - set: async (value: boolean): Promise => { + set: async (value: boolean | null): Promise => { await this._client.invokeCapability( 'Aspire.Hosting.ApplicationModel/EndpointUpdateContext.setIsProxied', { context: this._handle, value } @@ -10561,6 +10544,56 @@ export interface ContainerRegistryResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerRegistryResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerRegistryResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -10910,6 +10943,56 @@ export interface ContainerRegistryResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerRegistryResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerRegistryResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerRegistryResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -11313,6 +11396,109 @@ class ContainerRegistryResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ContainerRegistryResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ContainerRegistryResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ContainerRegistryResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ContainerRegistryResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withUrlsInternal(callback: (obj: ResourceUrlsCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (objData: unknown) => { @@ -12309,6 +12495,22 @@ class ContainerRegistryResourcePromiseImpl implements ContainerRegistryResourceP return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ContainerRegistryResourcePromise { + return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withUrls(callback: (obj: ResourceUrlsCallbackContext) => Promise): ContainerRegistryResourcePromise { return new ContainerRegistryResourcePromiseImpl(this._promise.then(obj => obj.withUrls(callback)), this._client); } @@ -12572,20 +12774,6 @@ export interface ContainerResource { * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): ContainerResourcePromise; - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -12663,17 +12851,6 @@ export interface ContainerResource { * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): ContainerResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -12762,6 +12939,56 @@ export interface ContainerResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ContainerResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ContainerResourcePromise; /** @@ -12814,6 +13041,17 @@ export interface ContainerResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -13430,20 +13668,6 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): ContainerResourcePromise; - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -13521,17 +13745,6 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): ContainerResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -13620,6 +13833,56 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ContainerResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ContainerResourcePromise; /** @@ -13672,6 +13935,17 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -14406,33 +14680,6 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withContainerRuntimeArgsInternal(args), this._client); } - /** @internal */ - private async _withLifetimeInternal(lifetime: ContainerLifetime): Promise { - const rpcArgs: Record = { builder: this._handle, lifetime }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withLifetime', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withLifetimeInternal(lifetime), this._client); - } - /** @internal */ private async _withImagePullPolicyInternal(pullPolicy: ImagePullPolicy): Promise { const rpcArgs: Record = { builder: this._handle, pullPolicy }; @@ -14613,30 +14860,6 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withContainerCertificatePathsInternal(customCertificatesDestination, defaultCertificateBundlePaths, defaultCertificateDirectoryPaths), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -14835,6 +15058,109 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -15076,6 +15402,30 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -16686,10 +17036,6 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withContainerRuntimeArgs(args)), this._client); } - withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withLifetime(lifetime)), this._client); - } - withImagePullPolicy(pullPolicy: ImagePullPolicy): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withImagePullPolicy(pullPolicy)), this._client); } @@ -16718,10 +17064,6 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withContainerCertificatePaths(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options)), this._client); } @@ -16750,6 +17092,22 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -16790,6 +17148,10 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -17166,6 +17528,56 @@ export interface CSharpAppResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): CSharpAppResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): CSharpAppResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): CSharpAppResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): CSharpAppResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): CSharpAppResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): CSharpAppResourcePromise; /** @@ -17218,6 +17630,17 @@ export interface CSharpAppResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): CSharpAppResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -17848,6 +18271,56 @@ export interface CSharpAppResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): CSharpAppResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): CSharpAppResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): CSharpAppResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): CSharpAppResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): CSharpAppResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): CSharpAppResourcePromise; /** @@ -17900,6 +18373,17 @@ export interface CSharpAppResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): CSharpAppResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -18662,6 +19146,109 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -18903,6 +19490,30 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -20502,6 +21113,22 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -20542,6 +21169,10 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -20926,6 +21557,56 @@ export interface DotnetToolResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): DotnetToolResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): DotnetToolResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): DotnetToolResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): DotnetToolResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): DotnetToolResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): DotnetToolResourcePromise; /** @@ -20978,6 +21659,17 @@ export interface DotnetToolResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): DotnetToolResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -21610,6 +22302,56 @@ export interface DotnetToolResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): DotnetToolResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): DotnetToolResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): DotnetToolResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): DotnetToolResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): DotnetToolResourcePromise; /** @@ -21662,6 +22404,17 @@ export interface DotnetToolResourcePromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -22743,6 +23599,30 @@ class DotnetToolResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -24346,6 +25226,22 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -24386,6 +25282,10 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -24740,6 +25640,56 @@ export interface ExecutableResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ExecutableResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExecutableResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExecutableResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExecutableResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExecutableResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ExecutableResourcePromise; /** @@ -24792,6 +25742,17 @@ export interface ExecutableResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ExecutableResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -25391,6 +26352,56 @@ export interface ExecutableResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExecutableResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExecutableResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExecutableResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExecutableResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ExecutableResourcePromise; /** @@ -25443,6 +26454,17 @@ export interface ExecutableResourcePromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -26420,6 +27545,30 @@ class ExecutableResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -27999,6 +29148,22 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -28039,6 +29204,10 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -28355,6 +29524,56 @@ export interface ExternalServiceResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ExternalServiceResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExternalServiceResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExternalServiceResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExternalServiceResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExternalServiceResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -28709,6 +29928,56 @@ export interface ExternalServiceResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExternalServiceResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExternalServiceResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExternalServiceResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExternalServiceResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -29136,6 +30405,109 @@ class ExternalServiceResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ExternalServiceResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ExternalServiceResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ExternalServiceResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ExternalServiceResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withUrlsInternal(callback: (obj: ResourceUrlsCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (objData: unknown) => { @@ -30136,6 +31508,22 @@ class ExternalServiceResourcePromiseImpl implements ExternalServiceResourcePromi return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ExternalServiceResourcePromise { + return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withUrls(callback: (obj: ResourceUrlsCallbackContext) => Promise): ExternalServiceResourcePromise { return new ExternalServiceResourcePromiseImpl(this._promise.then(obj => obj.withUrls(callback)), this._client); } @@ -30367,6 +31755,56 @@ export interface ParameterResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ParameterResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ParameterResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ParameterResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ParameterResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ParameterResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -30723,6 +32161,56 @@ export interface ParameterResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ParameterResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ParameterResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ParameterResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ParameterResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ParameterResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -31149,6 +32637,109 @@ class ParameterResourceImpl extends ResourceBuilderBase return new ParameterResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ParameterResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ParameterResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ParameterResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ParameterResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withUrlsInternal(callback: (obj: ResourceUrlsCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (objData: unknown) => { @@ -32149,6 +33740,22 @@ class ParameterResourcePromiseImpl implements ParameterResourcePromise { return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ParameterResourcePromise { + return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withUrls(callback: (obj: ResourceUrlsCallbackContext) => Promise): ParameterResourcePromise { return new ParameterResourcePromiseImpl(this._promise.then(obj => obj.withUrls(callback)), this._client); } @@ -32434,6 +34041,56 @@ export interface ProjectResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ProjectResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ProjectResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ProjectResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ProjectResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ProjectResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ProjectResourcePromise; /** @@ -32486,6 +34143,17 @@ export interface ProjectResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ProjectResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -33116,6 +34784,56 @@ export interface ProjectResourcePromise extends PromiseLike { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ProjectResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ProjectResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ProjectResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ProjectResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ProjectResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ProjectResourcePromise; /** @@ -33168,6 +34886,17 @@ export interface ProjectResourcePromise extends PromiseLike { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ProjectResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -33931,6 +35660,109 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -34172,6 +36004,30 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -35771,6 +37627,22 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -35811,6 +37683,10 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -36165,20 +38041,6 @@ export interface TestDatabaseResource { * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestDatabaseResourcePromise; - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): TestDatabaseResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -36256,17 +38118,6 @@ export interface TestDatabaseResource { * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestDatabaseResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -36355,6 +38206,56 @@ export interface TestDatabaseResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestDatabaseResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestDatabaseResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestDatabaseResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestDatabaseResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestDatabaseResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestDatabaseResourcePromise; /** @@ -36407,6 +38308,17 @@ export interface TestDatabaseResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestDatabaseResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -37023,20 +38935,6 @@ export interface TestDatabaseResourcePromise extends PromiseLike("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestDatabaseResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestDatabaseResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestDatabaseResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestDatabaseResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestDatabaseResourcePromise; /** @@ -37265,6 +39202,17 @@ export interface TestDatabaseResourcePromise extends PromiseLike { - const rpcArgs: Record = { builder: this._handle, lifetime }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withLifetime', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withLifetimeInternal(lifetime), this._client); - } - /** @internal */ private async _withImagePullPolicyInternal(pullPolicy: ImagePullPolicy): Promise { const rpcArgs: Record = { builder: this._handle, pullPolicy }; @@ -38205,30 +40126,6 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -38427,6 +40324,109 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -38668,6 +40668,30 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -40278,10 +42302,6 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withContainerRuntimeArgs(args)), this._client); } - withLifetime(lifetime: ContainerLifetime): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withLifetime(lifetime)), this._client); - } - withImagePullPolicy(pullPolicy: ImagePullPolicy): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withImagePullPolicy(pullPolicy)), this._client); } @@ -40310,10 +42330,6 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withContainerCertificatePaths(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options)), this._client); } @@ -40342,6 +42358,22 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -40382,6 +42414,10 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -40736,20 +42772,6 @@ export interface TestRedisResource { * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestRedisResourcePromise; - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -40827,17 +42849,6 @@ export interface TestRedisResource { * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestRedisResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -40926,6 +42937,56 @@ export interface TestRedisResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestRedisResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestRedisResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestRedisResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestRedisResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestRedisResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestRedisResourcePromise; /** @@ -40994,6 +43055,17 @@ export interface TestRedisResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -41658,20 +43730,6 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestRedisResourcePromise; - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -41749,17 +43807,6 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestRedisResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -41848,6 +43895,56 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestRedisResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestRedisResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestRedisResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestRedisResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestRedisResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestRedisResourcePromise; /** @@ -41916,6 +44013,17 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -42697,33 +44805,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withContainerRuntimeArgsInternal(args), this._client); } - /** @internal */ - private async _withLifetimeInternal(lifetime: ContainerLifetime): Promise { - const rpcArgs: Record = { builder: this._handle, lifetime }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withLifetime', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withLifetimeInternal(lifetime), this._client); - } - /** @internal */ private async _withImagePullPolicyInternal(pullPolicy: ImagePullPolicy): Promise { const rpcArgs: Record = { builder: this._handle, pullPolicy }; @@ -42904,30 +44985,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withContainerCertificatePathsInternal(customCertificatesDestination, defaultCertificateBundlePaths, defaultCertificateDirectoryPaths), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -43126,6 +45183,109 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -43403,6 +45563,30 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -45224,10 +47408,6 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withContainerRuntimeArgs(args)), this._client); } - withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withLifetime(lifetime)), this._client); - } - withImagePullPolicy(pullPolicy: ImagePullPolicy): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withImagePullPolicy(pullPolicy)), this._client); } @@ -45256,10 +47436,6 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withContainerCertificatePaths(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options)), this._client); } @@ -45288,6 +47464,22 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -45336,6 +47528,10 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -45742,20 +47938,6 @@ export interface TestVaultResource { * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestVaultResourcePromise; - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -45833,17 +48015,6 @@ export interface TestVaultResource { * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestVaultResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -45932,6 +48103,56 @@ export interface TestVaultResource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestVaultResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestVaultResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestVaultResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestVaultResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestVaultResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestVaultResourcePromise; /** @@ -45984,6 +48205,17 @@ export interface TestVaultResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -46602,20 +48834,6 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestVaultResourcePromise; - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -46693,17 +48911,6 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The updated resource builder. */ withContainerCertificatePaths(options?: WithContainerCertificatePathsOptions): TestVaultResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Builds the specified container image from a Dockerfile generated by a callback using the `DockerfileBuilder` API. * @@ -46792,6 +48999,56 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): TestVaultResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestVaultResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestVaultResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestVaultResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestVaultResourcePromise; /** Sets an environment variable */ withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestVaultResourcePromise; /** @@ -46844,6 +49101,17 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -47579,33 +49847,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withContainerRuntimeArgsInternal(args), this._client); } - /** @internal */ - private async _withLifetimeInternal(lifetime: ContainerLifetime): Promise { - const rpcArgs: Record = { builder: this._handle, lifetime }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withLifetime', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - /** - * Sets the lifetime behavior of the container resource. - * - * Marking a container resource to have a `Persistent` lifetime. - * ``` - * var builder = DistributedApplication.CreateBuilder(args); - * builder.AddContainer("mycontainer", "myimage") - * .WithLifetime(ContainerLifetime.Persistent); - * builder.Build().Run(); - * ``` - * @param lifetime The lifetime behavior of the container resource. The defaults behavior is `Session`. - * @returns The `IResourceBuilder`1`. - */ - withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withLifetimeInternal(lifetime), this._client); - } - /** @internal */ private async _withImagePullPolicyInternal(pullPolicy: ImagePullPolicy): Promise { const rpcArgs: Record = { builder: this._handle, pullPolicy }; @@ -47786,30 +50027,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withContainerCertificatePathsInternal(customCertificatesDestination, defaultCertificateBundlePaths, defaultCertificateDirectoryPaths), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the container. If set to `false`, endpoints belonging to the container resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime containers where it is desirable for the container to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that container endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the internal container port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the container resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBuilderInternal(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, stage?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -48008,6 +50225,109 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): Promise { value = isPromiseLike(value) ? await value : value; @@ -48249,6 +50569,30 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -49874,10 +52218,6 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withContainerRuntimeArgs(args)), this._client); } - withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withLifetime(lifetime)), this._client); - } - withImagePullPolicy(pullPolicy: ImagePullPolicy): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withImagePullPolicy(pullPolicy)), this._client); } @@ -49906,10 +52246,6 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withContainerCertificatePaths(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBuilder(contextPath: string, callback: (arg: DockerfileBuilderCallbackContext) => Promise, options?: WithDockerfileBuilderOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBuilder(contextPath, callback, options)), this._client); } @@ -49938,6 +52274,22 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString | TestRedisResource | EndpointReferenceExpression | Awaitable): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEnvironment(name, value)), this._client); } @@ -49978,6 +52330,10 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -50629,6 +52985,56 @@ export interface Resource { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -50978,6 +53384,56 @@ export interface ResourcePromise extends PromiseLike { * @returns The resource builder. */ withRequiredCommand(command: string, options?: WithRequiredCommandOptions): ResourcePromise; + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ResourcePromise; + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ResourcePromise; + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ResourcePromise; + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ResourcePromise; /** * Registers a callback to customize the URLs displayed for the resource. * @@ -51382,6 +53838,109 @@ class ResourceImpl extends ResourceBuilderBase implements Resou return new ResourcePromiseImpl(this._withRequiredCommandInternal(command, helpLink), this._client); } + /** @internal */ + private async _withSessionLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withSessionLifetime', + rpcArgs + ); + return new ResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a session lifetime. + * + * Marking a resource to have a session lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithSessionLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withSessionLifetime(): ResourcePromise { + return new ResourcePromiseImpl(this._withSessionLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withPersistentLifetimeInternal(): Promise { + const rpcArgs: Record = { builder: this._handle }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withPersistentLifetime', + rpcArgs + ); + return new ResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime. + * + * Marking a resource to have a persistent lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @returns The `IResourceBuilder`1`. + */ + withPersistentLifetime(): ResourcePromise { + return new ResourcePromiseImpl(this._withPersistentLifetimeInternal(), this._client); + } + + /** @internal */ + private async _withLifetimeOfInternal(sourceBuilder: Awaitable): Promise { + sourceBuilder = isPromiseLike(sourceBuilder) ? await sourceBuilder : sourceBuilder; + const rpcArgs: Record = { builder: this._handle, sourceBuilder }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetimeOf', + rpcArgs + ); + return new ResourceImpl(result, this._client); + } + + /** + * Configures a resource to match the lifetime of another resource. + * + * The resource lifetime is evaluated from `sourceBuilder` when the application model is prepared, so later lifetime + * changes to the source resource are reflected by this resource. + * @param sourceBuilder The resource builder whose lifetime should be used. + * @returns The `IResourceBuilder`1`. + */ + withLifetimeOf(sourceBuilder: Awaitable): ResourcePromise { + return new ResourcePromiseImpl(this._withLifetimeOfInternal(sourceBuilder), this._client); + } + + /** @internal */ + private async _withParentProcessLifetimeInternal(parentProcessId: number): Promise { + const rpcArgs: Record = { builder: this._handle, parentProcessId }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withParentProcessLifetime', + rpcArgs + ); + return new ResourceImpl(result, this._client); + } + + /** + * Configures a resource to use a persistent lifetime that ends when a parent process exits. + * + * The resource is tied to both the configured process ID and the process identity timestamp to avoid accidentally matching a reused process ID. + * Configure a resource to remain available across app host restarts, but clean it up when a parent process exits. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddProject("api") + * .WithParentProcessLifetime(parentProcessId: 1234); + * builder.Build().Run(); + * ``` + * @param parentProcessId The ID of the parent process to monitor. + * @returns The `IResourceBuilder`1`. + */ + withParentProcessLifetime(parentProcessId: number): ResourcePromise { + return new ResourcePromiseImpl(this._withParentProcessLifetimeInternal(parentProcessId), this._client); + } + /** @internal */ private async _withUrlsInternal(callback: (obj: ResourceUrlsCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (objData: unknown) => { @@ -52378,6 +54937,22 @@ class ResourcePromiseImpl implements ResourcePromise { return new ResourcePromiseImpl(this._promise.then(obj => obj.withRequiredCommand(command, options)), this._client); } + withSessionLifetime(): ResourcePromise { + return new ResourcePromiseImpl(this._promise.then(obj => obj.withSessionLifetime()), this._client); + } + + withPersistentLifetime(): ResourcePromise { + return new ResourcePromiseImpl(this._promise.then(obj => obj.withPersistentLifetime()), this._client); + } + + withLifetimeOf(sourceBuilder: Awaitable): ResourcePromise { + return new ResourcePromiseImpl(this._promise.then(obj => obj.withLifetimeOf(sourceBuilder)), this._client); + } + + withParentProcessLifetime(parentProcessId: number): ResourcePromise { + return new ResourcePromiseImpl(this._promise.then(obj => obj.withParentProcessLifetime(parentProcessId)), this._client); + } + withUrls(callback: (obj: ResourceUrlsCallbackContext) => Promise): ResourcePromise { return new ResourcePromiseImpl(this._promise.then(obj => obj.withUrls(callback)), this._client); } @@ -53024,6 +55599,17 @@ export interface ResourceWithEndpoints { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ResourceWithEndpointsPromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -53128,6 +55714,17 @@ export interface ResourceWithEndpointsPromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ResourceWithEndpointsImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { + return new ResourceWithEndpointsPromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -53618,6 +56239,10 @@ class ResourceWithEndpointsPromiseImpl implements ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { + return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } From 1f265e06de4bbbc3324875cd54039f911c6f8938 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 13:16:53 -0700 Subject: [PATCH 16/38] Stabilize persistent executable certificate test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Dcp/DcpExecutorTests.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 6716fd53d3a..f97b1c59664 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -2401,6 +2401,8 @@ public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath() { var builder = DistributedApplication.CreateBuilder(); + using var fileSystemService = new FileSystemService(new ConfigurationBuilder().Build()); + using var aspireStoreDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-store"); using var certificate = CreateTestCertificate(); var certificateAuthorities = builder.AddCertificateAuthorityCollection("certificates") @@ -2414,6 +2416,7 @@ public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath() var configDict = new Dictionary { + [AspireStore.AspireStorePathKeyName] = aspireStoreDirectory.Path, ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" }; @@ -2429,12 +2432,10 @@ public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath() var exe = Assert.Single(kubernetesService.CreatedResources.OfType(), e => e.AppModelResourceName == "TestExecutable"); var sslCertDir = Assert.Single(exe.Spec.Env!, e => e.Name == "SSL_CERT_DIR").Value; var sslCertFile = Assert.Single(exe.Spec.Env!, e => e.Name == "SSL_CERT_FILE").Value; - var expectedCertificatesRoot = Path.Join(".aspire", "dcp", "executables", "TestExecutable-12345678", "certificates"); + var expectedCertificatesRoot = Path.Join(aspireStoreDirectory.Path, ".aspire", "dcp", "executables", "TestExecutable-12345678", "certificates"); - Assert.EndsWith(Path.Join(expectedCertificatesRoot, "certs"), sslCertDir); - Assert.EndsWith(Path.Join(expectedCertificatesRoot, "cert.pem"), sslCertFile); - Assert.DoesNotContain("aspire-dcp", sslCertDir); - Assert.DoesNotContain("aspire-dcp", sslCertFile); + Assert.Equal(Path.Join(expectedCertificatesRoot, "certs"), sslCertDir); + Assert.Equal(Path.Join(expectedCertificatesRoot, "cert.pem"), sslCertFile); } [Fact] @@ -4193,8 +4194,13 @@ private static DcpExecutor CreateAppExecutor( var dcpEvts = events ?? new DcpExecutorEvents(); var fileSystemService = new FileSystemService(configuration); var locations = new Locations(fileSystemService); - var aspireStoreDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-store"); - var aspireStore = new AspireStore(Path.Join(aspireStoreDirectory.Path, ".aspire"), fileSystemService); + var aspireStoreDirectory = configuration[AspireStore.AspireStorePathKeyName]; + if (string.IsNullOrWhiteSpace(aspireStoreDirectory)) + { + aspireStoreDirectory = fileSystemService.TempDirectory.CreateTempSubdirectory("aspire-store").Path; + } + + var aspireStore = new AspireStore(Path.Join(aspireStoreDirectory, ".aspire"), fileSystemService); var hostEnv = hostEnvironment ?? new TestHostEnvironment(); var dcpDependencyCheckService = new TestDcpDependencyCheckService(); processMonitor ??= new TestDcpProcessMonitor(null); From 360f16fd27117377adec77e0ca52e204d3a01d1a Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 14:38:28 -0700 Subject: [PATCH 17/38] Reject persistent executable replicas Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Dcp/DcpNameGenerator.cs | 15 ++++++++ .../Dcp/DcpExecutorTests.cs | 36 +++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs index 567c4019e63..4ef706d4a62 100644 --- a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs +++ b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs @@ -34,6 +34,8 @@ public DcpNameGenerator(IConfiguration configuration, IOptions optio public void EnsureDcpInstancesPopulated(IResource resource) { + ThrowIfPersistentExecutableHasReplicas(resource); + if (resource.TryGetInstances(out _)) { return; @@ -67,6 +69,19 @@ private static void AddInstancesAnnotation(IResource resource, ImmutableArray 1 && resource.GetLifetimeType() == Lifetime.Persistent) + { + throw new InvalidOperationException($"Resource '{resource.Name}' uses multiple replicas and a persistent lifetime. These features do not work together."); + } + } + public (string Name, string Suffix) GetContainerName(IResource container) { var nameSuffix = container.GetLifetimeType() switch diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index f97b1c59664..2f9b6adc221 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -2276,6 +2276,42 @@ public async Task PersistentDcpResourcesDoNotIncludeMonitorProcessByDefault() }); } + [Fact] + public async Task PersistentProjectWithReplicasThrows() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddProject("project", launchProfileName: null) + .WithReplicas(2) + .WithPersistentLifetime(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + var exception = await Assert.ThrowsAsync(() => appExecutor.RunApplicationAsync()); + Assert.Equal("Resource 'project' uses multiple replicas and a persistent lifetime. These features do not work together.", exception.Message); + } + + [Fact] + public async Task PersistentPlainExecutableWithReplicasThrows() + { + var builder = DistributedApplication.CreateBuilder(); + + builder.AddExecutable("worker", "worker", Environment.CurrentDirectory) + .WithAnnotation(new ReplicaAnnotation(2)) + .WithPersistentLifetime(); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + + var exception = await Assert.ThrowsAsync(() => appExecutor.RunApplicationAsync()); + Assert.Equal("Resource 'worker' uses multiple replicas and a persistent lifetime. These features do not work together.", exception.Message); + } + [Fact] public async Task PersistentContainerWithOtlpExporterUsesStableServiceInstanceId() { From fbfdfa375057ff8c6ec788b1c0685085a2d5667a Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 14:41:50 -0700 Subject: [PATCH 18/38] Use allocated container port for dev tunnels Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DevTunnelResource.cs | 20 ++++++++++++++++--- ...DevTunnelResourceBuilderExtensionsTests.cs | 19 ++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs index b03828b5beb..ce72eaa4e24 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs @@ -89,17 +89,31 @@ public DevTunnelPortResource( internal async ValueTask GetTunnelPortAsync(CancellationToken cancellationToken = default) { + if (TargetEndpoint.Resource.IsContainer()) + { + // Dev tunnel hosting runs on the host, so container endpoints must forward the host-reachable + // allocated port rather than the container-internal target port. + return await GetResolvedEndpointPortAsync(EndpointProperty.Port, cancellationToken).ConfigureAwait(false) + ?? TargetEndpoint.Port; + } + if (TargetEndpoint.TargetPort is int targetPort) { return targetPort; } + return await GetResolvedEndpointPortAsync(EndpointProperty.TargetPort, cancellationToken).ConfigureAwait(false) + ?? TargetEndpoint.Port; + } + + private async ValueTask GetResolvedEndpointPortAsync(EndpointProperty property, CancellationToken cancellationToken) + { string? resolvedTargetPort = null; try { - resolvedTargetPort = await TargetEndpoint.Property(EndpointProperty.TargetPort).GetValueAsync(cancellationToken).ConfigureAwait(false); + resolvedTargetPort = await TargetEndpoint.Property(property).GetValueAsync(cancellationToken).ConfigureAwait(false); } - catch (InvalidOperationException) when (TargetEndpoint.IsAllocated) + catch (InvalidOperationException) when (property == EndpointProperty.TargetPort && TargetEndpoint.IsAllocated) { // Endpoint references can only resolve targetPort dynamically when DCP reports a target-port expression. } @@ -109,6 +123,6 @@ internal async ValueTask GetTunnelPortAsync(CancellationToken cancellationT return port; } - return TargetEndpoint.Port; + return null; } } diff --git a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs index ac51a098913..9a8f5fea8e1 100644 --- a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs +++ b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs @@ -107,6 +107,25 @@ public async Task WithReference_UsesTargetPortForDevTunnelPortWhenAvailable() Assert.Equal(5001, await tunnelPort.GetTunnelPortAsync()); } + [Fact] + public async Task WithReference_UsesAllocatedPortForContainerDevTunnelPort() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var target = builder.AddContainer("target", "image") + .WithHttpEndpoint(port: 5000, targetPort: 8080, name: "http"); + var tunnel = builder.AddDevTunnel("tunnel") + .WithReference(target); + + var tunnelPort = Assert.Single(tunnel.Resource.Ports); + tunnelPort.TargetEndpoint.EndpointAnnotation.AllocatedEndpoint = new( + tunnelPort.TargetEndpoint.EndpointAnnotation, + "localhost", + 5000); + + Assert.Equal(5000, await tunnelPort.GetTunnelPortAsync()); + } + [Fact] public async Task WithReference_ResolvesDynamicTargetPortForDevTunnelPortWhenAvailable() { From 2fda9ee5c9acf1f17a7fd24ede5a90ca437d7b76 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 14:50:57 -0700 Subject: [PATCH 19/38] Avoid storing Process handles in lifetime annotations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ParentProcessLifetimeAnnotation.cs | 13 ++++--- src/Aspire.Hosting/Dcp/ContainerCreator.cs | 10 ++---- src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs | 9 ++--- src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 10 ++---- .../DistributedApplicationBuilder.cs | 1 - .../ResourceBuilderExtensions.cs | 6 +++- .../Dcp/DcpExecutorTests.cs | 35 +++++-------------- .../DistributedApplicationTests.cs | 4 +-- .../ResourceBuilderLifetimeTests.cs | 25 +++++++++++-- 9 files changed, 54 insertions(+), 59 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs index 159468c02c5..7893d75e12c 100644 --- a/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs @@ -3,15 +3,18 @@ namespace Aspire.Hosting.ApplicationModel; -using SystemProcess = System.Diagnostics.Process; - /// /// Configures a persistent resource to be monitored by a parent process identity. /// -internal sealed class ParentProcessLifetimeAnnotation(SystemProcess parentProcess) : IResourceAnnotation +internal sealed class ParentProcessLifetimeAnnotation(int parentProcessId, DateTime parentProcessTimestamp) : IResourceAnnotation { /// - /// Gets the parent process to monitor. + /// Gets the ID of the parent process to monitor. + /// + public int ParentProcessId { get; } = parentProcessId; + + /// + /// Gets the identity timestamp of the parent process to monitor. /// - public SystemProcess ParentProcess { get; } = parentProcess ?? throw new ArgumentNullException(nameof(parentProcess)); + public DateTime ParentProcessTimestamp { get; } = parentProcessTimestamp; } diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index daba932b1ef..2b5efe0b163 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -54,7 +54,6 @@ internal sealed class ContainerCreator : IObjectCreator _logger; private readonly string _normalizedApplicationName; private readonly DcpAppResourceStore _appResources; @@ -70,7 +69,6 @@ public ContainerCreator( DistributedApplicationExecutionContext executionContext, ResourceLoggerService loggerService, IDcpDependencyCheckService dcpDependencyCheckService, - IDcpProcessMonitor processMonitor, IHostEnvironment hostEnvironment, ILogger logger, DcpAppResourceStore appResources) @@ -82,7 +80,6 @@ public ContainerCreator( _executionContext = executionContext; _loggerService = loggerService; _dcpDependencyCheckService = dcpDependencyCheckService; - _processMonitor = processMonitor; _logger = logger; _normalizedApplicationName = DcpExecutor.NormalizeApplicationName(hostEnvironment.ApplicationName); _appResources = appResources; @@ -210,13 +207,12 @@ public IEnumerable> PrepareObjects() return result; } - private void ApplyMonitorProcess(IResource resource, ContainerSpec spec) + private static void ApplyMonitorProcess(IResource resource, ContainerSpec spec) { if (resource.TryGetParentProcessLifetime(out var annotation)) { - var monitorProcess = _processMonitor.GetMonitorProcess(annotation.ParentProcess); - spec.MonitorPid = monitorProcess.ProcessId; - spec.MonitorTimestamp = monitorProcess.Timestamp; + spec.MonitorPid = annotation.ParentProcessId; + spec.MonitorTimestamp = annotation.ParentProcessTimestamp; } } diff --git a/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs b/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs index 14b07ee25e6..53a96fa7931 100644 --- a/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs +++ b/src/Aspire.Hosting/Dcp/DcpProcessMonitor.cs @@ -9,17 +9,12 @@ namespace Aspire.Hosting.Dcp; internal sealed record DcpProcessIdentity(int ProcessId, DateTime Timestamp); -internal interface IDcpProcessMonitor -{ - DcpProcessIdentity GetMonitorProcess(SystemProcess parentProcess); -} - -internal sealed partial class DcpProcessMonitor : IDcpProcessMonitor +internal static partial class DcpProcessMonitor { private const int DefaultLinuxClockTicksPerSecond = 100; private const int LinuxClockTicksPerSecondConfigName = 2; // _SC_CLK_TCK - public DcpProcessIdentity GetMonitorProcess(SystemProcess parentProcess) + internal static DcpProcessIdentity GetMonitorProcessIdentity(SystemProcess parentProcess) { ArgumentNullException.ThrowIfNull(parentProcess); diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index 3b2f8296a37..d7d8e888748 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -30,7 +30,6 @@ internal sealed class ExecutableCreator : IObjectCreator _logger; private readonly DcpAppResourceStore _appResources; @@ -42,7 +41,6 @@ public ExecutableCreator( DistributedApplicationExecutionContext executionContext, Locations locations, IAspireStore aspireStore, - IDcpProcessMonitor processMonitor, ILogger logger, DcpAppResourceStore appResources) { @@ -53,7 +51,6 @@ public ExecutableCreator( _executionContext = executionContext; _locations = locations; _aspireStore = aspireStore; - _processMonitor = processMonitor; _logger = logger; _appResources = appResources; } @@ -322,13 +319,12 @@ private void PreparePlainExecutables() } } - private void ApplyMonitorProcess(IResource resource, ExecutableSpec spec) + private static void ApplyMonitorProcess(IResource resource, ExecutableSpec spec) { if (resource.TryGetParentProcessLifetime(out var annotation)) { - var monitorProcess = _processMonitor.GetMonitorProcess(annotation.ParentProcess); - spec.MonitorPid = monitorProcess.ProcessId; - spec.MonitorTimestamp = monitorProcess.Timestamp; + spec.MonitorPid = annotation.ParentProcessId; + spec.MonitorTimestamp = annotation.ParentProcessTimestamp; } } diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 38b2400f005..efb0828d7a7 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -511,7 +511,6 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) // DCP stuff _innerBuilder.Services.AddSingleton(); - _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); _innerBuilder.Services.AddSingleton(); diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index b490140b97a..a9a32279559 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -10,6 +10,7 @@ using Aspire.Dashboard.Model; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Ats; +using Aspire.Hosting.Dcp; using Aspire.Hosting.Dcp.Process; using Aspire.Hosting.Publishing; using Aspire.Hosting.Utils; @@ -152,9 +153,12 @@ public static IResourceBuilder WithParentProcessLifetime(this IResourceBui throw new ArgumentOutOfRangeException(nameof(parentProcessId), "The parent process ID must be greater than zero."); } + using var parentProcess = SystemProcess.GetProcessById(parentProcessId); + var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); + return builder .WithPersistentLifetime() - .WithAnnotation(new ParentProcessLifetimeAnnotation(SystemProcess.GetProcessById(parentProcessId)), ResourceAnnotationMutationBehavior.Replace); + .WithAnnotation(new ParentProcessLifetimeAnnotation(parentProcessIdentity.ProcessId, parentProcessIdentity.Timestamp), ResourceAnnotationMutationBehavior.Replace); } private static IResourceBuilder ApplyLifetime(IResourceBuilder builder, Lifetime lifetime) diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 2f9b6adc221..badd51cb036 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -2385,7 +2385,8 @@ r.Metadata.Annotations is not null && public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() { var builder = DistributedApplication.CreateBuilder(); - var parentProcess = Process.GetCurrentProcess(); + using var parentProcess = Process.GetCurrentProcess(); + var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); builder.AddContainer("database", "image") .WithParentProcessLifetime(parentProcess.Id); @@ -2399,8 +2400,6 @@ public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() ["AppHost:Sha256"] = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" }; var configuration = new ConfigurationBuilder().AddInMemoryCollection(configDict).Build(); - var monitorProcess = new DcpProcessIdentity(parentProcess.Id, new DateTime(2026, 5, 14, 1, 2, 3, DateTimeKind.Utc)); - var processMonitor = new TestDcpProcessMonitor(monitorProcess); var kubernetesService = new TestKubernetesService(); using var app = builder.Build(); @@ -2408,15 +2407,14 @@ public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() var appExecutor = CreateAppExecutor( distributedAppModel, kubernetesService: kubernetesService, - configuration: configuration, - processMonitor: processMonitor); + configuration: configuration); await appExecutor.RunApplicationAsync(); var container = Assert.Single(kubernetesService.CreatedResources.OfType()); Assert.True(container.Spec.Persistent.GetValueOrDefault()); - Assert.Equal(monitorProcess.ProcessId, container.Spec.MonitorPid); - Assert.Equal(monitorProcess.Timestamp, container.Spec.MonitorTimestamp); + Assert.Equal(parentProcessIdentity.ProcessId, container.Spec.MonitorPid); + Assert.Equal(parentProcessIdentity.Timestamp, container.Spec.MonitorTimestamp); var executables = kubernetesService.CreatedResources.OfType() .Where(e => e.AppModelResourceName is "worker" or "project") @@ -2425,12 +2423,10 @@ public async Task ExplicitParentProcessLifetimeIncludesMonitorProcess() Assert.All(executables, exe => { Assert.True(exe.Spec.Persistent.GetValueOrDefault()); - Assert.Equal(monitorProcess.ProcessId, exe.Spec.MonitorPid); - Assert.Equal(monitorProcess.Timestamp, exe.Spec.MonitorTimestamp); + Assert.Equal(parentProcessIdentity.ProcessId, exe.Spec.MonitorPid); + Assert.Equal(parentProcessIdentity.Timestamp, exe.Spec.MonitorTimestamp); Assert.Equal(ExecutionType.Process, exe.Spec.ExecutionType); }); - Assert.Equal(3, processMonitor.MonitoredProcesses.Count); - Assert.All(processMonitor.MonitoredProcesses, process => Assert.Equal(parentProcess.Id, process.Id)); } [Fact] @@ -4196,8 +4192,7 @@ private static DcpExecutor CreateAppExecutor( DcpOptions? dcpOptions = null, ResourceLoggerService? resourceLoggerService = null, DcpExecutorEvents? events = null, - Hosting.Eventing.IDistributedApplicationEventing? distributedApplicationEventing = null, - IDcpProcessMonitor? processMonitor = null) + Hosting.Eventing.IDistributedApplicationEventing? distributedApplicationEventing = null) { if (configuration == null) { @@ -4239,7 +4234,6 @@ private static DcpExecutor CreateAppExecutor( var aspireStore = new AspireStore(Path.Join(aspireStoreDirectory, ".aspire"), fileSystemService); var hostEnv = hostEnvironment ?? new TestHostEnvironment(); var dcpDependencyCheckService = new TestDcpDependencyCheckService(); - processMonitor ??= new TestDcpProcessMonitor(null); var appResources = new DcpAppResourceStore(); @@ -4251,7 +4245,6 @@ private static DcpExecutor CreateAppExecutor( executionContext, locations, aspireStore, - processMonitor, NullLogger.Instance, appResources); @@ -4263,7 +4256,6 @@ private static DcpExecutor CreateAppExecutor( executionContext, resourceLoggerService, dcpDependencyCheckService, - processMonitor, hostEnv, NullLogger.Instance, appResources); @@ -4317,17 +4309,6 @@ private static void AssertEffectiveArgumentIndexesMatchSpecArgs(IReadOnlyList MonitoredProcesses { get; } = []; - - public DcpProcessIdentity GetMonitorProcess(Process process) - { - MonitoredProcesses.Add(process); - return monitorProcess ?? throw new InvalidOperationException("No test monitor process identity was configured."); - } - } - private static Aspire.Hosting.Dcp.ResourceSnapshotBuilder CreateSnapshotBuilder(DistributedApplicationModel model) { return new(new DcpResourceState(model.Resources.ToDictionary(r => r.Name), [])); diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 1e73ec65988..7f316f25a1f 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -1890,8 +1890,8 @@ public async Task ParentProcessLifetimeScopesExecutableAndContainerToParentProce { const string testName = "parent-process-lifetime-scope"; using var builder = TestDistributedApplicationBuilder.Create(_testOutputHelper); - var parentProcess = Process.GetCurrentProcess(); - var parentProcessIdentity = new DcpProcessMonitor().GetMonitorProcess(parentProcess); + using var parentProcess = Process.GetCurrentProcess(); + var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); var container = AddRedisContainer(builder, $"{testName}-container") .WithParentProcessLifetime(parentProcess.Id) diff --git a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs index d4ed396fc84..a6dcf18cc01 100644 --- a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs @@ -1,6 +1,8 @@ // 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; +using Aspire.Hosting.Dcp; using Aspire.Hosting.Utils; namespace Aspire.Hosting.Tests; @@ -45,6 +47,22 @@ public void WithPersistentLifetimeRemovesParentProcessLifetimeAnnotation() Assert.False(container.Resource.TryGetLastAnnotation(out _)); } + [Fact] + public void WithParentProcessLifetimeReplacesExistingParentProcessLifetimeAnnotation() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var originalTimestamp = new DateTime(2026, 5, 18, 1, 2, 3, DateTimeKind.Utc); + var container = builder.AddContainer("container", "image") + .WithAnnotation(new ParentProcessLifetimeAnnotation(parentProcessId: 1, parentProcessTimestamp: originalTimestamp)); + + container.WithParentProcessLifetime(Environment.ProcessId); + + var annotation = Assert.Single(container.Resource.Annotations.OfType()); + Assert.Equal(Environment.ProcessId, annotation.ParentProcessId); + Assert.NotEqual(originalTimestamp, annotation.ParentProcessTimestamp); + } + [Fact] public void WithLifetimeOfMatchesSourceResourceLifetime() { @@ -68,14 +86,17 @@ public void WithLifetimeOfMatchesSourceResourceLifetime() public void WithLifetimeOfMatchesSourceParentProcessLifetime() { using var builder = TestDistributedApplicationBuilder.Create(); + using var parentProcess = Process.GetCurrentProcess(); + var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); var source = builder.AddContainer("source", "image") - .WithParentProcessLifetime(Environment.ProcessId); + .WithParentProcessLifetime(parentProcess.Id); var container = builder.AddContainer("container", "image") .WithLifetimeOf(source); Assert.True(container.Resource.TryGetParentProcessLifetime(out var parentProcessLifetimeAnnotation)); - Assert.Equal(Environment.ProcessId, parentProcessLifetimeAnnotation.ParentProcess.Id); + Assert.Equal(parentProcessIdentity.ProcessId, parentProcessLifetimeAnnotation.ParentProcessId); + Assert.Equal(parentProcessIdentity.Timestamp, parentProcessLifetimeAnnotation.ParentProcessTimestamp); source.WithSessionLifetime(); From 6777519af92ece7d247816fdbf0ab69e12969349 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 17:10:12 -0700 Subject: [PATCH 20/38] Avoid unsafe lifetime annotation enumeration Snapshot resource annotations under lock before walking lifetime annotations so concurrent annotation mutations cannot invalidate the enumerator. Apply the same pattern to parent process lifetime lookup, which follows lifetime references through the same annotation collection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ApplicationModel/ResourceExtensions.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 10accf9fd56..5756cda1fac 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1069,8 +1069,10 @@ private static Lifetime GetLifetimeType(IResource resource, HashSet v throw new InvalidOperationException($"A circular lifetime reference was detected for resource '{resource.Name}'."); } - foreach (var annotation in resource.Annotations.Reverse()) + var annotations = GetAnnotationsSnapshot(resource); + for (var i = annotations.Length - 1; i >= 0; i--) { + var annotation = annotations[i]; switch (annotation) { case LifetimeAnnotation lifetimeAnnotation: @@ -1125,8 +1127,10 @@ private static bool TryGetParentProcessLifetime(IResource resource, HashSet= 0; i--) { + var resourceAnnotation = annotations[i]; switch (resourceAnnotation) { case ParentProcessLifetimeAnnotation parentProcessLifetimeAnnotation: @@ -1144,6 +1148,14 @@ private static bool TryGetParentProcessLifetime(IResource resource, HashSet /// Determines whether the specified resource has a pull policy annotation and retrieves the value if it does. /// From 16fac48122ea26e4b42e5ad55d57dc4d0e37a9a1 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 17:30:55 -0700 Subject: [PATCH 21/38] Add persistent executable playground coverage Add a lightweight persistent executable to the Stress playground AppHost by launching the existing Stress.Empty app with dotnet run --no-build. This exercises persistent executable behavior without introducing a demanding persistent process. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- playground/Stress/Stress.AppHost/AppHost.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/playground/Stress/Stress.AppHost/AppHost.cs b/playground/Stress/Stress.AppHost/AppHost.cs index 7b28dc312ac..a7112aa0d77 100644 --- a/playground/Stress/Stress.AppHost/AppHost.cs +++ b/playground/Stress/Stress.AppHost/AppHost.cs @@ -99,6 +99,9 @@ builder.AddExecutable("executableWithSingleArg", "dotnet", Environment.CurrentDirectory, "--version"); builder.AddExecutable("executableWithSingleEscapedArg", "dotnet", Environment.CurrentDirectory, "one two"); builder.AddExecutable("executableWithMultipleArgs", "dotnet", Environment.CurrentDirectory, "--version", "one two"); +var stressEmptyProjectPath = new Projects.Stress_Empty().ProjectPath; +builder.AddExecutable("persistentExecutable", "dotnet", Environment.CurrentDirectory, "run", "--project", stressEmptyProjectPath, "--no-build") + .WithPersistentLifetime(); IResourceBuilder? previousResourceBuilder = null; From 86e52c4394122d5e2a3b4b15f3920ffd4bdbd65c Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 18:42:30 -0700 Subject: [PATCH 22/38] Mark shared lifetime APIs experimental Add Experimental attributes to the new shared lifetime APIs while leaving the existing container-specific WithLifetime(ContainerLifetime) API stable. Suppress the new diagnostic at intentional in-repo call sites that exercise or propagate those APIs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AzureAppService/AzureAppService.AppHost/AppHost.cs | 2 ++ .../AzureContainerApps.AppHost/AppHost.cs | 1 + playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs | 2 ++ .../AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs | 1 + playground/Stress/Stress.AppHost/AppHost.cs | 1 + playground/TestShop/TestShop.AppHost/AppHost.cs | 2 ++ .../AzureEventHubsExtensions.cs | 1 + .../AzureServiceBusExtensions.cs | 1 + src/Aspire.Hosting/ResourceBuilderExtensions.cs | 8 ++++++-- .../AddAzureKustoTests.cs | 2 ++ .../AzureEventHubsExtensionsTests.cs | 2 ++ .../AzureResourceOptionsTests.cs | 2 ++ .../AzureServiceBusExtensionsTests.cs | 2 ++ .../DevTunnelResourceBuilderExtensionsTests.cs | 2 ++ tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs | 2 ++ .../PostgresFunctionalTests.cs | 2 ++ tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs | 1 + tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs | 1 + .../ExecutableResourceBuilderExtensionTests.cs | 1 + .../ProjectResourceBuilderExtensionTests.cs | 2 ++ .../Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs | 2 ++ 21 files changed, 38 insertions(+), 2 deletions(-) diff --git a/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs b/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs index 5efb805a14e..9d03da7982a 100644 --- a/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs +++ b/playground/AzureAppService/AzureAppService.AppHost/AppHost.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using Aspire.Hosting.Azure; using Azure.Provisioning.Storage; diff --git a/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs b/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs index b7585e41e4e..703410c09cf 100644 --- a/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs +++ b/playground/AzureContainerApps/AzureContainerApps.AppHost/AppHost.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREACADOMAINS001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using Aspire.Hosting.Azure; using Azure.Provisioning.Storage; diff --git a/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs b/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs index 04d6441382b..6004a5037c3 100644 --- a/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs +++ b/playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs @@ -1,6 +1,8 @@ using System.Text.Json.Nodes; using Aspire.Hosting.Azure; +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + var builder = DistributedApplication.CreateBuilder(args); var serviceBus = builder.AddAzureServiceBus("sbemulator"); diff --git a/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs b/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs index 2779f382c31..55053847beb 100644 --- a/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs +++ b/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/AppHost.cs @@ -3,6 +3,7 @@ #pragma warning disable AZPROVISION001 // Azure.Provisioning.Network is experimental #pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using Aspire.Hosting.Azure; using Azure.Provisioning.Network; diff --git a/playground/Stress/Stress.AppHost/AppHost.cs b/playground/Stress/Stress.AppHost/AppHost.cs index a7112aa0d77..63f90f517e9 100644 --- a/playground/Stress/Stress.AppHost/AppHost.cs +++ b/playground/Stress/Stress.AppHost/AppHost.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; #pragma warning disable ASPIREDOTNETTOOL +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. var builder = DistributedApplication.CreateBuilder(args); builder.Services.AddHttpClient(); diff --git a/playground/TestShop/TestShop.AppHost/AppHost.cs b/playground/TestShop/TestShop.AppHost/AppHost.cs index 939b2ef3de8..5bd77645105 100644 --- a/playground/TestShop/TestShop.AppHost/AppHost.cs +++ b/playground/TestShop/TestShop.AppHost/AppHost.cs @@ -2,6 +2,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + var builder = DistributedApplication.CreateBuilder(args); var catalogDb = builder.AddPostgres("postgres") diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs index 77c5eb11dfc..c4b5bc963c0 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using System.Text; using System.Text.Json; diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs index 7b0aaf7bcbb..0042acbac2e 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using System.Text; using System.Text.Json; diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index a9a32279559..5b5c77fceb2 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -27,6 +27,7 @@ namespace Aspire.Hosting; public static class ResourceBuilderExtensions { private const string ConnectionStringEnvironmentName = "ConnectionStrings__"; + private const string PersistenceExperimentalDiagnosticId = "ASPIREPERSISTENCE001"; private static readonly MethodInfo s_dispatchCustomWithReferenceMethod = typeof(ResourceBuilderExtensions).GetMethod(nameof(DispatchCustomWithReference), BindingFlags.NonPublic | BindingFlags.Static)!; /// @@ -49,6 +50,7 @@ public static class ResourceBuilderExtensions /// /// /// Thrown when the resource does not support lifetime configuration. + [Experimental(PersistenceExperimentalDiagnosticId, UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExport(Description = "Sets session lifetime behavior for the resource")] public static IResourceBuilder WithSessionLifetime(this IResourceBuilder builder) where T : IResource @@ -78,6 +80,7 @@ public static IResourceBuilder WithSessionLifetime(this IResourceBuilder /// /// Thrown when the resource does not support lifetime configuration. + [Experimental(PersistenceExperimentalDiagnosticId, UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExport(Description = "Sets persistent lifetime behavior for the resource")] public static IResourceBuilder WithPersistentLifetime(this IResourceBuilder builder) where T : IResource @@ -100,6 +103,7 @@ public static IResourceBuilder WithPersistentLifetime(this IResourceBuilde /// changes to the source resource are reflected by this resource. /// /// Thrown when the resource does not support lifetime configuration. + [Experimental(PersistenceExperimentalDiagnosticId, UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExport(Description = "Sets resource lifetime behavior to match another resource")] public static IResourceBuilder WithLifetimeOf(this IResourceBuilder builder, IResourceBuilder sourceBuilder) where T : IResource @@ -142,6 +146,7 @@ public static IResourceBuilder WithLifetimeOf(this IResourceBuild /// Thrown when is less than or equal to zero. /// Thrown when does not identify a running process. /// Thrown when the resource does not support lifetime configuration. + [Experimental(PersistenceExperimentalDiagnosticId, UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] [AspireExport(Description = "Sets persistent lifetime behavior tied to a parent process")] public static IResourceBuilder WithParentProcessLifetime(this IResourceBuilder builder, int parentProcessId) where T : IResource @@ -156,8 +161,7 @@ public static IResourceBuilder WithParentProcessLifetime(this IResourceBui using var parentProcess = SystemProcess.GetProcessById(parentProcessId); var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); - return builder - .WithPersistentLifetime() + return ApplyLifetime(builder, Lifetime.Persistent) .WithAnnotation(new ParentProcessLifetimeAnnotation(parentProcessIdentity.ProcessId, parentProcessIdentity.Timestamp), ResourceAnnotationMutationBehavior.Replace); } diff --git a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs index 26280a47431..37987570a02 100644 --- a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs +++ b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Testing; using Aspire.Hosting.Utils; diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs index a7638c2e9de..c9ca4148015 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using System.Text; using System.Text.Json.Nodes; using System.Reflection; diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs index fb9d4bde14e..8e8bb7a6e1b 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs index 734a2e29dba..f45196f17a1 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using System.Text.Json.Nodes; using System.Reflection; using Aspire.Hosting.ApplicationModel; diff --git a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs index 9a8f5fea8e1..960102de076 100644 --- a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs +++ b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; diff --git a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs index 7368b7c386d..21b9d8385e2 100644 --- a/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs +++ b/tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using System.Data; using Aspire.TestUtilities; using Aspire.Hosting.ApplicationModel; diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs index 7a4551e7b65..e4ac4040e8a 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresFunctionalTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using System.Data; using System.Net; using Aspire.TestUtilities; diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 94691ed24e1..5970b4167d7 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -3,6 +3,7 @@ #pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. #pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs index 5755c3a7e53..5f4971c2754 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIRECERTIFICATES001 +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using System.Collections.Concurrent; using System.Diagnostics; diff --git a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs index 7e33bc8d8c8..51ddd75b15f 100644 --- a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. #pragma warning disable IDE0005 // Using directive is unnecessary. using Aspire.Hosting.Dcp.Model; diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs index c341b81ef6f..267ca64514b 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using Aspire.Hosting.Utils; namespace Aspire.Hosting.Tests; diff --git a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs index a6dcf18cc01..27c936b13d1 100644 --- a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. + using System.Diagnostics; using Aspire.Hosting.Dcp; using Aspire.Hosting.Utils; From 0247c03ab08f86e271fb5b46d5451b63d28c2ed9 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Mon, 18 May 2026 20:37:10 -0700 Subject: [PATCH 23/38] Consolidate persistence lifetime annotations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ExecutableLifetimeAnnotation.cs | 18 ---- .../ApplicationModel/Lifetime.cs | 26 ------ .../ParentProcessLifetimeAnnotation.cs | 20 ----- .../ApplicationModel/PersistenceAnnotation.cs | 62 +++++++++++++ .../ApplicationModel/ResourceExtensions.cs | 86 ++++++++++--------- .../ContainerResourceBuilderExtensions.cs | 28 +++--- src/Aspire.Hosting/Dcp/ContainerCreator.cs | 6 +- src/Aspire.Hosting/Dcp/ExecutableCreator.cs | 6 +- .../ResourceBuilderExtensions.cs | 58 ++++++------- .../AddAzureKustoTests.cs | 7 +- .../AzureEventHubsExtensionsTests.cs | 25 +++--- .../AzureServiceBusExtensionsTests.cs | 24 ++++-- ...DevTunnelResourceBuilderExtensionsTests.cs | 10 +-- ...ExecutableResourceBuilderExtensionTests.cs | 6 +- .../PersistentContainerWarningTests.cs | 5 +- .../ProjectResourceBuilderExtensionTests.cs | 6 +- .../ResourceBuilderLifetimeTests.cs | 42 +++++---- 17 files changed, 223 insertions(+), 212 deletions(-) delete mode 100644 src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs delete mode 100644 src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs create mode 100644 src/Aspire.Hosting/ApplicationModel/PersistenceAnnotation.cs diff --git a/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs deleted file mode 100644 index fc5eb016df3..00000000000 --- a/src/Aspire.Hosting/ApplicationModel/ExecutableLifetimeAnnotation.cs +++ /dev/null @@ -1,18 +0,0 @@ -// 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; - -namespace Aspire.Hosting.ApplicationModel; - -/// -/// Annotation that controls the lifetime of an executable resource. -/// -[DebuggerDisplay("Type = {GetType().Name,nq}")] -public sealed class ExecutableLifetimeAnnotation : IResourceAnnotation -{ - /// - /// Gets or sets the lifetime type for the executable resource. - /// - public required Lifetime Lifetime { get; set; } -} diff --git a/src/Aspire.Hosting/ApplicationModel/Lifetime.cs b/src/Aspire.Hosting/ApplicationModel/Lifetime.cs index 1fa30b28552..851ab02a2bc 100644 --- a/src/Aspire.Hosting/ApplicationModel/Lifetime.cs +++ b/src/Aspire.Hosting/ApplicationModel/Lifetime.cs @@ -1,8 +1,6 @@ // 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; - namespace Aspire.Hosting.ApplicationModel; /// @@ -20,27 +18,3 @@ public enum Lifetime /// Persistent, } - -/// -/// Annotation that controls the lifetime of a resource. -/// -[DebuggerDisplay("Type = {GetType().Name,nq}")] -public sealed class LifetimeAnnotation : IResourceAnnotation -{ - /// - /// Gets or sets the lifetime type for the resource. - /// - public required Lifetime Lifetime { get; set; } -} - -/// -/// Annotation that configures a resource to match the lifetime of another resource. -/// -[DebuggerDisplay("Type = {GetType().Name,nq}, Source = {SourceResource.Name,nq}")] -internal sealed class LifetimeReferenceAnnotation(IResource sourceResource) : IResourceAnnotation -{ - /// - /// Gets the resource whose lifetime should be used. - /// - public IResource SourceResource { get; } = sourceResource ?? throw new ArgumentNullException(nameof(sourceResource)); -} diff --git a/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs deleted file mode 100644 index 7893d75e12c..00000000000 --- a/src/Aspire.Hosting/ApplicationModel/ParentProcessLifetimeAnnotation.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Aspire.Hosting.ApplicationModel; - -/// -/// Configures a persistent resource to be monitored by a parent process identity. -/// -internal sealed class ParentProcessLifetimeAnnotation(int parentProcessId, DateTime parentProcessTimestamp) : IResourceAnnotation -{ - /// - /// Gets the ID of the parent process to monitor. - /// - public int ParentProcessId { get; } = parentProcessId; - - /// - /// Gets the identity timestamp of the parent process to monitor. - /// - public DateTime ParentProcessTimestamp { get; } = parentProcessTimestamp; -} diff --git a/src/Aspire.Hosting/ApplicationModel/PersistenceAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/PersistenceAnnotation.cs new file mode 100644 index 00000000000..a4bd96396ad --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/PersistenceAnnotation.cs @@ -0,0 +1,62 @@ +// 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; +using System.Diagnostics.CodeAnalysis; + +namespace Aspire.Hosting.ApplicationModel; + +/// +/// Persistence modes for resources that support lifetime configuration. +/// +[Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +public enum PersistenceMode +{ + /// + /// Create the resource when the app host process starts and dispose of it when the app host process shuts down. + /// + Session, + + /// + /// Attempt to re-use a previously created resource if one exists. Do not destroy the resource on app host process shutdown. + /// + Persistent, + + /// + /// Match another resource's persistence behavior. + /// + Resource, + + /// + /// Use persistent behavior scoped to a parent process identity. + /// + ParentProcess, +} + +/// +/// Annotation that controls the persistence behavior of a resource. +/// +[Experimental("ASPIREPERSISTENCE001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +[DebuggerDisplay("Type = {GetType().Name,nq}, Mode = {Mode}")] +public sealed class PersistenceAnnotation : IResourceAnnotation +{ + /// + /// Gets or sets the persistence mode. + /// + public required PersistenceMode Mode { get; set; } + + /// + /// Gets or sets the source resource when is . + /// + public IResource? SourceResource { get; set; } + + /// + /// Gets or sets the parent process ID when is . + /// + public int? ParentProcessId { get; set; } + + /// + /// Gets or sets the parent process identity timestamp when is . + /// + public DateTime? ParentProcessTimestamp { get; set; } +} diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 5756cda1fac..5c4a859ed4b 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Persistence annotation APIs are experimental. + using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using Aspire.Dashboard.Model; @@ -1054,7 +1056,7 @@ internal static bool IsBuildOnlyContainer(this IResource resource) /// /// The resource to get the lifetime type for. /// - /// The from the for the resource (if the annotation exists). + /// The from the for the resource (if the annotation exists). /// Defaults to if the annotation is not set. /// internal static Lifetime GetLifetimeType(this IResource resource) @@ -1069,26 +1071,28 @@ private static Lifetime GetLifetimeType(IResource resource, HashSet v throw new InvalidOperationException($"A circular lifetime reference was detected for resource '{resource.Name}'."); } - var annotations = GetAnnotationsSnapshot(resource); - for (var i = annotations.Length - 1; i >= 0; i--) + if (resource.TryGetLastAnnotation(out var persistenceAnnotation)) { - var annotation = annotations[i]; - switch (annotation) + return persistenceAnnotation.Mode switch { - case LifetimeAnnotation lifetimeAnnotation: - return lifetimeAnnotation.Lifetime; - case LifetimeReferenceAnnotation lifetimeReferenceAnnotation: - return GetLifetimeType(lifetimeReferenceAnnotation.SourceResource, visitedResources); - case ExecutableLifetimeAnnotation executableLifetimeAnnotation: - return executableLifetimeAnnotation.Lifetime; - case ContainerLifetimeAnnotation containerLifetimeAnnotation: - return containerLifetimeAnnotation.Lifetime switch - { - ContainerLifetime.Session => Lifetime.Session, - ContainerLifetime.Persistent => Lifetime.Persistent, - _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), containerLifetimeAnnotation.Lifetime)}'.") - }; - } + PersistenceMode.Session => Lifetime.Session, + PersistenceMode.Persistent => Lifetime.Persistent, + PersistenceMode.Resource => persistenceAnnotation.SourceResource is { } sourceResource + ? GetLifetimeType(sourceResource, visitedResources) + : throw new InvalidOperationException($"Resource '{resource.Name}' has a resource persistence mode but no source resource."), + PersistenceMode.ParentProcess => Lifetime.Persistent, + _ => throw new InvalidOperationException($"Unknown persistence mode '{Enum.GetName(typeof(PersistenceMode), persistenceAnnotation.Mode)}'.") + }; + } + + if (resource.TryGetLastAnnotation(out var containerLifetimeAnnotation)) + { + return containerLifetimeAnnotation.Lifetime switch + { + ContainerLifetime.Session => Lifetime.Session, + ContainerLifetime.Persistent => Lifetime.Persistent, + _ => throw new InvalidOperationException($"Unknown container lifetime '{Enum.GetName(typeof(ContainerLifetime), containerLifetimeAnnotation.Lifetime)}'.") + }; } return Lifetime.Session; @@ -1113,49 +1117,47 @@ internal static string GetOtelServiceInstanceId(this IResource resource, DcpInst /// Determines whether the specified resource has a parent process lifetime. /// /// The resource to get parent process lifetime behavior for. - /// The parent process lifetime annotation if one exists. + /// The parent process ID if one exists. + /// The parent process identity timestamp if one exists. /// if the resource has a parent process lifetime, otherwise . - internal static bool TryGetParentProcessLifetime(this IResource resource, [NotNullWhen(true)] out ParentProcessLifetimeAnnotation? annotation) + internal static bool TryGetParentProcessLifetime(this IResource resource, out int parentProcessId, out DateTime parentProcessTimestamp) { - return TryGetParentProcessLifetime(resource, [], out annotation); + return TryGetParentProcessLifetime(resource, [], out parentProcessId, out parentProcessTimestamp); } - private static bool TryGetParentProcessLifetime(IResource resource, HashSet visitedResources, [NotNullWhen(true)] out ParentProcessLifetimeAnnotation? annotation) + private static bool TryGetParentProcessLifetime(IResource resource, HashSet visitedResources, out int parentProcessId, out DateTime parentProcessTimestamp) { if (!visitedResources.Add(resource)) { throw new InvalidOperationException($"A circular lifetime reference was detected for resource '{resource.Name}'."); } - var annotations = GetAnnotationsSnapshot(resource); - for (var i = annotations.Length - 1; i >= 0; i--) + if (resource.TryGetLastAnnotation(out var persistenceAnnotation)) { - var resourceAnnotation = annotations[i]; - switch (resourceAnnotation) + switch (persistenceAnnotation.Mode) { - case ParentProcessLifetimeAnnotation parentProcessLifetimeAnnotation: - annotation = parentProcessLifetimeAnnotation; + case PersistenceMode.ParentProcess when persistenceAnnotation.ParentProcessId is { } id && persistenceAnnotation.ParentProcessTimestamp is { } timestamp: + parentProcessId = id; + parentProcessTimestamp = timestamp; return true; - case LifetimeReferenceAnnotation lifetimeReferenceAnnotation: - return TryGetParentProcessLifetime(lifetimeReferenceAnnotation.SourceResource, visitedResources, out annotation); - case LifetimeAnnotation or ExecutableLifetimeAnnotation or ContainerLifetimeAnnotation: - annotation = null; + case PersistenceMode.ParentProcess: + throw new InvalidOperationException($"Resource '{resource.Name}' has a parent process persistence mode but no parent process identity."); + case PersistenceMode.Resource: + return persistenceAnnotation.SourceResource is { } sourceResource + ? TryGetParentProcessLifetime(sourceResource, visitedResources, out parentProcessId, out parentProcessTimestamp) + : throw new InvalidOperationException($"Resource '{resource.Name}' has a resource persistence mode but no source resource."); + case PersistenceMode.Session or PersistenceMode.Persistent: + parentProcessId = 0; + parentProcessTimestamp = default; return false; } } - annotation = null; + parentProcessId = 0; + parentProcessTimestamp = default; return false; } - private static IResourceAnnotation[] GetAnnotationsSnapshot(IResource resource) - { - lock (resource.Annotations) - { - return resource.Annotations.ToArray(); - } - } - /// /// Determines whether the specified resource has a pull policy annotation and retrieves the value if it does. /// diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index e4aadcb3cac..db5f953014b 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -4,6 +4,7 @@ #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 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 ASPIREFILESYSTEM001 // Type is for evaluation purposes only +#pragma warning disable ASPIREPERSISTENCE001 // Persistence annotation APIs are experimental. using System.Diagnostics.CodeAnalysis; using System.Text; @@ -556,25 +557,16 @@ public static IResourceBuilder WithLifetime(this IResourceBuilder build { ArgumentNullException.ThrowIfNull(builder); - foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) - { - builder.Resource.Annotations.Remove(annotation); - } - - foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) - { - builder.Resource.Annotations.Remove(annotation); - } - - var resourceLifetime = lifetime switch - { - ContainerLifetime.Session => Lifetime.Session, - ContainerLifetime.Persistent => Lifetime.Persistent, - _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) - }; - return builder - .WithAnnotation(new LifetimeAnnotation { Lifetime = resourceLifetime }, ResourceAnnotationMutationBehavior.Replace) + .WithAnnotation(new PersistenceAnnotation + { + Mode = lifetime switch + { + ContainerLifetime.Session => PersistenceMode.Session, + ContainerLifetime.Persistent => PersistenceMode.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + } + }, ResourceAnnotationMutationBehavior.Replace) .WithAnnotation(new ContainerLifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); } diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index 2b5efe0b163..7f58f9f329f 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -209,10 +209,10 @@ public IEnumerable> PrepareObjects() private static void ApplyMonitorProcess(IResource resource, ContainerSpec spec) { - if (resource.TryGetParentProcessLifetime(out var annotation)) + if (resource.TryGetParentProcessLifetime(out var parentProcessId, out var parentProcessTimestamp)) { - spec.MonitorPid = annotation.ParentProcessId; - spec.MonitorTimestamp = annotation.ParentProcessTimestamp; + spec.MonitorPid = parentProcessId; + spec.MonitorTimestamp = parentProcessTimestamp; } } diff --git a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs index d7d8e888748..99a5d7ca5e1 100644 --- a/src/Aspire.Hosting/Dcp/ExecutableCreator.cs +++ b/src/Aspire.Hosting/Dcp/ExecutableCreator.cs @@ -321,10 +321,10 @@ private void PreparePlainExecutables() private static void ApplyMonitorProcess(IResource resource, ExecutableSpec spec) { - if (resource.TryGetParentProcessLifetime(out var annotation)) + if (resource.TryGetParentProcessLifetime(out var parentProcessId, out var parentProcessTimestamp)) { - spec.MonitorPid = annotation.ParentProcessId; - spec.MonitorTimestamp = annotation.ParentProcessTimestamp; + spec.MonitorPid = parentProcessId; + spec.MonitorTimestamp = parentProcessTimestamp; } } diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 5b5c77fceb2..1d8d60c655a 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREPERSISTENCE001 // Persistence annotation APIs are experimental. + using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; using System.Net.Sockets; @@ -112,11 +114,15 @@ public static IResourceBuilder WithLifetimeOf(this IResourceBuild ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(sourceBuilder); - RemoveLifetimeAnnotations(builder); - if (builder.Resource is ContainerResource or ExecutableResource or ProjectResource) { - return builder.WithAnnotation(new LifetimeReferenceAnnotation(sourceBuilder.Resource), ResourceAnnotationMutationBehavior.Replace); + RemoveLegacyLifetimeAnnotations(builder); + + return builder.WithAnnotation(new PersistenceAnnotation + { + Mode = PersistenceMode.Resource, + SourceResource = sourceBuilder.Resource + }, ResourceAnnotationMutationBehavior.Replace); } throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); @@ -161,50 +167,44 @@ public static IResourceBuilder WithParentProcessLifetime(this IResourceBui using var parentProcess = SystemProcess.GetProcessById(parentProcessId); var parentProcessIdentity = DcpProcessMonitor.GetMonitorProcessIdentity(parentProcess); - return ApplyLifetime(builder, Lifetime.Persistent) - .WithAnnotation(new ParentProcessLifetimeAnnotation(parentProcessIdentity.ProcessId, parentProcessIdentity.Timestamp), ResourceAnnotationMutationBehavior.Replace); + RemoveLegacyLifetimeAnnotations(builder); + + return builder.WithAnnotation(new PersistenceAnnotation + { + Mode = PersistenceMode.ParentProcess, + ParentProcessId = parentProcessIdentity.ProcessId, + ParentProcessTimestamp = parentProcessIdentity.Timestamp + }, ResourceAnnotationMutationBehavior.Replace); } private static IResourceBuilder ApplyLifetime(IResourceBuilder builder, Lifetime lifetime) where T : IResource { - RemoveLifetimeAnnotations(builder); - if (builder.Resource is ContainerResource or ExecutableResource or ProjectResource) { - return builder.WithAnnotation(new LifetimeAnnotation { Lifetime = lifetime }, ResourceAnnotationMutationBehavior.Replace); + RemoveLegacyLifetimeAnnotations(builder); + + return builder.WithAnnotation(new PersistenceAnnotation + { + Mode = lifetime switch + { + Lifetime.Session => PersistenceMode.Session, + Lifetime.Persistent => PersistenceMode.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + } + }, ResourceAnnotationMutationBehavior.Replace); } throw new InvalidOperationException($"Resource '{builder.Resource.Name}' does not support lifetime configuration."); } - private static void RemoveLifetimeAnnotations(IResourceBuilder builder) + private static void RemoveLegacyLifetimeAnnotations(IResourceBuilder builder) where T : IResource { - foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) - { - builder.Resource.Annotations.Remove(annotation); - } - - foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) - { - builder.Resource.Annotations.Remove(annotation); - } - foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) { builder.Resource.Annotations.Remove(annotation); } - - foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) - { - builder.Resource.Annotations.Remove(annotation); - } - - foreach (var annotation in builder.Resource.Annotations.OfType().ToArray()) - { - builder.Resource.Annotations.Remove(annotation); - } } /// diff --git a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs index 37987570a02..2560c5455a3 100644 --- a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs +++ b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs @@ -297,7 +297,7 @@ public void RunAsEmulator_WithCustomImage_ShouldUseSpecifiedValues() } [Fact] - public void RunAsEmulator_WithCustomLifetime_ShouldConfigureLifetimeAnnotation() + public void RunAsEmulator_WithCustomLifetime_ShouldConfigurePersistenceAnnotation() { // Arrange using var builder = TestDistributedApplicationBuilder.Create(); @@ -309,9 +309,8 @@ public void RunAsEmulator_WithCustomLifetime_ShouldConfigureLifetimeAnnotation() }); // Assert - var lifetimeAnnotation = resourceBuilder.Resource.Annotations.OfType().SingleOrDefault(); - Assert.NotNull(lifetimeAnnotation); - Assert.Equal(Lifetime.Persistent, lifetimeAnnotation.Lifetime); + var persistenceAnnotation = Assert.Single(resourceBuilder.Resource.Annotations.OfType()); + Assert.Equal(PersistenceMode.Persistent, persistenceAnnotation.Mode); } [Fact] diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs index c9ca4148015..994756fddd2 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureEventHubsExtensionsTests.cs @@ -5,7 +5,6 @@ using System.Text; using System.Text.Json.Nodes; -using System.Reflection; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure.EventHubs; using Aspire.Hosting.Utils; @@ -526,11 +525,11 @@ public void AddAzureEventHubsWithEmulator_SetsStorageLifetime(bool isPersistent) Assert.NotNull(azurite); - var sourceResource = GetLifetimeReferenceSource(azurite); + var sourceResource = GetPersistenceReferenceSource(azurite); Assert.Same(eventHubs.Resource.Annotations, sourceResource.Annotations); - eventHubs.Resource.TryGetLastAnnotation(out var lifetimeAnnotation); - Assert.Equal(lifetime, lifetimeAnnotation?.Lifetime); + var persistenceAnnotation = Assert.Single(eventHubs.Resource.Annotations.OfType()); + Assert.Equal(ToPersistenceMode(lifetime), persistenceAnnotation.Mode); } [Fact] @@ -543,7 +542,7 @@ public void AddAzureEventHubsWithEmulator_DoesNotSetStorageLifetimeWithoutContai var azurite = builder.Resources.FirstOrDefault(x => x.Name == "eh-storage"); Assert.NotNull(azurite); - Assert.DoesNotContain(azurite.Annotations, a => a.GetType().Name == "LifetimeReferenceAnnotation"); + Assert.Empty(azurite.Annotations.OfType()); } [Fact] @@ -555,14 +554,20 @@ public void RunAsEmulator_CalledTwice_Throws() Assert.Throws(() => eventHubs.RunAsEmulator()); } - private static IResource GetLifetimeReferenceSource(IResource resource) + private static IResource GetPersistenceReferenceSource(IResource resource) { - var annotation = Assert.Single(resource.Annotations, a => a.GetType().Name == "LifetimeReferenceAnnotation"); - var sourceResource = annotation.GetType().GetProperty("SourceResource", BindingFlags.Instance | BindingFlags.Public)!.GetValue(annotation); - - return Assert.IsAssignableFrom(sourceResource); + var annotation = Assert.Single(resource.Annotations.OfType()); + return Assert.IsAssignableFrom(annotation.SourceResource); } + private static PersistenceMode ToPersistenceMode(Lifetime lifetime) => + lifetime switch + { + Lifetime.Session => PersistenceMode.Session, + Lifetime.Persistent => PersistenceMode.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + }; + [Fact] public void AzureEventHubsHasCorrectConnectionStrings() { diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs index f45196f17a1..b8f24faa8ef 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureServiceBusExtensionsTests.cs @@ -622,11 +622,11 @@ public void AddAzureServiceBusWithEmulator_SetsSqlLifetime(bool isPersistent) Assert.NotNull(sql); - var sourceResource = GetLifetimeReferenceSource(sql); + var sourceResource = GetPersistenceReferenceSource(sql); Assert.Same(serviceBus.Resource.Annotations, sourceResource.Annotations); - serviceBus.Resource.TryGetLastAnnotation(out var lifetimeAnnotation); - Assert.Equal(lifetime, lifetimeAnnotation?.Lifetime); + var persistenceAnnotation = Assert.Single(serviceBus.Resource.Annotations.OfType()); + Assert.Equal(ToPersistenceMode(lifetime), persistenceAnnotation.Mode); } [Fact] @@ -639,7 +639,7 @@ public void AddAzureServiceBusWithEmulator_DoesNotSetSqlLifetimeWithoutContainer var sql = builder.Resources.FirstOrDefault(x => x.Name == "sb-mssql"); Assert.NotNull(sql); - Assert.DoesNotContain(sql.Annotations, a => a.GetType().Name == "LifetimeReferenceAnnotation"); + Assert.Empty(sql.Annotations.OfType()); } [Fact] @@ -651,14 +651,20 @@ public void RunAsEmulator_CalledTwice_Throws() Assert.Throws(() => serviceBus.RunAsEmulator()); } - private static IResource GetLifetimeReferenceSource(IResource resource) + private static IResource GetPersistenceReferenceSource(IResource resource) { - var annotation = Assert.Single(resource.Annotations, a => a.GetType().Name == "LifetimeReferenceAnnotation"); - var sourceResource = annotation.GetType().GetProperty("SourceResource", BindingFlags.Instance | BindingFlags.Public)!.GetValue(annotation); - - return Assert.IsAssignableFrom(sourceResource); + var annotation = Assert.Single(resource.Annotations.OfType()); + return Assert.IsAssignableFrom(annotation.SourceResource); } + private static PersistenceMode ToPersistenceMode(Lifetime lifetime) => + lifetime switch + { + Lifetime.Session => PersistenceMode.Session, + Lifetime.Persistent => PersistenceMode.Persistent, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null) + }; + [Fact] public void AzureServiceBusHasCorrectConnectionStrings() { diff --git a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs index 960102de076..0066b97b898 100644 --- a/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs +++ b/tests/Aspire.Hosting.DevTunnels.Tests/DevTunnelResourceBuilderExtensionsTests.cs @@ -58,25 +58,25 @@ public void AddDevTunnel_WithSpecificTunnelId_SetsTunnelIdProperty() } [Fact] - public void AddDevTunnel_WithPersistentLifetime_AddsLifetimeAnnotation() + public void AddDevTunnel_WithPersistentLifetime_AddsPersistenceAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var tunnel = builder.AddDevTunnel("tunnel", "custom-id") .WithPersistentLifetime(); - Assert.True(tunnel.Resource.TryGetLastAnnotation(out var annotation)); - Assert.Equal(Lifetime.Persistent, annotation.Lifetime); + var annotation = Assert.Single(tunnel.Resource.Annotations.OfType()); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); } [Fact] - public void AddDevTunnel_DefaultLifetimeDoesNotAddLifetimeAnnotation() + public void AddDevTunnel_DefaultLifetimeDoesNotAddPersistenceAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var tunnel = builder.AddDevTunnel("tunnel", "custom-id"); - Assert.False(tunnel.Resource.TryGetLastAnnotation(out _)); + Assert.Empty(tunnel.Resource.Annotations.OfType()); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs index 51ddd75b15f..ca7ff076eee 100644 --- a/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ExecutableResourceBuilderExtensionTests.cs @@ -74,14 +74,14 @@ public void WithWorkingDirectoryAllowsEmptyString() } [Fact] - public void WithPersistentLifetimeAddsLifetimeAnnotation() + public void WithPersistentLifetimeAddsPersistenceAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var executable = builder.AddExecutable("myexe", "command", "workingdirectory") .WithPersistentLifetime(); - var annotation = executable.Resource.Annotations.OfType().Single(); - Assert.Equal(Lifetime.Persistent, annotation.Lifetime); + var annotation = executable.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs b/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs index 5eb50f7cec2..79befbc4c4b 100644 --- a/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs +++ b/tests/Aspire.Hosting.Tests/PersistentContainerWarningTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. #pragma warning disable ASPIREUSERSECRETS001 +#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental. using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.UserSecrets; @@ -26,7 +27,7 @@ public async Task PersistentContainerWithoutUserSecrets_LogsWarning() var resources = new ResourceCollection(); var container = new ContainerResource("my-container"); - container.Annotations.Add(new LifetimeAnnotation { Lifetime = Lifetime.Persistent }); + container.Annotations.Add(new PersistenceAnnotation { Mode = PersistenceMode.Persistent }); resources.Add(container); var model = new DistributedApplicationModel(resources); @@ -50,7 +51,7 @@ public async Task PersistentContainerWithUserSecrets_DoesNotLogWarning() var resources = new ResourceCollection(); var container = new ContainerResource("my-container"); - container.Annotations.Add(new LifetimeAnnotation { Lifetime = Lifetime.Persistent }); + container.Annotations.Add(new PersistenceAnnotation { Mode = PersistenceMode.Persistent }); resources.Add(container); var model = new DistributedApplicationModel(resources); diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs index 267ca64514b..f83a5de5c79 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceBuilderExtensionTests.cs @@ -11,15 +11,15 @@ namespace Aspire.Hosting.Tests; public class ProjectResourceBuilderExtensionTests { [Fact] - public void WithPersistentLifetimeAddsLifetimeAnnotation() + public void WithPersistentLifetimeAddsPersistenceAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var project = builder.AddProject("project", options => options.ExcludeLaunchProfile = true) .WithPersistentLifetime(); - var annotation = project.Resource.Annotations.OfType().Single(); - Assert.Equal(Lifetime.Persistent, annotation.Lifetime); + var annotation = project.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); } private sealed class TestProject : IProjectMetadata diff --git a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs index 27c936b13d1..ededfd678b9 100644 --- a/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs +++ b/tests/Aspire.Hosting.Tests/ResourceBuilderLifetimeTests.cs @@ -13,15 +13,15 @@ namespace Aspire.Hosting.Tests; public class ResourceBuilderLifetimeTests { [Fact] - public void WithPersistentLifetimeAddsLifetimeAnnotation() + public void WithPersistentLifetimeAddsPersistenceAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); var container = builder.AddContainer("container", "image") .WithPersistentLifetime(); - var annotation = container.Resource.Annotations.OfType().Single(); - Assert.Equal(Lifetime.Persistent, annotation.Lifetime); + var annotation = container.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); } [Fact] @@ -38,7 +38,7 @@ public void WithPersistentLifetimeRejectsUnsupportedResourceTypes() } [Fact] - public void WithPersistentLifetimeRemovesParentProcessLifetimeAnnotation() + public void WithPersistentLifetimeReplacesPersistenceAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); @@ -46,23 +46,26 @@ public void WithPersistentLifetimeRemovesParentProcessLifetimeAnnotation() .WithParentProcessLifetime(Environment.ProcessId) .WithPersistentLifetime(); - Assert.False(container.Resource.TryGetLastAnnotation(out _)); + var annotation = container.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Persistent, annotation.Mode); + Assert.Null(annotation.ParentProcessId); + Assert.Null(annotation.ParentProcessTimestamp); } [Fact] - public void WithParentProcessLifetimeReplacesExistingParentProcessLifetimeAnnotation() + public void WithParentProcessLifetimeReplacesExistingPersistenceAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); - var originalTimestamp = new DateTime(2026, 5, 18, 1, 2, 3, DateTimeKind.Utc); var container = builder.AddContainer("container", "image") - .WithAnnotation(new ParentProcessLifetimeAnnotation(parentProcessId: 1, parentProcessTimestamp: originalTimestamp)); + .WithPersistentLifetime(); container.WithParentProcessLifetime(Environment.ProcessId); - var annotation = Assert.Single(container.Resource.Annotations.OfType()); + var annotation = Assert.Single(container.Resource.Annotations.OfType()); + Assert.Equal(PersistenceMode.ParentProcess, annotation.Mode); Assert.Equal(Environment.ProcessId, annotation.ParentProcessId); - Assert.NotEqual(originalTimestamp, annotation.ParentProcessTimestamp); + Assert.NotNull(annotation.ParentProcessTimestamp); } [Fact] @@ -77,7 +80,9 @@ public void WithLifetimeOfMatchesSourceResourceLifetime() .WithLifetimeOf(source); Assert.Equal(Lifetime.Persistent, container.Resource.GetLifetimeType()); - Assert.Empty(container.Resource.Annotations.OfType()); + var annotation = Assert.Single(container.Resource.Annotations.OfType()); + Assert.Equal(PersistenceMode.Resource, annotation.Mode); + Assert.Same(source.Resource, annotation.SourceResource); source.WithSessionLifetime(); @@ -96,13 +101,13 @@ public void WithLifetimeOfMatchesSourceParentProcessLifetime() var container = builder.AddContainer("container", "image") .WithLifetimeOf(source); - Assert.True(container.Resource.TryGetParentProcessLifetime(out var parentProcessLifetimeAnnotation)); - Assert.Equal(parentProcessIdentity.ProcessId, parentProcessLifetimeAnnotation.ParentProcessId); - Assert.Equal(parentProcessIdentity.Timestamp, parentProcessLifetimeAnnotation.ParentProcessTimestamp); + Assert.True(container.Resource.TryGetParentProcessLifetime(out var parentProcessId, out var parentProcessTimestamp)); + Assert.Equal(parentProcessIdentity.ProcessId, parentProcessId); + Assert.Equal(parentProcessIdentity.Timestamp, parentProcessTimestamp); source.WithSessionLifetime(); - Assert.False(container.Resource.TryGetParentProcessLifetime(out _)); + Assert.False(container.Resource.TryGetParentProcessLifetime(out _, out _)); } [Fact] @@ -150,7 +155,7 @@ public void WithLifetimeOfDetectsCircularReferences() } [Fact] - public void WithSessionLifetimeRemovesParentProcessLifetimeAnnotation() + public void WithSessionLifetimeReplacesPersistenceAnnotation() { using var builder = TestDistributedApplicationBuilder.Create(); @@ -158,6 +163,9 @@ public void WithSessionLifetimeRemovesParentProcessLifetimeAnnotation() .WithParentProcessLifetime(Environment.ProcessId) .WithSessionLifetime(); - Assert.False(container.Resource.TryGetLastAnnotation(out _)); + var annotation = container.Resource.Annotations.OfType().Single(); + Assert.Equal(PersistenceMode.Session, annotation.Mode); + Assert.Null(annotation.ParentProcessId); + Assert.Null(annotation.ParentProcessTimestamp); } } From f24db522cd76e46d59342347273afbb2d3a7d7cd Mon Sep 17 00:00:00 2001 From: David Negstad Date: Tue, 19 May 2026 21:19:33 -0700 Subject: [PATCH 24/38] Preserve endpoint proxy API binary signatures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CompatibilitySuppressions.xml | 37 +------ .../ContainerResourceBuilderExtensions.cs | 13 +++ .../ResourceBuilderExtensions.cs | 92 ++++++++++++++++++ .../Aspire.Hosting.Tests/WithEndpointTests.cs | 97 ++++++++++++++++++- 4 files changed, 202 insertions(+), 37 deletions(-) diff --git a/src/Aspire.Hosting/CompatibilitySuppressions.xml b/src/Aspire.Hosting/CompatibilitySuppressions.xml index 27cc2519e8f..f2dd583e0d3 100644 --- a/src/Aspire.Hosting/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting/CompatibilitySuppressions.xml @@ -22,41 +22,6 @@ lib/net8.0/Aspire.Hosting.dll true - - CP0002 - M:Aspire.Hosting.ContainerResourceBuilderExtensions.WithEndpointProxySupport``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Boolean) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - - CP0002 - M:Aspire.Hosting.ResourceBuilderExtensions.WithEndpoint``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.String,System.String,System.Boolean,System.Nullable{System.Boolean},System.Nullable{System.Net.Sockets.ProtocolType}) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - - CP0002 - M:Aspire.Hosting.ResourceBuilderExtensions.WithEndpoint``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.String,System.String,System.Boolean,System.Nullable{System.Boolean}) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - - CP0002 - M:Aspire.Hosting.ResourceBuilderExtensions.WithHttpEndpoint``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.String,System.Boolean) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - - CP0002 - M:Aspire.Hosting.ResourceBuilderExtensions.WithHttpsEndpoint``1(Aspire.Hosting.ApplicationModel.IResourceBuilder{``0},System.Nullable{System.Int32},System.Nullable{System.Int32},System.String,System.String,System.Boolean) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - CP0006 M:Aspire.Hosting.Pipelines.IDeploymentStateManager.ClearAllStateAsync(System.Threading.CancellationToken) @@ -85,4 +50,4 @@ lib/net8.0/Aspire.Hosting.dll true - \ No newline at end of file + diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index db5f953014b..b20cef74238 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -23,6 +23,19 @@ namespace Aspire.Hosting; /// public static class ContainerResourceBuilderExtensions { + /// + /// Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + /// + /// The resource type. + /// The resource builder. + /// Should endpoints for the resource support using a proxy? + /// The . + [AspireExportIgnore(Reason = "Binary compatibility shim for the resource-level WithEndpointProxySupport overload.")] + public static IResourceBuilder WithEndpointProxySupport(IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource + { + return ResourceBuilderExtensions.WithEndpointProxySupport(builder, proxyEnabled); + } + /// /// Ensures that a container resource has PipelineStepAnnotations for building and pushing. /// diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 1d8d60c655a..50a0f1a9a8d 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -1589,6 +1589,31 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build return builder.WithAnnotation(annotation); } + /// + /// Exposes an endpoint on a resource. A reference to this endpoint can be retrieved using . + /// + /// The resource type. + /// The resource builder. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// An optional port. This is the port that will be given to other resource to communicate with this resource. + /// An optional scheme e.g. (http/https). Defaults to the argument if it is defined or "tcp" otherwise. + /// An optional name of the endpoint. Defaults to the scheme name if not specified. + /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. + /// Indicates that this endpoint should be exposed externally at publish time. + /// Network protocol: TCP or UDP are supported today, others possibly in future. + /// Specifies if the endpoint will be proxied by DCP. + /// The . + /// Throws an exception if an endpoint with the same name already exists on the specified resource. + /// + /// This overload preserves binary compatibility for callers compiled against the previous signature. + /// New source that omits binds to the nullable overload where omission is represented as . + /// + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port, int? targetPort, string? scheme, [EndpointName] string? name, string? env, bool isProxied, bool? isExternal, ProtocolType? protocol) where T : IResourceWithEndpoints + { + return WithEndpoint(builder, port, targetPort, scheme, name, env, (bool?)isProxied, isExternal, protocol); + } + /// /// Configures the environment variable callback for an endpoint's target port. /// If a callback already exists (from a prior call), the annotation's @@ -1670,6 +1695,31 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build return WithEndpoint(builder, port, targetPort, scheme, name, env, isProxied, isExternal, protocol: null); } + /// + /// Exposes an endpoint on a resource. This endpoint reference can be retrieved using . + /// The endpoint name will be the scheme name if not specified. + /// + /// The resource type. + /// The resource builder. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// An optional port. This is the port that will be given to other resource to communicate with this resource. + /// An optional scheme e.g. (http/https). Defaults to "tcp" if not specified. + /// An optional name of the endpoint. Defaults to the scheme name if not specified. + /// An optional name of the environment variable that will be used to inject the . If the target port is null one will be dynamically generated and assigned to the environment variable. + /// Indicates that this endpoint should be exposed externally at publish time. + /// Specifies if the endpoint will be proxied by DCP. + /// The . + /// Throws an exception if an endpoint with the same name already exists on the specified resource. + /// + /// This overload preserves binary compatibility for callers compiled against the previous signature. + /// New source that omits binds to the nullable overload where omission is represented as . + /// + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port, int? targetPort, string? scheme, [EndpointName] string? name, string? env, bool isProxied, bool? isExternal) where T : IResourceWithEndpoints + { + return WithEndpoint(builder, port, targetPort, scheme, name, env, (bool?)isProxied, isExternal, protocol: null); + } + /// /// Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. /// This endpoint reference can be retrieved using . @@ -1695,6 +1745,27 @@ public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder b return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "http", name: name, env: env, isProxied: isProxied); } + /// + /// Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. + /// + /// The resource type. + /// The resource builder. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// An optional port. This is the port that will be given to other resource to communicate with this resource. + /// An optional name of the endpoint. Defaults to "http" if not specified. + /// An optional name of the environment variable to inject. + /// Specifies if the endpoint will be proxied by DCP. + /// The . + /// + /// This overload preserves binary compatibility for callers compiled against the previous signature. + /// New source that omits binds to the nullable overload where omission is represented as . + /// + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static IResourceBuilder WithHttpEndpoint(this IResourceBuilder builder, int? port, int? targetPort, [EndpointName] string? name, string? env, bool isProxied) where T : IResourceWithEndpoints + { + return WithHttpEndpoint(builder, port, targetPort, name, env, (bool?)isProxied); + } + /// /// Exposes an HTTPS endpoint on a resource, or updates the existing HTTPS endpoint if one with the same name already exists. /// This endpoint reference can be retrieved using . @@ -1720,6 +1791,27 @@ public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder return builder.WithEndpoint(targetPort: targetPort, port: port, scheme: "https", name: name, env: env, isProxied: isProxied); } + /// + /// Exposes an HTTPS endpoint on a resource, or updates the existing HTTPS endpoint if one with the same name already exists. + /// + /// The resource type. + /// The resource builder. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// An optional host port. + /// An optional name of the endpoint. Defaults to "https" if not specified. + /// An optional name of the environment variable to inject. + /// Specifies if the endpoint will be proxied by DCP. + /// The . + /// + /// This overload preserves binary compatibility for callers compiled against the previous signature. + /// New source that omits binds to the nullable overload where omission is represented as . + /// + [AspireExportIgnore(Reason = "Binary compatibility shim for the nullable isProxied overload.")] + public static IResourceBuilder WithHttpsEndpoint(this IResourceBuilder builder, int? port, int? targetPort, [EndpointName] string? name, string? env, bool isProxied) where T : IResourceWithEndpoints + { + return WithHttpsEndpoint(builder, port, targetPort, name, env, (bool?)isProxied); + } + /// /// Marks existing http or https endpoints on a resource as external. /// diff --git a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs index f421fadb565..aac79011077 100644 --- a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs @@ -1,11 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Sockets; +using System.Reflection; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.DependencyInjection; -using System.Net.Sockets; namespace Aspire.Hosting.Tests; @@ -15,6 +16,85 @@ public class WithEndpointTests // copied from /src/Shared/StringComparers.cs to avoid ambiguous reference since StringComparers exists internally in multiple Hosting assemblies. private static StringComparison EndpointAnnotationName => StringComparison.OrdinalIgnoreCase; + [Fact] + public void EndpointIsProxiedBinaryCompatibilityOverloadsExist() + { + Assert.NotNull(GetPublicStaticMethod( + typeof(ResourceBuilderExtensions), + nameof(ResourceBuilderExtensions.WithEndpoint), + typeof(IResourceBuilder<>), + typeof(int?), + typeof(int?), + typeof(string), + typeof(string), + typeof(string), + typeof(bool), + typeof(bool?), + typeof(ProtocolType?))); + + Assert.NotNull(GetPublicStaticMethod( + typeof(ResourceBuilderExtensions), + nameof(ResourceBuilderExtensions.WithEndpoint), + typeof(IResourceBuilder<>), + typeof(int?), + typeof(int?), + typeof(string), + typeof(string), + typeof(string), + typeof(bool), + typeof(bool?))); + + Assert.NotNull(GetPublicStaticMethod( + typeof(ResourceBuilderExtensions), + nameof(ResourceBuilderExtensions.WithHttpEndpoint), + typeof(IResourceBuilder<>), + typeof(int?), + typeof(int?), + typeof(string), + typeof(string), + typeof(bool))); + + Assert.NotNull(GetPublicStaticMethod( + typeof(ResourceBuilderExtensions), + nameof(ResourceBuilderExtensions.WithHttpsEndpoint), + typeof(IResourceBuilder<>), + typeof(int?), + typeof(int?), + typeof(string), + typeof(string), + typeof(bool))); + + Assert.NotNull(GetPublicStaticMethod( + typeof(ContainerResourceBuilderExtensions), + nameof(ContainerResourceBuilderExtensions.WithEndpointProxySupport), + typeof(IResourceBuilder<>), + typeof(bool))); + } + + [Fact] + public void WithHttpEndpointOmittedIsProxiedLeavesProxySettingUnset() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("app", "image") + .WithHttpEndpoint(name: "http"); + + var endpoint = Assert.Single(container.Resource.Annotations.OfType()); + Assert.Null(endpoint.IsProxied); + } + + [Fact] + public void WithHttpEndpointBoolCompatibilityOverloadPreservesExplicitProxySetting() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var container = builder.AddContainer("app", "image"); + ResourceBuilderExtensions.WithHttpEndpoint(container, port: null, targetPort: null, name: "http", env: null, isProxied: true); + + var endpoint = Assert.Single(container.Resource.Annotations.OfType()); + Assert.True(endpoint.IsProxied); + } + [Fact] public void WithEndpointInvokesCallback() { @@ -32,6 +112,21 @@ public void WithEndpointInvokesCallback() Assert.Equal(2000, endpoint.Port); } + private static MethodInfo? GetPublicStaticMethod(Type declaringType, string methodName, params Type[] parameterTypes) + { + return declaringType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .SingleOrDefault(method => + method.Name == methodName && + method.GetParameters().Select(parameter => NormalizeGenericParameterType(parameter.ParameterType)).SequenceEqual(parameterTypes)); + } + + private static Type NormalizeGenericParameterType(Type parameterType) + { + return parameterType.IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(IResourceBuilder<>) + ? typeof(IResourceBuilder<>) + : parameterType; + } + [Fact] public void WithEndpointMakesTargetPortEqualToPortIfProxyless() { From e3b63501fdf60e3129a0f2f6b3ee495308f1a9ec Mon Sep 17 00:00:00 2001 From: David Negstad Date: Tue, 19 May 2026 21:34:18 -0700 Subject: [PATCH 25/38] Preserve endpoint annotation constructors Restore the old bool isProxied EndpointAnnotation constructor signatures as forwarding shims so older binaries keep binding while new source can omit isProxied and get the nullable default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ApplicationModel/EndpointAnnotation.cs | 71 ++++++++++++++++++- .../CompatibilitySuppressions.xml | 14 ---- .../Aspire.Hosting.Tests/WithEndpointTests.cs | 57 +++++++++++++++ 3 files changed, 127 insertions(+), 15 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index 71610c7558b..2009018ba3f 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -58,11 +58,45 @@ public EndpointAnnotation( ) { } + /// + /// Initializes a new instance of . + /// + /// Network protocol: TCP or UDP are supported today, others possibly in future. + /// If a service is URI-addressable, this is the URI scheme to use for constructing service URI. + /// Transport that is being used (e.g. http, http2, http3 etc). + /// Name of the service. + /// Desired port for the service. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// Indicates that this endpoint should be exposed externally at publish time. + /// Specifies if the endpoint will be proxied by DCP. + public EndpointAnnotation( + ProtocolType protocol, + string? uriScheme, + string? transport, + [EndpointName] string? name, + int? port, + int? targetPort, + bool? isExternal, + bool isProxied + ) : this( + protocol, + null, + uriScheme, + transport, + name, + port, + targetPort, + isExternal, + isProxied + ) + { } + /// /// Initializes a new instance of . /// /// Network protocol: TCP or UDP are supported today, others possibly in future. /// The ID of the network that is the "default" network for the Endpoint. + /// Clients connected to the same network can reach the endpoint without any routing or network address translation. /// If a service is URI-addressable, this is the URI scheme to use for constructing service URI. /// Transport that is being used (e.g. http, http2, http3 etc). /// Name of the service. @@ -70,7 +104,6 @@ public EndpointAnnotation( /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. /// Indicates that this endpoint should be exposed externally at publish time. /// Specifies if the endpoint will be proxied by DCP. Defaults to . - /// Clients connected to the same network can reach the endpoint without any routing or network address translation. public EndpointAnnotation( ProtocolType protocol, NetworkIdentifier? networkID, @@ -107,6 +140,42 @@ public EndpointAnnotation( #pragma warning restore CS0618 // Type or member is obsolete } + /// + /// Initializes a new instance of . + /// + /// Network protocol: TCP or UDP are supported today, others possibly in future. + /// The ID of the network that is the "default" network for the Endpoint. + /// Clients connected to the same network can reach the endpoint without any routing or network address translation. + /// If a service is URI-addressable, this is the URI scheme to use for constructing service URI. + /// Transport that is being used (e.g. http, http2, http3 etc). + /// Name of the service. + /// Desired port for the service. + /// This is the port the resource is listening on. If the endpoint is used for the container, it is the container port. + /// Indicates that this endpoint should be exposed externally at publish time. + /// Specifies if the endpoint will be proxied by DCP. + public EndpointAnnotation( + ProtocolType protocol, + NetworkIdentifier? networkID, + string? uriScheme, + string? transport, + [EndpointName] string? name, + int? port, + int? targetPort, + bool? isExternal, + bool isProxied + ) : this( + protocol, + networkID, + uriScheme, + transport, + name, + port, + targetPort, + isExternal, + (bool?)isProxied + ) + { } + /// /// Name of the service /// diff --git a/src/Aspire.Hosting/CompatibilitySuppressions.xml b/src/Aspire.Hosting/CompatibilitySuppressions.xml index f2dd583e0d3..14f5b8806b5 100644 --- a/src/Aspire.Hosting/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting/CompatibilitySuppressions.xml @@ -1,20 +1,6 @@  - - CP0002 - M:Aspire.Hosting.ApplicationModel.EndpointAnnotation.#ctor(System.Net.Sockets.ProtocolType,Aspire.Hosting.ApplicationModel.NetworkIdentifier,System.String,System.String,System.String,System.Nullable{System.Int32},System.Nullable{System.Int32},System.Nullable{System.Boolean},System.Boolean) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - - CP0002 - M:Aspire.Hosting.ApplicationModel.EndpointAnnotation.#ctor(System.Net.Sockets.ProtocolType,System.String,System.String,System.String,System.Nullable{System.Int32},System.Nullable{System.Int32},System.Nullable{System.Boolean},System.Boolean) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - CP0002 M:Aspire.Hosting.ApplicationModel.EndpointAnnotation.get_IsProxied diff --git a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs index aac79011077..b472b172317 100644 --- a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs @@ -69,6 +69,56 @@ public void EndpointIsProxiedBinaryCompatibilityOverloadsExist() nameof(ContainerResourceBuilderExtensions.WithEndpointProxySupport), typeof(IResourceBuilder<>), typeof(bool))); + + Assert.NotNull(GetPublicConstructor( + typeof(EndpointAnnotation), + typeof(ProtocolType), + typeof(string), + typeof(string), + typeof(string), + typeof(int?), + typeof(int?), + typeof(bool?), + typeof(bool))); + + Assert.NotNull(GetPublicConstructor( + typeof(EndpointAnnotation), + typeof(ProtocolType), + typeof(NetworkIdentifier), + typeof(string), + typeof(string), + typeof(string), + typeof(int?), + typeof(int?), + typeof(bool?), + typeof(bool))); + } + + [Fact] + public void EndpointAnnotationConstructorOmissionLeavesProxySettingUnset() + { + var endpoint = new EndpointAnnotation(ProtocolType.Tcp); + + Assert.Null(endpoint.IsProxied); + } + + [Fact] + public void EndpointAnnotationBoolCompatibilityConstructorPreservesExplicitProxySetting() + { + var constructor = GetPublicConstructor( + typeof(EndpointAnnotation), + typeof(ProtocolType), + typeof(string), + typeof(string), + typeof(string), + typeof(int?), + typeof(int?), + typeof(bool?), + typeof(bool)); + + var endpoint = Assert.IsType(constructor!.Invoke([ProtocolType.Tcp, "http", null, "http", null, null, null, true])); + + Assert.True(endpoint.IsProxied); } [Fact] @@ -120,6 +170,13 @@ public void WithEndpointInvokesCallback() method.GetParameters().Select(parameter => NormalizeGenericParameterType(parameter.ParameterType)).SequenceEqual(parameterTypes)); } + private static ConstructorInfo? GetPublicConstructor(Type declaringType, params Type[] parameterTypes) + { + return declaringType.GetConstructors(BindingFlags.Public | BindingFlags.Instance) + .SingleOrDefault(constructor => + constructor.GetParameters().Select(parameter => parameter.ParameterType).SequenceEqual(parameterTypes)); + } + private static Type NormalizeGenericParameterType(Type parameterType) { return parameterType.IsGenericType && parameterType.GetGenericTypeDefinition() == typeof(IResourceBuilder<>) From 4cfc2a3a6e988bd8fce7838a32b2171bf4543f69 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Tue, 19 May 2026 21:39:58 -0700 Subject: [PATCH 26/38] Cover proxy support override in DCP Add DCP executor tests proving WithEndpointProxySupport(false) overrides explicitly proxied endpoints for both executable and container resources during resource creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ApplicationModel/ResourceExtensions.cs | 2 +- .../Dcp/DcpExecutorTests.cs | 61 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 5c4a859ed4b..bf70ae7b40f 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -1177,7 +1177,7 @@ internal static bool TryGetContainerImagePullPolicy(this IResource resource, [No } /// - /// Determines whether a resource has proxy support enabled or not. Container resources may have a setting that disables proxying for their + /// Determines whether a resource has proxy support enabled or not. Resources may have a setting that disables proxying for their /// endpoints regardless of the endpoint proxy configuration. /// /// The resource to get proxy support for. diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 5970b4167d7..f609ac8fe2f 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -630,6 +630,35 @@ public async Task EndpointPortsExecutableWithEndpointProxySupportUsesProxylessEn Assert.Equal(desiredPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); } + [Fact] + public async Task EndpointPortsExecutableWithEndpointProxySupportOverridesExplicitProxiedEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 1001; + builder.AddExecutable("CoolProgram", "cool", Environment.CurrentDirectory, "--alpha", "--bravo") + .WithEndpoint(name: "EqualPortAndTargetPort", port: desiredPort, targetPort: desiredPort, env: "EQUAL_PORT_AND_TARGET_PORT", isProxied: true) + .WithEndpointProxySupport(false); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + await appExecutor.RunApplicationAsync(); + + var dcpExe = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(dcpExe.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "CoolProgram"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.Equal(desiredPort, spAnnList.Single(ann => ann.ServiceName == "CoolProgram").Port); + + var envVarVal = dcpExe.Spec.Env?.Single(v => v.Name == "EQUAL_PORT_AND_TARGET_PORT").Value; + Assert.False(string.IsNullOrWhiteSpace(envVarVal)); + Assert.Equal(desiredPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); + } + [Fact] public async Task EndpointPortsPersistentExecutableDefaultsToProxylessEndpoint() { @@ -1683,6 +1712,38 @@ public async Task EndpointPortsContainerProxylessPortAndTargetPortSet() Assert.Equal(desiredTargetPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); } + [Fact] + public async Task EndpointPortsContainerWithEndpointProxySupportOverridesExplicitProxiedEndpoint() + { + var builder = DistributedApplication.CreateBuilder(); + + const int desiredPort = TestKubernetesService.StartOfAutoPortRange - 998; + const int desiredTargetPort = TestKubernetesService.StartOfAutoPortRange - 997; + builder.AddContainer("database", "image") + .WithEndpoint(name: "PortAndTargetPortSet", port: desiredPort, targetPort: desiredTargetPort, env: "PORT_AND_TARGET_PORT_SET", isProxied: true) + .WithEndpointProxySupport(false); + + var kubernetesService = new TestKubernetesService(); + using var app = builder.Build(); + var distributedAppModel = app.Services.GetRequiredService(); + var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService); + await appExecutor.RunApplicationAsync(); + + var dcpCtr = Assert.Single(kubernetesService.CreatedResources.OfType()); + Assert.True(dcpCtr.TryGetAnnotationAsObjectList(CustomResource.ServiceProducerAnnotation, out var spAnnList)); + + var svc = kubernetesService.CreatedResources.OfType().Single(s => s.Name() == "database"); + Assert.Equal(AddressAllocationModes.Proxyless, svc.Spec.AddressAllocationMode); + Assert.Equal(desiredPort, svc.Status?.EffectivePort); + Assert.NotNull(dcpCtr.Spec.Ports); + Assert.Contains(dcpCtr.Spec.Ports!, p => p.HostPort == desiredPort && p.ContainerPort == desiredTargetPort); + Assert.Equal(desiredTargetPort, spAnnList.Single(ann => ann.ServiceName == "database").Port); + + var envVarVal = dcpCtr.Spec.Env?.Single(v => v.Name == "PORT_AND_TARGET_PORT_SET").Value; + Assert.False(string.IsNullOrWhiteSpace(envVarVal)); + Assert.Equal(desiredTargetPort, int.Parse(envVarVal, CultureInfo.InvariantCulture)); + } + [Fact] public async Task EndpointPortsContainerProxylessProtocolSet() { From 51442c7b972d1cf54b353f9644f85a2c7bca69e5 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Tue, 19 May 2026 22:32:40 -0700 Subject: [PATCH 27/38] Preserve EndpointAnnotation IsProxied signature Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ApplicationModel/EndpointAnnotation.cs | 47 ++++++++++++++++--- .../ApplicationModel/EndpointUpdateContext.cs | 4 +- .../CompatibilitySuppressions.xml | 7 --- src/Aspire.Hosting/Dcp/ContainerCreator.cs | 2 +- src/Aspire.Hosting/Dcp/DcpExecutor.cs | 10 ++-- src/Aspire.Hosting/Dcp/DcpModelUtilities.cs | 10 ++-- .../Orchestrator/ApplicationOrchestrator.cs | 6 +++ .../ResourceBuilderExtensions.cs | 2 +- .../Aspire.Hosting.Tests/WithEndpointTests.cs | 12 ++++- 9 files changed, 71 insertions(+), 29 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index 2009018ba3f..3d1cbc9db7c 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -23,6 +23,8 @@ public sealed class EndpointAnnotation : IResourceAnnotation private int? _targetPort; private bool _targetPortSetToNull; private bool? _tlsEnabled; + private bool _isProxied = true; + private bool? _isExplicitlyProxied; private readonly NetworkIdentifier _networkID; /// @@ -133,7 +135,7 @@ public EndpointAnnotation( _port = port; _targetPort = targetPort; IsExternal = isExternal ?? false; - IsProxied = isProxied; + IsExplicitlyProxied = isProxied; _networkID = networkID ?? KnownNetworkIdentifiers.LocalhostNetwork; #pragma warning disable CS0618 // Type or member is obsolete AllAllocatedEndpoints.TryAdd(_networkID, AllocatedEndpointSnapshot); @@ -198,7 +200,7 @@ public int? Port // It also depends on what the EndpointAnnotation is applied to. // In the Container case the TargetPort is the port that the process listens on inside the container, // and the Port is the host interface port, so it is fine for them to be different. - get => _port ?? (IsProxied.GetValueOrDefault(true) || _portSetToNull ? null : _targetPort); + get => _port ?? (IsProxied || _portSetToNull ? null : _targetPort); set { _port = value; @@ -217,7 +219,7 @@ public int? Port public int? TargetPort { // See comment on the Port setter, as this is the reciprocal logic - get => _targetPort ?? (IsProxied.GetValueOrDefault(true) || _targetPortSetToNull ? null : _port); + get => _targetPort ?? (IsProxied || _targetPortSetToNull ? null : _port); set { _targetPort = value; @@ -251,10 +253,43 @@ public string Transport /// /// Indicates that this endpoint should be managed by DCP. This means it can be replicated and use a different port internally than the one publicly exposed. - /// Setting to false means the endpoint will be handled and exposed by the resource. + /// Setting to means the endpoint will be handled and exposed by the resource. /// - /// Defaults to . The effective default is computed from the resource that owns the endpoint. - public bool? IsProxied { get; set; } + /// + /// Defaults to until DCP resolves the effective value from + /// and the resource that owns the endpoint. Read this value after endpoint allocation or another late lifecycle + /// callback when code needs the resolved value. + /// + public bool IsProxied + { + get => _isProxied; + set + { + _isProxied = value; + _isExplicitlyProxied = value; + } + } + + /// + /// Gets or sets a value indicating whether this endpoint was explicitly configured to be proxied. + /// + /// + /// A value means the effective proxy mode is resolved from the resource that owns the endpoint. + /// + public bool? IsExplicitlyProxied + { + get => _isExplicitlyProxied; + set + { + _isExplicitlyProxied = value; + _isProxied = value ?? true; + } + } + + internal void SetResolvedIsProxied(bool isProxied) + { + _isProxied = isProxied; + } /// /// Gets or sets a value indicating whether this endpoint is excluded from the default set when referencing the resource's endpoints diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs b/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs index ea6943f04c2..9f5e4ef779b 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs @@ -86,8 +86,8 @@ public bool IsExternal /// public bool? IsProxied { - get => _endpointAnnotation.IsProxied; - set => _endpointAnnotation.IsProxied = value; + get => _endpointAnnotation.IsExplicitlyProxied; + set => _endpointAnnotation.IsExplicitlyProxied = value; } /// diff --git a/src/Aspire.Hosting/CompatibilitySuppressions.xml b/src/Aspire.Hosting/CompatibilitySuppressions.xml index 14f5b8806b5..8ecd3607985 100644 --- a/src/Aspire.Hosting/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting/CompatibilitySuppressions.xml @@ -1,13 +1,6 @@  - - CP0002 - M:Aspire.Hosting.ApplicationModel.EndpointAnnotation.get_IsProxied - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - CP0006 M:Aspire.Hosting.Pipelines.IDeploymentStateManager.ClearAllStateAsync(System.Threading.CancellationToken) diff --git a/src/Aspire.Hosting/Dcp/ContainerCreator.cs b/src/Aspire.Hosting/Dcp/ContainerCreator.cs index 7f58f9f329f..19318f50d2c 100644 --- a/src/Aspire.Hosting/Dcp/ContainerCreator.cs +++ b/src/Aspire.Hosting/Dcp/ContainerCreator.cs @@ -951,7 +951,7 @@ private static List BuildContainerPorts(RenderedModelResource ContainerPort = ea.TargetPort, }; - if (!ea.IsProxied.GetValueOrDefault() && ea.SpecifiedPort is int hostPort) + if (!ea.IsProxied && ea.SpecifiedPort is int hostPort) { portSpec.HostPort = hostPort; } diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index e9930798cea..84a3e2ce7fb 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -657,17 +657,17 @@ private void PrepareServices() var svc = Service.Create(serviceName); - endpoint.IsProxied = GetEffectiveIsProxied(sp.ModelResource, endpoint); + endpoint.SetResolvedIsProxied(GetEffectiveIsProxied(sp.ModelResource, endpoint)); int? port; - if (_options.Value.RandomizePorts && endpoint.IsProxied.Value && endpoint.Port != null) + if (_options.Value.RandomizePorts && endpoint.IsProxied && endpoint.Port != null) { port = null; _logger.LogDebug("Randomizing port for {ServiceName}. Original port: {OriginalPort}", serviceName, endpoint.Port); } else { - port = sp.ModelResource.IsContainer() && !endpoint.IsProxied.Value + port = sp.ModelResource.IsContainer() && !endpoint.IsProxied ? endpoint.SpecifiedPort : endpoint.Port; } @@ -682,7 +682,7 @@ private void PrepareServices() svc.Spec.Address = endpoint.TargetHost; } - if (!endpoint.IsProxied.Value) + if (!endpoint.IsProxied) { svc.Spec.AddressAllocationMode = AddressAllocationModes.Proxyless; } @@ -703,7 +703,7 @@ static bool GetEffectiveIsProxied(IResource resource, EndpointAnnotation endpoin return false; } - if (endpoint.IsProxied is bool isProxied) + if (endpoint.IsExplicitlyProxied is bool isProxied) { return isProxied; } diff --git a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs index fd37460c0c4..6801c2b6768 100644 --- a/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs +++ b/src/Aspire.Hosting/Dcp/DcpModelUtilities.cs @@ -38,7 +38,7 @@ internal static void AddServicesProducedInfo( throw new InvalidOperationException($"The endpoint '{ea.Name}' for container resource '{modelResourceName}' must specify the {nameof(EndpointAnnotation.TargetPort)} value"); } } - else if (!ea.IsProxied.GetValueOrDefault()) + else if (!ea.IsProxied) { if (HasMultipleReplicas(appResource.DcpResource)) { @@ -52,7 +52,7 @@ internal static void AddServicesProducedInfo( } else { - Debug.Assert(ea.IsProxied.GetValueOrDefault()); + Debug.Assert(ea.IsProxied); if (ea.TargetPort is int && ea.Port is int && ea.TargetPort == ea.Port) { @@ -157,7 +157,7 @@ private static bool TryAddLocalhostAllocatedEndpoint(ServiceWithModelResource sp return true; } - if (!svc.HasCompleteAddress && sp.EndpointAnnotation.IsProxied.GetValueOrDefault()) + if (!svc.HasCompleteAddress && sp.EndpointAnnotation.IsProxied) { if (allowPending) { @@ -169,7 +169,7 @@ private static bool TryAddLocalhostAllocatedEndpoint(ServiceWithModelResource sp throw new InvalidDataException($"Service {svc.Metadata.Name} should have valid address at this point"); } - if (!sp.EndpointAnnotation.IsProxied.GetValueOrDefault() && svc.AllocatedPort is null) + if (!sp.EndpointAnnotation.IsProxied && svc.AllocatedPort is null) { if (allowPending) { @@ -261,7 +261,7 @@ private static bool IsDynamicProxylessContainerEndpoint(RenderedMo where TDcpResource : CustomResource, IKubernetesStaticMetadata { return resource.DcpResource is Container && - !sp.EndpointAnnotation.IsProxied.GetValueOrDefault() && + !sp.EndpointAnnotation.IsProxied && sp.EndpointAnnotation.SpecifiedPort is null; } diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index de852586d1b..736660b9007 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -503,11 +503,17 @@ static string TrimSuffix(string value, string suffix) private async Task OnResourceEndpointsAllocated(OnResourceEndpointsAllocatedContext context) { await PublishResourceEndpointUrls(context.Resource, context.CancellationToken).ConfigureAwait(false); + + // ResourceEndpointsAllocatedEvent can be published by DCP or by integrations. DCP has already + // processed URLs through this awaited internal event, so mark the following DCP public event + // to skip while still allowing integration-generated public events to process URLs. _skipNextPublicEndpointUrlProcessing.TryAdd(context.Resource.Name, 0); } private async Task OnPublicResourceEndpointsAllocated(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken) { + // Skip DCP-generated public events because their URLs were already processed by the internal + // event path. Public events from integrations do not have this marker and still need processing. if (_skipNextPublicEndpointUrlProcessing.TryRemove(@event.Resource.Name, out _)) { return; diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 50a0f1a9a8d..8c9d8d8e6f7 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -1562,7 +1562,7 @@ public static IResourceBuilder WithEndpoint(this IResourceBuilder build if (isProxied is not null) { - existing.IsProxied = isProxied; + existing.IsExplicitlyProxied = isProxied; } ConfigureEndpointEnvironmentVariable(builder, existing, env); diff --git a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs index b472b172317..acca68edd25 100644 --- a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs @@ -92,6 +92,10 @@ public void EndpointIsProxiedBinaryCompatibilityOverloadsExist() typeof(int?), typeof(bool?), typeof(bool))); + + var isProxiedProperty = typeof(EndpointAnnotation).GetProperty(nameof(EndpointAnnotation.IsProxied), BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(isProxiedProperty); + Assert.Equal(typeof(bool), isProxiedProperty.PropertyType); } [Fact] @@ -99,7 +103,8 @@ public void EndpointAnnotationConstructorOmissionLeavesProxySettingUnset() { var endpoint = new EndpointAnnotation(ProtocolType.Tcp); - Assert.Null(endpoint.IsProxied); + Assert.True(endpoint.IsProxied); + Assert.Null(endpoint.IsExplicitlyProxied); } [Fact] @@ -119,6 +124,7 @@ public void EndpointAnnotationBoolCompatibilityConstructorPreservesExplicitProxy var endpoint = Assert.IsType(constructor!.Invoke([ProtocolType.Tcp, "http", null, "http", null, null, null, true])); Assert.True(endpoint.IsProxied); + Assert.True(endpoint.IsExplicitlyProxied); } [Fact] @@ -130,7 +136,8 @@ public void WithHttpEndpointOmittedIsProxiedLeavesProxySettingUnset() .WithHttpEndpoint(name: "http"); var endpoint = Assert.Single(container.Resource.Annotations.OfType()); - Assert.Null(endpoint.IsProxied); + Assert.True(endpoint.IsProxied); + Assert.Null(endpoint.IsExplicitlyProxied); } [Fact] @@ -143,6 +150,7 @@ public void WithHttpEndpointBoolCompatibilityOverloadPreservesExplicitProxySetti var endpoint = Assert.Single(container.Resource.Annotations.OfType()); Assert.True(endpoint.IsProxied); + Assert.True(endpoint.IsExplicitlyProxied); } [Fact] From 2ecc32a413305297daa1f5a1195bd60b946fd7db Mon Sep 17 00:00:00 2001 From: David Negstad Date: Tue, 19 May 2026 22:55:12 -0700 Subject: [PATCH 28/38] Use public endpoint events for URL processing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/Dcp/DcpExecutor.cs | 48 ++++--------------- src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs | 1 - .../Orchestrator/ApplicationOrchestrator.cs | 24 +--------- 3 files changed, 10 insertions(+), 63 deletions(-) diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index 84a3e2ce7fb..0fc6f0ed485 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -59,12 +59,11 @@ internal sealed partial class DcpExecutor : IDcpExecutor, IDcpObjectFactory, IAs private readonly DistributedApplicationExecutionContext _executionContext; private readonly DcpAppResourceStore _appResources; - // Has an entry if we raised, or are raising, ResourceEndpointsAllocatedEvent for a resource with a given name. - // We want to ensure we raise the event only once for each app model resource, while also letting concurrent - // callers wait for in-flight URL callbacks before they continue to resource creation. + // Has an entry if we raised ResourceEndpointsAllocatedEvent for a resource with a given name. + // We want to ensure we raise the event only once for each app model resource. // There may be multiple physical replicas of the same app model resource // which can result in the event being raised multiple times if we are not careful. - private readonly Dictionary _endpointsAdvertised = new(StringComparers.ResourceName); + private readonly HashSet _endpointsAdvertised = new(StringComparers.ResourceName); private readonly CancellationTokenSource _shutdownCancellation = new(); private readonly DcpExecutorEvents _executorEvents; @@ -1192,48 +1191,17 @@ private static void ForgetCachedCallbackResults(IResource resource) private async Task PublishEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) { - TaskCompletionSource? publishCompletion = null; - Task? existingPublish; - lock (_endpointsAdvertised) { - if (_endpointsAdvertised.TryGetValue(resource.Name, out existingPublish)) - { - publishCompletion = null; - } - else + if (!_endpointsAdvertised.Add(resource.Name)) { - publishCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously); - _endpointsAdvertised.Add(resource.Name, publishCompletion.Task); - existingPublish = null; + return false; // Already published for this resource. } } - if (existingPublish is not null) - { - await existingPublish.ConfigureAwait(false); - return false; // Already published for this resource. - } - - try - { - await _executorEvents.PublishAsync(new OnResourceEndpointsAllocatedContext(ct, resource)).ConfigureAwait(false); - - var ev = new ResourceEndpointsAllocatedEvent(resource, _executionContext.ServiceProvider); - await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.NonBlockingConcurrent, ct).ConfigureAwait(false); - publishCompletion!.SetResult(); - return true; - } - catch (OperationCanceledException ex) - { - publishCompletion!.SetException(ex); - throw; - } - catch (Exception ex) - { - publishCompletion!.SetException(ex); - throw; - } + var ev = new ResourceEndpointsAllocatedEvent(resource, _executionContext.ServiceProvider); + await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.NonBlockingConcurrent, ct).ConfigureAwait(false); + return true; } private async Task PublishLateEndpointsAllocatedEventAsync(IResource resource, CancellationToken ct) diff --git a/src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs b/src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs index 2a660757ac9..d5f45145841 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutorEvents.cs @@ -8,7 +8,6 @@ namespace Aspire.Hosting.Dcp; internal record ResourceStatus(string? State, DateTime? StartupTimestamp, DateTime? FinishedTimestamp); internal record OnEndpointsAllocatedContext(CancellationToken CancellationToken); -internal record OnResourceEndpointsAllocatedContext(CancellationToken CancellationToken, IResource Resource); internal record OnResourceStartingContext(CancellationToken CancellationToken, string ResourceType, IResource Resource, string? DcpResourceName); internal record OnConnectionStringAvailableContext(CancellationToken CancellationToken, IResource Resource); internal record OnResourcesPreparedContext(CancellationToken CancellationToken); diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 736660b9007..20e70deb881 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -3,7 +3,6 @@ #pragma warning disable ASPIREINTERACTION001 -using System.Collections.Concurrent; using System.Collections.Immutable; using System.Data; using System.Diagnostics; @@ -38,7 +37,6 @@ internal sealed class ApplicationOrchestrator private readonly DistributedApplicationExecutionContext _executionContext; private readonly ParameterProcessor _parameterProcessor; private readonly CancellationTokenSource _shutdownCancellation = new(); - private readonly ConcurrentDictionary _skipNextPublicEndpointUrlProcessing = new(StringComparers.ResourceName); private IConfiguration? Configuration => _serviceProvider.GetService(); public ApplicationOrchestrator(DistributedApplicationModel model, @@ -73,12 +71,11 @@ public ApplicationOrchestrator(DistributedApplicationModel model, dcpExecutorEvents.Subscribe(OnResourcesPrepared); dcpExecutorEvents.Subscribe(OnResourceChanged); dcpExecutorEvents.Subscribe(OnEndpointsAllocated); - dcpExecutorEvents.Subscribe(OnResourceEndpointsAllocated); dcpExecutorEvents.Subscribe(OnResourceStarting); dcpExecutorEvents.Subscribe(OnConnectionStringAvailable); dcpExecutorEvents.Subscribe(OnResourceFailedToStart); - _eventing.Subscribe(OnPublicResourceEndpointsAllocated); + _eventing.Subscribe(OnResourceEndpointsAllocated); _eventing.Subscribe(PublishConnectionStringValue); // Implement WaitFor functionality using BeforeResourceStartedEvent. _eventing.Subscribe(WaitForInBeforeResourceStartedEvent); @@ -500,25 +497,8 @@ static string TrimSuffix(string value, string suffix) } } - private async Task OnResourceEndpointsAllocated(OnResourceEndpointsAllocatedContext context) + private async Task OnResourceEndpointsAllocated(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken) { - await PublishResourceEndpointUrls(context.Resource, context.CancellationToken).ConfigureAwait(false); - - // ResourceEndpointsAllocatedEvent can be published by DCP or by integrations. DCP has already - // processed URLs through this awaited internal event, so mark the following DCP public event - // to skip while still allowing integration-generated public events to process URLs. - _skipNextPublicEndpointUrlProcessing.TryAdd(context.Resource.Name, 0); - } - - private async Task OnPublicResourceEndpointsAllocated(ResourceEndpointsAllocatedEvent @event, CancellationToken cancellationToken) - { - // Skip DCP-generated public events because their URLs were already processed by the internal - // event path. Public events from integrations do not have this marker and still need processing. - if (_skipNextPublicEndpointUrlProcessing.TryRemove(@event.Resource.Name, out _)) - { - return; - } - await PublishResourceEndpointUrls(@event.Resource, cancellationToken).ConfigureAwait(false); } From f17260be72ec002a79110ef17b525eeb49832233 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 20 May 2026 10:49:40 -0700 Subject: [PATCH 29/38] Restore polyglot container compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AtsCapabilityScanner.cs | 101 ++- .../ContainerResourceBuilderExtensions.cs | 4 +- ...ContainerResourceCapabilities.verified.txt | 42 +- ...TwoPassScanningGeneratedAspire.verified.ts | 617 ++++++++++++------ 4 files changed, 522 insertions(+), 242 deletions(-) diff --git a/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs b/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs index 6c4018a6403..b04499e02ba 100644 --- a/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs +++ b/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs @@ -921,18 +921,18 @@ private static void AddToCompatibilityMap( /// /// Detects method name collisions after capability expansion. Since ATS doesn't support method /// overloading, each (TargetTypeId, MethodName) pair must be unique. When a concrete target has - /// a target-specific export, it shadows matching generic exports only for that target. Ambiguous - /// collisions still remove later capabilities and emit warnings. + /// a target-specific or more-derived export, it shadows matching generic exports only for that target. + /// Ambiguous collisions still remove later capabilities from the colliding target and emit warnings. /// private static void FilterMethodNameCollisions(List capabilities, List diagnostics) { var capabilitiesWithTargets = capabilities .Where(c => c.ExpandedTargetTypes.Count > 0) - .SelectMany(c => c.ExpandedTargetTypes.Select(t => (Target: t.TypeId, Capability: c))) + .SelectMany(c => c.ExpandedTargetTypes.Select(t => (Target: t, Capability: c))) .ToList(); var collisionGroups = capabilitiesWithTargets - .GroupBy(x => (x.Target, x.Capability.MethodName)) + .GroupBy(x => (Target: x.Target.TypeId, x.Capability.MethodName)) .Where(g => g.Count() > 1) .ToList(); @@ -941,13 +941,13 @@ private static void FilterMethodNameCollisions(List capabilit return; } - var capabilitiesToRemove = new HashSet(); var expandedTargetsToRemove = new Dictionary>(StringComparer.Ordinal); foreach (var collisionGroup in collisionGroups) { var methodName = collisionGroup.Key.MethodName; var targetTypeId = collisionGroup.Key.Target; + var targetType = collisionGroup.First().Target; var collidingCapabilities = collisionGroup .Select(x => x.Capability) .GroupBy(c => c.CapabilityId, StringComparer.Ordinal) @@ -959,23 +959,14 @@ private static void FilterMethodNameCollisions(List capabilit if (exactTargetCapabilities.Count == 1) { - var exactTargetCapability = exactTargetCapabilities[0]; - foreach (var collidingCapability in collidingCapabilities) - { - if (string.Equals(collidingCapability.CapabilityId, exactTargetCapability.CapabilityId, StringComparison.Ordinal)) - { - continue; - } - - if (!expandedTargetsToRemove.TryGetValue(collidingCapability.CapabilityId, out var targetIds)) - { - targetIds = new(StringComparer.Ordinal); - expandedTargetsToRemove[collidingCapability.CapabilityId] = targetIds; - } - - targetIds.Add(targetTypeId); - } + RemoveCollidingTargetFromOtherCapabilities(exactTargetCapabilities[0]); + continue; + } + var mostSpecificCapability = TryGetMostSpecificCapability(targetType, collidingCapabilities); + if (mostSpecificCapability is not null) + { + RemoveCollidingTargetFromOtherCapabilities(mostSpecificCapability); continue; } @@ -984,15 +975,39 @@ private static void FilterMethodNameCollisions(List capabilit var conflictingIdsStr = string.Join(", ", capIds); - // First capability keeps original name, others are removed + // First capability keeps the target, others lose this specific expanded target. for (var i = 1; i < capIds.Count; i++) { - capabilitiesToRemove.Add(capIds[i]); + RemoveExpandedTarget(capIds[i], targetTypeId); diagnostics.Add(AtsDiagnostic.Warning( - $"Method '{methodName}' on target '{targetTypeId}' has collisions ({conflictingIdsStr}). '{capIds[i]}' was removed. Use [AspireExport(MethodName = \"uniqueName\")] to set an explicit name.", + $"Method '{methodName}' on target '{targetTypeId}' has collisions ({conflictingIdsStr}). '{capIds[i]}' was removed from this target. Use [AspireExport(MethodName = \"uniqueName\")] to set an explicit name.", capIds[i])); } + + void RemoveCollidingTargetFromOtherCapabilities(AtsCapabilityInfo winningCapability) + { + foreach (var collidingCapability in collidingCapabilities) + { + if (string.Equals(collidingCapability.CapabilityId, winningCapability.CapabilityId, StringComparison.Ordinal)) + { + continue; + } + + RemoveExpandedTarget(collidingCapability.CapabilityId, targetTypeId); + } + } + + void RemoveExpandedTarget(string capabilityId, string expandedTargetTypeId) + { + if (!expandedTargetsToRemove.TryGetValue(capabilityId, out var targetIds)) + { + targetIds = new(StringComparer.Ordinal); + expandedTargetsToRemove[capabilityId] = targetIds; + } + + targetIds.Add(expandedTargetTypeId); + } } foreach (var capability in capabilities) @@ -1006,8 +1021,42 @@ private static void FilterMethodNameCollisions(List capabilit } capabilities.RemoveAll(c => - capabilitiesToRemove.Contains(c.CapabilityId) || - (c.TargetTypeId is not null && c.ExpandedTargetTypes.Count == 0)); + c.TargetTypeId is not null && c.ExpandedTargetTypes.Count == 0); + } + + private static AtsCapabilityInfo? TryGetMostSpecificCapability(AtsTypeRef targetType, IReadOnlyList capabilities) + { + var closestCapabilities = capabilities + .Select(capability => (Capability: capability, Distance: GetBaseTypeDistance(targetType, capability.TargetType))) + .Where(item => item.Distance is not null) + .OrderBy(item => item.Distance) + .ToList(); + + return closestCapabilities.Count > 0 && + (closestCapabilities.Count == 1 || closestCapabilities[0].Distance != closestCapabilities[1].Distance) + ? closestCapabilities[0].Capability + : null; + } + + private static int? GetBaseTypeDistance(AtsTypeRef targetType, AtsTypeRef? capabilityTargetType) + { + if (capabilityTargetType is not { IsInterface: false }) + { + return null; + } + + var distance = 0; + for (AtsTypeRef? currentType = targetType; currentType is not null; currentType = currentType.BaseType) + { + if (string.Equals(currentType.TypeId, capabilityTargetType.TypeId, StringComparison.Ordinal)) + { + return distance; + } + + distance++; + } + + return null; } /// diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index b20cef74238..3d7c48692b0 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -30,7 +30,7 @@ public static class ContainerResourceBuilderExtensions /// The resource builder. /// Should endpoints for the resource support using a proxy? /// The . - [AspireExportIgnore(Reason = "Binary compatibility shim for the resource-level WithEndpointProxySupport overload.")] + [AspireExport("withContainerEndpointProxySupport", MethodName = "withEndpointProxySupport", Description = "Configures endpoint proxy support")] public static IResourceBuilder WithEndpointProxySupport(IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource { return ResourceBuilderExtensions.WithEndpointProxySupport(builder, proxyEnabled); @@ -565,7 +565,7 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil /// /// /// - [AspireExportIgnore(Reason = "Polyglot app hosts use WithPersistentLifetime or WithSessionLifetime instead.")] + [AspireExport(Description = "Sets the lifetime behavior of the container resource")] public static IResourceBuilder WithLifetime(this IResourceBuilder builder, ContainerLifetime lifetime) where T : ContainerResource { ArgumentNullException.ThrowIfNull(builder); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt index a3dabf5f316..00d254229b1 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt @@ -377,6 +377,20 @@ } ] }, + { + CapabilityId: Aspire.Hosting/withContainerEndpointProxySupport, + MethodName: withEndpointProxySupport, + TargetType: { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + }, + ExpandedTargetTypes: [ + { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + } + ] + }, { CapabilityId: Aspire.Hosting/withContainerName, MethodName: withContainerName, @@ -517,20 +531,6 @@ } ] }, - { - CapabilityId: Aspire.Hosting/withEndpointProxySupport, - MethodName: withEndpointProxySupport, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, { CapabilityId: Aspire.Hosting/withEntrypoint, MethodName: withEntrypoint, @@ -811,6 +811,20 @@ } ] }, + { + CapabilityId: Aspire.Hosting/withLifetime, + MethodName: withLifetime, + TargetType: { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + }, + ExpandedTargetTypes: [ + { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + } + ] + }, { CapabilityId: Aspire.Hosting/withLifetimeOf, MethodName: withLifetimeOf, diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 52dae48c5c9..7886e72c0cc 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -490,6 +490,23 @@ export enum CommandResultFormat { Markdown = "Markdown", } +/** Lifetime modes for container resources. */ +export enum ContainerLifetime { + /** Create the resource when the app host process starts and dispose of it when the app host process shuts down. */ + Session = "Session", + /** + * Attempt to re-use a previously created resource (based on the container name) if one exists. Do not destroy the container on app host process shutdown. + * + * In the event that a container with the given name does not exist, a new container will always be created based on the + * current `ContainerResource` configuration. + * When an existing container IS found, Aspire MAY re-use it based on the following criteria: + * - + * - + * - + */ + Persistent = "Persistent", +} + /** Represents the type of a container mount. */ export enum ContainerMountType { /** A local directory or file that is mounted into the container. */ @@ -12705,6 +12722,12 @@ export interface ContainerResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Adds a bind mount to a container resource. * @@ -12774,6 +12797,22 @@ export interface ContainerResource { * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): ContainerResourcePromise; + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -13041,17 +13080,6 @@ export interface ContainerResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -13599,6 +13627,12 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Adds a bind mount to a container resource. * @@ -13668,6 +13702,22 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): ContainerResourcePromise; + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -13935,17 +13985,6 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -14516,6 +14555,25 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withContainerEndpointProxySupport', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -14680,6 +14738,35 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withContainerRuntimeArgsInternal(args), this._client); } + /** @internal */ + private async _withLifetimeInternal(lifetime: ContainerLifetime): Promise { + const rpcArgs: Record = { builder: this._handle, lifetime }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetime', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withLifetimeInternal(lifetime), this._client); + } + /** @internal */ private async _withImagePullPolicyInternal(pullPolicy: ImagePullPolicy): Promise { const rpcArgs: Record = { builder: this._handle, pullPolicy }; @@ -15402,30 +15489,6 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -17008,6 +17071,10 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withBindMount(source: string, target: string, options?: WithBindMountOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -17036,6 +17103,10 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withContainerRuntimeArgs(args)), this._client); } + withLifetime(lifetime: ContainerLifetime): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withLifetime(lifetime)), this._client); + } + withImagePullPolicy(pullPolicy: ImagePullPolicy): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withImagePullPolicy(pullPolicy)), this._client); } @@ -17148,10 +17219,6 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -37972,6 +38039,12 @@ export interface TestDatabaseResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestDatabaseResourcePromise; + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Adds a bind mount to a container resource. * @@ -38041,6 +38114,22 @@ export interface TestDatabaseResource { * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestDatabaseResourcePromise; + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): TestDatabaseResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -38308,17 +38397,6 @@ export interface TestDatabaseResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestDatabaseResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -38866,6 +38944,12 @@ export interface TestDatabaseResourcePromise extends PromiseLike): TestDatabaseResourcePromise; + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Adds a bind mount to a container resource. * @@ -38935,6 +39019,22 @@ export interface TestDatabaseResourcePromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withContainerEndpointProxySupport', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -39946,6 +40054,35 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, lifetime }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetime', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withLifetimeInternal(lifetime), this._client); + } + /** @internal */ private async _withImagePullPolicyInternal(pullPolicy: ImagePullPolicy): Promise { const rpcArgs: Record = { builder: this._handle, pullPolicy }; @@ -40668,30 +40805,6 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -42274,6 +42387,10 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withBindMount(source: string, target: string, options?: WithBindMountOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -42302,6 +42419,10 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withContainerRuntimeArgs(args)), this._client); } + withLifetime(lifetime: ContainerLifetime): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withLifetime(lifetime)), this._client); + } + withImagePullPolicy(pullPolicy: ImagePullPolicy): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withImagePullPolicy(pullPolicy)), this._client); } @@ -42414,10 +42535,6 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -42703,6 +42820,12 @@ export interface TestRedisResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Adds a bind mount to a container resource. * @@ -42772,6 +42895,22 @@ export interface TestRedisResource { * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestRedisResourcePromise; + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -43055,17 +43194,6 @@ export interface TestRedisResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -43661,6 +43789,12 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Adds a bind mount to a container resource. * @@ -43730,6 +43864,22 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestRedisResourcePromise; + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -44013,17 +44163,6 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -44641,6 +44780,25 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withContainerEndpointProxySupport', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -44805,6 +44963,35 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withContainerRuntimeArgsInternal(args), this._client); } + /** @internal */ + private async _withLifetimeInternal(lifetime: ContainerLifetime): Promise { + const rpcArgs: Record = { builder: this._handle, lifetime }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetime', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withLifetimeInternal(lifetime), this._client); + } + /** @internal */ private async _withImagePullPolicyInternal(pullPolicy: ImagePullPolicy): Promise { const rpcArgs: Record = { builder: this._handle, pullPolicy }; @@ -45563,30 +45750,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -47380,6 +47543,10 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withBindMount(source: string, target: string, options?: WithBindMountOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -47408,6 +47575,10 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withContainerRuntimeArgs(args)), this._client); } + withLifetime(lifetime: ContainerLifetime): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withLifetime(lifetime)), this._client); + } + withImagePullPolicy(pullPolicy: ImagePullPolicy): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withImagePullPolicy(pullPolicy)), this._client); } @@ -47528,10 +47699,6 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -47869,6 +48036,12 @@ export interface TestVaultResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Adds a bind mount to a container resource. * @@ -47938,6 +48111,22 @@ export interface TestVaultResource { * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestVaultResourcePromise; + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -48205,17 +48394,6 @@ export interface TestVaultResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -48765,6 +48943,12 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Adds a bind mount to a container resource. * @@ -48834,6 +49018,22 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withContainerRuntimeArgs(args: string[]): TestVaultResourcePromise; + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise; /** * Sets the pull policy for the container resource. * @param pullPolicy The pull policy behavior for the container resource. @@ -49101,17 +49301,6 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -49683,6 +49872,25 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withContainerEndpointProxySupport', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -49847,6 +50055,35 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withContainerRuntimeArgsInternal(args), this._client); } + /** @internal */ + private async _withLifetimeInternal(lifetime: ContainerLifetime): Promise { + const rpcArgs: Record = { builder: this._handle, lifetime }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withLifetime', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Sets the lifetime behavior of the container resource. + * + * Prefer `WithPersistentLifetime``1` or + * `WithSessionLifetime``1` for new code. + * Marking a container resource to have a `Persistent` lifetime. + * ``` + * var builder = DistributedApplication.CreateBuilder(args); + * builder.AddContainer("mycontainer", "myimage") + * .WithPersistentLifetime(); + * builder.Build().Run(); + * ``` + * @param lifetime The lifetime behavior of the container resource. The default behavior is `Session`. + * @returns The `IResourceBuilder`1`. + */ + withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withLifetimeInternal(lifetime), this._client); + } + /** @internal */ private async _withImagePullPolicyInternal(pullPolicy: ImagePullPolicy): Promise { const rpcArgs: Record = { builder: this._handle, pullPolicy }; @@ -50569,30 +50806,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -52190,6 +52403,10 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withBindMount(source: string, target: string, options?: WithBindMountOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -52218,6 +52435,10 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withContainerRuntimeArgs(args)), this._client); } + withLifetime(lifetime: ContainerLifetime): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withLifetime(lifetime)), this._client); + } + withImagePullPolicy(pullPolicy: ImagePullPolicy): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withImagePullPolicy(pullPolicy)), this._client); } @@ -52330,10 +52551,6 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } From f27309b75bf641e6ce572a91fa1aee6a9d95dba4 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 20 May 2026 10:58:07 -0700 Subject: [PATCH 30/38] Revert ATS scanner collision handling change Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AtsCapabilityScanner.cs | 101 +++++------------- 1 file changed, 26 insertions(+), 75 deletions(-) diff --git a/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs b/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs index b04499e02ba..6c4018a6403 100644 --- a/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs +++ b/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs @@ -921,18 +921,18 @@ private static void AddToCompatibilityMap( /// /// Detects method name collisions after capability expansion. Since ATS doesn't support method /// overloading, each (TargetTypeId, MethodName) pair must be unique. When a concrete target has - /// a target-specific or more-derived export, it shadows matching generic exports only for that target. - /// Ambiguous collisions still remove later capabilities from the colliding target and emit warnings. + /// a target-specific export, it shadows matching generic exports only for that target. Ambiguous + /// collisions still remove later capabilities and emit warnings. /// private static void FilterMethodNameCollisions(List capabilities, List diagnostics) { var capabilitiesWithTargets = capabilities .Where(c => c.ExpandedTargetTypes.Count > 0) - .SelectMany(c => c.ExpandedTargetTypes.Select(t => (Target: t, Capability: c))) + .SelectMany(c => c.ExpandedTargetTypes.Select(t => (Target: t.TypeId, Capability: c))) .ToList(); var collisionGroups = capabilitiesWithTargets - .GroupBy(x => (Target: x.Target.TypeId, x.Capability.MethodName)) + .GroupBy(x => (x.Target, x.Capability.MethodName)) .Where(g => g.Count() > 1) .ToList(); @@ -941,13 +941,13 @@ private static void FilterMethodNameCollisions(List capabilit return; } + var capabilitiesToRemove = new HashSet(); var expandedTargetsToRemove = new Dictionary>(StringComparer.Ordinal); foreach (var collisionGroup in collisionGroups) { var methodName = collisionGroup.Key.MethodName; var targetTypeId = collisionGroup.Key.Target; - var targetType = collisionGroup.First().Target; var collidingCapabilities = collisionGroup .Select(x => x.Capability) .GroupBy(c => c.CapabilityId, StringComparer.Ordinal) @@ -959,14 +959,23 @@ private static void FilterMethodNameCollisions(List capabilit if (exactTargetCapabilities.Count == 1) { - RemoveCollidingTargetFromOtherCapabilities(exactTargetCapabilities[0]); - continue; - } + var exactTargetCapability = exactTargetCapabilities[0]; + foreach (var collidingCapability in collidingCapabilities) + { + if (string.Equals(collidingCapability.CapabilityId, exactTargetCapability.CapabilityId, StringComparison.Ordinal)) + { + continue; + } + + if (!expandedTargetsToRemove.TryGetValue(collidingCapability.CapabilityId, out var targetIds)) + { + targetIds = new(StringComparer.Ordinal); + expandedTargetsToRemove[collidingCapability.CapabilityId] = targetIds; + } + + targetIds.Add(targetTypeId); + } - var mostSpecificCapability = TryGetMostSpecificCapability(targetType, collidingCapabilities); - if (mostSpecificCapability is not null) - { - RemoveCollidingTargetFromOtherCapabilities(mostSpecificCapability); continue; } @@ -975,39 +984,15 @@ private static void FilterMethodNameCollisions(List capabilit var conflictingIdsStr = string.Join(", ", capIds); - // First capability keeps the target, others lose this specific expanded target. + // First capability keeps original name, others are removed for (var i = 1; i < capIds.Count; i++) { - RemoveExpandedTarget(capIds[i], targetTypeId); + capabilitiesToRemove.Add(capIds[i]); diagnostics.Add(AtsDiagnostic.Warning( - $"Method '{methodName}' on target '{targetTypeId}' has collisions ({conflictingIdsStr}). '{capIds[i]}' was removed from this target. Use [AspireExport(MethodName = \"uniqueName\")] to set an explicit name.", + $"Method '{methodName}' on target '{targetTypeId}' has collisions ({conflictingIdsStr}). '{capIds[i]}' was removed. Use [AspireExport(MethodName = \"uniqueName\")] to set an explicit name.", capIds[i])); } - - void RemoveCollidingTargetFromOtherCapabilities(AtsCapabilityInfo winningCapability) - { - foreach (var collidingCapability in collidingCapabilities) - { - if (string.Equals(collidingCapability.CapabilityId, winningCapability.CapabilityId, StringComparison.Ordinal)) - { - continue; - } - - RemoveExpandedTarget(collidingCapability.CapabilityId, targetTypeId); - } - } - - void RemoveExpandedTarget(string capabilityId, string expandedTargetTypeId) - { - if (!expandedTargetsToRemove.TryGetValue(capabilityId, out var targetIds)) - { - targetIds = new(StringComparer.Ordinal); - expandedTargetsToRemove[capabilityId] = targetIds; - } - - targetIds.Add(expandedTargetTypeId); - } } foreach (var capability in capabilities) @@ -1021,42 +1006,8 @@ void RemoveExpandedTarget(string capabilityId, string expandedTargetTypeId) } capabilities.RemoveAll(c => - c.TargetTypeId is not null && c.ExpandedTargetTypes.Count == 0); - } - - private static AtsCapabilityInfo? TryGetMostSpecificCapability(AtsTypeRef targetType, IReadOnlyList capabilities) - { - var closestCapabilities = capabilities - .Select(capability => (Capability: capability, Distance: GetBaseTypeDistance(targetType, capability.TargetType))) - .Where(item => item.Distance is not null) - .OrderBy(item => item.Distance) - .ToList(); - - return closestCapabilities.Count > 0 && - (closestCapabilities.Count == 1 || closestCapabilities[0].Distance != closestCapabilities[1].Distance) - ? closestCapabilities[0].Capability - : null; - } - - private static int? GetBaseTypeDistance(AtsTypeRef targetType, AtsTypeRef? capabilityTargetType) - { - if (capabilityTargetType is not { IsInterface: false }) - { - return null; - } - - var distance = 0; - for (AtsTypeRef? currentType = targetType; currentType is not null; currentType = currentType.BaseType) - { - if (string.Equals(currentType.TypeId, capabilityTargetType.TypeId, StringComparison.Ordinal)) - { - return distance; - } - - distance++; - } - - return null; + capabilitiesToRemove.Contains(c.CapabilityId) || + (c.TargetTypeId is not null && c.ExpandedTargetTypes.Count == 0)); } /// From 1f79c9e70652a6a7f04000eeef7bd268c1462025 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 20 May 2026 11:12:15 -0700 Subject: [PATCH 31/38] Use generalized endpoint proxy export for polyglot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ContainerResourceBuilderExtensions.cs | 2 +- ...ContainerResourceCapabilities.verified.txt | 28 +- ...TwoPassScanningGeneratedAspire.verified.ts | 340 ++++++++++-------- 3 files changed, 215 insertions(+), 155 deletions(-) diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 3d7c48692b0..5fa2bb9babe 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -30,7 +30,7 @@ public static class ContainerResourceBuilderExtensions /// The resource builder. /// Should endpoints for the resource support using a proxy? /// The . - [AspireExport("withContainerEndpointProxySupport", MethodName = "withEndpointProxySupport", Description = "Configures endpoint proxy support")] + [AspireExportIgnore(Reason = "Binary compatibility shim for the resource-level WithEndpointProxySupport overload.")] public static IResourceBuilder WithEndpointProxySupport(IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource { return ResourceBuilderExtensions.WithEndpointProxySupport(builder, proxyEnabled); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt index 00d254229b1..2e47b91ead0 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt @@ -377,20 +377,6 @@ } ] }, - { - CapabilityId: Aspire.Hosting/withContainerEndpointProxySupport, - MethodName: withEndpointProxySupport, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, { CapabilityId: Aspire.Hosting/withContainerName, MethodName: withContainerName, @@ -531,6 +517,20 @@ } ] }, + { + CapabilityId: Aspire.Hosting/withEndpointProxySupport, + MethodName: withEndpointProxySupport, + TargetType: { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints, + IsInterface: true + }, + ExpandedTargetTypes: [ + { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + } + ] + }, { CapabilityId: Aspire.Hosting/withEntrypoint, MethodName: withEntrypoint, diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 7886e72c0cc..15949ba1695 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -12722,12 +12722,6 @@ export interface ContainerResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Adds a bind mount to a container resource. * @@ -13080,6 +13074,17 @@ export interface ContainerResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -13627,12 +13632,6 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Adds a bind mount to a container resource. * @@ -13985,6 +13984,17 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -14555,25 +14565,6 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withContainerEndpointProxySupport', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -15489,6 +15480,30 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -17071,10 +17086,6 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withBindMount(source: string, target: string, options?: WithBindMountOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -17219,6 +17230,10 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -38039,12 +38054,6 @@ export interface TestDatabaseResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestDatabaseResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Adds a bind mount to a container resource. * @@ -38397,6 +38406,17 @@ export interface TestDatabaseResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestDatabaseResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -38944,12 +38964,6 @@ export interface TestDatabaseResourcePromise extends PromiseLike): TestDatabaseResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Adds a bind mount to a container resource. * @@ -39302,6 +39316,17 @@ export interface TestDatabaseResourcePromise extends PromiseLike { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withContainerEndpointProxySupport', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -40805,6 +40811,30 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -42387,10 +42417,6 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withBindMount(source: string, target: string, options?: WithBindMountOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -42535,6 +42561,10 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -42820,12 +42850,6 @@ export interface TestRedisResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Adds a bind mount to a container resource. * @@ -43194,6 +43218,17 @@ export interface TestRedisResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -43789,12 +43824,6 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Adds a bind mount to a container resource. * @@ -44163,6 +44192,17 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -44780,25 +44820,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withContainerEndpointProxySupport', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -45750,6 +45771,30 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -47543,10 +47588,6 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withBindMount(source: string, target: string, options?: WithBindMountOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -47699,6 +47740,10 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -48036,12 +48081,6 @@ export interface TestVaultResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Adds a bind mount to a container resource. * @@ -48394,6 +48433,17 @@ export interface TestVaultResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -48943,12 +48993,6 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Adds a bind mount to a container resource. * @@ -49301,6 +49345,17 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -49872,25 +49927,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withContainerEndpointProxySupport', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - /** - * Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -50806,6 +50842,30 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -52403,10 +52463,6 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withBindMount(source: string, target: string, options?: WithBindMountOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -52551,6 +52607,10 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } From 90b8bb40d6f8483cea5447a010dd5dd8dc68f33b Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 20 May 2026 12:29:08 -0700 Subject: [PATCH 32/38] Make endpoint allocation events sequential Dispatch ResourceEndpointsAllocatedEvent with blocking sequential semantics and remove annotation locks that were only needed for concurrent URL annotation mutation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ApplicationModel/EndpointReference.cs | 7 ++----- .../ApplicationModel/ResourceExtensions.cs | 12 ++---------- src/Aspire.Hosting/Dcp/DcpExecutor.cs | 2 +- .../Orchestrator/ApplicationOrchestrator.cs | 10 ++-------- tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs | 8 ++++++-- 5 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 37be1745806..343ae86cb65 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -227,11 +227,8 @@ public ReferenceExpression GetTlsValue(ReferenceExpression enabledValue, Referen return _endpointAnnotation; } - lock (Resource.Annotations) - { - _endpointAnnotation ??= Resource.Annotations.OfType() - .SingleOrDefault(a => string.Equals(a.Name, EndpointName, StringComparisons.EndpointAnnotationName)); - } + _endpointAnnotation ??= Resource.Annotations.OfType() + .SingleOrDefault(a => string.Equals(a.Name, EndpointName, StringComparisons.EndpointAnnotationName)); return _endpointAnnotation; } diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index bf70ae7b40f..d6aeecef5ee 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -29,11 +29,7 @@ public static class ResourceExtensions [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool TryGetLastAnnotation(this IResource resource, [NotNullWhen(true)] out T? annotation) where T : IResourceAnnotation { - T? lastAnnotation; - lock (resource.Annotations) - { - lastAnnotation = resource.Annotations.OfType().LastOrDefault(); - } + var lastAnnotation = resource.Annotations.OfType().LastOrDefault(); if (lastAnnotation is not null) { @@ -57,11 +53,7 @@ public static bool TryGetLastAnnotation(this IResource resource, [NotNullWhen [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool TryGetAnnotationsOfType(this IResource resource, [NotNullWhen(true)] out IEnumerable? result) where T : IResourceAnnotation { - T[] matchingTypeAnnotations; - lock (resource.Annotations) - { - matchingTypeAnnotations = resource.Annotations.OfType().ToArray(); - } + var matchingTypeAnnotations = resource.Annotations.OfType().ToArray(); if (matchingTypeAnnotations.Length > 0) { diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index bc3cc41fd17..d56d7607b48 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -1203,7 +1203,7 @@ private async Task PublishEndpointsAllocatedEventAsync(IResource resource, } var ev = new ResourceEndpointsAllocatedEvent(resource, _executionContext.ServiceProvider); - await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.NonBlockingConcurrent, ct).ConfigureAwait(false); + await _distributedApplicationEventing.PublishAsync(ev, EventDispatchBehavior.BlockingSequential, ct).ConfigureAwait(false); return true; } diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index fd0b272df7f..67458641902 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -359,10 +359,7 @@ static string TrimSuffix(string value, string suffix) } // Remove it from the resource here, we'll add it back later to avoid duplicates. - lock (resource.Annotations) - { - resource.Annotations.Remove(staticUrl); - } + resource.Annotations.Remove(staticUrl); } } @@ -475,10 +472,7 @@ static string TrimSuffix(string value, string suffix) var count = 0; foreach (var url in urls) { - lock (resource.Annotations) - { - resource.Annotations.Add(url); - } + resource.Annotations.Add(url); count++; if (_logger.IsEnabled(LogLevel.Trace)) { diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index f609ac8fe2f..9acbc48c0a5 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -1648,7 +1648,7 @@ public async Task EndpointPortsContainerProxylessNoPortTargetPortSetPublishesAll } [Fact] - public async Task ResourceEndpointsAllocatedEventSubscribersDoNotBlockDcpStartup() + public async Task ResourceEndpointsAllocatedEventSubscribersBlockDcpStartup() { var builder = DistributedApplication.CreateBuilder(); @@ -1672,10 +1672,14 @@ public async Task ResourceEndpointsAllocatedEventSubscribersDoNotBlockDcpStartup var distributedAppModel = app.Services.GetRequiredService(); var appExecutor = CreateAppExecutor(distributedAppModel, kubernetesService: kubernetesService, distributedApplicationEventing: eventing); - await appExecutor.RunApplicationAsync().DefaultTimeout(); + var runTask = appExecutor.RunApplicationAsync(); await subscriberEntered.Task.DefaultTimeout(); + var startupWasBlocked = !runTask.IsCompleted; releaseSubscriber.SetResult(); + await runTask.DefaultTimeout(); + + Assert.True(startupWasBlocked); } [Fact] From 8254cf0b2833ccbc694a4076d7bb23168460d50e Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 20 May 2026 12:29:08 -0700 Subject: [PATCH 33/38] Update ATS and polyglot codegen baselines Regenerate the Aspire.Hosting ATS baseline and non-TypeScript CodeGeneration snapshots for the container lifetime and endpoint proxy surface changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/api/Aspire.Hosting.ats.txt | 6 +- ...TwoPassScanningGeneratedAspire.verified.go | 60 ++++++++++++++++ ...oPassScanningGeneratedAspire.verified.java | 68 ++++++++++++++++++- ...TwoPassScanningGeneratedAspire.verified.py | 21 ++++++ ...TwoPassScanningGeneratedAspire.verified.rs | 59 ++++++++++++++++ 5 files changed, 212 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt index 2c4a155c267..829545e7538 100644 --- a/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt +++ b/src/Aspire.Hosting/api/Aspire.Hosting.ats.txt @@ -620,7 +620,7 @@ Aspire.Hosting/withDockerfileBaseImage(buildImage?: string, runtimeImage?: strin Aspire.Hosting/withDockerfileBuilder(contextPath: string, callback: callback, stage?: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withEndpoint(port?: number, targetPort?: number, scheme?: string, name?: string, env?: string, isProxied?: boolean, isExternal?: boolean, protocol?: enum:System.Net.Sockets.ProtocolType) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints Aspire.Hosting/withEndpointCallback(endpointName: string, callback: callback, createIfNotExists?: boolean) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints -Aspire.Hosting/withEndpointProxySupport(proxyEnabled: boolean) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource +Aspire.Hosting/withEndpointProxySupport(proxyEnabled: boolean) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints Aspire.Hosting/withEntrypoint(entrypoint: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withEnvironment(name: string, value: string|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression|Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference|Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource|Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithConnectionString|Aspire.Hosting/Aspire.Hosting.ApplicationModel.IExpressionValue) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withEnvironmentCallback(callback: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment @@ -646,11 +646,14 @@ Aspire.Hosting/withImageRegistry(registry: string) -> Aspire.Hosting/Aspire.Host Aspire.Hosting/withImageSHA256(sha256: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withImageTag(tag: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withLifetime(lifetime: enum:Aspire.Hosting.ApplicationModel.ContainerLifetime) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource +Aspire.Hosting/withLifetimeOf(sourceBuilder: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withMcpServer(path?: string, endpointName?: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEndpoints Aspire.Hosting/withOtlpExporter(protocol?: enum:Aspire.Hosting.OtlpProtocol) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withoutHttpsCertificate() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment Aspire.Hosting/withParameterBuildSecret(name: string, value: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource Aspire.Hosting/withParameterHttpsDeveloperCertificate(password?: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ParameterResource) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment +Aspire.Hosting/withParentProcessLifetime(parentProcessId: number) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource +Aspire.Hosting/withPersistentLifetime() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withPipelineConfiguration(callback: callback) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withPipelineStepFactory(stepName: string, callback: callback, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withProcessCommand(commandName: string, displayName: string, options: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ProcessCommandExportOptions) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource @@ -661,6 +664,7 @@ Aspire.Hosting/withRemoteImageName(remoteImageName: string) -> Aspire.Hosting/As Aspire.Hosting/withRemoteImageTag(remoteImageTag: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IComputeResource Aspire.Hosting/withReplicas(replicas: number) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.ProjectResource Aspire.Hosting/withRequiredCommand(command: string, helpLink?: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource +Aspire.Hosting/withSessionLifetime() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource Aspire.Hosting/withToolIgnoreExistingFeeds() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource Aspire.Hosting/withToolIgnoreFailedSources() -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource Aspire.Hosting/withToolPackage(packageId: string) -> Aspire.Hosting/Aspire.Hosting.ApplicationModel.DotnetToolResource diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index ae2e95fa6cf..2440ac457c8 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -31,6 +31,14 @@ const ( ContainerMountTypeVolume ContainerMountType = "Volume" ) +// ContainerLifetime represents ContainerLifetime. +type ContainerLifetime string + +const ( + ContainerLifetimeSession ContainerLifetime = "Session" + ContainerLifetimePersistent ContainerLifetime = "Persistent" +) + // ImagePullPolicy represents ImagePullPolicy. type ImagePullPolicy string @@ -1097,6 +1105,7 @@ type Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource interface { WithImageRegistry(registry string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithImageSHA256(sha256 string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithImageTag(tag string) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource + WithLifetime(lifetime ContainerLifetime) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithLifetimeOf(sourceBuilder Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithMcpServer(options ...*WithMcpServerOptions) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource WithMergeEndpoint(endpointName string, port float64) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource @@ -2239,6 +2248,18 @@ func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithImageTag(t return s } +// WithLifetime sets the lifetime behavior of the container resource +func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithLifetime(lifetime ContainerLifetime) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["lifetime"] = serializeValue(lifetime) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithLifetimeOf sets resource lifetime behavior to match another resource func (s *aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource) WithLifetimeOf(sourceBuilder Resource) Aspire_Hosting_CodeGeneration_Go_TestsTestVaultResource { if s.err != nil { return s } @@ -6284,6 +6305,7 @@ type ContainerResource interface { WithImageRegistry(registry string) ContainerResource WithImageSHA256(sha256 string) ContainerResource WithImageTag(tag string) ContainerResource + WithLifetime(lifetime ContainerLifetime) ContainerResource WithLifetimeOf(sourceBuilder Resource) ContainerResource WithMcpServer(options ...*WithMcpServerOptions) ContainerResource WithMergeEndpoint(endpointName string, port float64) ContainerResource @@ -7425,6 +7447,18 @@ func (s *containerResource) WithImageTag(tag string) ContainerResource { return s } +// WithLifetime sets the lifetime behavior of the container resource +func (s *containerResource) WithLifetime(lifetime ContainerLifetime) ContainerResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["lifetime"] = serializeValue(lifetime) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithLifetimeOf sets resource lifetime behavior to match another resource func (s *containerResource) WithLifetimeOf(sourceBuilder Resource) ContainerResource { if s.err != nil { return s } @@ -20199,6 +20233,7 @@ type TestDatabaseResource interface { WithImageRegistry(registry string) TestDatabaseResource WithImageSHA256(sha256 string) TestDatabaseResource WithImageTag(tag string) TestDatabaseResource + WithLifetime(lifetime ContainerLifetime) TestDatabaseResource WithLifetimeOf(sourceBuilder Resource) TestDatabaseResource WithMcpServer(options ...*WithMcpServerOptions) TestDatabaseResource WithMergeEndpoint(endpointName string, port float64) TestDatabaseResource @@ -21340,6 +21375,18 @@ func (s *testDatabaseResource) WithImageTag(tag string) TestDatabaseResource { return s } +// WithLifetime sets the lifetime behavior of the container resource +func (s *testDatabaseResource) WithLifetime(lifetime ContainerLifetime) TestDatabaseResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["lifetime"] = serializeValue(lifetime) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithLifetimeOf sets resource lifetime behavior to match another resource func (s *testDatabaseResource) WithLifetimeOf(sourceBuilder Resource) TestDatabaseResource { if s.err != nil { return s } @@ -22200,6 +22247,7 @@ type TestRedisResource interface { WithImageRegistry(registry string) TestRedisResource WithImageSHA256(sha256 string) TestRedisResource WithImageTag(tag string) TestRedisResource + WithLifetime(lifetime ContainerLifetime) TestRedisResource WithLifetimeOf(sourceBuilder Resource) TestRedisResource WithMcpServer(options ...*WithMcpServerOptions) TestRedisResource WithMergeEndpoint(endpointName string, port float64) TestRedisResource @@ -23563,6 +23611,18 @@ func (s *testRedisResource) WithImageTag(tag string) TestRedisResource { return s } +// WithLifetime sets the lifetime behavior of the container resource +func (s *testRedisResource) WithLifetime(lifetime ContainerLifetime) TestRedisResource { + if s.err != nil { return s } + ctx := context.Background() + reqArgs := map[string]any{ + "builder": s.handle.ToJSON(), + } + reqArgs["lifetime"] = serializeValue(lifetime) + if _, err := s.client.invokeCapability(ctx, "Aspire.Hosting/withLifetime", reqArgs); err != nil { s.setErr(err) } + return s +} + // WithLifetimeOf sets resource lifetime behavior to match another resource func (s *testRedisResource) WithLifetimeOf(sourceBuilder Resource) TestRedisResource { if s.err != nil { return s } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 962fca84662..53c62e3db85 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1,4 +1,4 @@ -// ===== AddContainerOptions.java ===== +// ===== AddContainerOptions.java ===== // AddContainerOptions.java - GENERATED CODE - DO NOT EDIT package aspire; @@ -3732,6 +3732,35 @@ public String valueExpression() { } +// ===== ContainerLifetime.java ===== +// ContainerLifetime.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** ContainerLifetime enum. */ +public enum ContainerLifetime implements WireValueEnum { + SESSION("Session"), + PERSISTENT("Persistent"); + + private final String value; + + ContainerLifetime(String value) { + this.value = value; + } + + public String getValue() { return value; } + + public static ContainerLifetime fromValue(String value) { + for (ContainerLifetime e : values()) { + if (e.value.equals(value)) return e; + } + throw new IllegalArgumentException("Unknown value: " + value); + } +} + // ===== ContainerMountAnnotation.java ===== // ContainerMountAnnotation.java - GENERATED CODE - DO NOT EDIT @@ -4693,6 +4722,15 @@ public ContainerResource withContainerRuntimeArgs(String[] args) { return this; } + /** Sets the lifetime behavior of the container resource */ + public ContainerResource withLifetime(ContainerLifetime lifetime) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("lifetime", AspireClient.serializeValue(lifetime)); + getClient().invokeCapability("Aspire.Hosting/withLifetime", reqArgs); + return this; + } + /** Sets the container image pull policy */ public ContainerResource withImagePullPolicy(ImagePullPolicy pullPolicy) { Map reqArgs = new HashMap<>(); @@ -17719,6 +17757,15 @@ public TestDatabaseResource withContainerRuntimeArgs(String[] args) { return this; } + /** Sets the lifetime behavior of the container resource */ + public TestDatabaseResource withLifetime(ContainerLifetime lifetime) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("lifetime", AspireClient.serializeValue(lifetime)); + getClient().invokeCapability("Aspire.Hosting/withLifetime", reqArgs); + return this; + } + /** Sets the container image pull policy */ public TestDatabaseResource withImagePullPolicy(ImagePullPolicy pullPolicy) { Map reqArgs = new HashMap<>(); @@ -19656,6 +19703,15 @@ public TestRedisResource withContainerRuntimeArgs(String[] args) { return this; } + /** Sets the lifetime behavior of the container resource */ + public TestRedisResource withLifetime(ContainerLifetime lifetime) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("lifetime", AspireClient.serializeValue(lifetime)); + getClient().invokeCapability("Aspire.Hosting/withLifetime", reqArgs); + return this; + } + /** Sets the container image pull policy */ public TestRedisResource withImagePullPolicy(ImagePullPolicy pullPolicy) { Map reqArgs = new HashMap<>(); @@ -21686,6 +21742,15 @@ public TestVaultResource withContainerRuntimeArgs(String[] args) { return this; } + /** Sets the lifetime behavior of the container resource */ + public TestVaultResource withLifetime(ContainerLifetime lifetime) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("lifetime", AspireClient.serializeValue(lifetime)); + getClient().invokeCapability("Aspire.Hosting/withLifetime", reqArgs); + return this; + } + /** Sets the container image pull policy */ public TestVaultResource withImagePullPolicy(ImagePullPolicy pullPolicy) { Map reqArgs = new HashMap<>(); @@ -24192,6 +24257,7 @@ public WithVolumeOptions isReadOnly(Boolean value) { .modules/ContainerImagePushOptions.java .modules/ContainerImagePushOptionsCallbackContext.java .modules/ContainerImageReference.java +.modules/ContainerLifetime.java .modules/ContainerMountAnnotation.java .modules/ContainerMountType.java .modules/ContainerPortReference.java diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index c652f7cce84..6c7fdd9023f 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1499,6 +1499,8 @@ def _validate_dict_types(args: typing.Any, arg_types: typing.Any) -> bool: CommandResultFormat = typing.Literal["Text", "Json", "Markdown"] +ContainerLifetime = typing.Literal["Session", "Persistent"] + ContainerMountType = typing.Literal["BindMount", "Volume"] DistributedApplicationOperation = typing.Literal["Run", "Publish"] @@ -7537,6 +7539,7 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): image: str | tuple[str, str] image_sha256: str container_runtime_args: typing.Iterable[str] + lifetime: ContainerLifetime image_pull_policy: ImagePullPolicy publish_as_container: typing.Literal[True] dockerfile: str | DockerfileParameters @@ -7671,6 +7674,17 @@ def with_container_runtime_args(self, args: typing.Iterable[str]) -> typing.Self self._handle = self._wrap_builder(result) return self + def with_lifetime(self, lifetime: ContainerLifetime) -> typing.Self: + """Sets the lifetime behavior of the container resource""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['lifetime'] = lifetime + result = self._client.invoke_capability( + 'Aspire.Hosting/withLifetime', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def with_image_pull_policy(self, pull_policy: ImagePullPolicy) -> typing.Self: """Sets the container image pull policy""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -8335,6 +8349,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withContainerRuntimeArgs', rpc_args)) else: raise TypeError("Invalid type for option 'container_runtime_args'. Expected: Iterable[str]") + if _lifetime := kwargs.pop("lifetime", None): + if _validate_type(_lifetime, ContainerLifetime): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["lifetime"] = typing.cast(ContainerLifetime, _lifetime) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withLifetime', rpc_args)) + else: + raise TypeError("Invalid type for option 'lifetime'. Expected: ContainerLifetime") if _image_pull_policy := kwargs.pop("image_pull_policy", None): if _validate_type(_image_pull_policy, ImagePullPolicy): rpc_args: dict[str, typing.Any] = {"builder": handle} diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index ffc37352217..b2d194e8499 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -39,6 +39,25 @@ impl std::fmt::Display for ContainerMountType { } } +/// ContainerLifetime +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ContainerLifetime { + #[default] + #[serde(rename = "Session")] + Session, + #[serde(rename = "Persistent")] + Persistent, +} + +impl std::fmt::Display for ContainerLifetime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Session => write!(f, "Session"), + Self::Persistent => write!(f, "Persistent"), + } + } +} + /// ImagePullPolicy #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ImagePullPolicy { @@ -3879,6 +3898,16 @@ impl ContainerResource { Ok(ContainerResource::new(handle, self.client.clone())) } + /// Sets the lifetime behavior of the container resource + pub fn with_lifetime(&self, lifetime: ContainerLifetime) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("lifetime".to_string(), serde_json::to_value(&lifetime).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Sets the container image pull policy pub fn with_image_pull_policy(&self, pull_policy: ImagePullPolicy) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14258,6 +14287,16 @@ impl TestDatabaseResource { Ok(ContainerResource::new(handle, self.client.clone())) } + /// Sets the lifetime behavior of the container resource + pub fn with_lifetime(&self, lifetime: ContainerLifetime) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("lifetime".to_string(), serde_json::to_value(&lifetime).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Sets the container image pull policy pub fn with_image_pull_policy(&self, pull_policy: ImagePullPolicy) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15700,6 +15739,16 @@ impl TestRedisResource { Ok(ContainerResource::new(handle, self.client.clone())) } + /// Sets the lifetime behavior of the container resource + pub fn with_lifetime(&self, lifetime: ContainerLifetime) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("lifetime".to_string(), serde_json::to_value(&lifetime).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Sets the container image pull policy pub fn with_image_pull_policy(&self, pull_policy: ImagePullPolicy) -> Result> { let mut args: HashMap = HashMap::new(); @@ -17248,6 +17297,16 @@ impl TestVaultResource { Ok(ContainerResource::new(handle, self.client.clone())) } + /// Sets the lifetime behavior of the container resource + pub fn with_lifetime(&self, lifetime: ContainerLifetime) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("lifetime".to_string(), serde_json::to_value(&lifetime).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withLifetime", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(ContainerResource::new(handle, self.client.clone())) + } + /// Sets the container image pull policy pub fn with_image_pull_policy(&self, pull_policy: ImagePullPolicy) -> Result> { let mut args: HashMap = HashMap::new(); From f1de1ef1dbe92e693553e28b94f24663d630d416 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 20 May 2026 12:59:20 -0700 Subject: [PATCH 34/38] Suppress endpoint proxy ATS return-type break Declare the intentional TypeScript API compatibility break for broadening withEndpointProxySupport from ContainerResource to IResourceWithEndpoints. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Aspire.Hosting/api/Aspire.Hosting.tscompat.suppression.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/Aspire.Hosting/api/Aspire.Hosting.tscompat.suppression.txt diff --git a/src/Aspire.Hosting/api/Aspire.Hosting.tscompat.suppression.txt b/src/Aspire.Hosting/api/Aspire.Hosting.tscompat.suppression.txt new file mode 100644 index 00000000000..b0628ee3e5e --- /dev/null +++ b/src/Aspire.Hosting/api/Aspire.Hosting.tscompat.suppression.txt @@ -0,0 +1 @@ +BREAK capability-return-type-changed Aspire.Hosting Aspire.Hosting/withEndpointProxySupport -- https://github.com/microsoft/aspire/pull/17112 -- Generalized endpoint proxy support from containers to all endpoint-capable resources while preserving source-level TypeScript compatibility. From 114ae802b673739f6fa3a06ea5f95858e949064c Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 20 May 2026 13:16:45 -0700 Subject: [PATCH 35/38] Fix endpoint proxy overload resolution Restore the container WithEndpointProxySupport shim as an extension method and avoid ambiguity with the generalized endpoint-capable overload by routing through typed C# overloads and a shared implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ContainerResourceBuilderExtensions.cs | 4 ++-- .../ResourceBuilderExtensions.cs | 24 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 5fa2bb9babe..ba90fbc5200 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -31,9 +31,9 @@ public static class ContainerResourceBuilderExtensions /// Should endpoints for the resource support using a proxy? /// The . [AspireExportIgnore(Reason = "Binary compatibility shim for the resource-level WithEndpointProxySupport overload.")] - public static IResourceBuilder WithEndpointProxySupport(IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource { - return ResourceBuilderExtensions.WithEndpointProxySupport(builder, proxyEnabled); + return ResourceBuilderExtensions.WithEndpointProxySupportCore(builder, proxyEnabled); } /// diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 8c9d8d8e6f7..85e6f2734dd 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -1650,10 +1650,9 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder /// Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. /// If set to false, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. /// - /// The resource type. /// The resource builder. /// Should endpoints for the resource support using a proxy? - /// The . + /// The resource builder. /// /// This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same /// port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. @@ -1661,7 +1660,26 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder /// endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. /// [AspireExport(Description = "Configures endpoint proxy support")] - public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : IResourceWithEndpoints + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) + { + return WithEndpointProxySupportCore(builder, proxyEnabled); + } + + /// + [AspireExportIgnore(Reason = "C# typed overload for the resource-level WithEndpointProxySupport export.")] + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) + { + return WithEndpointProxySupportCore(builder, proxyEnabled); + } + + /// + [AspireExportIgnore(Reason = "C# typed overload for the resource-level WithEndpointProxySupport export.")] + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) + { + return WithEndpointProxySupportCore(builder, proxyEnabled); + } + + internal static IResourceBuilder WithEndpointProxySupportCore(IResourceBuilder builder, bool proxyEnabled) where T : IResourceWithEndpoints { ArgumentNullException.ThrowIfNull(builder); From fd3384964568fd495c5d8cd03b8cddcb1b1dca17 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 20 May 2026 13:47:06 -0700 Subject: [PATCH 36/38] Clean up endpoint proxy export Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ContainerResourceBuilderExtensions.cs | 19 +- .../ResourceBuilderExtensions.cs | 42 - ...TwoPassScanningGeneratedAspire.verified.ts | 903 +++++++++--------- 3 files changed, 467 insertions(+), 497 deletions(-) diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index ba90fbc5200..18ef0ca2410 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -24,16 +24,27 @@ namespace Aspire.Hosting; public static class ContainerResourceBuilderExtensions { /// - /// Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + /// Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. /// /// The resource type. /// The resource builder. /// Should endpoints for the resource support using a proxy? /// The . - [AspireExportIgnore(Reason = "Binary compatibility shim for the resource-level WithEndpointProxySupport overload.")] - public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource + /// + /// This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + /// port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + /// The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + /// endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + /// + // Keep this method on ContainerResourceBuilderExtensions for binary compatibility; moving it changes the declaring type in metadata. + [AspireExport(Description = "Configures endpoint proxy support")] + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : IResourceWithEndpoints { - return ResourceBuilderExtensions.WithEndpointProxySupportCore(builder, proxyEnabled); + ArgumentNullException.ThrowIfNull(builder); + + builder.WithAnnotation(new ProxySupportAnnotation { ProxyEnabled = proxyEnabled }, ResourceAnnotationMutationBehavior.Replace); + + return builder; } /// diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 85e6f2734dd..b38b970b8ca 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -1646,48 +1646,6 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder })); } - /// - /// Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - /// If set to false, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - /// - /// The resource builder. - /// Should endpoints for the resource support using a proxy? - /// The resource builder. - /// - /// This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - /// port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - /// The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - /// endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - /// - [AspireExport(Description = "Configures endpoint proxy support")] - public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) - { - return WithEndpointProxySupportCore(builder, proxyEnabled); - } - - /// - [AspireExportIgnore(Reason = "C# typed overload for the resource-level WithEndpointProxySupport export.")] - public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) - { - return WithEndpointProxySupportCore(builder, proxyEnabled); - } - - /// - [AspireExportIgnore(Reason = "C# typed overload for the resource-level WithEndpointProxySupport export.")] - public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) - { - return WithEndpointProxySupportCore(builder, proxyEnabled); - } - - internal static IResourceBuilder WithEndpointProxySupportCore(IResourceBuilder builder, bool proxyEnabled) where T : IResourceWithEndpoints - { - ArgumentNullException.ThrowIfNull(builder); - - builder.WithAnnotation(new ProxySupportAnnotation { ProxyEnabled = proxyEnabled }, ResourceAnnotationMutationBehavior.Replace); - - return builder; - } - /// /// Exposes an endpoint on a resource. This endpoint reference can be retrieved using . /// The endpoint name will be the scheme name if not specified. diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index d1c0ddef9c2..14a6be035ef 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -1,4 +1,4 @@ -// aspire.ts - Capability-based Aspire SDK +// aspire.ts - Capability-based Aspire SDK // This SDK uses the ATS (Aspire Type System) capability API. // Capabilities are endpoints like 'Aspire.Hosting/createBuilder'. // @@ -12803,6 +12803,17 @@ export interface ContainerResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Adds a bind mount to a container resource. * @@ -13155,17 +13166,6 @@ export interface ContainerResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -13713,6 +13713,17 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Adds a bind mount to a container resource. * @@ -14065,17 +14076,6 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -14646,6 +14646,30 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -15561,30 +15585,6 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -17167,6 +17167,10 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withBindMount(source: string, target: string, options?: WithBindMountOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -17311,10 +17315,6 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -17600,6 +17600,17 @@ export interface CSharpAppResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): CSharpAppResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -17793,17 +17804,6 @@ export interface CSharpAppResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): CSharpAppResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -18343,6 +18343,17 @@ export interface CSharpAppResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): CSharpAppResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -18536,17 +18547,6 @@ export interface CSharpAppResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): CSharpAppResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -19108,6 +19108,30 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -19653,30 +19677,6 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new CSharpAppResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -21248,6 +21248,10 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options)), this._client); } @@ -21332,10 +21336,6 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -21621,6 +21621,17 @@ export interface DotnetToolResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): DotnetToolResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -21822,17 +21833,6 @@ export interface DotnetToolResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): DotnetToolResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -22366,6 +22366,17 @@ export interface DotnetToolResourcePromise extends PromiseLike): DotnetToolResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -22567,17 +22578,6 @@ export interface DotnetToolResourcePromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -23762,30 +23786,6 @@ class DotnetToolResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new DotnetToolResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -25337,6 +25337,10 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options)), this._client); } @@ -25445,10 +25449,6 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -25737,6 +25737,17 @@ export interface ExecutableResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ExecutableResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -25905,17 +25916,6 @@ export interface ExecutableResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ExecutableResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -26449,6 +26449,17 @@ export interface ExecutableResourcePromise extends PromiseLike): ExecutableResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -26617,17 +26628,6 @@ export interface ExecutableResourcePromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -27708,30 +27732,6 @@ class ExecutableResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ExecutableResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -29283,6 +29283,10 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options)), this._client); } @@ -29367,10 +29371,6 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -34148,6 +34148,17 @@ export interface ProjectResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ProjectResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -34341,17 +34352,6 @@ export interface ProjectResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ProjectResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -34891,6 +34891,17 @@ export interface ProjectResourcePromise extends PromiseLike { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ProjectResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -35084,17 +35095,6 @@ export interface ProjectResourcePromise extends PromiseLike { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ProjectResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -35657,6 +35657,30 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -36202,30 +36226,6 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ProjectResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -37797,6 +37797,10 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options)), this._client); } @@ -37881,10 +37885,6 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -38170,6 +38170,17 @@ export interface TestDatabaseResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestDatabaseResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Adds a bind mount to a container resource. * @@ -38522,17 +38533,6 @@ export interface TestDatabaseResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestDatabaseResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -39080,6 +39080,17 @@ export interface TestDatabaseResourcePromise extends PromiseLike): TestDatabaseResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Adds a bind mount to a container resource. * @@ -39432,17 +39443,6 @@ export interface TestDatabaseResourcePromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -40927,30 +40951,6 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -42533,6 +42533,10 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withBindMount(source: string, target: string, options?: WithBindMountOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -42677,10 +42681,6 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -42966,6 +42966,17 @@ export interface TestRedisResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Adds a bind mount to a container resource. * @@ -43334,17 +43345,6 @@ export interface TestRedisResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -43940,6 +43940,17 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Adds a bind mount to a container resource. * @@ -44308,17 +44319,6 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -44936,6 +44936,30 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -45887,30 +45911,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -47704,6 +47704,10 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withBindMount(source: string, target: string, options?: WithBindMountOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -47856,10 +47860,6 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -48197,6 +48197,17 @@ export interface TestVaultResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Adds a bind mount to a container resource. * @@ -48549,17 +48560,6 @@ export interface TestVaultResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -49109,6 +49109,17 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Adds a bind mount to a container resource. * @@ -49461,17 +49472,6 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -50043,6 +50043,30 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -50958,30 +50982,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -52579,6 +52579,10 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withBindMount(source: string, target: string, options?: WithBindMountOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -52723,10 +52727,6 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -55962,6 +55962,17 @@ class ResourceWithContainerFilesPromiseImpl implements ResourceWithContainerFile /** Represents a resource that has endpoints associated with it. */ export interface ResourceWithEndpoints { toJSON(): MarshalledHandle; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise; /** * Marks the resource as hosting a Model Context Protocol (MCP) server on the specified endpoint. * @@ -55992,17 +56003,6 @@ export interface ResourceWithEndpoints { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ResourceWithEndpointsPromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -56077,6 +56077,17 @@ export interface ResourceWithEndpoints { } export interface ResourceWithEndpointsPromise extends PromiseLike { + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise; /** * Marks the resource as hosting a Model Context Protocol (MCP) server on the specified endpoint. * @@ -56107,17 +56118,6 @@ export interface ResourceWithEndpointsPromise extends PromiseLike { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ResourceWithEndpointsImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The `IResourceBuilder`1`. + */ + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { + return new ResourceWithEndpointsPromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withMcpServerInternal(path?: string, endpointName?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -56341,30 +56365,6 @@ class ResourceWithEndpointsImpl extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ResourceWithEndpointsImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { - return new ResourceWithEndpointsPromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -56612,6 +56612,10 @@ class ResourceWithEndpointsPromiseImpl implements ResourceWithEndpointsPromise { return this._promise.then(onfulfilled, onrejected); } + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { + return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withMcpServer(options?: WithMcpServerOptions): ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withMcpServer(options)), this._client); } @@ -56632,10 +56636,6 @@ class ResourceWithEndpointsPromiseImpl implements ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { - return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withHttpEndpoint(options?: WithHttpEndpointOptions): ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -57628,3 +57628,4 @@ registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceW registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment', (handle, client) => new ResourceWithEnvironmentImpl(handle as IResourceWithEnvironmentHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithWaitSupport', (handle, client) => new ResourceWithWaitSupportImpl(handle as IResourceWithWaitSupportHandle, client)); + From 01ebf384b00b1815acfeab59b04b7bd313052653 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Wed, 20 May 2026 13:59:28 -0700 Subject: [PATCH 37/38] Update non-TypeScript codegen baselines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...oPassScanningGeneratedAspire.verified.java | 147 ++++++++-------- ...TwoPassScanningGeneratedAspire.verified.py | 125 +++++++------- ...TwoPassScanningGeneratedAspire.verified.rs | 163 +++++++++--------- 3 files changed, 219 insertions(+), 216 deletions(-) diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 53c62e3db85..f28efda7586 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1,4 +1,4 @@ -// ===== AddContainerOptions.java ===== +// ===== AddContainerOptions.java ===== // AddContainerOptions.java - GENERATED CODE - DO NOT EDIT package aspire; @@ -1588,6 +1588,15 @@ public CSharpAppResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } + /** Configures endpoint proxy support */ + public CSharpAppResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Sets the base image for a Dockerfile build */ public CSharpAppResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -2033,15 +2042,6 @@ private CSharpAppResource withEndpointImpl(Double port, Double targetPort, Strin return this; } - /** Configures endpoint proxy support */ - public CSharpAppResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Adds an HTTP endpoint */ public CSharpAppResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -4644,6 +4644,15 @@ public ContainerResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } + /** Configures endpoint proxy support */ + public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + public ContainerResource withBindMount(String source, String target) { return withBindMount(source, target, null); } @@ -5289,15 +5298,6 @@ private ContainerResource withEndpointImpl(Double port, Double targetPort, Strin return this; } - /** Configures endpoint proxy support */ - public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Adds an HTTP endpoint */ public ContainerResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -6953,6 +6953,15 @@ public DotnetToolResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } + /** Configures endpoint proxy support */ + public DotnetToolResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Sets the base image for a Dockerfile build */ public DotnetToolResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -7446,15 +7455,6 @@ private DotnetToolResource withEndpointImpl(Double port, Double targetPort, Stri return this; } - /** Configures endpoint proxy support */ - public DotnetToolResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Adds an HTTP endpoint */ public DotnetToolResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -9108,6 +9108,15 @@ public ExecutableResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } + /** Configures endpoint proxy support */ + public ExecutableResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Sets the base image for a Dockerfile build */ public ExecutableResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -9550,15 +9559,6 @@ private ExecutableResource withEndpointImpl(Double port, Double targetPort, Stri return this; } - /** Configures endpoint proxy support */ - public ExecutableResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Adds an HTTP endpoint */ public ExecutableResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -15118,6 +15118,15 @@ public ProjectResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } + /** Configures endpoint proxy support */ + public ProjectResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Sets the base image for a Dockerfile build */ public ProjectResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -15563,15 +15572,6 @@ private ProjectResource withEndpointImpl(Double port, Double targetPort, String return this; } - /** Configures endpoint proxy support */ - public ProjectResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Adds an HTTP endpoint */ public ProjectResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -17679,6 +17679,15 @@ public TestDatabaseResource withContainerRegistry(ResourceBuilderBase registry) return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } + /** Configures endpoint proxy support */ + public TestDatabaseResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + public TestDatabaseResource withBindMount(String source, String target) { return withBindMount(source, target, null); } @@ -18324,15 +18333,6 @@ private TestDatabaseResource withEndpointImpl(Double port, Double targetPort, St return this; } - /** Configures endpoint proxy support */ - public TestDatabaseResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Adds an HTTP endpoint */ public TestDatabaseResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -19625,6 +19625,15 @@ public TestRedisResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } + /** Configures endpoint proxy support */ + public TestRedisResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + public TestRedisResource withBindMount(String source, String target) { return withBindMount(source, target, null); } @@ -20296,15 +20305,6 @@ private TestRedisResource withEndpointImpl(Double port, Double targetPort, Strin return this; } - /** Configures endpoint proxy support */ - public TestRedisResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Adds an HTTP endpoint */ public TestRedisResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -21664,6 +21664,15 @@ public TestVaultResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } + /** Configures endpoint proxy support */ + public TestVaultResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + public TestVaultResource withBindMount(String source, String target) { return withBindMount(source, target, null); } @@ -22309,15 +22318,6 @@ private TestVaultResource withEndpointImpl(Double port, Double targetPort, Strin return this; } - /** Configures endpoint proxy support */ - public TestVaultResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Adds an HTTP endpoint */ public TestVaultResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -24404,3 +24404,4 @@ public WithVolumeOptions isReadOnly(Boolean value) { .modules/WithPipelineStepFactoryOptions.java .modules/WithReferenceOptions.java .modules/WithVolumeOptions.java + diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 6c7fdd9023f..449c9934db7 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1,4 +1,4 @@ -# ------------------------------------------------------------- +# ------------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. See LICENSE in project root for information. # @@ -6345,6 +6345,10 @@ def clear_container_files_sources(self) -> typing.Self: class AbstractResourceWithEndpoints(AbstractResource): """Abstract base class for AbstractResourceWithEndpoints interface.""" + @abc.abstractmethod + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + @abc.abstractmethod def with_mcp_server(self, *, path: str = "/mcp", endpoint_name: str | None = None) -> typing.Self: """Configures an MCP server endpoint on the resource""" @@ -6365,10 +6369,6 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" - @abc.abstractmethod - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - @abc.abstractmethod def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" @@ -7532,6 +7532,7 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack class ContainerResourceKwargs(_BaseResourceKwargs, total=False): """ContainerResource options.""" + endpoint_proxy_support: bool bind_mount: tuple[str, str] | BindMountParameters entrypoint: str image_tag: str @@ -7562,7 +7563,6 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] - endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -7592,6 +7592,17 @@ class ContainerResource(_BaseResource, AbstractResourceWithEnvironment, Abstract def __repr__(self) -> str: return "ContainerResource(handle={self._handle.handle_id})" + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def with_bind_mount(self, source: str, target: str, *, is_read_only: bool = False) -> typing.Self: """Adds a bind mount""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -7976,17 +7987,6 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['proxyEnabled'] = proxy_enabled - result = self._client.invoke_capability( - 'Aspire.Hosting/withEndpointProxySupport', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -8288,6 +8288,13 @@ def with_env_vars(self, vars: typing.Mapping[str, str]) -> typing.Self: return self def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack[ContainerResourceKwargs]) -> None: + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _bind_mount := kwargs.pop("bind_mount", None): if _validate_tuple_types(_bind_mount, (str, str)): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8572,13 +8579,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") - if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): - if _validate_type(_endpoint_proxy_support, bool): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) - else: - raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8797,6 +8797,7 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack class ProjectResourceKwargs(_BaseResourceKwargs, total=False): """ProjectResource options.""" + endpoint_proxy_support: bool mcp_server: McpServerParameters | typing.Literal[True] otlp_exporter: OtlpProtocol | typing.Literal[True] replicas: int @@ -8812,7 +8813,6 @@ class ProjectResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] - endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -8842,6 +8842,17 @@ class ProjectResource(_BaseResource, AbstractResourceWithEnvironment, AbstractRe def __repr__(self) -> str: return "ProjectResource(handle={self._handle.handle_id})" + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def with_mcp_server(self, *, path: str = "/mcp", endpoint_name: str | None = None) -> typing.Self: """Configures an MCP server endpoint on the resource""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -9044,17 +9055,6 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['proxyEnabled'] = proxy_enabled - result = self._client.invoke_capability( - 'Aspire.Hosting/withEndpointProxySupport', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -9353,6 +9353,13 @@ def with_env_vars(self, vars: typing.Mapping[str, str]) -> typing.Self: return self def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack[ProjectResourceKwargs]) -> None: + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _mcp_server := kwargs.pop("mcp_server", None): if _validate_dict_types(_mcp_server, McpServerParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -9504,13 +9511,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") - if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): - if _validate_type(_endpoint_proxy_support, bool): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) - else: - raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -9738,6 +9738,7 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack class ExecutableResourceKwargs(_BaseResourceKwargs, total=False): """ExecutableResource options.""" + endpoint_proxy_support: bool publish_as_docker_file: typing.Callable[[ContainerResource], None] executable_command: str working_dir: str @@ -9753,7 +9754,6 @@ class ExecutableResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] - endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -9782,6 +9782,17 @@ class ExecutableResource(_BaseResource, AbstractResourceWithEnvironment, Abstrac def __repr__(self) -> str: return "ExecutableResource(handle={self._handle.handle_id})" + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def publish_as_docker_file(self, configure: typing.Callable[[ContainerResource], None]) -> typing.Self: """Publishes an executable as a Docker file""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -9984,17 +9995,6 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['proxyEnabled'] = proxy_enabled - result = self._client.invoke_capability( - 'Aspire.Hosting/withEndpointProxySupport', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -10281,6 +10281,13 @@ def with_env_vars(self, vars: typing.Mapping[str, str]) -> typing.Self: return self def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack[ExecutableResourceKwargs]) -> None: + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _publish_as_docker_file := kwargs.pop("publish_as_docker_file", None): if _validate_type(_publish_as_docker_file, typing.Callable[[ContainerResource], None]): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -10430,13 +10437,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") - if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): - if _validate_type(_endpoint_proxy_support, bool): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) - else: - raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -11351,3 +11351,4 @@ def create_builder( _register_handle_wrapper("Aspire.Hosting.CodeGeneration.Python.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.TestTypes.TestDatabaseResource", TestDatabaseResource) _register_handle_wrapper("Aspire.Hosting.CodeGeneration.Python.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.TestTypes.TestRedisResource", TestRedisResource) _register_handle_wrapper("Aspire.Hosting.CodeGeneration.Python.Tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests.TestTypes.TestVaultResource", TestVaultResource) + diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index b2d194e8499..9cbe5cf9322 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -1,4 +1,4 @@ -//! aspire.rs - Capability-based Aspire SDK +//! aspire.rs - Capability-based Aspire SDK //! GENERATED CODE - DO NOT EDIT use std::collections::HashMap; @@ -1675,6 +1675,16 @@ impl CSharpAppResource { Ok(IResource::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -1952,16 +1962,6 @@ impl CSharpAppResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3821,6 +3821,16 @@ impl ContainerResource { Ok(IResource::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds a bind mount pub fn with_bind_mount(&self, source: &str, target: &str, is_read_only: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -4274,16 +4284,6 @@ impl ContainerResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -5685,6 +5685,16 @@ impl DotnetToolResource { Ok(IResource::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -6020,16 +6030,6 @@ impl DotnetToolResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7497,6 +7497,16 @@ impl ExecutableResource { Ok(IResource::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7775,16 +7785,6 @@ impl ExecutableResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12402,6 +12402,16 @@ impl ProjectResource { Ok(IResource::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12679,16 +12689,6 @@ impl ProjectResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14210,6 +14210,16 @@ impl TestDatabaseResource { Ok(IResource::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds a bind mount pub fn with_bind_mount(&self, source: &str, target: &str, is_read_only: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14663,16 +14673,6 @@ impl TestDatabaseResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15662,6 +15662,16 @@ impl TestRedisResource { Ok(IResource::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds a bind mount pub fn with_bind_mount(&self, source: &str, target: &str, is_read_only: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -16135,16 +16145,6 @@ impl TestRedisResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -17220,6 +17220,16 @@ impl TestVaultResource { Ok(IResource::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds a bind mount pub fn with_bind_mount(&self, source: &str, target: &str, is_read_only: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -17673,16 +17683,6 @@ impl TestVaultResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -18605,3 +18605,4 @@ pub fn create_builder(options: Option) -> Result Date: Wed, 20 May 2026 14:28:20 -0700 Subject: [PATCH 38/38] Preserve endpoint proxy binary compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ContainerResourceBuilderExtensions.cs | 12 +- .../ResourceBuilderExtensions.cs | 28 + ...oPassScanningGeneratedAspire.verified.java | 144 +-- ...TwoPassScanningGeneratedAspire.verified.py | 122 +-- ...TwoPassScanningGeneratedAspire.verified.rs | 160 ++-- ...TwoPassScanningGeneratedAspire.verified.ts | 900 +++++++++--------- .../Aspire.Hosting.Tests/WithEndpointTests.cs | 8 +- 7 files changed, 701 insertions(+), 673 deletions(-) diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 18ef0ca2410..d68b13ce307 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -24,7 +24,7 @@ namespace Aspire.Hosting; public static class ContainerResourceBuilderExtensions { /// - /// Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + /// Set whether a container resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. /// /// The resource type. /// The resource builder. @@ -37,14 +37,10 @@ public static class ContainerResourceBuilderExtensions /// endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. /// // Keep this method on ContainerResourceBuilderExtensions for binary compatibility; moving it changes the declaring type in metadata. - [AspireExport(Description = "Configures endpoint proxy support")] - public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : IResourceWithEndpoints + [AspireExportIgnore(Reason = "Binary compatibility shim for the resource-level WithEndpointProxySupport overload.")] + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) where T : ContainerResource { - ArgumentNullException.ThrowIfNull(builder); - - builder.WithAnnotation(new ProxySupportAnnotation { ProxyEnabled = proxyEnabled }, ResourceAnnotationMutationBehavior.Replace); - - return builder; + return ResourceBuilderExtensions.SetEndpointProxySupport(builder, proxyEnabled); } /// diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index b38b970b8ca..f3ddab5429f 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -1646,6 +1646,34 @@ private static void ConfigureEndpointEnvironmentVariable(IResourceBuilder })); } + /// + /// Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. + /// If set to false, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + /// + /// The resource builder. + /// Should endpoints for the resource support using a proxy? + /// The resource builder. + /// + /// This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + /// port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + /// The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + /// endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + /// + [AspireExport(Description = "Configures endpoint proxy support")] + public static IResourceBuilder WithEndpointProxySupport(this IResourceBuilder builder, bool proxyEnabled) + { + return SetEndpointProxySupport(builder, proxyEnabled); + } + + internal static IResourceBuilder SetEndpointProxySupport(IResourceBuilder builder, bool proxyEnabled) where T : IResourceWithEndpoints + { + ArgumentNullException.ThrowIfNull(builder); + + builder.WithAnnotation(new ProxySupportAnnotation { ProxyEnabled = proxyEnabled }, ResourceAnnotationMutationBehavior.Replace); + + return builder; + } + /// /// Exposes an endpoint on a resource. This endpoint reference can be retrieved using . /// The endpoint name will be the scheme name if not specified. diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index f28efda7586..07011cada6e 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1588,15 +1588,6 @@ public CSharpAppResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } - /** Configures endpoint proxy support */ - public CSharpAppResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Sets the base image for a Dockerfile build */ public CSharpAppResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -2042,6 +2033,15 @@ private CSharpAppResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public CSharpAppResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public CSharpAppResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -4644,15 +4644,6 @@ public ContainerResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } - /** Configures endpoint proxy support */ - public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public ContainerResource withBindMount(String source, String target) { return withBindMount(source, target, null); } @@ -5298,6 +5289,15 @@ private ContainerResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public ContainerResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public ContainerResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -6953,15 +6953,6 @@ public DotnetToolResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } - /** Configures endpoint proxy support */ - public DotnetToolResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Sets the base image for a Dockerfile build */ public DotnetToolResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -7455,6 +7446,15 @@ private DotnetToolResource withEndpointImpl(Double port, Double targetPort, Stri return this; } + /** Configures endpoint proxy support */ + public DotnetToolResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public DotnetToolResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -9108,15 +9108,6 @@ public ExecutableResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } - /** Configures endpoint proxy support */ - public ExecutableResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Sets the base image for a Dockerfile build */ public ExecutableResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -9559,6 +9550,15 @@ private ExecutableResource withEndpointImpl(Double port, Double targetPort, Stri return this; } + /** Configures endpoint proxy support */ + public ExecutableResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public ExecutableResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -15118,15 +15118,6 @@ public ProjectResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } - /** Configures endpoint proxy support */ - public ProjectResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - /** Sets the base image for a Dockerfile build */ public ProjectResource withDockerfileBaseImage(WithDockerfileBaseImageOptions options) { var buildImage = options == null ? null : options.getBuildImage(); @@ -15572,6 +15563,15 @@ private ProjectResource withEndpointImpl(Double port, Double targetPort, String return this; } + /** Configures endpoint proxy support */ + public ProjectResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public ProjectResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -17679,15 +17679,6 @@ public TestDatabaseResource withContainerRegistry(ResourceBuilderBase registry) return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } - /** Configures endpoint proxy support */ - public TestDatabaseResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public TestDatabaseResource withBindMount(String source, String target) { return withBindMount(source, target, null); } @@ -18333,6 +18324,15 @@ private TestDatabaseResource withEndpointImpl(Double port, Double targetPort, St return this; } + /** Configures endpoint proxy support */ + public TestDatabaseResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public TestDatabaseResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -19625,15 +19625,6 @@ public TestRedisResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } - /** Configures endpoint proxy support */ - public TestRedisResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public TestRedisResource withBindMount(String source, String target) { return withBindMount(source, target, null); } @@ -20305,6 +20296,15 @@ private TestRedisResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public TestRedisResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public TestRedisResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); @@ -21664,15 +21664,6 @@ public TestVaultResource withContainerRegistry(ResourceBuilderBase registry) { return withContainerRegistry(new IResource(registry.getHandle(), registry.getClient())); } - /** Configures endpoint proxy support */ - public TestVaultResource withEndpointProxySupport(boolean proxyEnabled) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); - getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); - return this; - } - public TestVaultResource withBindMount(String source, String target) { return withBindMount(source, target, null); } @@ -22318,6 +22309,15 @@ private TestVaultResource withEndpointImpl(Double port, Double targetPort, Strin return this; } + /** Configures endpoint proxy support */ + public TestVaultResource withEndpointProxySupport(boolean proxyEnabled) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("proxyEnabled", AspireClient.serializeValue(proxyEnabled)); + getClient().invokeCapability("Aspire.Hosting/withEndpointProxySupport", reqArgs); + return this; + } + /** Adds an HTTP endpoint */ public TestVaultResource withHttpEndpoint(WithHttpEndpointOptions options) { var port = options == null ? null : options.getPort(); diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 449c9934db7..c705b211f5f 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -6345,10 +6345,6 @@ def clear_container_files_sources(self) -> typing.Self: class AbstractResourceWithEndpoints(AbstractResource): """Abstract base class for AbstractResourceWithEndpoints interface.""" - @abc.abstractmethod - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - @abc.abstractmethod def with_mcp_server(self, *, path: str = "/mcp", endpoint_name: str | None = None) -> typing.Self: """Configures an MCP server endpoint on the resource""" @@ -6369,6 +6365,10 @@ def with_https_endpoint_callback(self, callback: typing.Callable[[EndpointUpdate def with_endpoint(self, *, port: int | None = None, target_port: int | None = None, scheme: str | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None, is_external: bool | None = None, protocol: ProtocolType | None = None) -> typing.Self: """Adds a network endpoint""" + @abc.abstractmethod + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + @abc.abstractmethod def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" @@ -7532,7 +7532,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack class ContainerResourceKwargs(_BaseResourceKwargs, total=False): """ContainerResource options.""" - endpoint_proxy_support: bool bind_mount: tuple[str, str] | BindMountParameters entrypoint: str image_tag: str @@ -7563,6 +7562,7 @@ class ContainerResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] + endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -7592,17 +7592,6 @@ class ContainerResource(_BaseResource, AbstractResourceWithEnvironment, Abstract def __repr__(self) -> str: return "ContainerResource(handle={self._handle.handle_id})" - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['proxyEnabled'] = proxy_enabled - result = self._client.invoke_capability( - 'Aspire.Hosting/withEndpointProxySupport', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_bind_mount(self, source: str, target: str, *, is_read_only: bool = False) -> typing.Self: """Adds a bind mount""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -7987,6 +7976,17 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -8288,13 +8288,6 @@ def with_env_vars(self, vars: typing.Mapping[str, str]) -> typing.Self: return self def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack[ContainerResourceKwargs]) -> None: - if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): - if _validate_type(_endpoint_proxy_support, bool): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) - else: - raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _bind_mount := kwargs.pop("bind_mount", None): if _validate_tuple_types(_bind_mount, (str, str)): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8579,6 +8572,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -8797,7 +8797,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack class ProjectResourceKwargs(_BaseResourceKwargs, total=False): """ProjectResource options.""" - endpoint_proxy_support: bool mcp_server: McpServerParameters | typing.Literal[True] otlp_exporter: OtlpProtocol | typing.Literal[True] replicas: int @@ -8813,6 +8812,7 @@ class ProjectResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] + endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -8842,17 +8842,6 @@ class ProjectResource(_BaseResource, AbstractResourceWithEnvironment, AbstractRe def __repr__(self) -> str: return "ProjectResource(handle={self._handle.handle_id})" - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['proxyEnabled'] = proxy_enabled - result = self._client.invoke_capability( - 'Aspire.Hosting/withEndpointProxySupport', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def with_mcp_server(self, *, path: str = "/mcp", endpoint_name: str | None = None) -> typing.Self: """Configures an MCP server endpoint on the resource""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -9055,6 +9044,17 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -9353,13 +9353,6 @@ def with_env_vars(self, vars: typing.Mapping[str, str]) -> typing.Self: return self def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack[ProjectResourceKwargs]) -> None: - if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): - if _validate_type(_endpoint_proxy_support, bool): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) - else: - raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _mcp_server := kwargs.pop("mcp_server", None): if _validate_dict_types(_mcp_server, McpServerParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -9511,6 +9504,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -9738,7 +9738,6 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack class ExecutableResourceKwargs(_BaseResourceKwargs, total=False): """ExecutableResource options.""" - endpoint_proxy_support: bool publish_as_docker_file: typing.Callable[[ContainerResource], None] executable_command: str working_dir: str @@ -9754,6 +9753,7 @@ class ExecutableResourceKwargs(_BaseResourceKwargs, total=False): http_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpEndpointCallbackParameters https_endpoint_callback: typing.Callable[[EndpointUpdateContext], None] | HttpsEndpointCallbackParameters endpoint: EndpointParameters | typing.Literal[True] + endpoint_proxy_support: bool http_endpoint: HttpEndpointParameters | typing.Literal[True] https_endpoint: HttpsEndpointParameters | typing.Literal[True] external_http_endpoints: typing.Literal[True] @@ -9782,17 +9782,6 @@ class ExecutableResource(_BaseResource, AbstractResourceWithEnvironment, Abstrac def __repr__(self) -> str: return "ExecutableResource(handle={self._handle.handle_id})" - def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: - """Configures endpoint proxy support""" - rpc_args: dict[str, typing.Any] = {'builder': self._handle} - rpc_args['proxyEnabled'] = proxy_enabled - result = self._client.invoke_capability( - 'Aspire.Hosting/withEndpointProxySupport', - rpc_args, - ) - self._handle = self._wrap_builder(result) - return self - def publish_as_docker_file(self, configure: typing.Callable[[ContainerResource], None]) -> typing.Self: """Publishes an executable as a Docker file""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -9995,6 +9984,17 @@ def with_endpoint(self, *, port: int | None = None, target_port: int | None = No self._handle = self._wrap_builder(result) return self + def with_endpoint_proxy_support(self, proxy_enabled: bool) -> typing.Self: + """Configures endpoint proxy support""" + rpc_args: dict[str, typing.Any] = {'builder': self._handle} + rpc_args['proxyEnabled'] = proxy_enabled + result = self._client.invoke_capability( + 'Aspire.Hosting/withEndpointProxySupport', + rpc_args, + ) + self._handle = self._wrap_builder(result) + return self + def with_http_endpoint(self, *, port: int | None = None, target_port: int | None = None, name: str | None = None, env: str | None = None, is_proxied: bool | None = None) -> typing.Self: """Adds an HTTP endpoint""" rpc_args: dict[str, typing.Any] = {'builder': self._handle} @@ -10281,13 +10281,6 @@ def with_env_vars(self, vars: typing.Mapping[str, str]) -> typing.Self: return self def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack[ExecutableResourceKwargs]) -> None: - if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): - if _validate_type(_endpoint_proxy_support, bool): - rpc_args: dict[str, typing.Any] = {"builder": handle} - rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) - handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) - else: - raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _publish_as_docker_file := kwargs.pop("publish_as_docker_file", None): if _validate_type(_publish_as_docker_file, typing.Callable[[ContainerResource], None]): rpc_args: dict[str, typing.Any] = {"builder": handle} @@ -10437,6 +10430,13 @@ def __init__(self, handle: Handle, client: AspireClient, **kwargs: typing.Unpack handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpoint', rpc_args)) else: raise TypeError("Invalid type for option 'endpoint'. Expected: EndpointParameters or Literal[True]") + if _endpoint_proxy_support := kwargs.pop("endpoint_proxy_support", None): + if _validate_type(_endpoint_proxy_support, bool): + rpc_args: dict[str, typing.Any] = {"builder": handle} + rpc_args["proxyEnabled"] = typing.cast(bool, _endpoint_proxy_support) + handle = self._wrap_builder(client.invoke_capability('Aspire.Hosting/withEndpointProxySupport', rpc_args)) + else: + raise TypeError("Invalid type for option 'endpoint_proxy_support'. Expected: bool") if _http_endpoint := kwargs.pop("http_endpoint", None): if _validate_dict_types(_http_endpoint, HttpEndpointParameters): rpc_args: dict[str, typing.Any] = {"builder": handle} diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 9cbe5cf9322..e05d15eb545 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -1675,16 +1675,6 @@ impl CSharpAppResource { Ok(IResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -1962,6 +1952,16 @@ impl CSharpAppResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3821,16 +3821,6 @@ impl ContainerResource { Ok(IResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds a bind mount pub fn with_bind_mount(&self, source: &str, target: &str, is_read_only: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -4284,6 +4274,16 @@ impl ContainerResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -5685,16 +5685,6 @@ impl DotnetToolResource { Ok(IResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -6030,6 +6020,16 @@ impl DotnetToolResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7497,16 +7497,6 @@ impl ExecutableResource { Ok(IResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -7785,6 +7775,16 @@ impl ExecutableResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12402,16 +12402,6 @@ impl ProjectResource { Ok(IResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Sets the base image for a Dockerfile build pub fn with_dockerfile_base_image(&self, build_image: Option<&str>, runtime_image: Option<&str>) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12689,6 +12679,16 @@ impl ProjectResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14210,16 +14210,6 @@ impl TestDatabaseResource { Ok(IResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds a bind mount pub fn with_bind_mount(&self, source: &str, target: &str, is_read_only: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14673,6 +14663,16 @@ impl TestDatabaseResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -15662,16 +15662,6 @@ impl TestRedisResource { Ok(IResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds a bind mount pub fn with_bind_mount(&self, source: &str, target: &str, is_read_only: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -16145,6 +16135,16 @@ impl TestRedisResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -17220,16 +17220,6 @@ impl TestVaultResource { Ok(IResource::new(handle, self.client.clone())) } - /// Configures endpoint proxy support - pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEndpoints::new(handle, self.client.clone())) - } - /// Adds a bind mount pub fn with_bind_mount(&self, source: &str, target: &str, is_read_only: Option) -> Result> { let mut args: HashMap = HashMap::new(); @@ -17683,6 +17673,16 @@ impl TestVaultResource { Ok(IResourceWithEndpoints::new(handle, self.client.clone())) } + /// Configures endpoint proxy support + pub fn with_endpoint_proxy_support(&self, proxy_enabled: bool) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("builder".to_string(), self.handle.to_json()); + args.insert("proxyEnabled".to_string(), serde_json::to_value(&proxy_enabled).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEndpointProxySupport", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IResourceWithEndpoints::new(handle, self.client.clone())) + } + /// Adds an HTTP endpoint pub fn with_http_endpoint(&self, port: Option, target_port: Option, name: Option<&str>, env: Option<&str>, is_proxied: Option) -> Result> { let mut args: HashMap = HashMap::new(); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 14a6be035ef..27774180169 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -12803,17 +12803,6 @@ export interface ContainerResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Adds a bind mount to a container resource. * @@ -13166,6 +13155,17 @@ export interface ContainerResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -13713,17 +13713,6 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ContainerResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Adds a bind mount to a container resource. * @@ -14076,6 +14065,17 @@ export interface ContainerResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ContainerResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -14646,30 +14646,6 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ContainerResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -15585,6 +15561,30 @@ class ContainerResourceImpl extends ResourceBuilderBase return new ContainerResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ContainerResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -17167,10 +17167,6 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { - return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withBindMount(source: string, target: string, options?: WithBindMountOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -17315,6 +17311,10 @@ class ContainerResourcePromiseImpl implements ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ContainerResourcePromise { + return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ContainerResourcePromise { return new ContainerResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -17600,17 +17600,6 @@ export interface CSharpAppResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): CSharpAppResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -17804,6 +17793,17 @@ export interface CSharpAppResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): CSharpAppResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -18343,17 +18343,6 @@ export interface CSharpAppResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): CSharpAppResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -18547,6 +18536,17 @@ export interface CSharpAppResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): CSharpAppResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -19108,30 +19108,6 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new CSharpAppResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -19677,6 +19653,30 @@ class CSharpAppResourceImpl extends ResourceBuilderBase return new CSharpAppResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new CSharpAppResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -21248,10 +21248,6 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { - return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options)), this._client); } @@ -21336,6 +21332,10 @@ class CSharpAppResourcePromiseImpl implements CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): CSharpAppResourcePromise { + return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): CSharpAppResourcePromise { return new CSharpAppResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -21621,17 +21621,6 @@ export interface DotnetToolResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): DotnetToolResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -21833,6 +21822,17 @@ export interface DotnetToolResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): DotnetToolResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -22366,17 +22366,6 @@ export interface DotnetToolResourcePromise extends PromiseLike): DotnetToolResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -22578,6 +22567,17 @@ export interface DotnetToolResourcePromise extends PromiseLike { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new DotnetToolResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -23786,6 +23762,30 @@ class DotnetToolResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new DotnetToolResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -25337,10 +25337,6 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { - return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options)), this._client); } @@ -25449,6 +25445,10 @@ class DotnetToolResourcePromiseImpl implements DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): DotnetToolResourcePromise { + return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): DotnetToolResourcePromise { return new DotnetToolResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -25737,17 +25737,6 @@ export interface ExecutableResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ExecutableResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -25916,6 +25905,17 @@ export interface ExecutableResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ExecutableResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -26449,17 +26449,6 @@ export interface ExecutableResourcePromise extends PromiseLike): ExecutableResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -26628,6 +26617,17 @@ export interface ExecutableResourcePromise extends PromiseLike { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ExecutableResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -27732,6 +27708,30 @@ class ExecutableResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ExecutableResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -29283,10 +29283,6 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { - return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options)), this._client); } @@ -29371,6 +29367,10 @@ class ExecutableResourcePromiseImpl implements ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ExecutableResourcePromise { + return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ExecutableResourcePromise { return new ExecutableResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -34148,17 +34148,6 @@ export interface ProjectResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ProjectResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -34352,6 +34341,17 @@ export interface ProjectResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ProjectResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -34891,17 +34891,6 @@ export interface ProjectResourcePromise extends PromiseLike { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): ProjectResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Configures custom base images for generated Dockerfiles. * @@ -35095,6 +35084,17 @@ export interface ProjectResourcePromise extends PromiseLike { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ProjectResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -35657,30 +35657,6 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ProjectResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withDockerfileBaseImageInternal(buildImage?: string, runtimeImage?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -36226,6 +36202,30 @@ class ProjectResourceImpl extends ResourceBuilderBase imp return new ProjectResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ProjectResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -37797,10 +37797,6 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { - return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withDockerfileBaseImage(options?: WithDockerfileBaseImageOptions): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withDockerfileBaseImage(options)), this._client); } @@ -37885,6 +37881,10 @@ class ProjectResourcePromiseImpl implements ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ProjectResourcePromise { + return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ProjectResourcePromise { return new ProjectResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -38170,17 +38170,6 @@ export interface TestDatabaseResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestDatabaseResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Adds a bind mount to a container resource. * @@ -38533,6 +38522,17 @@ export interface TestDatabaseResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestDatabaseResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -39080,17 +39080,6 @@ export interface TestDatabaseResourcePromise extends PromiseLike): TestDatabaseResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise; /** * Adds a bind mount to a container resource. * @@ -39443,6 +39432,17 @@ export interface TestDatabaseResourcePromise extends PromiseLike { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestDatabaseResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -40951,6 +40927,30 @@ class TestDatabaseResourceImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestDatabaseResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -42533,10 +42533,6 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withBindMount(source: string, target: string, options?: WithBindMountOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -42681,6 +42677,10 @@ class TestDatabaseResourcePromiseImpl implements TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestDatabaseResourcePromise { return new TestDatabaseResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -42966,17 +42966,6 @@ export interface TestRedisResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Adds a bind mount to a container resource. * @@ -43345,6 +43334,17 @@ export interface TestRedisResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -43940,17 +43940,6 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestRedisResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Adds a bind mount to a container resource. * @@ -44319,6 +44308,17 @@ export interface TestRedisResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestRedisResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -44936,30 +44936,6 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestRedisResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -45911,6 +45887,30 @@ class TestRedisResourceImpl extends ResourceBuilderBase return new TestRedisResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestRedisResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -47704,10 +47704,6 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { - return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withBindMount(source: string, target: string, options?: WithBindMountOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -47860,6 +47856,10 @@ class TestRedisResourcePromiseImpl implements TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestRedisResourcePromise { + return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestRedisResourcePromise { return new TestRedisResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -48197,17 +48197,6 @@ export interface TestVaultResource { * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Adds a bind mount to a container resource. * @@ -48560,6 +48549,17 @@ export interface TestVaultResource { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -49109,17 +49109,6 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The resource builder for chaining. */ withContainerRegistry(registry: Awaitable): TestVaultResourcePromise; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Adds a bind mount to a container resource. * @@ -49472,6 +49461,17 @@ export interface TestVaultResourcePromise extends PromiseLike * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): TestVaultResourcePromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -50043,30 +50043,6 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withContainerRegistryInternal(registry), this._client); } - /** @internal */ - private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new TestVaultResourceImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withBindMountInternal(source: string, target: string, isReadOnly?: boolean): Promise { const rpcArgs: Record = { builder: this._handle, source, target }; @@ -50982,6 +50958,30 @@ class TestVaultResourceImpl extends ResourceBuilderBase return new TestVaultResourcePromiseImpl(this._withEndpointInternal(port, targetPort, scheme, name, env, isProxied, isExternal, protocol), this._client); } + /** @internal */ + private async _withEndpointProxySupportInternal(proxyEnabled: boolean): Promise { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new TestVaultResourceImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -52579,10 +52579,6 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withContainerRegistry(registry)), this._client); } - withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { - return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withBindMount(source: string, target: string, options?: WithBindMountOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withBindMount(source, target, options)), this._client); } @@ -52727,6 +52723,10 @@ class TestVaultResourcePromiseImpl implements TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): TestVaultResourcePromise { + return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): TestVaultResourcePromise { return new TestVaultResourcePromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } @@ -55962,17 +55962,6 @@ class ResourceWithContainerFilesPromiseImpl implements ResourceWithContainerFile /** Represents a resource that has endpoints associated with it. */ export interface ResourceWithEndpoints { toJSON(): MarshalledHandle; - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise; /** * Marks the resource as hosting a Model Context Protocol (MCP) server on the specified endpoint. * @@ -56003,6 +55992,17 @@ export interface ResourceWithEndpoints { * @returns The `IResourceBuilder`1`. */ withEndpoint(options?: WithEndpointOptions): ResourceWithEndpointsPromise; + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise; /** * Exposes an HTTP endpoint on a resource, or updates the existing HTTP endpoint if one with the same name already exists. This endpoint reference can be retrieved using `GetEndpoint``1`. The endpoint name will be "http" if not specified. * @@ -56077,17 +56077,6 @@ export interface ResourceWithEndpoints { } export interface ResourceWithEndpointsPromise extends PromiseLike { - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise; /** * Marks the resource as hosting a Model Context Protocol (MCP) server on the specified endpoint. * @@ -56118,6 +56107,17 @@ export interface ResourceWithEndpointsPromise extends PromiseLike { - const rpcArgs: Record = { builder: this._handle, proxyEnabled }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEndpointProxySupport', - rpcArgs - ); - return new ResourceWithEndpointsImpl(result, this._client); - } - - /** - * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. - * - * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same - * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. - * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less - * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. - * @param proxyEnabled Should endpoints for the resource support using a proxy? - * @returns The `IResourceBuilder`1`. - */ - withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { - return new ResourceWithEndpointsPromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); - } - /** @internal */ private async _withMcpServerInternal(path?: string, endpointName?: string): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -56365,6 +56341,30 @@ class ResourceWithEndpointsImpl extends ResourceBuilderBase { + const rpcArgs: Record = { builder: this._handle, proxyEnabled }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withEndpointProxySupport', + rpcArgs + ); + return new ResourceWithEndpointsImpl(result, this._client); + } + + /** + * Set whether a resource can use proxied endpoints or whether they should be disabled for all endpoints belonging to the resource. If set to `false`, endpoints belonging to the resource will ignore the configured proxy settings and run proxy-less. + * + * This method is intended to support scenarios with persistent lifetime resources where it is desirable for the resource to be accessible over the same + * port whether the Aspire application is running or not. Proxied endpoints bind ports that are only accessible while the Aspire application is running. + * The user needs to be careful to ensure that endpoints are using unique ports when disabling proxy support as by default for proxy-less + * endpoints, Aspire will allocate the target port as the host port, which will increase the chance of port conflicts. + * @param proxyEnabled Should endpoints for the resource support using a proxy? + * @returns The resource builder. + */ + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { + return new ResourceWithEndpointsPromiseImpl(this._withEndpointProxySupportInternal(proxyEnabled), this._client); + } + /** @internal */ private async _withHttpEndpointInternal(port?: number, targetPort?: number, name?: string, env?: string, isProxied?: boolean): Promise { const rpcArgs: Record = { builder: this._handle }; @@ -56612,10 +56612,6 @@ class ResourceWithEndpointsPromiseImpl implements ResourceWithEndpointsPromise { return this._promise.then(onfulfilled, onrejected); } - withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { - return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); - } - withMcpServer(options?: WithMcpServerOptions): ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withMcpServer(options)), this._client); } @@ -56636,6 +56632,10 @@ class ResourceWithEndpointsPromiseImpl implements ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpoint(options)), this._client); } + withEndpointProxySupport(proxyEnabled: boolean): ResourceWithEndpointsPromise { + return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withEndpointProxySupport(proxyEnabled)), this._client); + } + withHttpEndpoint(options?: WithHttpEndpointOptions): ResourceWithEndpointsPromise { return new ResourceWithEndpointsPromiseImpl(this._promise.then(obj => obj.withHttpEndpoint(options)), this._client); } diff --git a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs index acca68edd25..11852cbe782 100644 --- a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs @@ -64,11 +64,15 @@ public void EndpointIsProxiedBinaryCompatibilityOverloadsExist() typeof(string), typeof(bool))); - Assert.NotNull(GetPublicStaticMethod( + var withEndpointProxySupport = GetPublicStaticMethod( typeof(ContainerResourceBuilderExtensions), nameof(ContainerResourceBuilderExtensions.WithEndpointProxySupport), typeof(IResourceBuilder<>), - typeof(bool))); + typeof(bool)); + + Assert.NotNull(withEndpointProxySupport); + var constraint = Assert.Single(withEndpointProxySupport.GetGenericArguments()[0].GetGenericParameterConstraints()); + Assert.Equal(typeof(ContainerResource), constraint); Assert.NotNull(GetPublicConstructor( typeof(EndpointAnnotation),