Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
a8bad02
Add a new deferred value provider for tls connection properties
danegsta Mar 3, 2026
a5aa70a
Respond to PR feedback
danegsta Mar 3, 2026
19280d2
Update comment
danegsta Mar 3, 2026
9bde1bc
Update baselines for code generation tests
danegsta Mar 3, 2026
3fbf2ef
Update comments with links to redis scheme registrations
danegsta Mar 3, 2026
0ecdf86
Make DeferredValueProvider async, use appropriate string comparison
danegsta Mar 4, 2026
fc4a4b5
Relax scheme constraints in manifest schema
danegsta Mar 4, 2026
42a8315
Omit the manifest schema going forward. Keeps schema tests to avoid r…
danegsta Mar 4, 2026
6c1d3e9
Merge remote-tracking branch 'upstream/release/13.2' into danegsta/re…
danegsta Mar 6, 2026
c47ee79
Add ConditionalReferenceExpression with polyglot codegen support
danegsta Mar 7, 2026
c914fcf
Fix outdated code generation snapshots
danegsta Mar 7, 2026
c2a6b4b
Fix CRE marshaller test to use pattern matching for auto-generated name
danegsta Mar 7, 2026
f277a91
Merge remote-tracking branch 'upstream/release/13.2' into danegsta/re…
danegsta Mar 7, 2026
02747ac
Update TwoPassScanning snapshots after merge with release/13.2
danegsta Mar 7, 2026
af482ba
Merge ConditionalReferenceExpression into ReferenceExpression
danegsta Mar 7, 2026
b433616
Ensure snapshots get updated
danegsta Mar 7, 2026
a20d5b7
Add explicit matchValue parameter to CreateConditional
danegsta Mar 7, 2026
2a77de0
Reorder matchValue parameter in polyglot language definitions
danegsta Mar 7, 2026
c58d780
Fix CreateConditional callers in RemoteHost tests
danegsta Mar 7, 2026
6e13b90
Handle conditional ReferenceExpression in Azure publishing paths
danegsta Mar 7, 2026
96eaa09
Resolve static conditionals at publish time, emit ternary only for pa…
danegsta Mar 7, 2026
415b3d3
Add test cases for all conditional expression branches
danegsta Mar 7, 2026
8bada82
Add tests for nested parameters and nested conditionals in Bicep
danegsta Mar 7, 2026
556d29b
Fix Go Handle.ToJSON return type for map compatibility
danegsta Mar 7, 2026
2f3177b
Implement Serialize for Rust ReferenceExpression
danegsta Mar 7, 2026
b45f0c6
Add Deserialize impl for Rust ReferenceExpression
danegsta Mar 7, 2026
e831995
Remove handle mode from ReferenceExpression in Go, Rust, Python, Java
danegsta Mar 7, 2026
081599d
Rename Go factory to NewConditionalReferenceExpression
danegsta Mar 7, 2026
03655e5
Wrap format and args in Option in Rust ReferenceExpression
danegsta Mar 7, 2026
1188db2
Revert ReferenceExpressionTypeId skip blocks from Python and TypeScri…
danegsta Mar 7, 2026
3d867a6
Revert transport.go Handle.ToJSON return type to map[string]string
danegsta Mar 7, 2026
1cc9f31
Remove low-value ConditionalReferenceExpression tests
danegsta Mar 7, 2026
3ad5a25
Fix TypeScript base.ts: import wrapIfHandle for conditional mode
danegsta Mar 7, 2026
bf5a740
Fix incorrect use of wrapIfHandle in ReferenceExpression serialization
danegsta Mar 7, 2026
abbd868
Disable HTTPS certificate for Redis in EndToEnd test AppHost
danegsta Mar 8, 2026
75b7b8c
Add conditional ReferenceExpression support to App Service, Docker Co…
danegsta Mar 8, 2026
09d773b
Use Helm ternary for parameter-based conditionals in Kubernetes publi…
danegsta Mar 8, 2026
7a00975
Fix BuildHelmTernary fallback detection and add HelmExtensions tests
danegsta Mar 8, 2026
0052dbc
Use Helm if/else for conditionals with expression branches
danegsta Mar 8, 2026
1f48c5b
Resolve merge conflict in Java two-pass scanning snapshot
danegsta Mar 9, 2026
434aac0
Populate ValueProviders for conditional ReferenceExpressions
danegsta Mar 9, 2026
d4edea5
Remove Redis connection string caching and fix manifest conditional r…
danegsta Mar 9, 2026
0acd987
Update Go and Rust expression key from $refExpr to $expr
danegsta Mar 9, 2026
51c4b97
Add tests for conditional ReferenceExpression ValueProviders and Refe…
danegsta Mar 9, 2026
3b85a72
Add conditional ReferenceExpression tests to ResourceDependencyTests
danegsta Mar 9, 2026
8c03ad2
Fix ExpressionResolver to handle conditional ReferenceExpressions
danegsta Mar 9, 2026
30cbdc3
Simplify Helm conditional to if/else only with case-insensitive compa…
danegsta Mar 10, 2026
0a0623a
Use toLower for case-insensitive Bicep conditional comparison
danegsta Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public static IResourceBuilder<RedisResource> AddRedis(
builder.Services.AddHealthChecks().AddRedis(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey);

var redisBuilder = builder.AddResource(redis)
.WithEndpoint(port: port, targetPort: 6379, name: RedisResource.PrimaryEndpointName)
.WithEndpoint(port: port, targetPort: 6379, name: RedisResource.PrimaryEndpointName, scheme: "redis")
.WithImage(RedisContainerImageTags.Image, RedisContainerImageTags.Tag)
.WithImageRegistry(RedisContainerImageTags.Registry)
.WithHealthCheck(healthCheckKey)
Expand Down Expand Up @@ -181,21 +181,21 @@ public static IResourceBuilder<RedisResource> AddRedis(
// configure the environment variables to use it.
redisBuilder
.WithEndpoint(targetPort: 6380, name: RedisResource.SecondaryEndpointName)
.WithEndpoint(RedisResource.PrimaryEndpointName, e =>
{
e.UriScheme = "rediss";
Comment thread
danegsta marked this conversation as resolved.
Outdated
e.TlsEnabled = true;
Comment thread
danegsta marked this conversation as resolved.
})
.WithArgs(argsCtx =>
{
argsCtx.Args.Add("--tls-port");
argsCtx.Args.Add(redis.GetEndpoint(RedisResource.PrimaryEndpointName).Property(EndpointProperty.Port));
argsCtx.Args.Add("--port");
argsCtx.Args.Add(redis.GetEndpoint(RedisResource.SecondaryEndpointName).Property(EndpointProperty.Port));
});

redis.TlsEnabled = true;
});
}

