Skip to content

Infer AspireUnion metadata from C# union types in exported APIs #15687

@davidfowl

Description

@davidfowl

Summary

When an exported API uses a C# union type as a parameter, Aspire should infer the equivalent AspireUnion metadata automatically instead of requiring authors to write AspireUnion manually.

This is based on the preview C# union feature documented for the .NET 11 preview train.

Motivation

Today, authors can model an exported union-shaped parameter by writing something like:

[AspireExport(MethodName = "runAsExisting")]
internal static IResourceBuilder<DurableTaskSchedulerResource> RunAsExisting(
    this IResourceBuilder<DurableTaskSchedulerResource> builder,
    [AspireUnion(typeof(string), typeof(IResourceBuilder<ParameterResource>))]
    object connection)

That works, but it forces Aspire-specific metadata into APIs that could otherwise be expressed naturally in C#.

With C# unions, authors should be able to write a normal union parameter and have the exporter infer the same union shape.

Example

C# authoring model

public union ExistingConnection(string, IResourceBuilder<ParameterResource>);

[AspireExport(MethodName = "runAsExisting")]
internal static IResourceBuilder<DurableTaskSchedulerResource> RunAsExisting(
    this IResourceBuilder<DurableTaskSchedulerResource> builder,
    ExistingConnection connection)

Aspire export model

The exporter should treat the parameter as if it had:

[AspireUnion(typeof(string), typeof(IResourceBuilder<ParameterResource>))]

without requiring the user to write that attribute.

In other words, a parameter whose type is a C# union should be exported the same way as a parameter annotated with the equivalent AspireUnion case list.

Proposed behavior

  1. If an exported parameter type is a C# union type, infer its case types from the union declaration / [Union] pattern.
  2. Normalize that inferred information into the same internal representation currently used for AspireUnion.
  3. Continue to support explicit AspireUnion for non-C#-union scenarios and as a fallback/manual escape hatch.

Inference rule

For a union declaration:

union U(T1, T2, ... Tn)

an exported parameter of type U should be treated as if it were annotated with:

[AspireUnion(typeof(T1), typeof(T2), ..., typeof(Tn))]

while keeping the declared parameter type as U.

Notes

  • The author-facing API does not need to be rewritten to object.
  • Only the exporter/analyzer pipeline needs to understand the union shape.
  • If we generate bridge/shim code internally, that lowering can erase to another representation, but the source signature should stay as the C# union type.
  • This should work both for the union declaration syntax and for custom union types that follow the [System.Runtime.CompilerServices.Union] pattern.

Edge cases to consider

  • Nullable case types
  • Generic case types
  • Nested unions
  • Precedence/interaction if explicit AspireUnion is also present
  • Diagnostics when a union type is malformed or not exportable

Expected outcome

Aspire-exported APIs can adopt C# unions naturally, and the existing AspireUnion machinery becomes the normalized implementation detail rather than something users must author directly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-app-modelIssues pertaining to the APIs in Aspire.Hosting, e.g. DistributedApplicationarea-polyglotIssues related to polyglot apphosts

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions