Skip to content

Options interface names depend on what else is loaded, so they aren't a stable part of a package's API #19141

Description

@adamint

Problem

TypeScript options interfaces are named after the method that produced them. RegisterOptionsInterface in TypeScriptApiProjector resolves a collision — two capabilities sharing a method name with optional parameters that can't merge — by giving the first registration the base name and suffixing later ones (RunAsEmulatorOptions, RunAsEmulator1Options).

Both halves of that are load-dependent. Whether a collision happens depends on which packages the app host references, and which side wins the base name depends on scan order. So the name a given package's method gets is not a function of that package.

This is reachable today. Aspire.Hosting.Azure.EventHubs and Aspire.Hosting.Azure.ServiceBus both expose:

public static IResourceBuilder<AzureEventHubsResource> RunAsEmulator(
    this IResourceBuilder<AzureEventHubsResource> builder,
    Action<IResourceBuilder<AzureEventHubsEmulatorResource>>? configureContainer = null)

public static IResourceBuilder<AzureServiceBusResource> RunAsEmulator(
    this IResourceBuilder<AzureServiceBusResource> builder,
    Action<IResourceBuilder<AzureServiceBusEmulatorResource>>? configureContainer = null)

Same method name, same parameter name, different callback types — incompatible, so they cannot share an interface.

Consequences

sdk generate: an app host referencing only ServiceBus gets RunAsEmulatorOptions. Add EventHubs and, depending on scan order, the same method may now be typed RunAsEmulator1Options. A name in the generated SDK can change because an unrelated package was added.

sdk export (#19032): each export runs in its own scanner app host referencing one package plus core and codegen, so no export ever sees a sibling collision. Both packages publish RunAsEmulatorOptions with incompatible members. aspire.dev concatenates declaration fragments and type-checks the result, so those two fragments conflict.

Note the compatible case is fine — TypeScript merges identical interface declarations, so fragments that agree are harmless. Only the incompatible collision breaks.

Why the obvious fix doesn't work

Handing the exporter a wider context so it can reproduce generation's naming was tried in #19032 and backed out (d6d6a1b). The scanner app host is built from the requested package alone, so the wider context doesn't contain the sibling. It also made things worse: core is in every context, so it newly decided integration-vs-core collisions by scan order while the core export — resolved from a core-only context — wouldn't agree.

More fundamentally, there's no single "generated SDK" for an export to match, because generation's own names vary by app host.

Suggested direction

Make the name a function of the capability's identity rather than of discovery. That has to be unconditional rather than collision-triggered, since whether a collision occurs is itself load-dependent — for example, always qualifying by the owning assembly.

That renames essentially every options interface in emitted TypeScript, so it's a breaking change to the SDK contract and needs a deliberate decision, including on the aspire.dev side that renders these names.

Pointers

  • src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptApiProjector.csRegisterOptionsInterface, GetOptionsInterfaceName, AreOptionsCompatible, _capabilityOptionsInterfaceMap
  • src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.csExportApi
  • src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs — builds the one-package scanner app host

Found while reviewing #19032.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-codegenarea-integrationsIssues pertaining to Aspire Integrations packagesarea-polyglotIssues related to polyglot apphoststriage:bot-seenAspire triage bot has seen this issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions