Multi-language integrations

Preview feature

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. 1

    Loads your integration assembly

  2. 2

    Scans for ATS attributes on methods, types, and properties, such as [AspireExport].

  3. 3

    Generates a typed TypeScript SDK with matching methods

  4. 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 package provides build-time validation that catches common export mistakes. Add it to your integration project:

<PackageReference Include="Aspire.Hosting.Integration.Analyzers" Version="13.2.0">
    <PrivateAssets>all</PrivateAssets>
    <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>

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:

<PropertyGroup>
    <NoWarn>$(NoWarn);ASPIREATS001</NoWarn>
</PropertyGroup>

Then annotate your extension methods with [AspireExport]:

[AspireExport("addMyDatabase", Description = "Adds a MyDatabase container resource")]
public static IResourceBuilder<MyDatabaseResource> 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<MyDatabaseDatabaseResource> AddDatabase(
    this IResourceBuilder<MyDatabaseResource> builder,
    [ResourceName] string name,
    string? databaseName = null)
{
    // Your existing implementation...
}
  
[AspireExport("withDataVolume", Description = "Adds a data volume to the MyDatabase server")]
public static IResourceBuilder<MyDatabaseResource> WithDataVolume(
    this IResourceBuilder<MyDatabaseResource> builder,
    string? name = null)
{
    // Your existing implementation...
}

This generates the following highlighted TypeScript APIs:

const builder = await createBuilder();
  
const db = await builder
    .addMyDatabase("db", { port: 5432 })
    .addDatabase("mydata")
    .withDataVolume();
  
const app = await builder.build();
await app.run();

[!TIP] Naming convention The export ID becomes the method name in generated SDKs. Use camelCase (e.g., addMyDatabase, withDataVolume). The full capability ID is computed as {AssemblyName}/{methodName} β€” for example, MyCompany.Hosting.MyDatabase/addMyDatabase.

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:

[AspireExport(ExposeProperties = true)]
public sealed class MyDatabaseResource(string name)
    : ContainerResource(name), IResourceWithConnectionString
{
    /// <summary>
    /// Gets the primary endpoint for the database.
    /// </summary>
    public EndpointReference PrimaryEndpoint => new(this, "tcp");
  
    /// <summary>
    /// Internal implementation detail β€” not exported.
    /// </summary>
    [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:

[AspireExport(ExposeProperties = true, ExposeMethods = true)]
public class EnvironmentCallbackContext
{
    public Dictionary<string, object> 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:

[AspireDto]
public sealed class AddMyDatabaseOptions
{
    public required string Name { get; init; }
    public int? Port { get; init; }
    public string? ImageTag { get; init; }
}

Note

DTOs should only contain properties that can be serialized to and from JSON. Avoid using complex .NET typesβ€”such as IConfiguration, ILogger, or delegate types like Action and Func<T>β€”as they are not serializable and are not suitable for DTOs.

Handle incompatible overloads

Some C# overloads use types that can't be represented in TypeScript (e.g., Action<T> delegates with non-serializable contexts, interpolated string handlers, or C#-specific types). Mark these with [AspireExportIgnore]:

// This overload works in TypeScript β€” simple parameters
[AspireExport("withConnectionStringLimit", Description = "Sets connection limit")]
public static IResourceBuilder<MyDatabaseResource> WithConnectionStringLimit(
    this IResourceBuilder<MyDatabaseResource> 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<MyDatabaseResource> WithConnectionStringLimit(
    this IResourceBuilder<MyDatabaseResource> builder,
    ForwarderConfig config)
{
    // ...
}

Caution

The analyzer (ASPIREEXPORT008) warns when public extension methods on exported types lack either [AspireExport] or [AspireExportIgnore]. Every public method must be explicitly exported or excluded.

Union types

When a parameter accepts multiple types, use [AspireUnion] to declare the valid options:

[AspireExport("withEnvironment", Description = "Sets an environment variable")]
public static IResourceBuilder<T> WithEnvironment<T>(
    this IResourceBuilder<T> 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:

{
  "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.

Note

Project references require the .NET SDK to be installed (for dotnet build). The NuGet-only path (version strings) does not require the .NET SDK.

Test your exports

  1. 1

    Create a TypeScript AppHost for testing:

    mkdir test-apphost && cd test-apphost
    aspire init --language typescript
    
  2. 2

    Add your integration via project reference in aspire.config.json:

    {
      "packages": {
        "MyCompany.Hosting.MyDatabase": "../src/MyCompany.Hosting.MyDatabase/MyCompany.Hosting.MyDatabase.csproj"
      }
    }
  3. 3

    Run aspire run to generate the TypeScript SDK:

    aspire run
    
  4. 4

    Check the generated .modules/ directory for your integration's TypeScript types. Verify that your exported methods appear with the correct signatures.

  5. 5

    Use the generated API in 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<T>, IDistributedApplicationBuilder, resource types marked with [AspireExport]
DTOs Classes/structs marked with [AspireDto]
Collections List<T>, Dictionary<string, T>, arrays β€” where T is ATS-compatible
Delegates Action<T>, Func<T>, 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