Skip to content

Add conditional mode to ReferenceExpression with polyglot codegen support#14919

Merged
danegsta merged 48 commits intorelease/13.2from
danegsta/redisTls
Mar 10, 2026
Merged

Add conditional mode to ReferenceExpression with polyglot codegen support#14919
danegsta merged 48 commits intorelease/13.2from
danegsta/redisTls

Conversation

@danegsta
Copy link
Member

@danegsta danegsta commented Mar 3, 2026

Description

Extends ReferenceExpression with a conditional mode that models ternary-style values in connection strings and other reference expressions. This enables proper manifest publishing and polyglot code generation for conditional logic like TLS ssl=true in Redis connection strings, replacing the closure-based DeferredValueProvider.

ReferenceExpression conditional mode

ReferenceExpression.CreateConditional(condition, conditionMatch, whenTrue, whenFalse) creates a conditional expression that evaluates an IValueProvider condition against a given "truthy" string value and selects between two ReferenceExpression branches. Key design:

  • Single type: Conditional state lives as internal fields on ReferenceExpression rather than a separate type. IsConditional, Condition, WhenTrue, WhenFalse properties expose the state.
  • Auto-generated names: The manifest entry name is derived from the condition's ValueExpression at construction time (e.g., {redis.bindings.tcp.tlsEnabled}cond-redis-bindings-tcp-tlsenabled), eliminating the need for explicit naming.
  • Manifest integration: Written as value.v0 entries in the manifest, referenced via {name.value} in connection strings.

Unified ATS wire format

Conditional expressions use the same $expr marker as value-mode expressions. The presence of a condition property distinguishes the two modes:

  • Value mode: { "$expr": { "format": "...", "valueProviders": [...] } }
  • Conditional mode: { "$expr": { "condition": <$handle>, "matchValue": <string>, "whenTrue": <$expr>, "whenFalse": <$expr> } }

All 5 polyglot language base files (TypeScript, Go, Python, Java, Rust) support:

  • createConditional() factory method for client-side construction
  • toJSON() serialization using the unified $expr format
  • Server-side unmarshalling via ReferenceExpressionRef in AtsMarshaller

Redis TLS

Re-enables TLS by default for Redis container endpoints. EndpointReference.GetTlsValue now returns a conditional ReferenceExpression instead of using a closure-based DeferredValueProvider, making it compatible with polyglot code generation.

Example

ReferenceExpression.CreateConditional(
    condition: primaryEndpoint.Property(EndpointProperty.TlsEnabled),
    matchValue: bool.TrueString,
    whenTrue: ReferenceExpression.Create($",ssl=true"),
    whenFalse: ReferenceExpression.Empty);

Fixes #13645

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <para/> and <code/> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?

@danegsta danegsta requested a review from mitchdenny as a code owner March 3, 2026 22:10
Copilot AI review requested due to automatic review settings March 3, 2026 22:10
@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14919

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14919"

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new lazy/deferred value provider to support reference-expression fragments that depend on state only known later in the lifecycle (notably Redis TLS), and updates Redis endpoint/URI scheme handling to be scheme-driven rather than hard-coded.

Changes:

  • Introduces DeferredValueProvider (runtime + manifest expression callbacks) and an EndpointReference.TlsValue(...) helper.
  • Adds EndpointAnnotation.TlsEnabled / EndpointReference.TlsEnabled and wires Redis TLS to endpoint annotations (including ssl=true connection string fragment).
  • Updates Redis tests/manifests to use binding {...scheme} and default Redis endpoint scheme to redis (and rediss when TLS is enabled).

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Aspire.Hosting.Tests/DeferredValueProviderTests.cs New unit tests covering deferred provider behavior and interaction with ReferenceExpressionBuilder.
tests/Aspire.Hosting.Redis.Tests/ConnectionPropertiesTests.cs Updates expected Redis URI manifest expression to use {...scheme} binding.
tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs Updates Redis endpoint scheme expectations and adds TLS/dynamic-resolution coverage.
src/Aspire.Hosting/ApplicationModel/EndpointReference.cs Adds TlsEnabled and TlsValue(...) to support TLS-dependent dynamic fragments.
src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs Adds TlsEnabled flag for endpoint-level TLS state.
src/Aspire.Hosting/ApplicationModel/DeferredValueProvider.cs Adds new general-purpose deferred value provider implementation.
src/Aspire.Hosting.Redis/RedisResource.cs Switches TLS handling to endpoint-based TLS state and scheme-driven URI/connection string fragments.
src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs Sets Redis primary endpoint scheme to redis and updates TLS enablement to mutate endpoint annotation state.

