From 690f1e1752c88fe3adf603f0b35ad3632c9d97bc Mon Sep 17 00:00:00 2001 From: "aspire-repo-bot[bot]" <268009190+aspire-repo-bot[bot]@users.noreply.github.com> Date: Tue, 5 May 2026 01:17:12 +0000 Subject: [PATCH 1/2] Document unified withEnvironment API and IExpressionValue for multi-language AppHosts - Update multi-language-integration-authoring.mdx: - Expand the union types example to show the full withEnvironment union including IExpressionValue - Add new IExpressionValue section explaining the interface, its role, and how to implement it in custom types - Add migration table from old withEnvironment* aliases to unified API - Update supported types table to list IExpressionValue - Update aspire-13-3.mdx whats-new with a new section covering the unified withEnvironment API, the IExpressionValue interface, and the deprecation of the per-kind alias methods Documents changes from microsoft/aspire#15649 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../multi-language-integration-authoring.mdx | 86 ++++++++++++++++++- .../content/docs/whats-new/aspire-13-3.mdx | 32 ++++++- 2 files changed, 115 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx b/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx index 9e170ee22..f5021d699 100644 --- a/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx +++ b/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx @@ -315,7 +315,13 @@ When a parameter accepts multiple types, use `[AspireUnion]` to declare the vali public static IResourceBuilder WithEnvironment( this IResourceBuilder builder, string name, - [AspireUnion(typeof(string), typeof(ReferenceExpression), typeof(EndpointReference))] + [AspireUnion( + typeof(string), + typeof(ReferenceExpression), + typeof(EndpointReference), + typeof(IResourceBuilder), + typeof(IResourceBuilder), + typeof(IExpressionValue))] object value) where T : IResourceWithEnvironment { @@ -325,6 +331,82 @@ public static IResourceBuilder WithEnvironment( All types in the union must be ATS-compatible. The analyzer (ASPIREEXPORT005, ASPIREEXPORT006) validates union declarations at build time. +## IExpressionValue — unified environment values + +`IExpressionValue` is a new public interface in `Aspire.Hosting.ApplicationModel` that represents a value with both a runtime value and a publish-time manifest expression. Any type that implements both `IValueProvider` and `IManifestExpressionProvider` can also implement `IExpressionValue` to make it usable anywhere an environment variable value is expected. + +```csharp title="C# — IExpressionValue interface" +public interface IExpressionValue : IValueProvider, IManifestExpressionProvider +{ +} +``` + +### Why it matters for multi-language apphosts + +The built-in `WithEnvironment` export accepts `IExpressionValue` as one of its union type options, in addition to `string`, `ReferenceExpression`, `EndpointReference`, parameter builders, and connection string resource builders. This gives polyglot apphosts a single, consistent `withEnvironment(name, value)` call that works with all of these value types. + +In generated TypeScript, the union collapses to the concrete wrapper types for all exported resource types that implement `IExpressionValue`: + +```typescript title="TypeScript — generated withEnvironment signature" +withEnvironment( + name: string, + value: string | ReferenceExpressionHandle | EndpointReferenceHandle | ParameterResourceHandle | /* ... IExpressionValue implementors */ +): this; +``` + +### Implementing IExpressionValue in a custom type + +If you build a custom value type that provides both a runtime value and a manifest expression, implement `IExpressionValue` to make it passable directly to `WithEnvironment`: + +```csharp title="C# — Custom IExpressionValue implementation" +[AspireExport(ExposeProperties = true)] +public sealed class MySecretReference(string secretName) : IExpressionValue +{ + public string SecretName { get; } = secretName; + + // IManifestExpressionProvider — used when publishing + public string ValueExpression => $"{{{{myVault.secrets.{SecretName}}}}}"; + + // IValueProvider — used at runtime + public async ValueTask GetValueAsync(CancellationToken cancellationToken) + { + // Fetch the secret value at runtime + return await FetchSecretAsync(SecretName, cancellationToken); + } +} +``` + +Because `MySecretReference` is exported with `[AspireExport]`, the code generator recognizes it as an `IExpressionValue` implementor and includes it in the generated union type for `withEnvironment`. + +### Migrating from the old withEnvironment* aliases + +Earlier versions of the ATS TypeScript SDK generated separate methods for each value kind: + +| Old method (deprecated) | Replacement | +|---|---| +| `withEnvironmentExpression(name, expr)` | `withEnvironment(name, expr)` | +| `withEnvironmentEndpoint(name, endpoint)` | `withEnvironment(name, endpoint)` | +| `withEnvironmentParameter(name, param)` | `withEnvironment(name, param)` | +| `withEnvironmentConnectionString(name, resource)` | `withEnvironment(name, resource)` | +| `withEnvironmentFromOutput(name, output)` | `withEnvironment(name, output)` | +| `withEnvironmentFromKeyVaultSecret(name, secret)` | `withEnvironment(name, secret)` | + +These aliases remain available as deprecated shims in the generated SDK. Update your TypeScript apphost code to use the single `withEnvironment(name, value)` call instead: + +```typescript title="TypeScript — before (deprecated aliases)" +const api = await builder.addApi("api") + .withEnvironmentEndpoint("SERVICE_URL", cache.primaryEndpoint) + .withEnvironmentParameter("API_KEY", apiKeyParam) + .withEnvironmentConnectionString("DB", database); +``` + +```typescript title="TypeScript — after (unified API)" +const api = await builder.addApi("api") + .withEnvironment("SERVICE_URL", cache.primaryEndpoint) + .withEnvironment("API_KEY", apiKeyParam) + .withEnvironment("DB", database); +``` + ## Analyzer diagnostics The `Aspire.Hosting.Integration.Analyzers` package reports these diagnostics: @@ -429,7 +511,7 @@ The following types are ATS-compatible and can be used in exported method signat | **Collections** | `List`, `Dictionary`, arrays — where `T` is ATS-compatible | | **Delegates** | `Action`, `Func`, and other delegate types (use `RunSyncOnBackgroundThread = true` for synchronous delegates invoked inline) | | **Services** | `ILogger`, `IServiceProvider`, `IConfiguration` (already exported by the core framework) | -| **Special** | `ParameterResource`, `ReferenceExpression`, `EndpointReference`, `CancellationToken` | +| **Special** | `ParameterResource`, `ReferenceExpression`, `EndpointReference`, `IExpressionValue`, `CancellationToken` | | **Nullable** | Any of the above as nullable (`T?`) | Types that are **not** ATS-compatible include: interpolated string handlers and custom complex types without `[AspireExport]` or `[AspireDto]`. diff --git a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx index c32a65546..011e7b53d 100644 --- a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx +++ b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx @@ -1,6 +1,6 @@ --- title: What's new in Aspire 13.3 -description: Aspire 13.3 introduces Azure Network Security Perimeter support, enables the container tunnel by default, and more. +description: Aspire 13.3 introduces Azure Network Security Perimeter support, enables the container tunnel by default, unified multi-language withEnvironment API, and more. sidebar: label: Aspire 13.3 order: 0 @@ -136,3 +136,33 @@ You can also disable it in `launchSettings.json`: For more details on container networking, see [Inner-loop networking overview](/fundamentals/networking-overview/). + +## 🔀 Unified withEnvironment API for multi-language AppHosts + +Aspire 13.3 introduces a unified `withEnvironment(name, value)` API for polyglot AppHosts (TypeScript, Java, Python, Go, Rust). Previously, environment variable injection required separate methods for each value kind (`withEnvironmentEndpoint`, `withEnvironmentParameter`, `withEnvironmentConnectionString`, and so on). Now, a single call handles all value types: + +```typescript title="TypeScript — apphost.ts" +const api = await builder.addApi("api") + .withEnvironment("SERVICE_URL", cache.primaryEndpoint) + .withEnvironment("API_KEY", apiKeyParam) + .withEnvironment("DB", database); +``` + +The `value` argument accepts any of: a plain `string`, a `ReferenceExpression`, an `EndpointReference`, a parameter builder, a connection string resource builder, or an `IExpressionValue` — a new public interface that any type implementing both `IValueProvider` and `IManifestExpressionProvider` can implement to plug into this unified path. + +### Deprecation of old withEnvironment* aliases + +The previous per-kind aliases are still generated for backward compatibility but are marked `@deprecated` in the SDK. Migrate to `withEnvironment(name, value)` at your convenience: + +| Deprecated | Replacement | +|---|---| +| `withEnvironmentExpression` | `withEnvironment` | +| `withEnvironmentEndpoint` | `withEnvironment` | +| `withEnvironmentParameter` | `withEnvironment` | +| `withEnvironmentConnectionString` | `withEnvironment` | +| `withEnvironmentFromOutput` | `withEnvironment` | +| `withEnvironmentFromKeyVaultSecret` | `withEnvironment` | + + + For integration authors, see [Multi-language integrations](/extensibility/multi-language-integration-authoring/#iexpressionvalue--unified-environment-values) for details on `IExpressionValue` and how to annotate union-type parameters. + From 9f76429271cc5a34edad923f391ad6b0ed996e44 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 May 2026 07:26:21 -0700 Subject: [PATCH 2/2] Address withEnvironment docs review Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../architecture/resource-api-patterns.mdx | 5 +- .../multi-language-integration-authoring.mdx | 76 ------------------- .../content/docs/whats-new/aspire-13-3.mdx | 4 +- 3 files changed, 6 insertions(+), 79 deletions(-) diff --git a/src/frontend/src/content/docs/architecture/resource-api-patterns.mdx b/src/frontend/src/content/docs/architecture/resource-api-patterns.mdx index 7366782f8..f1cb1745d 100644 --- a/src/frontend/src/content/docs/architecture/resource-api-patterns.mdx +++ b/src/frontend/src/content/docs/architecture/resource-api-patterns.mdx @@ -156,9 +156,11 @@ Custom value objects defer evaluation and allow the framework to discover depend |--|--|--|--| | `IValueProvider` | `ValueTask GetValueAsync(CancellationToken)` | Run | Resolve live values at runtime | | `IManifestExpressionProvider` | `string ValueExpression { get; }` | Publish | Emit structured expressions in manifests | +| `IExpressionValue` | Inherits `IValueProvider` and `IManifestExpressionProvider` | Run and publish | Mark a value object as usable wherever an expression-backed value is accepted | | `IValueWithReferences` _(opt.)_ | `IEnumerable References { get; }` | Both (if needed) | Declare dependencies on other resources | - **Implement** `IValueProvider` and `IManifestExpressionProvider` on all structured value types. +- **Implement** `IExpressionValue` when a structured value type should be accepted by APIs such as `WithEnvironment(...)`. - **Implement** `IValueWithReferences` only when your type holds resource references. ### Attaching to resources @@ -198,5 +200,6 @@ public static IResourceBuilder WithEnvironment( |--|--|--| | `IValueProvider` | `GetValueAsync(...)` | Deferred runtime resolution | | `IManifestExpressionProvider` | `ValueExpression` | Structured publish-time expression | +| `IExpressionValue` | `IValueProvider` + `IManifestExpressionProvider` | Reusable expression-backed value | | `IValueWithReferences` _(opt.)_ | `References` | Declare resource dependencies | -| `WithEnvironment(...)` | `new("NAME", valueProvider)` | Attach structured values unflattened | \ No newline at end of file +| `WithEnvironment(...)` | `new("NAME", valueProvider)` | Attach structured values unflattened | diff --git a/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx b/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx index f5021d699..86489a04b 100644 --- a/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx +++ b/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx @@ -331,82 +331,6 @@ public static IResourceBuilder WithEnvironment( All types in the union must be ATS-compatible. The analyzer (ASPIREEXPORT005, ASPIREEXPORT006) validates union declarations at build time. -## IExpressionValue — unified environment values - -`IExpressionValue` is a new public interface in `Aspire.Hosting.ApplicationModel` that represents a value with both a runtime value and a publish-time manifest expression. Any type that implements both `IValueProvider` and `IManifestExpressionProvider` can also implement `IExpressionValue` to make it usable anywhere an environment variable value is expected. - -```csharp title="C# — IExpressionValue interface" -public interface IExpressionValue : IValueProvider, IManifestExpressionProvider -{ -} -``` - -### Why it matters for multi-language apphosts - -The built-in `WithEnvironment` export accepts `IExpressionValue` as one of its union type options, in addition to `string`, `ReferenceExpression`, `EndpointReference`, parameter builders, and connection string resource builders. This gives polyglot apphosts a single, consistent `withEnvironment(name, value)` call that works with all of these value types. - -In generated TypeScript, the union collapses to the concrete wrapper types for all exported resource types that implement `IExpressionValue`: - -```typescript title="TypeScript — generated withEnvironment signature" -withEnvironment( - name: string, - value: string | ReferenceExpressionHandle | EndpointReferenceHandle | ParameterResourceHandle | /* ... IExpressionValue implementors */ -): this; -``` - -### Implementing IExpressionValue in a custom type - -If you build a custom value type that provides both a runtime value and a manifest expression, implement `IExpressionValue` to make it passable directly to `WithEnvironment`: - -```csharp title="C# — Custom IExpressionValue implementation" -[AspireExport(ExposeProperties = true)] -public sealed class MySecretReference(string secretName) : IExpressionValue -{ - public string SecretName { get; } = secretName; - - // IManifestExpressionProvider — used when publishing - public string ValueExpression => $"{{{{myVault.secrets.{SecretName}}}}}"; - - // IValueProvider — used at runtime - public async ValueTask GetValueAsync(CancellationToken cancellationToken) - { - // Fetch the secret value at runtime - return await FetchSecretAsync(SecretName, cancellationToken); - } -} -``` - -Because `MySecretReference` is exported with `[AspireExport]`, the code generator recognizes it as an `IExpressionValue` implementor and includes it in the generated union type for `withEnvironment`. - -### Migrating from the old withEnvironment* aliases - -Earlier versions of the ATS TypeScript SDK generated separate methods for each value kind: - -| Old method (deprecated) | Replacement | -|---|---| -| `withEnvironmentExpression(name, expr)` | `withEnvironment(name, expr)` | -| `withEnvironmentEndpoint(name, endpoint)` | `withEnvironment(name, endpoint)` | -| `withEnvironmentParameter(name, param)` | `withEnvironment(name, param)` | -| `withEnvironmentConnectionString(name, resource)` | `withEnvironment(name, resource)` | -| `withEnvironmentFromOutput(name, output)` | `withEnvironment(name, output)` | -| `withEnvironmentFromKeyVaultSecret(name, secret)` | `withEnvironment(name, secret)` | - -These aliases remain available as deprecated shims in the generated SDK. Update your TypeScript apphost code to use the single `withEnvironment(name, value)` call instead: - -```typescript title="TypeScript — before (deprecated aliases)" -const api = await builder.addApi("api") - .withEnvironmentEndpoint("SERVICE_URL", cache.primaryEndpoint) - .withEnvironmentParameter("API_KEY", apiKeyParam) - .withEnvironmentConnectionString("DB", database); -``` - -```typescript title="TypeScript — after (unified API)" -const api = await builder.addApi("api") - .withEnvironment("SERVICE_URL", cache.primaryEndpoint) - .withEnvironment("API_KEY", apiKeyParam) - .withEnvironment("DB", database); -``` - ## Analyzer diagnostics The `Aspire.Hosting.Integration.Analyzers` package reports these diagnostics: diff --git a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx index 011e7b53d..902526e41 100644 --- a/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx +++ b/src/frontend/src/content/docs/whats-new/aspire-13-3.mdx @@ -1,6 +1,6 @@ --- title: What's new in Aspire 13.3 -description: Aspire 13.3 introduces Azure Network Security Perimeter support, enables the container tunnel by default, unified multi-language withEnvironment API, and more. +description: Aspire 13.3 introduces Azure Network Security Perimeter support, enables the container tunnel by default, and more. sidebar: label: Aspire 13.3 order: 0 @@ -164,5 +164,5 @@ The previous per-kind aliases are still generated for backward compatibility but | `withEnvironmentFromKeyVaultSecret` | `withEnvironment` | - For integration authors, see [Multi-language integrations](/extensibility/multi-language-integration-authoring/#iexpressionvalue--unified-environment-values) for details on `IExpressionValue` and how to annotate union-type parameters. + For integration authors, see [Multi-language integrations](/extensibility/multi-language-integration-authoring/#union-types) for union-type parameter annotations. For custom value type guidance, see [Resource API patterns](/architecture/resource-api-patterns/#custom-value-objects).