diff --git a/src/frontend/config/sidebar/docs.topics.ts b/src/frontend/config/sidebar/docs.topics.ts index 32a997bae..1d9fad26c 100644 --- a/src/frontend/config/sidebar/docs.topics.ts +++ b/src/frontend/config/sidebar/docs.topics.ts @@ -1083,6 +1083,10 @@ export const docsTopics: StarlightSidebarTopicsUserConfig = { ja: 'カスタム リソース URL', }, }, + { + label: 'Multi-language integrations', + slug: 'extensibility/multi-language-integration-authoring', + }, ], }, { diff --git a/src/frontend/src/content/docs/app-host/typescript-apphost-migration.mdx b/src/frontend/src/content/docs/app-host/typescript-apphost-migration.mdx index 97c3f3faa..0c6f00333 100644 --- a/src/frontend/src/content/docs/app-host/typescript-apphost-migration.mdx +++ b/src/frontend/src/content/docs/app-host/typescript-apphost-migration.mdx @@ -361,3 +361,4 @@ Once you've verified the migration is successful: - [TypeScript AppHost troubleshooting](/app-host/typescript-apphost-troubleshooting/) - [AppHost overview](/get-started/app-host/) - [Aspire CLI reference](/reference/cli/overview/) +- [Multi-language integrations](/extensibility/multi-language-integration-authoring/) — make your integration work with TypeScript AppHosts diff --git a/src/frontend/src/content/docs/app-host/typescript-apphost.mdx b/src/frontend/src/content/docs/app-host/typescript-apphost.mdx index 9c1c75e4f..b4c2b8be5 100644 --- a/src/frontend/src/content/docs/app-host/typescript-apphost.mdx +++ b/src/frontend/src/content/docs/app-host/typescript-apphost.mdx @@ -299,3 +299,4 @@ When reporting issues, please include: - [AppHost overview](/get-started/app-host/) - [Aspire CLI reference](/reference/cli/overview/) - [Integrations overview](/integrations/overview/) +- [Multi-language integrations](/extensibility/multi-language-integration-authoring/) — make your integration work with TypeScript AppHosts 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 new file mode 100644 index 000000000..5576644da --- /dev/null +++ b/src/frontend/src/content/docs/extensibility/multi-language-integration-authoring.mdx @@ -0,0 +1,340 @@ +--- +title: Multi-language integrations +description: Learn how to annotate your Aspire hosting integration so it works with TypeScript AppHosts. +--- + +import { + Aside, + Badge, + Code, + Steps, + Tabs, + TabItem, +} from '@astrojs/starlight/components'; +import LearnMore from '@components/LearnMore.astro'; + + + +Aspire hosting integrations are C# libraries that extend the AppHost with new resource types. By default, these integrations are only available in C# AppHosts. To make them available in TypeScript AppHosts, you annotate your APIs with ATS (Aspire Type System) attributes. + +This guide walks you through the process of exporting your integration for multi-language use. + +## How it works + +When a TypeScript AppHost adds your integration, the Aspire CLI: + + + +1. Loads your integration assembly +2. Scans for ATS attributes on methods, types, and properties, such as `[AspireExport]`. +3. Generates a typed TypeScript SDK with matching methods +4. The generated SDK communicates with your C# code via JSON-RPC at runtime + + + +Your C# code runs as-is — the TypeScript SDK is a thin client that calls into it. You don't need to rewrite anything in TypeScript. + +## Install the analyzer + +The [`📦 Aspire.Hosting.Integration.Analyzers`](https://www.nuget.org/packages/Aspire.Hosting.Integration.Analyzers) package provides build-time validation that catches common export mistakes. Add it to your integration project: + +```xml title="XML — MyIntegration.csproj" + + all + runtime; build; native; contentfiles; analyzers + +``` + +The analyzer reports diagnostics that help you get your exports right before users encounter runtime errors. Common scenarios include detecting incompatible parameter types, missing export annotations on public methods, duplicate export IDs, and synchronous callbacks that could deadlock in multi-language app hosts. + +## Export extension methods + +Suppress the experimental diagnostic in your project file: + +```xml title="XML — MyIntegration.csproj" + + $(NoWarn);ASPIREATS001 + +``` + +Then annotate your extension methods with `[AspireExport]`: + +```csharp title="C# — MyDatabaseBuilderExtensions.cs" +[AspireExport("addMyDatabase", Description = "Adds a MyDatabase container resource")] +public static IResourceBuilder AddMyDatabase( + this IDistributedApplicationBuilder builder, + [ResourceName] string name, + int? port = null) +{ + // Your existing implementation... +} + +[AspireExport("addDatabase", Description = "Adds a database to the MyDatabase server")] +public static IResourceBuilder AddDatabase( + this IResourceBuilder builder, + [ResourceName] string name, + string? databaseName = null) +{ + // Your existing implementation... +} + +[AspireExport("withDataVolume", Description = "Adds a data volume to the MyDatabase server")] +public static IResourceBuilder WithDataVolume( + this IResourceBuilder builder, + string? name = null) +{ + // Your existing implementation... +} +``` + +This generates the following highlighted TypeScript APIs: + +```typescript title="TypeScript — Generated SDK" {5-8} +import { createBuilder } from './.modules/aspire.js'; + +const builder = await createBuilder(); + +const db = await builder + .addMyDatabase("db", { port: 5432 }) + .addDatabase("mydata") + .withDataVolume(); + +const app = await builder.build(); +await app.run(); +```` + + + +## Export resource types + +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: + +```csharp title="C# — MyDatabaseResource.cs" +[AspireExport(ExposeProperties = true)] +public sealed class MyDatabaseResource(string name) + : ContainerResource(name), IResourceWithConnectionString +{ + /// + /// Gets the primary endpoint for the database. + /// + public EndpointReference PrimaryEndpoint => new(this, "tcp"); + + /// + /// Internal implementation detail — not exported. + /// + [AspireExportIgnore] + public string InternalConnectionPool { get; set; } = ""; +} + +[AspireExport] +public sealed class MyDatabaseDatabaseResource(string name, MyDatabaseResource parent) + : Resource(name) +{ + // Your existing implementation... +} +```` + +When `ExposeProperties = true`, each public property becomes a capability in the generated SDK. Use `[AspireExportIgnore]` on properties that shouldn't be exposed. + +You can also set `ExposeMethods = true` to export public instance methods as capabilities: + +```csharp title="C# — Context type with exposed methods" +[AspireExport(ExposeProperties = true, ExposeMethods = true)] +public class EnvironmentCallbackContext +{ + public Dictionary EnvironmentVariables { get; } + + public void AddEnvironmentVariable(string key, string value) + { + EnvironmentVariables[key] = value; + } +} +```` + +## Export configuration DTOs + +If your integration accepts structured configuration, mark the options class with `[AspireDto]`. DTOs are serialized as JSON between the TypeScript AppHost and the .NET runtime: + +```csharp title="C# — MyDatabaseOptions.cs" +[AspireDto] +public sealed class AddMyDatabaseOptions +{ + public required string Name { get; init; } + public int? Port { get; init; } + public string? ImageTag { get; init; } +} +``` + + + +## Handle incompatible overloads + +Some C# overloads use types that can't be represented in TypeScript (e.g., `Action` delegates with non-serializable contexts, interpolated string handlers, or C#-specific types). Mark these with `[AspireExportIgnore]`: + +```csharp title="C# — Exclude incompatible overloads" +// This overload works in TypeScript — simple parameters +[AspireExport("withConnectionStringLimit", Description = "Sets connection limit")] +public static IResourceBuilder WithConnectionStringLimit( + this IResourceBuilder builder, + int maxConnections) +{ + // ... +} + +// This overload uses a C#-specific type — exclude it +[AspireExportIgnore(Reason = "ForwarderConfig is not ATS-compatible. Use the DTO-based overload.")] +public static IResourceBuilder WithConnectionStringLimit( + this IResourceBuilder builder, + ForwarderConfig config) +{ + // ... +} +``` + + + +## Union types + +When a parameter accepts multiple types, use `[AspireUnion]` to declare the valid options: + +```csharp title="C# — Union type parameter" +[AspireExport("withEnvironment", Description = "Sets an environment variable")] +public static IResourceBuilder WithEnvironment( + this IResourceBuilder builder, + string name, + [AspireUnion(typeof(string), typeof(ReferenceExpression), typeof(EndpointReference))] + object value) + where T : IResourceWithEnvironment +{ + // ... +} +``` + +All types in the union must be ATS-compatible. The analyzer (ASPIREEXPORT005, ASPIREEXPORT006) validates union declarations at build time. + +## Analyzer diagnostics + +The `Aspire.Hosting.Integration.Analyzers` package reports these diagnostics: + +| ID | Severity | Description | +| --------------- | -------- | ------------------------------------------------------------------------------------------- | +| ASPIREEXPORT001 | Error | `[AspireExport]` method must be static | +| ASPIREEXPORT002 | Error | Invalid export ID format (must match `[a-zA-Z][a-zA-Z0-9.]*`) | +| ASPIREEXPORT003 | Error | Return type is not ATS-compatible | +| ASPIREEXPORT004 | Error | Parameter type is not ATS-compatible | +| ASPIREEXPORT005 | Warning | `[AspireUnion]` requires at least 2 types | +| ASPIREEXPORT006 | Warning | Union type is not ATS-compatible | +| ASPIREEXPORT007 | Warning | Duplicate export ID for the same target type | +| ASPIREEXPORT008 | Warning | Public extension method on exported type missing `[AspireExport]` or `[AspireExportIgnore]` | +| ASPIREEXPORT009 | Warning | Export name may collide with other integrations | +| ASPIREEXPORT010 | Warning | Synchronous callback invoked inline — may deadlock in multi-language app hosts | + +A clean build with zero analyzer warnings means your integration is ready for multi-language use. + +## Local development with project references + +You can test your integration locally without publishing to a NuGet feed. In your TypeScript AppHost's `aspire.config.json`, set the package value to a `.csproj` path instead of a version number: + +```json title="JSON — aspire.config.json" +{ + "appHost": { + "path": "apphost.ts", + "language": "typescript/nodejs" + }, + "packages": { + "Aspire.Hosting.Redis": "13.2.0", + "MyCompany.Hosting.MyDatabase": "../src/MyCompany.Hosting.MyDatabase/MyCompany.Hosting.MyDatabase.csproj" + } +} +``` + +When the CLI detects a `.csproj` path, it builds the project locally and generates the TypeScript SDK from the resulting assemblies. This lets you iterate on your exports without publishing to a feed. + + + +## Test your exports + + + +1. Create a TypeScript AppHost for testing: + + ```bash title="Create test AppHost" + mkdir test-apphost && cd test-apphost + aspire init --language typescript + ``` + +2. Add your integration via project reference in `aspire.config.json`: + + ```json title="JSON — aspire.config.json (packages section)" + { + "packages": { + "MyCompany.Hosting.MyDatabase": "../src/MyCompany.Hosting.MyDatabase/MyCompany.Hosting.MyDatabase.csproj" + } + } + ``` + +3. Run `aspire run` to generate the TypeScript SDK: + + ```bash title="Generate SDK and start" + aspire run + ``` + +4. Check the generated `.modules/` directory for your integration's TypeScript types. Verify that your exported methods appear with the correct signatures. + +5. Use the generated API in `apphost.ts`: + + ```typescript title="TypeScript — apphost.ts" + import { createBuilder } from './.modules/aspire.js'; + + const builder = await createBuilder(); + + const db = await builder + .addMyDatabase('db', { port: 5432 }) + .addDatabase('mydata') + .withDataVolume(); + + await builder.build().run(); + ``` + + + +## Supported types + +The following types are ATS-compatible and can be used in exported method signatures: + +| Category | Types | +| --------------- | ---------------------------------------------------------------------------------------------------- | +| **Primitives** | `string`, `bool`, `int`, `long`, `float`, `double`, `decimal` | +| **Value types** | `DateTime`, `TimeSpan`, `Guid`, `Uri` | +| **Enums** | Any enum type | +| **Handles** | `IResourceBuilder`, `IDistributedApplicationBuilder`, resource types marked with `[AspireExport]` | +| **DTOs** | Classes/structs marked with `[AspireDto]` | +| **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` | +| **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]`. + +## See also + +- [TypeScript AppHost](/app-host/typescript-apphost/) — Getting started with TypeScript AppHosts +- [Custom resources](/extensibility/custom-resources/) — Creating custom resource types +- [Integrations overview](/integrations/overview/) — Available integrations