// Disable HTTPS developer certificate by default to avoid connection string timing issues
redisBuilder.WithoutHttpsCertificate();

return redisBuilder;
}

Expand Down
27 changes: 13 additions & 14 deletions src/Aspire.Hosting.Redis/RedisResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,17 @@ public RedisResource(string name, ParameterResource password) : this(name)
public ParameterResource? PasswordParameter { get; private set; }

/// <summary>
/// Determines whether Tls is enabled for the resource
/// Gets or sets a value indicating whether TLS is enabled for the Redis server.
/// </summary>
public bool TlsEnabled { get; internal set; }
/// <remarks>
/// This property proxies through to <see cref="EndpointAnnotation.TlsEnabled"/> on the
/// <see cref="PrimaryEndpoint"/>. When set to <see langword="true"/>, the connection string
/// expression dynamically includes <c>,ssl=true</c> and the URI expression uses the
/// <c>rediss://</c> scheme. This value is resolved lazily at expression evaluation time,
/// avoiding timing issues when TLS is enabled later in the application lifecycle
/// (e.g., during the <c>BeforeStartEvent</c>).
/// </remarks>
public bool TlsEnabled => PrimaryEndpoint.TlsEnabled;
Comment thread
danegsta marked this conversation as resolved.

/// <summary>
/// Arguments for the Dockerfile
Expand All @@ -72,10 +80,7 @@ private ReferenceExpression BuildConnectionString()
builder.Append($",password={PasswordParameter}");
}

if (TlsEnabled)
{
builder.Append($",ssl=true");
}
builder.Append($"{PrimaryEndpoint.GetTlsValue(enabledValue: ",ssl=true", disabledValue: null)}");