You can also share your feedback on Copilot code review. Take the survey.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 3, 2026

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit 0a0623a:

Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateEmptyAppHostProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateStartWaitAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ❌ Upload failed
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
TypeScriptAppHostWithProjectReferenceIntegration ▶️ View Recording

📹 Recordings uploaded automatically from CI run #22884320770

@danegsta danegsta force-pushed the danegsta/redisTls branch from 9404e2f to a5aa70a Compare March 3, 2026 23:27
@davidfowl
Copy link
Contributor

How does this affect the typescript support?

cc @sebastienros @IEvangelist

danegsta and others added 2 commits March 6, 2026 12:49
Replace DeferredValueProvider with ConditionalReferenceExpression, a new
type that models conditional values in connection strings (e.g., TLS
ssl=true/empty). The CRE auto-generates its manifest name from the
condition's ValueExpression at construction time.

Key changes:
- ConditionalReferenceExpression type with Create() factory, auto-name
  generation via condition sanitization, and manifest value.v0 support
- EndpointReference.GetTlsValue returns CRE instead of using closure
- ManifestPublishingContext writes CRE entries as value.v0 resources
- Polyglot create()/toJSON() in all 5 ATS base files (TS, Go, Python,
  Java, Rust) with $condExpr JSON serialization format
- ConditionalReferenceExpressionRef for server-side $condExpr
  unmarshalling in AtsMarshaller
- Redis connection string tests updated with pattern matching to handle
  auto-generated CRE names and verify manifest value entries

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danegsta danegsta changed the title Add a new deferred value provider for dynamic properties in endpoint references Add ConditionalReferenceExpression with polyglot codegen support Mar 7, 2026
danegsta and others added 3 commits March 6, 2026 20:42
The MarshalToJson_ConditionalReferenceExpression_PreservesValueAfterRoundTrip
test was asserting an exact name ('test-tls') but names are now auto-generated
from the condition's ValueExpression. Use StartsWith assertion instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace wrapIfHandle (a deserialization helper) with proper serialization
logic in ReferenceExpression.toJSON() for the condition field. Remove the
now-unnecessary import.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
}
}

