Skip to content

Commit e2faff6

Browse files
docs: update multi-language integration guide for ATS-first API design
Documents the getter-only-to-async-method behavior introduced in microsoft/aspire#16403 and adds a new 'Callback context types and the ATS-first editor pattern' section explaining: - How getter-only C# properties map to async methods in generated TypeScript SDKs, while mutable-collection properties remain as readonly getters - The ATS-first editor/facade pattern for callback context types (EnvironmentEditor, etc.) that should be used instead of ExposeProperties = true on callback context classes - How to define, export, and consume callback extension methods like withEnvironmentCallback, withArgsCallback, and withUrls Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8f11c76 commit e2faff6

1 file changed

Lines changed: 128 additions & 10 deletions

File tree

src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx

Lines changed: 128 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ The export ID becomes the method name in generated SDKs. Use camelCase (e.g., `a
114114

115115
## Export resource types
116116

117-
Mark your resource types with `[AspireExport]` so the TypeScript SDK can reference them as typed handles. Set `ExposeProperties = true` to make the resource's properties accessible as get/set capabilities — most resources should include this:
117+
Mark your resource types with `[AspireExport]` so the TypeScript SDK can reference them as typed handles. Set `ExposeProperties = true` to make all public properties accessible as capabilities, or annotate individual properties with `[AspireExport]` for fine-grained control:
118118

119119
```csharp title="C# — MyDatabaseResource.cs"
120120
[AspireExport(ExposeProperties = true)]
@@ -139,24 +139,142 @@ public sealed class MyDatabaseDatabaseResource(string name, MyDatabaseResource p
139139
{
140140
// Your existing implementation...
141141
}
142-
````
142+
```
143143

144144
When `ExposeProperties = true`, each public property becomes a capability in the generated SDK. Use `[AspireExportIgnore]` on properties that shouldn't be exposed.
145145

146-
You can also set `ExposeMethods = true` to export public instance methods as capabilities:
146+
You can also set `ExposeMethods = true` to export public instance methods as capabilities alongside properties.
147+
148+
### How getter-only properties appear in TypeScript
149+
150+
The code generator distinguishes between read-only (getter-only) properties and read-write or mutable-collection properties:
147151

148-
```csharp title="C# — Context type with exposed methods"
149-
[AspireExport(ExposeProperties = true, ExposeMethods = true)]
150-
public class EnvironmentCallbackContext
152+
- **Getter-only properties** (no setter, and not a mutable collection type) are generated as async methods in TypeScript: `property(): Promise<T>`.
153+
- **Read-write properties** and **mutable-collection properties** (such as `AspireList<T>` or `AspireDict<K,V>`) are generated as `readonly` getter properties.
154+
155+
For example, a C# class with both kinds of property:
156+
157+
```csharp title="C# — Mixed property kinds"
158+
[AspireExport(ExposeProperties = true)]
159+
public class MyCallbackContext
151160
{
152-
public Dictionary<string, object> EnvironmentVariables { get; }
161+
/// <summary>Getter-only — becomes an async method in TypeScript.</summary>
162+
public IResource Resource => _resource;
163+
164+
/// <summary>Mutable collection — stays a readonly getter in TypeScript.</summary>
165+
public AspireList<string> Tags { get; } = new();
166+
}
167+
```
168+
169+
Generates the following TypeScript interface:
170+
171+
```typescript title="TypeScript — Generated interface"
172+
export interface MyCallbackContext {
173+
toJSON(): MarshalledHandle;
174+
resource(): Promise<IResourceHandle>; // getter-only → async method
175+
readonly tags: AspireList<string>; // mutable collection → readonly getter
176+
}
177+
```
178+
179+
TypeScript AppHost authors call getter-only properties as functions:
180+
181+
```typescript title="TypeScript — Consuming the generated API"
182+
const resource = await context.resource();
183+
const tags = context.tags; // no await needed for mutable collections
184+
```
185+
186+
:::tip
187+
Prefer getter-only properties (`=> value;` or `{ get; }` without a setter) when the TypeScript side should treat the value as a read-once async operation. Use `AspireList<T>` and `AspireDict<K,V>` when TypeScript code needs to add, remove, or iterate values directly.
188+
:::
189+
190+
## Callback context types and the ATS-first editor pattern
191+
192+
When you export a method that accepts a callback (such as `withEnvironmentCallback`, `withArgsCallback`, or `withUrls`), the callback receives a *context* object. For TypeScript compatibility, context types should follow the ATS-first design:
193+
194+
1. Use `[AspireExport]` (not `ExposeProperties = true`) on the context class.
195+
2. Annotate only the properties that TypeScript callers need with individual `[AspireExport]` attributes.
196+
3. For mutable state (environment variables, command-line arguments, URL lists), expose a small *editor* class rather than the raw collection.
197+
198+
### Defining an editor class
199+
200+
An editor wraps a mutable collection and exposes specific operations — typically `add`, `set`, or `remove` — instead of handing the raw collection to TypeScript:
153201

154-
public void AddEnvironmentVariable(string key, string value)
202+
```csharp title="C# — EnvironmentEditor.cs"
203+
/// <summary>
204+
/// Provides an ATS-first editor for environment variables within polyglot callbacks.
205+
/// </summary>
206+
[AspireExport]
207+
internal sealed class EnvironmentEditor(Dictionary<string, object> environmentVariables)
208+
{
209+
/// <summary>Sets an environment variable.</summary>
210+
[AspireExport(Description = "Sets an environment variable")]
211+
public void Set(
212+
string name,
213+
[AspireUnion(
214+
typeof(string),
215+
typeof(ReferenceExpression),
216+
typeof(EndpointReference),
217+
typeof(IResourceBuilder<ParameterResource>),
218+
typeof(IResourceBuilder<IResourceWithConnectionString>))]
219+
object value)
155220
{
156-
EnvironmentVariables[key] = value;
221+
environmentVariables[name] = value;
157222
}
158223
}
159-
````
224+
```
225+
226+
### Defining the callback context
227+
228+
Use individual `[AspireExport]` attributes on each property the TypeScript caller needs. Pass the editor as a getter-only property so TypeScript callers receive it as an async method:
229+
230+
```csharp title="C# — MyCallbackContext.cs"
231+
[AspireExport]
232+
public sealed class MyCallbackContext(
233+
DistributedApplicationExecutionContext executionContext,
234+
IResource resource,
235+
Dictionary<string, object> environmentVariables)
236+
{
237+
/// <summary>Gets the resource associated with this callback.</summary>
238+
[AspireExport(Description = "Gets the resource associated with this callback")]
239+
public IResource Resource => resource;
240+
241+
/// <summary>Gets the execution context.</summary>
242+
[AspireExport(Description = "Gets the execution context")]
243+
public DistributedApplicationExecutionContext ExecutionContext => executionContext;
244+
245+
/// <summary>Gets the environment variable editor.</summary>
246+
[AspireExport(Description = "Gets the environment variable editor")]
247+
internal EnvironmentEditor Environment => new(environmentVariables);
248+
}
249+
```
250+
251+
### Exporting the callback method
252+
253+
Export the extension method that accepts the callback, using `Action<MyCallbackContext>` as the parameter type:
254+
255+
```csharp title="C# — MyResourceBuilderExtensions.cs"
256+
[AspireExport("withMyCallback", Description = "Configures the resource using a callback")]
257+
public static IResourceBuilder<MyResource> WithMyCallback(
258+
this IResourceBuilder<MyResource> builder,
259+
Action<MyCallbackContext> configure)
260+
{
261+
return builder.WithAnnotation(new MyCallbackAnnotation(configure));
262+
}
263+
```
264+
265+
The generated TypeScript API accepts an async arrow function:
266+
267+
```typescript title="TypeScript — Consuming the callback API"
268+
await myResource.withMyCallback(async (context) => {
269+
const resource = await context.resource();
270+
const env = await context.environment();
271+
await env.set("MY_KEY", "my-value");
272+
});
273+
```
274+
275+
:::note
276+
The `Action<T>` delegate type is ATS-compatible. The TypeScript side receives an async function; the Aspire runtime bridges the async TypeScript call to the synchronous C# delegate on a background thread.
277+
:::
160278

161279
## Export configuration DTOs
162280

0 commit comments

Comments
 (0)