return builder.Build();
}
Expand Down Expand Up @@ -127,14 +132,8 @@ public ReferenceExpression UriExpression
get
{
var builder = new ReferenceExpressionBuilder();
if (TlsEnabled)
{
builder.AppendLiteral("rediss://");
}
else
{
builder.AppendLiteral("redis://");
}
builder.Append($"{PrimaryEndpoint.Property(EndpointProperty.Scheme)}");
builder.AppendLiteral("://");

if (PasswordParameter is not null)
{
Expand Down
74 changes: 74 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/DeferredValueProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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;

/// <summary>
/// A general-purpose value provider that resolves its value and manifest expression lazily via callbacks.
/// This enables dynamic values to be embedded in <see cref="ReferenceExpression"/> instances, where the
/// actual value is determined at resolution time rather than at expression build time.
/// </summary>
/// <remarks>
/// <para>
/// Use this type when a portion of a connection string or expression depends on state that isn't known
/// until later in the application lifecycle (e.g., whether TLS has been enabled on an endpoint).
/// </para>
/// <para>
/// Callbacks receive a <see cref="ValueProviderContext"/> that provides access to the execution context,
/// the calling resource, and network information. When no separate manifest expression callback is provided,
/// the value callback is used for both <see cref="GetValueAsync(ValueProviderContext, CancellationToken)"/> and <see cref="ValueExpression"/>. A
/// <see langword="null"/> return from the value callback is treated as an empty string for the manifest expression.
/// </para>
/// </remarks>
public class DeferredValueProvider : IValueProvider, IManifestExpressionProvider
{
Comment thread
danegsta marked this conversation as resolved.
Outdated
private readonly Func<ValueProviderContext, string?> _valueCallback;
private readonly Func<string>? _manifestExpressionCallback;

/// <summary>
/// Initializes a new instance of <see cref="DeferredValueProvider"/> with a context-free callback.
/// </summary>
/// <param name="valueCallback">A callback that returns the value. A <see langword="null"/> return is treated
/// as an empty string for the manifest expression. Called each time the value is resolved.</param>
/// <param name="manifestExpressionCallback">An optional callback that returns the manifest expression string.
/// When <see langword="null"/>, the <paramref name="valueCallback"/> is used for both runtime and manifest values.</param>
public DeferredValueProvider(Func<string?> valueCallback, Func<string>? manifestExpressionCallback = null)
Comment thread
danegsta marked this conversation as resolved.
Outdated
{
ArgumentNullException.ThrowIfNull(valueCallback);
_valueCallback = _ => valueCallback();
_manifestExpressionCallback = manifestExpressionCallback;
}

/// <summary>
/// Initializes a new instance of <see cref="DeferredValueProvider"/> with a context-aware callback.
/// </summary>
/// <param name="valueCallback">A callback that receives a <see cref="ValueProviderContext"/> and returns
/// the value. A <see langword="null"/> return is treated as an empty string for the manifest expression.
/// Called each time the value is resolved.</param>
/// <param name="manifestExpressionCallback">An optional callback that returns the manifest expression string.
/// When <see langword="null"/>, the <paramref name="valueCallback"/> is invoked with an empty
/// <see cref="ValueProviderContext"/> for the manifest expression.</param>
public DeferredValueProvider(Func<ValueProviderContext, string?> valueCallback, Func<string>? manifestExpressionCallback = null)
{
ArgumentNullException.ThrowIfNull(valueCallback);
_valueCallback = valueCallback;
_manifestExpressionCallback = manifestExpressionCallback;
}

/// <inheritdoc />
public string ValueExpression => _manifestExpressionCallback is not null
? _manifestExpressionCallback()
Comment thread
danegsta marked this conversation as resolved.
Outdated
: _valueCallback(new ValueProviderContext()) ?? "";

/// <inheritdoc />
public ValueTask<string?> GetValueAsync(CancellationToken cancellationToken = default)
{
return GetValueAsync(new ValueProviderContext(), cancellationToken);
}

/// <inheritdoc />
public ValueTask<string?> GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken = default)
{
return new(_valueCallback(context));
}
}
17 changes: 17 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public sealed class EndpointAnnotation : IResourceAnnotation
private bool _portSetToNull;
private int? _targetPort;
private bool _targetPortSetToNull;
private bool? _tlsEnabled;
private readonly NetworkIdentifier _networkID;

/// <summary>
Expand Down Expand Up @@ -184,6 +185,22 @@ public string Transport
/// <remarks>Defaults to <c>true</c>.</remarks>
public bool IsProxied { get; set; } = true;

/// <summary>
/// Gets or sets a value indicating whether TLS is enabled for this endpoint.
/// </summary>
/// <remarks>
/// This property is used to track TLS state on the endpoint so that connection string expressions
/// can dynamically include TLS-related parameters (e.g., <c>ssl=true</c> for Redis) at resolution time
/// rather than at expression build time. For HTTP-based endpoints, the <see cref="UriScheme"/> property
/// being set to <c>https</c> already implies TLS. This property is primarily useful for non-HTTP protocols
/// (e.g., Redis, databases) that need explicit TLS configuration in their connection strings.
/// </remarks>
public bool TlsEnabled
{
get => _tlsEnabled ?? (UriScheme == "https");
Comment thread
danegsta marked this conversation as resolved.
Outdated
set => _tlsEnabled = value;
}

/// <summary>
/// Gets or sets a value indicating whether the endpoint is from a launch profile.
/// </summary>
Expand Down
28 changes: 28 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/EndpointReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ public sealed class EndpointReference : IManifestExpressionProvider, IValueProvi
/// </summary>
public bool IsHttps => StringComparers.EndpointAnnotationUriScheme.Equals(Scheme, "https");

/// <summary>
/// Gets a value indicating whether TLS is enabled for this endpoint.
/// </summary>
/// <remarks>
/// Returns <see langword="false"/> if the endpoint annotation has not been added to the resource yet.
/// Once the annotation exists, this property delegates to <see cref="EndpointAnnotation.TlsEnabled"/>.
/// </remarks>
public bool TlsEnabled => Exists && EndpointAnnotation.TlsEnabled;
Comment thread
danegsta marked this conversation as resolved.

string IManifestExpressionProvider.ValueExpression => GetExpression();

/// <summary>
Expand Down Expand Up @@ -116,6 +125,25 @@ public EndpointReferenceExpression Property(EndpointProperty property)
return new(this, property);
}