impl<'de> Deserialize<'de> for ReferenceExpression {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sebastienros this is why we should delete all other language 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had to walk back several overzealous copilot changes such as this. It seems that it gets a bit carried away once it starts trying to update all the languages.

@davidfowl
Copy link
Contributor

docker and kubernetes both have special ReferenceExpression logic as well.

danegsta and others added 5 commits March 7, 2026 16:30
The EndToEnd tests are incompatible with TLS-enabled Redis. Opt out
of the developer certificate for the Redis resource in TestProgram.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mpose, and Kubernetes publish contexts

- Azure App Service: Handle conditional expressions with Bicep ternary
  fallback for parameter-based conditions
- Docker Compose: Convert ProcessValue to async, resolve conditions
  statically at generation time via GetValueAsync
- Kubernetes: Resolve conditions via GetValueAsync with
  ValueProviderContext at generation time
- Add tests with both static and parameter-based conditions for all
  three contexts with Verify snapshots

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sher

When a conditional ReferenceExpression's condition is a ParameterResource,
emit a Helm ternary expression (e.g., {{ ternary "val1" "val2"
(eq .Values.parameters.x "True") | quote }}) instead of resolving
statically. This defers evaluation to helm install/upgrade time, allowing
users to override the condition parameter in values.yaml.

- Add BuildHelmTernary helper in KubernetesResource
- Add HelmFlowControlPattern regex for ternary expression detection
- Update ShouldDoubleQuoteString to render ternary as plain YAML
- Non-parameter conditions still resolve statically at generation time

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The is HelmValue type check failed because ProcessValueAsync's Format=={0}
optimization converts HelmValues to strings via ToString(). Changed to detect
Helm expressions in string content using ContainsHelmExpression() instead.

Added HelmExtensionsTests with Theory tests for ShouldDoubleQuoteString and
HelmFlowControlPattern regex. Added fallback integration test verifying that
conditionals with parameter branches fall back to static resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace static fallback with Go template {{ if eq ... }}...{{ else }}...{{ end }}
syntax when conditional branches contain Helm expressions. This preserves
deploy-time dynamism for parameter-based conditions even when branches
reference other parameters.

Key changes:
- BuildHelmTernary → BuildHelmConditional with ternary for literals,
  if/else for expression branches, static fallback for complex cases
- TryFormatBranch: scalar expressions get | quote, literals wrapped as
  {{ "literal" | quote }} to avoid bare quotes in YAML plain scalars
- AllocateBranchParameters: stores branch parameter values in
  AdditionalConfigValues to populate values.yaml without case-insensitive
  key collisions in ToConfigMap's processedKeys
- HelmFlowControlPattern regex now matches {{ if in addition to {{ ternary
- ShouldDoubleQuoteString checks flow control before ScalarExpressionPattern

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
danegsta and others added 5 commits March 9, 2026 09:55
Re-generated the verified snapshot by running the test against the
current codebase.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Conditional expressions now expose the union of both branches'
ValueProviders, enabling publish contexts to discover all referenced
parameters and resources without inspecting WhenTrue/WhenFalse directly.

Guard RegisterFormattedParameters against the parallel array mismatch
(ValueProviders is now non-empty but ManifestExpressions/StringFormats
remain empty for conditionals) by recursing into each branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…egistration

The hash-based conditional name (XxHash32 of condition + branches) is
now stable, so caching the built ReferenceExpression in RedisResource
is unnecessary.

Fix RegisterConditionalExpressions to register the expression itself
when it is conditional, not just nested conditionals in ValueProviders.
With ValueProviders now containing the union of both branches' providers
(parameters, endpoints, etc.), the top-level conditional was no longer
being discovered through the nested provider scan.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Align with the TypeScript base.ts which already uses the current $expr
key for reference expression serialization.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rences

Validate that ValueProviders returns the union of both branches' providers,
References includes condition + all branch references, nested conditionals
propagate providers through the chain, and duplicate conditionals produce
identical hash-based names.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@karolz-ms
Copy link
Contributor

We should make sure that the new ConditionalExpression plays well with the GetResourceDependenciesAsync() API. Specifically, dependencies can be introduced via whenTrue branch, whenFalse branch, and via the condition itself. We should make sure that GetResourceDependenciesAsync() correctly detects dependencies introduced by any of these ConditionalExpression parameters, and that dependencies are correctly de-duplicated if they are affecting more than one parameter. And also, in non-direct discovery mode, dependencies should be detected if condition or branches of the ConditionalExpression refers to them indirectly.

Relevant test file: https://github.com/dotnet/aspire/blob/release/13.2/tests/Aspire.Hosting.Tests/ResourceDependencyTests.cs

Add 4 test cases for resource dependency tracking with conditional
reference expressions: basic branch dependencies, endpoint references,
nested conditionals, and deduplication.

Fix bug in ReferenceExpression.References where the condition object
itself was not yielded - only its sub-references via IValueWithReferences.
Since ParameterResource does not implement IValueWithReferences, the
condition resource was silently dropped from dependency tracking.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danegsta
Copy link
Member Author

danegsta commented Mar 9, 2026

We should make sure that the new ConditionalExpression plays well with the GetResourceDependenciesAsync() API. Specifically, dependencies can be introduced via whenTrue branch, whenFalse branch, and via the condition itself. We should make sure that GetResourceDependenciesAsync() correctly detects dependencies introduced by any of these ConditionalExpression parameters, and that dependencies are correctly de-duplicated if they are affecting more than one parameter. And also, in non-direct discovery mode, dependencies should be detected if condition or branches of the ConditionalExpression refers to them indirectly.

Relevant test file: https://github.com/dotnet/aspire/blob/release/13.2/tests/Aspire.Hosting.Tests/ResourceDependencyTests.cs

Added test coverage.

danegsta and others added 3 commits March 9, 2026 15:11
ExpressionResolver.EvalExpressionAsync had no handling for conditional
ReferenceExpressions. Since conditional expressions have Format=""
(empty string), the method returned null, and the dashboard silently
dropped the environment variable (resolvedValue?.Value != null check).

Add conditional branch to EvalExpressionAsync that evaluates the
condition, selects the matching branch, and recurses — mirroring the
logic already present in ReferenceExpression.GetValueAsync.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rison

Remove ternary and static fallback paths from BuildHelmConditional. All
conditionals now use {{ if eq ... }}...{{ else }}...{{ end }} syntax which
natively handles mixed content (literal + Helm expressions) without needing
TryFormatBranch or printf workarounds.

Add | lower pipe for case-insensitive comparison, matching .NET's
StringComparison.OrdinalIgnoreCase used in other execution/publish paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Apply toLower() to condition values in Azure Container Apps and App
Service Bicep ternary expressions, matching the OrdinalIgnoreCase
semantics used in all other execution and publish paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danegsta danegsta merged commit 268592d into release/13.2 Mar 10, 2026
747 of 751 checks passed
@danegsta danegsta deleted the danegsta/redisTls branch March 10, 2026 05:46
@dotnet-policy-service dotnet-policy-service bot added this to the 13.2 milestone Mar 10, 2026
@davidfowl
Copy link
Contributor

@sebastienros @mitchdenny follow up here to make sure deployment works well e2e

@davidfowl
Copy link
Contributor

Those don’t auto run without explicit triggering

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants