diff --git a/src/Aspire.Hosting/ApplicationModel/ContainerLifetimeAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/ContainerLifetimeAnnotation.cs
new file mode 100644
index 00000000000..5cc5bfc0002
--- /dev/null
+++ b/src/Aspire.Hosting/ApplicationModel/ContainerLifetimeAnnotation.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// Lifetime modes for container resources
+///
+public enum ContainerLifetimeType
+{
+ ///
+ /// The default lifetime behavior should apply. This will create the resource when the AppHost starts and dispose of it when the AppHost shuts down.
+ ///
+ Default,
+ ///
+ /// The resource is persistent and will not be disposed of when the AppHost shuts down.
+ ///
+ Persistent,
+}
+
+///
+/// Annotation that controls the lifetime of a container resource (default behavior that matches the lifetime of the AppHost or a persistent lifetime across AppHost restarts)
+///
+[DebuggerDisplay("Type = {GetType().Name,nq}")]
+public sealed class ContainerLifetimeAnnotation : IResourceAnnotation
+{
+ ///
+ /// Gets or sets the lifetime type for the container resource.
+ ///
+ public required ContainerLifetimeType LifetimeType { get; set; }
+}
\ No newline at end of file
diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs
index 6f0f92d18e1..9801d6b1b5f 100644
--- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs
+++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs
@@ -84,9 +84,9 @@ public static bool TryGetEnvironmentVariables(this IResource resource, [NotNullW
/// var container = builder.AddContainer("elasticsearch", "library/elasticsearch", "8.14.0")
/// .WithEnvironment("discovery.type", "single-node")
/// .WithEnvironment("xpack.security.enabled", "true");
- ///
+ ///
/// var env = await container.Resource.GetEnvironmentVariableValuesAsync();
- ///
+ ///
/// Assert.Collection(env,
/// env =>
/// {
@@ -179,7 +179,7 @@ public static IEnumerable GetEndpoints(this IResourceWithEndp
///
/// The which contains annotations.
/// The name of the endpoint.
- /// An object representing the endpoint reference
+ /// An object representing the endpoint reference
/// for the specified endpoint.
public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName)
{
@@ -233,4 +233,20 @@ public static int GetReplicaCount(this IResource resource)
return 1;
}
}
+
+ ///
+ /// Gets the lifetime type of the container for the specified resoruce. Defaults to if
+ /// no is found.
+ ///
+ /// The resource to the get the ContainerLifetimeType for.
+ /// The from the for the resource (if the annotation exists). Defaults to if the annotation is not set.
+ internal static ContainerLifetimeType GetContainerLifetimeType(this IResource resource)
+ {
+ if (resource.TryGetLastAnnotation(out var lifetimeAnnotation))
+ {
+ return lifetimeAnnotation.LifetimeType;
+ }
+
+ return ContainerLifetimeType.Default;
+ }
}
diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs
index 777a757e974..5f6dc2412c0 100644
--- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs
+++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
@@ -221,6 +222,27 @@ public static IResourceBuilder WithContainerRuntimeArgs(this IResourceBuil
return builder.WithAnnotation(annotation);
}
+ ///
+ /// Sets the lifetime behavior of the container resource.
+ ///
+ /// The resource type.
+ /// Builder for the container resource.
+ /// The lifetime behavior of the container resource (defaults behavior is )
+ /// The .
+ ///
+ /// Marking a container resource to have a lifetime.
+ ///
+ /// var builder = DistributedApplication.CreateBuilder(args);
+ /// builder.AddContainer("mycontainer", "myimage")
+ /// .WithContainerLifetime(ContainerLifetimeType.Persistent);
+ ///
+ ///
+ [Experimental("ASPIRECONTAINERLIFETIME001")]
+ public static IResourceBuilder WithContainerLifetime(this IResourceBuilder builder, ContainerLifetimeType lifetimeType) where T : ContainerResource
+ {
+ return builder.WithAnnotation(new ContainerLifetimeAnnotation { LifetimeType = lifetimeType }, ResourceAnnotationMutationBehavior.Replace);
+ }
+
private static IResourceBuilder ThrowResourceIsNotContainer(IResourceBuilder builder) where T : ContainerResource
{
throw new InvalidOperationException($"The resource '{builder.Resource.Name}' does not have a container image specified. Use WithImage to specify the container image and tag.");
diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
index e29d5ff2dea..465d06d5c94 100644
--- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
+++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs
@@ -1327,11 +1327,23 @@ private void PrepareContainers()
throw new InvalidOperationException();
}
- var nameSuffix = GetRandomNameSuffix();
+ var nameSuffix = string.Empty;
+
+ if (container.GetContainerLifetimeType() == ContainerLifetimeType.Default)
+ {
+ nameSuffix = GetRandomNameSuffix();
+ }
+
var containerObjectName = GetObjectNameForResource(container, nameSuffix);
var ctr = Container.Create(containerObjectName, containerImageName);
ctr.Spec.ContainerName = containerObjectName; // Use the same name for container orchestrator (Docker, Podman) resource and DCP object name.
+
+ if (container.GetContainerLifetimeType() == ContainerLifetimeType.Persistent)
+ {
+ ctr.Spec.Persistent = true;
+ }
+
ctr.Annotate(CustomResource.ResourceNameAnnotation, container.Name);
ctr.Annotate(CustomResource.OtelServiceNameAnnotation, container.Name);
ctr.Annotate(CustomResource.OtelServiceInstanceIdAnnotation, nameSuffix);
diff --git a/src/Aspire.Hosting/PublicAPI.Unshipped.txt b/src/Aspire.Hosting/PublicAPI.Unshipped.txt
index a375d4c358b..111fa6cf9ff 100644
--- a/src/Aspire.Hosting/PublicAPI.Unshipped.txt
+++ b/src/Aspire.Hosting/PublicAPI.Unshipped.txt
@@ -11,6 +11,13 @@ Aspire.Hosting.ApplicationModel.BeforeStartEvent
Aspire.Hosting.ApplicationModel.BeforeStartEvent.BeforeStartEvent(System.IServiceProvider! services, Aspire.Hosting.ApplicationModel.DistributedApplicationModel! model) -> void
Aspire.Hosting.ApplicationModel.BeforeStartEvent.Model.get -> Aspire.Hosting.ApplicationModel.DistributedApplicationModel!
Aspire.Hosting.ApplicationModel.BeforeStartEvent.Services.get -> System.IServiceProvider!
+Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation
+Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.ContainerLifetimeAnnotation() -> void
+Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.LifetimeType.get -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType
+Aspire.Hosting.ApplicationModel.ContainerLifetimeAnnotation.LifetimeType.set -> void
+Aspire.Hosting.ApplicationModel.ContainerLifetimeType
+Aspire.Hosting.ApplicationModel.ContainerLifetimeType.Default = 0 -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType
+Aspire.Hosting.ApplicationModel.ContainerLifetimeType.Persistent = 1 -> Aspire.Hosting.ApplicationModel.ContainerLifetimeType
Aspire.Hosting.ApplicationModel.ResourceNotificationService.ResourceNotificationService(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Hosting.IHostApplicationLifetime! hostApplicationLifetime) -> void
Aspire.Hosting.DistributedApplicationBuilder.Eventing.get -> Aspire.Hosting.Eventing.IDistributedApplicationEventing!
Aspire.Hosting.Eventing.DistributedApplicationEventing
@@ -37,6 +44,7 @@ Aspire.Hosting.IDistributedApplicationBuilder.Eventing.get -> Aspire.Hosting.Eve
static Aspire.Hosting.ApplicationModel.ResourceExtensions.GetEnvironmentVariableValuesAsync(this Aspire.Hosting.ApplicationModel.IResourceWithEnvironment! resource, Aspire.Hosting.DistributedApplicationOperation applicationOperation = Aspire.Hosting.DistributedApplicationOperation.Run) -> System.Threading.Tasks.ValueTask!>
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, string? targetState = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, System.Collections.Generic.IEnumerable! targetStates, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+static Aspire.Hosting.ContainerResourceBuilderExtensions.WithContainerLifetime(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.ContainerLifetimeType lifetimeType) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
static Aspire.Hosting.ProjectResourceBuilderExtensions.WithEndpointsInEnvironment(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, System.Func! filter) -> Aspire.Hosting.ApplicationModel.IResourceBuilder!
Aspire.Hosting.DistributedApplicationExecutionContext.DistributedApplicationExecutionContext(Aspire.Hosting.DistributedApplicationExecutionContextOptions! options) -> void
Aspire.Hosting.DistributedApplicationExecutionContext.ServiceProvider.get -> System.IServiceProvider!