/// <summary>
/// Creates a <see cref="DeferredValueProvider"/> that resolves to <paramref name="enabledValue"/> when
/// <see cref="EndpointAnnotation.TlsEnabled"/> is <see langword="true"/> on this endpoint, or to
/// <paramref name="disabledValue"/> otherwise.
/// </summary>
/// <remarks>
/// The returned provider evaluates the TLS state lazily each time its value is resolved, making it
/// safe to embed in a <see cref="ReferenceExpression"/> that is built before TLS is configured
/// (e.g., before <c>BeforeStartEvent</c> fires).
/// </remarks>
/// <param name="enabledValue">The value to return when TLS is enabled (e.g., <c>",ssl=true"</c>).</param>
/// <param name="disabledValue">The value to return when TLS is not enabled. Defaults to an empty string.</param>
/// <returns>A <see cref="DeferredValueProvider"/> whose value tracks the TLS state of this endpoint.</returns>
public DeferredValueProvider GetTlsValue(string enabledValue, string? disabledValue)
Comment thread
danegsta marked this conversation as resolved.
Outdated
{
return new DeferredValueProvider(
() => TlsEnabled ? enabledValue : disabledValue);
}

/// <summary>
/// Gets the port for this endpoint.
/// </summary>
Expand Down
73 changes: 67 additions & 6 deletions tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public void AddRedisContainerWithDefaultsAddsAnnotationMetadata()
Assert.Null(endpoint.Port);
Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
Assert.Equal("tcp", endpoint.Transport);
Assert.Equal("tcp", endpoint.UriScheme);
Assert.Equal("redis", endpoint.UriScheme);

var containerAnnotation = Assert.Single(containerResource.Annotations.OfType<ContainerImageAnnotation>());
Assert.Equal(RedisContainerImageTags.Tag, containerAnnotation.Tag);
Expand Down Expand Up @@ -72,7 +72,7 @@ public void AddRedisContainerAddsAnnotationMetadata()
Assert.Equal(9813, endpoint.Port);
Assert.Equal(ProtocolType.Tcp, endpoint.Protocol);
Assert.Equal("tcp", endpoint.Transport);
Assert.Equal("tcp", endpoint.UriScheme);
Assert.Equal("redis", endpoint.UriScheme);

