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.cs — RegisterOptionsInterface, GetOptionsInterfaceName, AreOptionsCompatible, _capabilityOptionsInterfaceMap
src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs — ExportApi
src/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs — builds the one-package scanner app host
Found while reviewing #19032.
Problem
TypeScript options interfaces are named after the method that produced them.
RegisterOptionsInterfaceinTypeScriptApiProjectorresolves 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.EventHubsandAspire.Hosting.Azure.ServiceBusboth expose: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 getsRunAsEmulatorOptions. Add EventHubs and, depending on scan order, the same method may now be typedRunAsEmulator1Options. 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 publishRunAsEmulatorOptionswith 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.cs—RegisterOptionsInterface,GetOptionsInterfaceName,AreOptionsCompatible,_capabilityOptionsInterfaceMapsrc/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs—ExportApisrc/Aspire.Cli/Commands/Sdk/SdkExportCommand.cs— builds the one-package scanner app hostFound while reviewing #19032.