var containerAnnotation = Assert.Single(containerResource.Annotations.OfType<ContainerImageAnnotation>());
Assert.Equal(RedisContainerImageTags.Tag, containerAnnotation.Tag);
Expand Down Expand Up @@ -154,7 +154,7 @@ public async Task VerifyDefaultManifest()
},
"bindings": {
"tcp": {
"scheme": "tcp",
"scheme": "redis",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 6379
Expand Down Expand Up @@ -185,7 +185,7 @@ public async Task VerifyWithoutPasswordManifest()
],
"bindings": {
"tcp": {
"scheme": "tcp",
"scheme": "redis",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 6379
Expand Down Expand Up @@ -223,7 +223,7 @@ public async Task VerifyWithPasswordManifest()
},
"bindings": {
"tcp": {
"scheme": "tcp",
"scheme": "redis",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 6379
Expand Down Expand Up @@ -258,7 +258,7 @@ public async Task VerifyWithPasswordValueNotProvidedManifest()
},
"bindings": {
"tcp": {
"scheme": "tcp",
"scheme": "redis",
"protocol": "tcp",
"transport": "tcp",
"targetPort": 6379
Expand Down Expand Up @@ -835,6 +835,63 @@ public async Task RedisWithCertificateHasCorrectConnectionString()
await builder.Eventing.PublishAsync(beforeStartEvent);

Assert.True(redis.Resource.TlsEnabled);

// Verify the connection string expression includes ssl=true after TLS is enabled
var connectionStringExpression = redis.Resource.ConnectionStringExpression;
Assert.Contains(",ssl=true", connectionStringExpression.ValueExpression);

// Verify the endpoint annotation also has TlsEnabled
var endpoint = Assert.Single(redis.Resource.Annotations.OfType<EndpointAnnotation>(), e => e.Name == "tcp");
Assert.True(endpoint.TlsEnabled);
Assert.Equal("rediss", endpoint.UriScheme);

// Verify the URI expression uses the endpoint scheme
var uriExpression = redis.Resource.UriExpression;
Assert.Contains("{myredis.bindings.tcp.scheme}", uriExpression.ValueExpression);
}

[Fact]
public async Task RedisConnectionStringResolvesWithTlsDynamically()
{
using var builder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
using var cert = CreateTestCertificate();

var redis = builder.AddRedis("myredis")
.WithHttpsCertificate(cert)
.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6379));

// Before BeforeStartEvent, TLS is not yet enabled
Assert.False(redis.Resource.TlsEnabled);

// The manifest expression does not include ssl=true before TLS is enabled
var expressionBeforeTls = redis.Resource.ConnectionStringExpression;
Assert.DoesNotContain(",ssl=true", expressionBeforeTls.ValueExpression);

// But the expression has a DeferredValueProvider that will resolve dynamically
// Resolve the runtime value — should NOT have ssl=true yet
var resolvedBeforeTls = await expressionBeforeTls.GetValueAsync(default(CancellationToken));
Assert.NotNull(resolvedBeforeTls);
Assert.DoesNotContain(",ssl=true", resolvedBeforeTls);

using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

// Simulate the BeforeStartEvent to enable TLS
var beforeStartEvent = new BeforeStartEvent(app.Services, appModel);
await builder.Eventing.PublishAsync(beforeStartEvent);

// Now TLS is enabled
Assert.True(redis.Resource.TlsEnabled);

// The deferred value provider resolves dynamically — the SAME captured expression
// now resolves with ssl=true because the callback reads current TlsEnabled state
var resolvedAfterTls = await expressionBeforeTls.GetValueAsync(default(CancellationToken));
Assert.NotNull(resolvedAfterTls);
Assert.Contains(",ssl=true", resolvedAfterTls);

// The new expression from the getter also reflects TLS in its manifest expression
var expressionAfterTls = redis.Resource.ConnectionStringExpression;
Assert.Contains(",ssl=true", expressionAfterTls.ValueExpression);
}

[Fact]
Expand All @@ -850,6 +907,10 @@ public void RedisWithoutCertificateHasCorrectConnectionString()
// Simulate the BeforeStartEvent
var beforeStartEvent = new BeforeStartEvent(app.Services, appModel);
Assert.False(redis.Resource.TlsEnabled);

// Verify the connection string expression does NOT include ssl=true
var connectionStringExpression = redis.Resource.ConnectionStringExpression;
Assert.DoesNotContain(",ssl=true", connectionStringExpression.ValueExpression);
}

private static X509Certificate2 CreateTestCertificate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public void RedisResourceGetConnectionPropertiesReturnsExpectedValues()
property =>
{
Assert.Equal("Uri", property.Key);
Assert.Equal("redis://:{password.value}@{redis.bindings.tcp.host}:{redis.bindings.tcp.port}", property.Value.ValueExpression);
Assert.Equal("{redis.bindings.tcp.scheme}://:{password.value}@{redis.bindings.tcp.host}:{redis.bindings.tcp.port}", property.Value.ValueExpression);
});
}
}
Loading
Loading