|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using System.Reflection;
using Aspire.Hosting.Ats;
namespace Aspire.Hosting.CodeGeneration.TypeScript;
/// <summary>
/// Represents a builder class to be generated with its capabilities.
/// Internal type replacing BuilderModel - used only within the generator.
/// </summary>
internal sealed class BuilderModel
{
public required string TypeId { get; init; }
public required string BuilderClassName { get; init; }
public required List<AtsCapabilityInfo> Capabilities { get; init; }
public bool IsInterface { get; init; }
public AtsTypeRef? TargetType { get; init; }
}
/// <summary>
/// Generates a TypeScript SDK using the ATS (Aspire Type System) capability-based API.
/// Produces typed builder classes with fluent methods that use invokeCapability().
/// </summary>
/// <remarks>
/// <para>
/// <b>ATS to TypeScript Type Mapping</b>
/// </para>
/// <para>
/// The generator maps ATS types to TypeScript types according to the following rules:
/// </para>
/// <para>
/// <b>Primitive Types:</b>
/// <list type="table">
/// <listheader>
/// <term>ATS Type</term>
/// <description>TypeScript Type</description>
/// </listheader>
/// <item><term><c>string</c></term><description><c>string</c></description></item>
/// <item><term><c>number</c></term><description><c>number</c></description></item>
/// <item><term><c>boolean</c></term><description><c>boolean</c></description></item>
/// <item><term><c>any</c></term><description><c>unknown</c></description></item>
/// </list>
/// </para>
/// <para>
/// <b>Handle Types:</b>
/// Type IDs use the format <c>{AssemblyName}/{TypeName}</c>.
/// <list type="table">
/// <listheader>
/// <term>ATS Type ID</term>
/// <description>TypeScript Type</description>
/// </listheader>
/// <item><term><c>Aspire.Hosting/IDistributedApplicationBuilder</c></term><description><c>BuilderHandle</c></description></item>
/// <item><term><c>Aspire.Hosting/DistributedApplication</c></term><description><c>ApplicationHandle</c></description></item>
/// <item><term><c>Aspire.Hosting/DistributedApplicationExecutionContext</c></term><description><c>ExecutionContextHandle</c></description></item>
/// <item><term><c>Aspire.Hosting.Redis/RedisResource</c></term><description><c>RedisResourceBuilderHandle</c></description></item>
/// <item><term><c>Aspire.Hosting/ContainerResource</c></term><description><c>ContainerResourceBuilderHandle</c></description></item>
/// <item><term><c>Aspire.Hosting.ApplicationModel/IResource</c></term><description><c>IResourceHandle</c></description></item>
/// </list>
/// </para>
/// <para>
/// <b>Handle Type Naming Rules:</b>
/// <list type="bullet">
/// <item><description>Core types: Use type name + "Handle"</description></item>
/// <item><description>Interface types: Use interface name + "Handle" (keep the I prefix)</description></item>
/// <item><description>Resource types: Use type name + "BuilderHandle"</description></item>
/// </list>
/// </para>
/// <para>
/// <b>Special Types:</b>
/// <list type="table">
/// <listheader>
/// <term>ATS Type</term>
/// <description>TypeScript Type</description>
/// </listheader>
/// <item><term><c>callback</c></term><description><c>(context: EnvironmentContextHandle) => Promise<void></c></description></item>
/// <item><term><c>T[]</c> (array)</term><description><c>T[]</c> (array of mapped type)</description></item>
/// </list>
/// </para>
/// <para>
/// <b>Builder Class Generation:</b>
/// <list type="bullet">
/// <item><description><c>Aspire.Hosting.Redis/RedisResource</c> → <c>RedisResourceBuilder</c> class with <c>RedisResourceBuilderPromise</c> thenable wrapper</description></item>
/// <item><description><c>Aspire.Hosting.ApplicationModel/IResource</c> → <c>ResourceBuilderBase</c> abstract class (interface types get "BuilderBase" suffix)</description></item>
/// <item><description>Concrete builders extend interface builders based on type hierarchy</description></item>
/// </list>
/// </para>
/// <para>
/// <b>Method Naming:</b>
/// <list type="bullet">
/// <item><description>Derived from capability ID: <c>Aspire.Hosting.Redis/addRedis</c> → <c>addRedis</c></description></item>
/// <item><description>Can be overridden via <c>[AspireExport(MethodName = "...")]</c></description></item>
/// <item><description>TypeScript uses camelCase (the canonical form from capability IDs)</description></item>
/// </list>
/// </para>
/// </remarks>
public sealed class AtsTypeScriptCodeGenerator : ICodeGenerator
{
private TextWriter _writer = null!;
// Mapping of typeId -> wrapper class name for all generated wrapper types
// Used to resolve parameter types to wrapper classes instead of handle types
private readonly Dictionary<string, string> _wrapperClassNames = new(StringComparer.Ordinal);
// Set of type IDs that have Promise wrappers (types with chainable methods)
// Used to determine return types for methods
private readonly HashSet<string> _typesWithPromiseWrappers = new(StringComparer.Ordinal);
// Set of generated options interfaces to avoid duplicates
private readonly HashSet<string> _generatedOptionsInterfaces = new(StringComparer.Ordinal);
// Collected options interfaces to generate (interface name -> list of optional params)
private readonly Dictionary<string, List<AtsParameterInfo>> _optionsInterfacesToGenerate = new(StringComparer.Ordinal);
// Mapping of enum type IDs to TypeScript enum names
private readonly Dictionary<string, string> _enumTypeNames = new(StringComparer.Ordinal);
/// <summary>
/// Checks if an AtsTypeRef represents a handle type.
/// </summary>
private static bool IsHandleType(AtsTypeRef? typeRef) =>
typeRef != null && typeRef.Category == AtsTypeCategory.Handle;
/// <summary>
/// Maps an AtsTypeRef to a TypeScript type using category-based dispatch.
/// This is the preferred method - uses type metadata rather than string parsing.
/// </summary>
private string MapTypeRefToTypeScript(AtsTypeRef? typeRef)
{
if (typeRef == null)
{
return "unknown";
}
// Check for wrapper class first (handles custom types like ReferenceExpression)
if (_wrapperClassNames.TryGetValue(typeRef.TypeId, out var wrapperClassName))
{
return wrapperClassName;
}
return typeRef.Category switch
{
AtsTypeCategory.Primitive => MapPrimitiveType(typeRef.TypeId),
AtsTypeCategory.Enum => MapEnumType(typeRef.TypeId),
AtsTypeCategory.Handle => GetWrapperOrHandleName(typeRef.TypeId),
AtsTypeCategory.Dto => GetDtoInterfaceName(typeRef.TypeId),
AtsTypeCategory.Callback => "Function", // Callbacks handled separately with full signature
AtsTypeCategory.Array => $"{MapTypeRefToTypeScript(typeRef.ElementType)}[]",
AtsTypeCategory.List => $"AspireList<{MapTypeRefToTypeScript(typeRef.ElementType)}>",
AtsTypeCategory.Dict => typeRef.IsReadOnly
? $"Record<{MapTypeRefToTypeScript(typeRef.KeyType)}, {MapTypeRefToTypeScript(typeRef.ValueType)}>"
: $"AspireDict<{MapTypeRefToTypeScript(typeRef.KeyType)}, {MapTypeRefToTypeScript(typeRef.ValueType)}>",
AtsTypeCategory.Union => MapUnionTypeToTypeScript(typeRef),
AtsTypeCategory.Unknown => "any", // Unknown types use 'any' since they're not in the ATS universe
_ => "any" // Fallback for any unhandled categories
};
}
/// <summary>
/// Maps primitive type IDs to TypeScript types.
/// </summary>
private static string MapPrimitiveType(string typeId) => typeId switch
{
AtsConstants.String or AtsConstants.Char => "string",
AtsConstants.Number => "number",
AtsConstants.Boolean => "boolean",
AtsConstants.Void => "void",
AtsConstants.Any => "any",
AtsConstants.DateTime or AtsConstants.DateTimeOffset or
AtsConstants.DateOnly or AtsConstants.TimeOnly => "string",
AtsConstants.TimeSpan => "number",
AtsConstants.Guid or AtsConstants.Uri => "string",
AtsConstants.CancellationToken => "AbortSignal",
_ => typeId
};
/// <summary>
/// Maps an enum type ID to the generated TypeScript enum name.
/// Throws if the enum type wasn't collected during scanning.
/// </summary>
private string MapEnumType(string typeId)
{
if (!_enumTypeNames.TryGetValue(typeId, out var enumName))
{
throw new InvalidOperationException(
$"Enum type '{typeId}' was not found in the scanned enum types. " +
$"This indicates the enum type was not discovered during assembly scanning.");
}
return enumName;
}
/// <summary>
/// Maps a union type to TypeScript union syntax (T1 | T2 | ...).
/// </summary>
private string MapUnionTypeToTypeScript(AtsTypeRef typeRef)
{
if (typeRef.UnionTypes == null || typeRef.UnionTypes.Count == 0)
{
return "unknown";
}
var memberTypes = typeRef.UnionTypes
.Select(MapTypeRefToTypeScript)
.Distinct();
return string.Join(" | ", memberTypes);
}
/// <summary>
/// Gets the wrapper class name or handle type name for a handle type ID.
/// Prefers wrapper class if one exists, otherwise generates a handle type name.
/// </summary>
private string GetWrapperOrHandleName(string typeId)
{
if (_wrapperClassNames.TryGetValue(typeId, out var wrapperClassName))
{
return wrapperClassName;
}
return GetHandleTypeName(typeId);
}
/// <summary>
/// Gets a TypeScript interface name for a DTO type.
/// </summary>
private static string GetDtoInterfaceName(string typeId)
{
// Extract simple type name and use as interface name
var simpleTypeName = ExtractSimpleTypeName(typeId);
return simpleTypeName;
}
/// <summary>
/// Maps a parameter to its TypeScript type, handling callbacks specially.
/// For interface handle types, generates union types to accept both handles and wrapper classes.
/// </summary>
private string MapParameterToTypeScript(AtsParameterInfo param)
{
if (param.IsCallback)
{
return GenerateCallbackTypeSignature(param.CallbackParameters, param.CallbackReturnType);
}
var baseType = MapTypeRefToTypeScript(param.Type);
// For interface handle types, use ResourceBuilderBase as the parameter type
// All wrapper classes extend ResourceBuilderBase and have toJSON() for serialization
if (IsInterfaceHandleType(param.Type))
{
return "ResourceBuilderBase";
}
return baseType;
}
/// <summary>
/// Checks if a type reference is an interface handle type.
/// Interface handles need union types to accept wrapper classes.
/// </summary>
private static bool IsInterfaceHandleType(AtsTypeRef? typeRef)
{
if (typeRef == null)
{
return false;
}
return typeRef.Category == AtsTypeCategory.Handle && typeRef.IsInterface;
}
/// <summary>
/// Gets the TypeId from a capability's return type.
/// </summary>
private static string? GetReturnTypeId(AtsCapabilityInfo capability) => capability.ReturnType?.TypeId;
/// <inheritdoc />
public string Language => "TypeScript";
/// <inheritdoc />
public Dictionary<string, string> GenerateDistributedApplication(AtsContext context)
{
var files = new Dictionary<string, string>();
// Add embedded resource files (transport.ts, base.ts)
files["transport.ts"] = GetEmbeddedResource("transport.ts");
files["base.ts"] = GetEmbeddedResource("base.ts");
// Generate the capability-based aspire.ts SDK
files["aspire.ts"] = GenerateAspireSdk(context);
return files;
}
private static string GetEmbeddedResource(string name)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"Aspire.Hosting.CodeGeneration.TypeScript.Resources.{name}";
using var stream = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Embedded resource '{name}' not found.");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
/// <summary>
/// Gets a valid TypeScript method name from a capability method name.
/// Handles dotted names like "EnvironmentContext.resource" by extracting just the final part.
/// </summary>
private static string GetTypeScriptMethodName(string methodName)
{
var dotIndex = methodName.LastIndexOf('.');
return dotIndex >= 0 ? methodName[(dotIndex + 1)..] : methodName;
}
/// <summary>
/// Generates the aspire.ts SDK file with capability-based API.
/// </summary>
private string GenerateAspireSdk(AtsContext context)
{
using var stringWriter = new StringWriter(CultureInfo.InvariantCulture);
_writer = stringWriter;
// Header
WriteLine("""
// aspire.ts - Capability-based Aspire SDK
// This SDK uses the ATS (Aspire Type System) capability API.
// Capabilities are endpoints like 'Aspire.Hosting/createBuilder'.
//
// GENERATED CODE - DO NOT EDIT
import {
AspireClient as AspireClientRpc,
Handle,
MarshalledHandle,
CapabilityError,
registerCallback,
wrapIfHandle,
registerHandleWrapper
} from './transport.js';
import {
ResourceBuilderBase,
ReferenceExpression,
refExpr,
AspireDict,
AspireList
} from './base.js';
""");
WriteLine();
var capabilities = context.Capabilities;
var dtoTypes = context.DtoTypes;
var enumTypes = context.EnumTypes;
// Get builder models (flattened - each builder has all its applicable capabilities)
var allBuilders = CreateBuilderModels(capabilities);
var entryPoints = GetEntryPointCapabilities(capabilities);
// All builders (no special filtering)
var builders = allBuilders;
// Entry point methods that don't extend any type go on AspireClient
var clientMethods = entryPoints
.Where(c => string.IsNullOrEmpty(c.TargetTypeId))
.ToList();
// Collect all unique type IDs for handle type aliases
// Exclude DTO types - they have their own interfaces, not handle aliases
var dtoTypeIds = new HashSet<string>(dtoTypes.Select(d => d.TypeId));
var typeIds = new HashSet<string>();
foreach (var cap in capabilities)
{
if (!string.IsNullOrEmpty(cap.TargetTypeId) && !dtoTypeIds.Contains(cap.TargetTypeId))
{
typeIds.Add(cap.TargetTypeId);
}
if (IsHandleType(cap.ReturnType) && !dtoTypeIds.Contains(cap.ReturnType!.TypeId))
{
typeIds.Add(GetReturnTypeId(cap)!);
}
// Add parameter type IDs (for types like IResourceBuilder<IResource>)
foreach (var param in cap.Parameters)
{
if (IsHandleType(param.Type) && !dtoTypeIds.Contains(param.Type!.TypeId))
{
typeIds.Add(param.Type!.TypeId);
}
// Also collect callback parameter types
if (param.IsCallback && param.CallbackParameters != null)
{
foreach (var cbParam in param.CallbackParameters)
{
if (IsHandleType(cbParam.Type) && !dtoTypeIds.Contains(cbParam.Type.TypeId))
{
typeIds.Add(cbParam.Type.TypeId);
}
}
}
}
}
// Generate handle type aliases
GenerateHandleTypeAliases(typeIds);
// Generate enum types
GenerateEnumTypes(enumTypes);
// Generate DTO interfaces
GenerateDtoInterfaces(dtoTypes);
// Separate builders into categories:
// 1. Resource builders: IResource*, ContainerResource, etc.
// 2. Type classes: everything else (context types, wrapper types)
var resourceBuilders = builders.Where(b => b.TargetType?.IsResourceBuilder == true).ToList();
var typeClasses = builders.Where(b => b.TargetType?.IsResourceBuilder != true).ToList();
// Build wrapper class name mapping for type resolution BEFORE generating options interfaces
// This allows parameter types to use wrapper class names instead of handle types
_wrapperClassNames.Clear();
_typesWithPromiseWrappers.Clear();
_generatedOptionsInterfaces.Clear();
_optionsInterfacesToGenerate.Clear();
foreach (var builder in resourceBuilders)
{
_wrapperClassNames[builder.TypeId] = builder.BuilderClassName;
// All resource builders get Promise wrappers
_typesWithPromiseWrappers.Add(builder.TypeId);
}
foreach (var typeClass in typeClasses)
{
_wrapperClassNames[typeClass.TypeId] = DeriveClassName(typeClass.TypeId);
// Type classes with methods get Promise wrappers
if (HasChainableMethods(typeClass))
{
_typesWithPromiseWrappers.Add(typeClass.TypeId);
}
}
// Add ReferenceExpression (defined in base.ts, not generated)
_wrapperClassNames[AtsConstants.ReferenceExpressionTypeId] = "ReferenceExpression";
// Pre-scan all capabilities to collect options interfaces
// This must happen AFTER wrapper class names are populated so types resolve correctly
foreach (var builder in builders)
{
foreach (var cap in builder.Capabilities)
{
var (_, optionalParams) = SeparateParameters(cap.Parameters);
if (optionalParams.Count > 0)
{
RegisterOptionsInterface(cap.MethodName, optionalParams);
}
}
}
// Generate collected options interfaces
GenerateOptionsInterfaces();
// Generate type classes (context types and wrapper types)
foreach (var typeClass in typeClasses)
{
GenerateTypeClass(typeClass);
}
// Generate resource builder classes
foreach (var builder in resourceBuilders)
{
GenerateBuilderClass(builder);
}
// Generate AspireClient with remaining entry point methods
GenerateAspireClient(clientMethods);
// Generate connection helper
GenerateConnectionHelper();
// Generate global error handling
GenerateGlobalErrorHandling();
// Generate handle wrapper registrations (after all classes are defined)
GenerateHandleWrapperRegistrations(typeClasses, resourceBuilders);
return stringWriter.ToString();
}
private void WriteLine(string? text = null)
{
if (text != null)
{
_writer.WriteLine(text);
}
else
{
_writer.WriteLine();
}
}
private void Write(string text)
{
_writer.Write(text);
}
private void GenerateHandleTypeAliases(HashSet<string> typeIds)
{
WriteLine("// ============================================================================");
WriteLine("// Handle Type Aliases (Internal - not exported to users)");
WriteLine("// ============================================================================");
WriteLine();
foreach (var typeId in typeIds.OrderBy(t => t))
{
var handleName = GetHandleTypeName(typeId);
var description = GetTypeDescription(typeId);
WriteLine($"/** {description} */");
// Internal type alias - not exported (users work with wrapper classes)
WriteLine($"type {handleName} = Handle<'{typeId}'>;");
WriteLine();
}
}
/// <summary>
/// Generates TypeScript enums from discovered enum types.
/// </summary>
private void GenerateEnumTypes(IReadOnlyList<AtsEnumTypeInfo> enumTypes)
{
if (enumTypes.Count == 0)
{
return;
}
WriteLine("// ============================================================================");
WriteLine("// Enum Types");
WriteLine("// ============================================================================");
WriteLine();
foreach (var enumType in enumTypes.OrderBy(e => e.Name))
{
// Track enum name for type mapping
_enumTypeNames[enumType.TypeId] = enumType.Name;
WriteLine($"/** Enum type for {enumType.Name} */");
WriteLine($"export enum {enumType.Name} {{");
foreach (var value in enumType.Values)
{
// Enums serialize as strings in JSON
WriteLine($" {value} = \"{value}\",");
}
WriteLine("}");
WriteLine();
}
}
/// <summary>
/// Generates TypeScript interfaces for DTO types marked with [AspireDto].
/// </summary>
private void GenerateDtoInterfaces(IReadOnlyList<AtsDtoTypeInfo> dtoTypes)
{
if (dtoTypes.Count == 0)
{
return;
}
WriteLine("// ============================================================================");
WriteLine("// DTO Interfaces");
WriteLine("// ============================================================================");
WriteLine();
foreach (var dto in dtoTypes.OrderBy(d => d.Name))
{
var interfaceName = GetDtoInterfaceName(dto.TypeId);
WriteLine($"/** DTO interface for {dto.Name} */");
WriteLine($"export interface {interfaceName} {{");
foreach (var prop in dto.Properties)
{
var tsType = MapTypeRefToTypeScript(prop.Type);
// All DTO properties are optional in TypeScript to allow partial objects
// Convert PascalCase to camelCase for TypeScript
var propName = ToCamelCase(prop.Name);
WriteLine($" {propName}?: {tsType};");
}
WriteLine("}");
WriteLine();
}
}
/// <summary>
/// Converts a PascalCase name to camelCase.
/// </summary>
private static string ToCamelCase(string name)
{
if (string.IsNullOrEmpty(name))
{
return name;
}
if (char.IsLower(name[0]))
{
return name;
}
return char.ToLowerInvariant(name[0]) + name[1..];
}
/// <summary>
/// Converts a camelCase name to PascalCase.
/// </summary>
private static string ToPascalCase(string name)
{
if (string.IsNullOrEmpty(name))
{
return name;
}
if (char.IsUpper(name[0]))
{
return name;
}
return char.ToUpperInvariant(name[0]) + name[1..];
}
/// <summary>
/// Gets the options interface name for a method.
/// Strips any type prefix (e.g., "TypeName.methodName" -> "MethodName").
/// </summary>
private static string GetOptionsInterfaceName(string methodName)
{
// Strip type prefix if present (e.g., "EndpointReference.getExpression" -> "getExpression")
var simpleName = methodName.Contains('.')
? methodName[(methodName.LastIndexOf('.') + 1)..]
: methodName;
return $"{ToPascalCase(simpleName)}Options";
}
/// <summary>
/// Separates parameters into required and optional lists.
/// Required = not optional and not nullable.
/// </summary>
private static (List<AtsParameterInfo> Required, List<AtsParameterInfo> Optional) SeparateParameters(
IEnumerable<AtsParameterInfo> parameters)
{
var required = new List<AtsParameterInfo>();
var optional = new List<AtsParameterInfo>();
foreach (var param in parameters)
{
if (param.IsOptional || param.IsNullable)
{
optional.Add(param);
}
else
{
required.Add(param);
}
}
return (required, optional);
}
/// <summary>
/// Registers an options interface to be generated later.
/// Uses method name to create the interface name.
/// </summary>
private void RegisterOptionsInterface(string methodName, List<AtsParameterInfo> optionalParams)
{
if (optionalParams.Count == 0)
{
return;
}
var interfaceName = GetOptionsInterfaceName(methodName);
if (_generatedOptionsInterfaces.Add(interfaceName))
{
_optionsInterfacesToGenerate[interfaceName] = optionalParams;
}
}
/// <summary>
/// Generates all collected options interfaces.
/// </summary>
private void GenerateOptionsInterfaces()
{
if (_optionsInterfacesToGenerate.Count == 0)
{
return;
}
WriteLine("// ============================================================================");
WriteLine("// Options Interfaces");
WriteLine("// ============================================================================");
WriteLine();
foreach (var (interfaceName, optionalParams) in _optionsInterfacesToGenerate.OrderBy(kvp => kvp.Key))
{
WriteLine($"export interface {interfaceName} {{");
foreach (var param in optionalParams)
{
var tsType = MapParameterToTypeScript(param);
WriteLine($" {param.Name}?: {tsType};");
}
WriteLine("}");
WriteLine();
}
}
private static string GetTypeDescription(string typeId)
{
var typeName = ExtractSimpleTypeName(typeId);
return $"Handle to {typeName}";
}
private void GenerateBuilderClass(BuilderModel builder)
{
WriteLine("// ============================================================================");
WriteLine($"// {builder.BuilderClassName}");
WriteLine("// ============================================================================");
WriteLine();
var handleType = GetHandleTypeName(builder.TypeId);
// Generate builder class extending ResourceBuilderBase
WriteLine($"export class {builder.BuilderClassName} extends ResourceBuilderBase<{handleType}> {{");
// Constructor
WriteLine($" constructor(handle: {handleType}, client: AspireClientRpc) {{");
WriteLine($" super(handle, client);");
WriteLine(" }");
WriteLine();
// Generate internal methods and public fluent methods
// Capabilities are already flattened - no need to collect from parents
// Filter out property getters and setters - they are not methods
foreach (var capability in builder.Capabilities.Where(c =>
c.CapabilityKind != AtsCapabilityKind.PropertyGetter &&
c.CapabilityKind != AtsCapabilityKind.PropertySetter))
{
GenerateBuilderMethod(builder, capability);
}
WriteLine("}");
WriteLine();
// Generate thenable wrapper class
GenerateThenableClass(builder);
}
private void GenerateBuilderMethod(BuilderModel builder, AtsCapabilityInfo capability)
{
var methodName = capability.MethodName;
var internalMethodName = $"_{methodName}Internal";
// Separate required and optional parameters
var (requiredParams, optionalParams) = SeparateParameters(capability.Parameters);
var hasOptionals = optionalParams.Count > 0;
var optionsInterfaceName = GetOptionsInterfaceName(methodName);
// Build parameter list for public method
var publicParamDefs = new List<string>();
foreach (var param in requiredParams)
{
var tsType = MapParameterToTypeScript(param);
publicParamDefs.Add($"{param.Name}: {tsType}");
}
if (hasOptionals)
{
publicParamDefs.Add($"options?: {optionsInterfaceName}");
}
var publicParamsString = string.Join(", ", publicParamDefs);
// Build parameter list for internal method (all params positional for callback registration)
var internalParamDefs = new List<string>();
foreach (var param in capability.Parameters)
{
var tsType = MapParameterToTypeScript(param);
var optional = param.IsOptional || param.IsNullable ? "?" : "";
internalParamDefs.Add($"{param.Name}{optional}: {tsType}");
}
var internalParamsString = string.Join(", ", internalParamDefs);
// Use the actual target parameter name from the capability (e.g., "resource" for withReference)
var targetParamName = capability.TargetParameterName ?? "builder";
// Determine return type - use the builder's own type for fluent methods
var returnHandle = capability.ReturnsBuilder
? GetHandleTypeName(builder.TypeId)
: "void";
var returnsBuilder = capability.ReturnsBuilder;
// Check if this method returns a non-builder, non-void type (e.g., getEndpoint returns EndpointReference)
var hasNonBuilderReturn = !returnsBuilder && capability.ReturnType != null;
if (hasNonBuilderReturn)
{
// Generate a simple async method that returns the actual type
var returnType = MapTypeRefToTypeScript(capability.ReturnType);
if (!string.IsNullOrEmpty(capability.Description))
{
WriteLine($" /** {capability.Description} */");
}
Write($" async {methodName}(");
Write(publicParamsString);
WriteLine($"): Promise<{returnType}> {{");
// Extract optional params from options object
foreach (var param in optionalParams)
{
WriteLine($" const {param.Name} = options?.{param.Name};");
}
// Handle callback registration if any
var callbackParams2 = capability.Parameters.Where(p => p.IsCallback).ToList();
foreach (var callbackParam in callbackParams2)
{
GenerateCallbackRegistration(callbackParam);
}
// Handle cancellation token registration if any
var cancellationParams2 = capability.Parameters.Where(IsCancellationToken).ToList();
foreach (var ctParam in cancellationParams2)
{
GenerateCancellationRegistration(ctParam);
}
// Build args object with conditional inclusion
GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams, cancellationParams2);
WriteLine($" return await this._client.invokeCapability<{returnType}>(");
WriteLine($" '{capability.CapabilityId}',");
WriteLine($" rpcArgs");
WriteLine(" );");
WriteLine(" }");
WriteLine();
return;
}
// Generate internal async method for fluent builder methods
WriteLine($" /** @internal */");
Write($" async {internalMethodName}(");
Write(internalParamsString);
Write($"): Promise<{builder.BuilderClassName}> {{");
WriteLine();
// Handle callback registration if any
var callbackParams = capability.Parameters.Where(p => p.IsCallback).ToList();
foreach (var callbackParam in callbackParams)
{
GenerateCallbackRegistration(callbackParam);
}
// Handle cancellation token registration if any
var cancellationParams = capability.Parameters.Where(IsCancellationToken).ToList();
foreach (var ctParam in cancellationParams)
{
GenerateCancellationRegistration(ctParam);
}
// Build args object with conditional inclusion
GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams, cancellationParams);
if (returnsBuilder)
{
WriteLine($" const result = await this._client.invokeCapability<{returnHandle}>(");
WriteLine($" '{capability.CapabilityId}',");
WriteLine($" rpcArgs");
WriteLine(" );");
WriteLine($" return new {builder.BuilderClassName}(result, this._client);");
}
else
{
WriteLine($" await this._client.invokeCapability<void>(");
WriteLine($" '{capability.CapabilityId}',");
WriteLine($" rpcArgs");
WriteLine(" );");
WriteLine($" return this;");
}
WriteLine(" }");
WriteLine();
// Generate public fluent method (returns thenable wrapper)
if (!string.IsNullOrEmpty(capability.Description))
{
WriteLine($" /** {capability.Description} */");
}
var promiseClass = $"{builder.BuilderClassName}Promise";
Write($" {methodName}(");
Write(publicParamsString);
Write($"): {promiseClass} {{");
WriteLine();
// Extract optional params from options object and forward to internal method
foreach (var param in optionalParams)
{
WriteLine($" const {param.Name} = options?.{param.Name};");
}
// Forward all params to internal method
var allParamNames = capability.Parameters.Select(p => p.Name);
Write($" return new {promiseClass}(this.{internalMethodName}(");
Write(string.Join(", ", allParamNames));
WriteLine("));");
WriteLine(" }");
WriteLine();
}
/// <summary>
/// Generates an args object with conditional inclusion of optional parameters.
/// </summary>
private void GenerateArgsObjectWithConditionals(
string targetParamName,
List<AtsParameterInfo> requiredParams,
List<AtsParameterInfo> optionalParams,
List<AtsParameterInfo>? cancellationParams = null)
{
var cancellationParamNames = new HashSet<string>(cancellationParams?.Select(p => p.Name) ?? []);
// Build the required args inline
var requiredArgs = new List<string> { $"{targetParamName}: this._handle" };
foreach (var param in requiredParams)
{
if (param.IsCallback)
{
requiredArgs.Add($"callback: {param.Name}Id");
}
else if (cancellationParamNames.Contains(param.Name))
{
// Use the registered cancellation ID
requiredArgs.Add($"{param.Name}: {param.Name}Id");
}
else
{
requiredArgs.Add(param.Name);
}
}
WriteLine($" const rpcArgs: Record<string, unknown> = {{ {string.Join(", ", requiredArgs)} }};");
// Conditionally add optional params
foreach (var param in optionalParams)
{
var isCancellation = cancellationParamNames.Contains(param.Name);
var argName = param.IsCallback || isCancellation ? $"{param.Name}Id" : param.Name;
var paramName = param.Name;
var rpcParamName = param.IsCallback ? "callback" : paramName;
WriteLine($" if ({paramName} !== undefined) rpcArgs.{rpcParamName} = {argName};");
}
}
private void GenerateThenableClass(BuilderModel builder)
{
var promiseClass = $"{builder.BuilderClassName}Promise";
WriteLine($"/**");
WriteLine($" * Thenable wrapper for {builder.BuilderClassName} that enables fluent chaining.");
WriteLine($" * @example");
WriteLine($" * await builder.addSomething().withX().withY();");
WriteLine($" */");
WriteLine($"export class {promiseClass} implements PromiseLike<{builder.BuilderClassName}> {{");
WriteLine($" constructor(private _promise: Promise<{builder.BuilderClassName}>) {{}}");
WriteLine();
// Generate then() for PromiseLike interface
WriteLine($" then<TResult1 = {builder.BuilderClassName}, TResult2 = never>(");
WriteLine($" onfulfilled?: ((value: {builder.BuilderClassName}) => TResult1 | PromiseLike<TResult1>) | null,");
WriteLine(" onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null");
WriteLine(" ): PromiseLike<TResult1 | TResult2> {");
WriteLine(" return this._promise.then(onfulfilled, onrejected);");
WriteLine(" }");
WriteLine();
// Generate fluent methods that chain via .then()
// Capabilities are already flattened - no need to collect from parents
// Filter out property getters and setters - they are not methods
foreach (var capability in builder.Capabilities.Where(c =>
c.CapabilityKind != AtsCapabilityKind.PropertyGetter &&
c.CapabilityKind != AtsCapabilityKind.PropertySetter))
{
var methodName = capability.MethodName;
// Separate required and optional parameters
var (requiredParams, optionalParams) = SeparateParameters(capability.Parameters);
var hasOptionals = optionalParams.Count > 0;
var optionsInterfaceName = GetOptionsInterfaceName(methodName);
// Build parameter list using options pattern
var publicParamDefs = new List<string>();
foreach (var param in requiredParams)
{
var tsType = MapParameterToTypeScript(param);
publicParamDefs.Add($"{param.Name}: {tsType}");
}
if (hasOptionals)
{
publicParamDefs.Add($"options?: {optionsInterfaceName}");
}
var paramsString = string.Join(", ", publicParamDefs);
// Forward args to underlying object's method (which handles options extraction)
var forwardArgs = new List<string>();
foreach (var param in requiredParams)
{
forwardArgs.Add(param.Name);
}
if (hasOptionals)
{
forwardArgs.Add("options");
}
var argsString = string.Join(", ", forwardArgs);
// Check if this method returns a non-builder type
var hasNonBuilderReturn = !capability.ReturnsBuilder && capability.ReturnType != null;
if (!string.IsNullOrEmpty(capability.Description))
{
WriteLine($" /** {capability.Description} */");
}
if (hasNonBuilderReturn)
{
// For non-builder returns, call the public method directly
var returnType = MapTypeRefToTypeScript(capability.ReturnType);
Write($" {methodName}(");
Write(paramsString);
WriteLine($"): Promise<{returnType}> {{");
Write($" return this._promise.then(obj => obj.{methodName}(");
Write(argsString);
WriteLine("));");
WriteLine(" }");
}
else
{
// For fluent builder methods, call the public method which wraps the internal
Write($" {methodName}(");
Write(paramsString);
Write($"): {promiseClass} {{");
WriteLine();
// Forward to the public method on the underlying object, wrapping result in promise class
Write($" return new {promiseClass}(this._promise.then(obj => obj.{methodName}(");
Write(argsString);
WriteLine(")));");
WriteLine(" }");
}
WriteLine();
}
WriteLine("}");
WriteLine();
}
private void GenerateAspireClient(List<AtsCapabilityInfo> entryPoints)
{
// Entry point methods (capabilities with no TargetTypeId) are generated as standalone functions
// They're generated in GenerateConnectionHelper after the createBuilder() function
// This method now only handles the comment header
if (entryPoints.Count > 0)
{
WriteLine("// ============================================================================");
WriteLine("// Entry Point Functions");
WriteLine("// ============================================================================");
WriteLine();
foreach (var capability in entryPoints)
{
GenerateEntryPointFunction(capability);
}
}
}
private void GenerateEntryPointFunction(AtsCapabilityInfo capability)
{
var methodName = capability.MethodName;
// Build parameter list
var paramDefs = new List<string> { "client: AspireClientRpc" };
var paramArgs = new List<string>();
foreach (var param in capability.Parameters)
{
var tsType = MapParameterToTypeScript(param);
var optional = param.IsOptional || param.IsNullable ? "?" : "";
paramDefs.Add($"{param.Name}{optional}: {tsType}");
paramArgs.Add(param.Name);
}
var paramsString = string.Join(", ", paramDefs);
var argsObject = paramArgs.Count > 0
? $"{{ {string.Join(", ", paramArgs)} }}"
: "{}";
// Determine return type - check if return type has a Promise wrapper
var capReturnTypeId = GetReturnTypeId(capability);
var returnPromiseWrapper = GetPromiseWrapperForReturnType(capability.ReturnType);
// Generate JSDoc
if (!string.IsNullOrEmpty(capability.Description))
{
WriteLine($"/**");
WriteLine($" * {capability.Description}");
WriteLine($" */");
}
// Generate function based on return type
if (returnPromiseWrapper != null && !string.IsNullOrEmpty(capReturnTypeId))
{
// Return type has Promise wrapper - generate fluent function
var returnWrapperClass = _wrapperClassNames.GetValueOrDefault(capReturnTypeId)
?? DeriveClassName(capReturnTypeId);
var handleType = GetHandleTypeName(capReturnTypeId);
Write($"export function {methodName}(");
Write(paramsString);
WriteLine($"): {returnPromiseWrapper} {{");
WriteLine($" const promise = client.invokeCapability<{handleType}>(");
WriteLine($" '{capability.CapabilityId}',");
WriteLine($" {argsObject}");
WriteLine($" ).then(handle => new {returnWrapperClass}(handle, client));");
WriteLine($" return new {returnPromiseWrapper}(promise);");
WriteLine("}");
}
else
{
// No Promise wrapper - return plain value
var returnType = !string.IsNullOrEmpty(capReturnTypeId)
? MapTypeRefToTypeScript(capability.ReturnType)
: "void";
Write($"export async function {methodName}(");
Write(paramsString);
WriteLine($"): Promise<{returnType}> {{");
if (returnType == "void")
{
WriteLine($" await client.invokeCapability<void>(");
}
else
{
WriteLine($" return await client.invokeCapability<{returnType}>(");
}
WriteLine($" '{capability.CapabilityId}',");
WriteLine($" {argsObject}");
WriteLine(" );");
WriteLine("}");
}
WriteLine();
}
private string GenerateCallbackTypeSignature(IReadOnlyList<AtsCallbackParameterInfo>? callbackParameters, AtsTypeRef? callbackReturnType)
{
// Build parameter list
var paramList = new List<string>();
if (callbackParameters is not null)
{
foreach (var param in callbackParameters)
{
var tsType = MapTypeRefToTypeScript(param.Type);
paramList.Add($"{param.Name}: {tsType}");
}
}
var paramsString = paramList.Count > 0 ? string.Join(", ", paramList) : "";
// Determine return type
var returnType = callbackReturnType == null || callbackReturnType.TypeId == AtsConstants.Void
? "void"
: MapTypeRefToTypeScript(callbackReturnType);
// Callbacks are always async in TypeScript
return $"({paramsString}) => Promise<{returnType}>";
}
private void GenerateCallbackRegistration(AtsParameterInfo callbackParam)
{
var callbackParameters = callbackParam.CallbackParameters;
var isOptional = callbackParam.IsOptional || callbackParam.IsNullable;
var callbackName = callbackParam.Name;
// Determine parameter signature for registerCallback
string paramSignature;
if (callbackParameters is null || callbackParameters.Count == 0)
{
paramSignature = "";
}
else if (callbackParameters.Count == 1)
{
paramSignature = $"{callbackParameters[0].Name}Data: unknown";
}
else
{
paramSignature = "argsData: unknown";
}
// For optional callbacks, wrap the registration in a conditional
if (isOptional)
{
WriteLine($" const {callbackName}Id = {callbackName} ? registerCallback(async ({paramSignature}) => {{");
}
else
{
WriteLine($" const {callbackName}Id = registerCallback(async ({paramSignature}) => {{");
}
// Generate the callback body
GenerateCallbackBody(callbackParam, callbackParameters);
// Close the callback registration
if (isOptional)
{
WriteLine(" }) : undefined;");
}
else
{
WriteLine(" });");
}
}
/// <summary>
/// Checks if a parameter is a CancellationToken type.
/// </summary>
private static bool IsCancellationToken(AtsParameterInfo param)
{
return param.Type?.TypeId == AtsConstants.CancellationToken;
}
/// <summary>
/// Generates cancellation registration for a CancellationToken parameter.
/// </summary>
private void GenerateCancellationRegistration(AtsParameterInfo param)
{
var isOptional = param.IsOptional || param.IsNullable;
var paramName = param.Name;
// For optional cancellation tokens, wrap the registration in a conditional
if (isOptional)
{
WriteLine($" const {paramName}Id = {paramName} ? registerCancellation({paramName}) : undefined;");
}
else
{
WriteLine($" const {paramName}Id = registerCancellation({paramName});");
}
}
/// <summary>
/// Generates the body of a callback function.
/// </summary>
private void GenerateCallbackBody(AtsParameterInfo callbackParam, IReadOnlyList<AtsCallbackParameterInfo>? callbackParameters)
{
var callbackName = callbackParam.Name;
// Check if callback has a return type - if so, we need to return the value
var hasReturnType = callbackParam.CallbackReturnType != null
&& callbackParam.CallbackReturnType.TypeId != AtsConstants.Void;
var returnPrefix = hasReturnType ? "return " : "";
if (callbackParameters is null || callbackParameters.Count == 0)
{
// No parameters - just call the callback
WriteLine($" {returnPrefix}await {callbackName}();");
}
else if (callbackParameters.Count == 1)
{
// Single parameter callback
var cbParam = callbackParameters[0];
var tsType = MapTypeRefToTypeScript(cbParam.Type);
var cbTypeId = cbParam.Type.TypeId;
if (_wrapperClassNames.TryGetValue(cbTypeId, out var wrapperClassName))
{
// For types with wrapper classes, create an instance of the wrapper
var handleType = GetHandleTypeName(cbTypeId);
WriteLine($" const {cbParam.Name}Handle = wrapIfHandle({cbParam.Name}Data) as {handleType};");
WriteLine($" const {cbParam.Name} = new {wrapperClassName}({cbParam.Name}Handle, this._client);");
}
else
{
// For raw handle types, just wrap and cast
WriteLine($" const {cbParam.Name} = wrapIfHandle({cbParam.Name}Data) as {tsType};");
}
WriteLine($" {returnPrefix}await {callbackName}({cbParam.Name});");
}
else
{
// Multi-parameter callback - .NET sends as { p0, p1, ... }
var paramNames = callbackParameters.Select((p, i) => $"p{i}").ToList();
var destructure = string.Join(", ", paramNames);
WriteLine($" const args = argsData as {{ {destructure}: unknown }};");
var callArgs = new List<string>();
for (var i = 0; i < callbackParameters.Count; i++)
{
var cbParam = callbackParameters[i];
var tsType = MapTypeRefToTypeScript(cbParam.Type);
var cbTypeId = cbParam.Type.TypeId;
if (_wrapperClassNames.TryGetValue(cbTypeId, out var wrapperClassName))
{
// For types with wrapper classes, create an instance of the wrapper
var handleType = GetHandleTypeName(cbTypeId);
WriteLine($" const {cbParam.Name}Handle = wrapIfHandle(args.p{i}) as {handleType};");
WriteLine($" const {cbParam.Name} = new {wrapperClassName}({cbParam.Name}Handle, this._client);");
}
else
{
// For raw handle types, just wrap and cast
WriteLine($" const {cbParam.Name} = wrapIfHandle(args.p{i}) as {tsType};");
}
callArgs.Add(cbParam.Name);
}
WriteLine($" {returnPrefix}await {callbackName}({string.Join(", ", callArgs)});");
}
}
private void GenerateConnectionHelper()
{
var builderHandle = GetHandleTypeName(AtsConstants.BuilderTypeId);
WriteLine($$"""
// ============================================================================
// Connection Helper
// ============================================================================
/**
* Creates and connects to the Aspire AppHost.
* Reads connection info from environment variables set by `aspire run`.
*/
export async function connect(): Promise<AspireClientRpc> {
const socketPath = process.env.REMOTE_APP_HOST_SOCKET_PATH;
if (!socketPath) {
throw new Error(
'REMOTE_APP_HOST_SOCKET_PATH environment variable not set. ' +
'Run this application using `aspire run`.'
);
}
const client = new AspireClientRpc(socketPath);
await client.connect();
// Exit the process if the server connection is lost
client.onDisconnect(() => {
console.error('Connection to AppHost lost. Exiting...');
process.exit(1);
});
return client;
}
/**
* Creates a new distributed application builder.
* This is the entry point for building Aspire applications.
*
* @param options - Optional configuration options for the builder
* @returns A DistributedApplicationBuilder instance
*
* @example
* const builder = await createBuilder();
* builder.addRedis("cache");
* builder.addContainer("api", "mcr.microsoft.com/dotnet/samples:aspnetapp");
* const app = await builder.build();
* await app.run();
*/
export async function createBuilder(options?: CreateBuilderOptions): Promise<DistributedApplicationBuilder> {
const client = await connect();
// Default args and projectDirectory if not provided
const effectiveOptions: CreateBuilderOptions = {
...options,
args: options?.args ?? process.argv.slice(2),
projectDirectory: options?.projectDirectory ?? process.env.ASPIRE_PROJECT_DIRECTORY ?? process.cwd()
};
const handle = await client.invokeCapability<{{builderHandle}}>(
'Aspire.Hosting/createBuilderWithOptions',
{ options: effectiveOptions }
);
return new DistributedApplicationBuilder(handle, client);
}
// Re-export commonly used types
export { Handle, CapabilityError, registerCallback } from './transport.js';
export { refExpr, ReferenceExpression } from './base.js';
""");
WriteLine();
}
private void GenerateGlobalErrorHandling()
{
WriteLine("""
// ============================================================================
// Global Error Handling
// ============================================================================
/**
* Set up global error handlers to ensure the process exits properly on errors.
* Node.js doesn't exit on unhandled rejections by default, so we need to handle them.
*/
process.on('unhandledRejection', (reason: unknown) => {
const error = reason instanceof Error ? reason : new Error(String(reason));
if (reason instanceof CapabilityError) {
console.error(`\n❌ Capability Error: ${error.message}`);
console.error(` Code: ${(reason as CapabilityError).code}`);
if ((reason as CapabilityError).capability) {
console.error(` Capability: ${(reason as CapabilityError).capability}`);
}
} else {
console.error(`\n❌ Unhandled Error: ${error.message}`);
if (error.stack) {
console.error(error.stack);
}
}
process.exit(1);
});
process.on('uncaughtException', (error: Error) => {
console.error(`\n❌ Uncaught Exception: ${error.message}`);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
});
""");
}
/// <summary>
/// Generates handle wrapper registrations for all type classes and builder classes.
/// This allows callback handles to be wrapped as typed instances.
/// </summary>
private void GenerateHandleWrapperRegistrations(List<BuilderModel> typeClasses, List<BuilderModel> resourceBuilders)
{
WriteLine();
WriteLine("// ============================================================================");
WriteLine("// Handle Wrapper Registrations");
WriteLine("// ============================================================================");
WriteLine();
WriteLine("// Register wrapper factories for typed handle wrapping in callbacks");
// Register type classes (context types like EnvironmentCallbackContext)
foreach (var typeClass in typeClasses)
{
var className = _wrapperClassNames.GetValueOrDefault(typeClass.TypeId) ?? DeriveClassName(typeClass.TypeId);
var handleType = GetHandleTypeName(typeClass.TypeId);
WriteLine($"registerHandleWrapper('{typeClass.TypeId}', (handle, client) => new {className}(handle as {handleType}, client));");
}
// Register resource builder classes
foreach (var builder in resourceBuilders)
{
var className = _wrapperClassNames.GetValueOrDefault(builder.TypeId) ?? DeriveClassName(builder.TypeId);
var handleType = GetHandleTypeName(builder.TypeId);
WriteLine($"registerHandleWrapper('{builder.TypeId}', (handle, client) => new {className}(handle as {handleType}, client));");
}
WriteLine();
}
/// <summary>
/// Generates a type class (context type or wrapper type).
/// Uses property-like object pattern for exposed properties.
/// For types with methods, also generates a Promise wrapper class for fluent chaining.
/// </summary>
private void GenerateTypeClass(BuilderModel model)
{
var handleType = GetHandleTypeName(model.TypeId);
var className = DeriveClassName(model.TypeId);
var hasMethods = HasChainableMethods(model);
WriteLine("// ============================================================================");
WriteLine($"// {className}");
WriteLine("// ============================================================================");
WriteLine();
// Separate capabilities by type using CapabilityKind enum
var getters = model.Capabilities.Where(c => c.CapabilityKind == AtsCapabilityKind.PropertyGetter).ToList();
var setters = model.Capabilities.Where(c => c.CapabilityKind == AtsCapabilityKind.PropertySetter).ToList();
var contextMethods = model.Capabilities.Where(c => c.CapabilityKind == AtsCapabilityKind.InstanceMethod).ToList();
var otherMethods = model.Capabilities.Where(c => c.CapabilityKind == AtsCapabilityKind.Method).ToList();
// Combine methods for thenable generation
var allMethods = contextMethods.Concat(otherMethods).ToList();
WriteLine($"/**");
WriteLine($" * Type class for {className}.");
WriteLine($" */");
WriteLine($"export class {className} {{");
WriteLine($" constructor(private _handle: {handleType}, private _client: AspireClientRpc) {{}}");
WriteLine();
WriteLine($" /** Serialize for JSON-RPC transport */");
WriteLine($" toJSON(): MarshalledHandle {{ return this._handle.toJSON(); }}");
WriteLine();
// Group getters and setters by property name to create property-like objects
var properties = GroupPropertiesByName(getters, setters);
// Generate property-like objects
foreach (var prop in properties)
{
GeneratePropertyLikeObject(prop.PropertyName, prop.Getter, prop.Setter);
}
// Generate methods - use thenable pattern if this type has a Promise wrapper
if (hasMethods)
{
foreach (var method in allMethods)
{
GenerateTypeClassMethod(model, method);
}
}
else
{
// No Promise wrapper - generate plain async methods
foreach (var method in contextMethods)
{
GenerateContextMethod(method);
}
foreach (var method in otherMethods)
{
GenerateWrapperMethod(method);
}
}
WriteLine("}");
WriteLine();
// Generate thenable wrapper class if this type has methods
if (hasMethods)
{
GenerateTypeClassThenableWrapper(model, allMethods);
}
}
/// <summary>
/// Groups getters and setters by property name.
/// </summary>
private static List<(string PropertyName, AtsCapabilityInfo? Getter, AtsCapabilityInfo? Setter)> GroupPropertiesByName(
List<AtsCapabilityInfo> getters, List<AtsCapabilityInfo> setters)
{
var result = new List<(string PropertyName, AtsCapabilityInfo? Getter, AtsCapabilityInfo? Setter)>();
var processedNames = new HashSet<string>();
// Process getters
foreach (var getter in getters)
{
var propName = ExtractPropertyName(getter.MethodName);
if (processedNames.Contains(propName))
{
continue;
}
processedNames.Add(propName);
// Find matching setter (setPropertyName for propertyName)
var setterName = "set" + char.ToUpperInvariant(propName[0]) + propName[1..];
var setter = setters.FirstOrDefault(s => ExtractPropertyName(s.MethodName).Equals(setterName, StringComparison.OrdinalIgnoreCase));
result.Add((propName, getter, setter));
}
// Process any setters without matching getters
foreach (var setter in setters)
{
var setterMethodName = ExtractPropertyName(setter.MethodName);
// setPropertyName -> propertyName
if (setterMethodName.StartsWith("set", StringComparison.OrdinalIgnoreCase) && setterMethodName.Length > 3)
{
var propName = char.ToLowerInvariant(setterMethodName[3]) + setterMethodName[4..];
if (!processedNames.Contains(propName))
{
processedNames.Add(propName);
result.Add((propName, null, setter));
}
}
}
return result;
}
/// <summary>
/// Extracts the property name from a method name like "ClassName.propertyName" or "setPropertyName".
/// </summary>
private static string ExtractPropertyName(string methodName)
{
// Handle "ClassName.propertyName" format
if (methodName.Contains('.'))
{
return methodName[(methodName.LastIndexOf('.') + 1)..];
}
return methodName;
}
/// <summary>
/// Generates a property-like object with get and/or set methods.
/// For dictionary types, generates a direct AspireDict field instead.
/// </summary>
private void GeneratePropertyLikeObject(string propertyName, AtsCapabilityInfo? getter, AtsCapabilityInfo? setter)
{
// Determine the return type from getter
string returnType = "unknown";
string? description = null;
if (getter != null)
{
returnType = MapTypeRefToTypeScript(getter.ReturnType);
description = getter.Description;
// Check if this is a dictionary type - generate direct AspireDict field instead
if (IsDictionaryType(getter.ReturnType))
{
GenerateDictionaryProperty(propertyName, getter);
return;
}
// Check if return type is a wrapper class - use property-like object returning wrapper
if (getter.ReturnType?.TypeId != null && _wrapperClassNames.TryGetValue(getter.ReturnType.TypeId, out var wrapperClassName))
{
GenerateWrapperPropertyObject(propertyName, getter, wrapperClassName);
return;
}
}
// Generate property-like object for scalar types
if (!string.IsNullOrEmpty(description))
{
WriteLine($" /** {description} */");
}
WriteLine($" {propertyName} = {{");
// Generate get method
if (getter != null)
{
WriteLine($" get: async (): Promise<{returnType}> => {{");
WriteLine($" return await this._client.invokeCapability<{returnType}>(");
WriteLine($" '{getter.CapabilityId}',");
WriteLine($" {{ context: this._handle }}");
WriteLine(" );");
WriteLine(" },");
}
// Generate set method
if (setter != null)
{
var valueParam = setter.Parameters.FirstOrDefault(p => p.Name == "value");
if (valueParam != null)
{
var valueType = MapTypeRefToTypeScript(valueParam.Type);
WriteLine($" set: async (value: {valueType}): Promise<void> => {{");
WriteLine($" await this._client.invokeCapability<void>(");
WriteLine($" '{setter.CapabilityId}',");
WriteLine($" {{ context: this._handle, value }}");
WriteLine(" );");
WriteLine(" }");
}
}
WriteLine(" };");
WriteLine();
}
/// <summary>
/// Generates a property-like object that returns a wrapper class.
/// </summary>
private void GenerateWrapperPropertyObject(string propertyName, AtsCapabilityInfo getter, string wrapperClassName)
{
var handleType = GetHandleTypeName(getter.ReturnType!.TypeId);
if (!string.IsNullOrEmpty(getter.Description))
{
WriteLine($" /** {getter.Description} */");
}
WriteLine($" {propertyName} = {{");
WriteLine($" get: async (): Promise<{wrapperClassName}> => {{");
WriteLine($" const handle = await this._client.invokeCapability<{handleType}>(");
WriteLine($" '{getter.CapabilityId}',");
WriteLine($" {{ context: this._handle }}");
WriteLine(" );");
WriteLine($" return new {wrapperClassName}(handle, this._client);");
WriteLine(" },");
WriteLine(" };");
WriteLine();
}
/// <summary>
/// Checks if a type reference is a dictionary type.
/// </summary>
private static bool IsDictionaryType(AtsTypeRef? typeRef)
{
return typeRef?.Category == AtsTypeCategory.Dict;
}
/// <summary>
/// Generates a direct AspireDict property for dictionary types.
/// </summary>
private void GenerateDictionaryProperty(string propertyName, AtsCapabilityInfo getter)
{
// Determine key and value types
var keyType = "string";
var valueType = "unknown";
// Try to extract key and value types from Dict type
if (getter.ReturnType?.KeyType != null)
{
keyType = MapTypeRefToTypeScript(getter.ReturnType.KeyType);
}
if (getter.ReturnType?.ValueType != null)
{
// Union types will be mapped correctly via MapTypeRefToTypeScript
valueType = MapTypeRefToTypeScript(getter.ReturnType.ValueType);
}
var typeId = $"'{getter.CapabilityId.Replace(".get", "")}'";
var getterCapabilityId = $"'{getter.CapabilityId}'";
if (!string.IsNullOrEmpty(getter.Description))
{
WriteLine($" /** {getter.Description} */");
}
// Generate a getter property that returns AspireDict
// Pass the getter capability ID so AspireDict can lazily fetch the actual dictionary handle
WriteLine($" private _{propertyName}?: AspireDict<{keyType}, {valueType}>;");
WriteLine($" get {propertyName}(): AspireDict<{keyType}, {valueType}> {{");
WriteLine($" if (!this._{propertyName}) {{");
WriteLine($" this._{propertyName} = new AspireDict<{keyType}, {valueType}>(");
WriteLine($" this._handle,");
WriteLine($" this._client,");
WriteLine($" {typeId},");
WriteLine($" {getterCapabilityId}");
WriteLine(" );");
WriteLine(" }");
WriteLine($" return this._{propertyName};");
WriteLine(" }");
WriteLine();
}
/// <summary>
/// Generates a context instance method (from ExposeMethods=true).
/// </summary>
private void GenerateContextMethod(AtsCapabilityInfo method)
{
// Use OwningTypeName if available to extract method name, otherwise parse from MethodName
var methodName = !string.IsNullOrEmpty(method.OwningTypeName) && method.MethodName.Contains('.')
? method.MethodName[(method.MethodName.LastIndexOf('.') + 1)..]
: method.MethodName;
// Filter out target parameter
var targetParamName = method.TargetParameterName ?? "context";
var userParams = method.Parameters.Where(p => p.Name != targetParamName).ToList();
// Separate required and optional parameters
var (requiredParams, optionalParams) = SeparateParameters(userParams);
var hasOptionals = optionalParams.Count > 0;
var optionsInterfaceName = GetOptionsInterfaceName(methodName);
// Build parameter list using options pattern
var paramDefs = new List<string>();
foreach (var param in requiredParams)
{
var tsType = MapParameterToTypeScript(param);
paramDefs.Add($"{param.Name}: {tsType}");
}
if (hasOptionals)
{
paramDefs.Add($"options?: {optionsInterfaceName}");
}
var paramsString = string.Join(", ", paramDefs);
// Determine return type
var returnType = GetReturnTypeId(method) != null
? MapTypeRefToTypeScript(method.ReturnType)
: "void";
// Generate JSDoc
if (!string.IsNullOrEmpty(method.Description))
{
WriteLine($" /** {method.Description} */");
}
// Generate async method
Write($" async {methodName}(");
Write(paramsString);
WriteLine($"): Promise<{returnType}> {{");
// Extract optional params from options object
foreach (var param in optionalParams)
{
WriteLine($" const {param.Name} = options?.{param.Name};");
}
// Build args object with conditional inclusion
var requiredArgs = new List<string> { $"{targetParamName}: this._handle" };
foreach (var param in requiredParams)
{
requiredArgs.Add(param.Name);
}
WriteLine($" const rpcArgs: Record<string, unknown> = {{ {string.Join(", ", requiredArgs)} }};");
foreach (var param in optionalParams)
{
WriteLine($" if ({param.Name} !== undefined) rpcArgs.{param.Name} = {param.Name};");
}
if (returnType == "void")
{
WriteLine($" await this._client.invokeCapability<void>(");
}
else
{
WriteLine($" return await this._client.invokeCapability<{returnType}>(");
}
WriteLine($" '{method.CapabilityId}',");
WriteLine($" rpcArgs");
WriteLine(" );");
WriteLine(" }");
WriteLine();
}
/// <summary>
/// Generates a method on a wrapper class.
/// </summary>
private void GenerateWrapperMethod(AtsCapabilityInfo capability)
{
var methodName = GetTypeScriptMethodName(capability.MethodName);
// First arg is the handle (implicit via this._handle) - use metadata instead of string parsing
var firstParamName = capability.TargetParameterName ?? "builder";
// Filter out the implicit handle parameter
var userParams = capability.Parameters.Where(p => p.Name != firstParamName).ToList();
// Separate required and optional parameters
var (requiredParams, optionalParams) = SeparateParameters(userParams);
var hasOptionals = optionalParams.Count > 0;
var optionsInterfaceName = GetOptionsInterfaceName(methodName);
// Build parameter list using options pattern
var paramDefs = new List<string>();
foreach (var param in requiredParams)
{
var tsType = MapParameterToTypeScript(param);
paramDefs.Add($"{param.Name}: {tsType}");
}
if (hasOptionals)
{
paramDefs.Add($"options?: {optionsInterfaceName}");
}
var paramsString = string.Join(", ", paramDefs);
// Determine return type
var returnType = MapTypeRefToTypeScript(capability.ReturnType);
// Generate JSDoc
if (!string.IsNullOrEmpty(capability.Description))
{
WriteLine($" /** {capability.Description} */");
}
// Generate async method
Write($" async {methodName}(");
Write(paramsString);
WriteLine($"): Promise<{returnType}> {{");
// Extract optional params from options object
foreach (var param in optionalParams)
{
WriteLine($" const {param.Name} = options?.{param.Name};");
}
// Build args object with conditional inclusion
var requiredArgs = new List<string> { $"{firstParamName}: this._handle" };
foreach (var param in requiredParams)
{
requiredArgs.Add(param.Name);
}
WriteLine($" const rpcArgs: Record<string, unknown> = {{ {string.Join(", ", requiredArgs)} }};");
foreach (var param in optionalParams)
{
WriteLine($" if ({param.Name} !== undefined) rpcArgs.{param.Name} = {param.Name};");
}
if (returnType == "void")
{
WriteLine($" await this._client.invokeCapability<void>(");
}
else
{
WriteLine($" return await this._client.invokeCapability<{returnType}>(");
}
WriteLine($" '{capability.CapabilityId}',");
WriteLine($" rpcArgs");
WriteLine(" );");
WriteLine(" }");
WriteLine();
}
/// <summary>
/// Generates a method on a type class using the thenable pattern.
/// Generates both an internal async method and a public fluent method.
/// </summary>
private void GenerateTypeClassMethod(BuilderModel model, AtsCapabilityInfo capability)
{
var className = DeriveClassName(model.TypeId);
var promiseClass = $"{className}Promise";
// Use OwningTypeName if available to extract method name, otherwise parse from MethodName
var methodName = !string.IsNullOrEmpty(capability.OwningTypeName) && capability.MethodName.Contains('.')
? capability.MethodName[(capability.MethodName.LastIndexOf('.') + 1)..]
: GetTypeScriptMethodName(capability.MethodName);
var internalMethodName = $"_{methodName}Internal";
// Filter out target parameter
var targetParamName = capability.TargetParameterName ?? "context";
var userParams = capability.Parameters.Where(p => p.Name != targetParamName).ToList();
// Separate required and optional parameters
var (requiredParams, optionalParams) = SeparateParameters(userParams);
var hasOptionals = optionalParams.Count > 0;
var optionsInterfaceName = GetOptionsInterfaceName(methodName);
// Build parameter list for public method
var publicParamDefs = new List<string>();
foreach (var param in requiredParams)
{
var tsType = MapParameterToTypeScript(param);
publicParamDefs.Add($"{param.Name}: {tsType}");
}
if (hasOptionals)
{
publicParamDefs.Add($"options?: {optionsInterfaceName}");
}
var publicParamsString = string.Join(", ", publicParamDefs);
// Build parameter list for internal method (all params positional)
var internalParamDefs = new List<string>();
foreach (var param in userParams)
{
var tsType = MapParameterToTypeScript(param);
var optional = param.IsOptional || param.IsNullable ? "?" : "";
internalParamDefs.Add($"{param.Name}{optional}: {tsType}");
}
var internalParamsString = string.Join(", ", internalParamDefs);
// Check if return type has a Promise wrapper
var returnPromiseWrapper = GetPromiseWrapperForReturnType(capability.ReturnType);
var returnType = MapTypeRefToTypeScript(capability.ReturnType);
var isVoid = capability.ReturnType == null || capability.ReturnType.TypeId == AtsConstants.Void;
// Generate JSDoc
if (!string.IsNullOrEmpty(capability.Description))
{
WriteLine($" /** {capability.Description} */");
}
// If return type has a Promise wrapper, generate internal + fluent pattern
if (returnPromiseWrapper != null)
{
var returnWrapperClass = _wrapperClassNames.GetValueOrDefault(capability.ReturnType!.TypeId)
?? DeriveClassName(capability.ReturnType.TypeId);
var returnHandleType = GetHandleTypeName(capability.ReturnType.TypeId);
// Generate internal async method
WriteLine($" /** @internal */");
Write($" async {internalMethodName}(");
Write(internalParamsString);
WriteLine($"): Promise<{returnWrapperClass}> {{");
// Handle callback registration if any
var callbackParams = userParams.Where(p => p.IsCallback).ToList();
foreach (var callbackParam in callbackParams)
{
GenerateCallbackRegistration(callbackParam);
}
// Build args with conditional inclusion
GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams);
WriteLine($" const result = await this._client.invokeCapability<{returnHandleType}>(");
WriteLine($" '{capability.CapabilityId}',");
WriteLine($" rpcArgs");
WriteLine(" );");
WriteLine($" return new {returnWrapperClass}(result, this._client);");
WriteLine(" }");
WriteLine();
// Generate public fluent method that returns thenable wrapper
Write($" {methodName}(");
Write(publicParamsString);
WriteLine($"): {returnPromiseWrapper} {{");
// Extract optional params and forward
foreach (var param in optionalParams)
{
WriteLine($" const {param.Name} = options?.{param.Name};");
}
Write($" return new {returnPromiseWrapper}(this.{internalMethodName}(");
Write(string.Join(", ", userParams.Select(p => p.Name)));
WriteLine("));");
WriteLine(" }");
}
else if (isVoid)
{
// Void return - generate internal + fluent returning this type's Promise wrapper
// Generate internal async method
WriteLine($" /** @internal */");
Write($" async {internalMethodName}(");
Write(internalParamsString);
WriteLine($"): Promise<{className}> {{");
// Handle callback registration if any
var callbackParams = userParams.Where(p => p.IsCallback).ToList();
foreach (var callbackParam in callbackParams)
{
GenerateCallbackRegistration(callbackParam);
}
// Build args with conditional inclusion
GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams);
WriteLine($" await this._client.invokeCapability<void>(");
WriteLine($" '{capability.CapabilityId}',");
WriteLine($" rpcArgs");
WriteLine(" );");
WriteLine($" return this;");
WriteLine(" }");
WriteLine();
// Generate public fluent method
Write($" {methodName}(");
Write(publicParamsString);
WriteLine($"): {promiseClass} {{");
// Extract optional params and forward
foreach (var param in optionalParams)
{
WriteLine($" const {param.Name} = options?.{param.Name};");
}
Write($" return new {promiseClass}(this.{internalMethodName}(");
Write(string.Join(", ", userParams.Select(p => p.Name)));
WriteLine("));");
WriteLine(" }");
}
else
{
// Non-void, non-wrapper return - plain async method
Write($" async {methodName}(");
Write(publicParamsString);
WriteLine($"): Promise<{returnType}> {{");
// Extract optional params from options object
foreach (var param in optionalParams)
{
WriteLine($" const {param.Name} = options?.{param.Name};");
}
// Handle callback registration if any
var callbackParams = userParams.Where(p => p.IsCallback).ToList();
foreach (var callbackParam in callbackParams)
{
GenerateCallbackRegistration(callbackParam);
}
// Build args with conditional inclusion
GenerateArgsObjectWithConditionals(targetParamName, requiredParams, optionalParams);
WriteLine($" return await this._client.invokeCapability<{returnType}>(");
WriteLine($" '{capability.CapabilityId}',");
WriteLine($" rpcArgs");
WriteLine(" );");
WriteLine(" }");
}
WriteLine();
}
/// <summary>
/// Generates a thenable wrapper class for a type class.
/// </summary>
private void GenerateTypeClassThenableWrapper(BuilderModel model, List<AtsCapabilityInfo> methods)
{
var className = DeriveClassName(model.TypeId);
var promiseClass = $"{className}Promise";
WriteLine($"/**");
WriteLine($" * Thenable wrapper for {className} that enables fluent chaining.");
WriteLine($" */");
WriteLine($"export class {promiseClass} implements PromiseLike<{className}> {{");
WriteLine($" constructor(private _promise: Promise<{className}>) {{}}");
WriteLine();
// Generate then() for PromiseLike interface
WriteLine($" then<TResult1 = {className}, TResult2 = never>(");
WriteLine($" onfulfilled?: ((value: {className}) => TResult1 | PromiseLike<TResult1>) | null,");
WriteLine(" onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null");
WriteLine(" ): PromiseLike<TResult1 | TResult2> {");
WriteLine(" return this._promise.then(onfulfilled, onrejected);");
WriteLine(" }");
WriteLine();
// Generate fluent methods that chain via .then()
foreach (var capability in methods)
{
var methodName = !string.IsNullOrEmpty(capability.OwningTypeName) && capability.MethodName.Contains('.')
? capability.MethodName[(capability.MethodName.LastIndexOf('.') + 1)..]
: GetTypeScriptMethodName(capability.MethodName);
var targetParamName = capability.TargetParameterName ?? "context";
var userParams = capability.Parameters.Where(p => p.Name != targetParamName).ToList();
// Separate required and optional parameters
var (requiredParams, optionalParams) = SeparateParameters(userParams);
var hasOptionals = optionalParams.Count > 0;
var optionsInterfaceName = GetOptionsInterfaceName(methodName);
// Build parameter list using options pattern
var publicParamDefs = new List<string>();
foreach (var param in requiredParams)
{
var tsType = MapParameterToTypeScript(param);
publicParamDefs.Add($"{param.Name}: {tsType}");
}
if (hasOptionals)
{
publicParamDefs.Add($"options?: {optionsInterfaceName}");
}
var paramsString = string.Join(", ", publicParamDefs);
// Forward args to underlying object's public method
var forwardArgs = new List<string>();
foreach (var param in requiredParams)
{
forwardArgs.Add(param.Name);
}
if (hasOptionals)
{
forwardArgs.Add("options");
}
var argsString = string.Join(", ", forwardArgs);
// Check if return type has a Promise wrapper
var returnPromiseWrapper = GetPromiseWrapperForReturnType(capability.ReturnType);
var returnType = MapTypeRefToTypeScript(capability.ReturnType);
var isVoid = capability.ReturnType == null || capability.ReturnType.TypeId == AtsConstants.Void;
if (!string.IsNullOrEmpty(capability.Description))
{
WriteLine($" /** {capability.Description} */");
}
if (returnPromiseWrapper != null)
{
// Return type has Promise wrapper - forward to public method, wrap result
Write($" {methodName}(");
Write(paramsString);
WriteLine($"): {returnPromiseWrapper} {{");
Write($" return new {returnPromiseWrapper}(this._promise.then(obj => obj.{methodName}(");
Write(argsString);
WriteLine(")));");
WriteLine(" }");
}
else if (isVoid)
{
// Void return - forward to public method, wrap result in this class's promise
Write($" {methodName}(");
Write(paramsString);
WriteLine($"): {promiseClass} {{");
Write($" return new {promiseClass}(this._promise.then(obj => obj.{methodName}(");
Write(argsString);
WriteLine(")));");
WriteLine(" }");
}
else
{
// Non-void, non-wrapper return - plain Promise
Write($" {methodName}(");
Write(paramsString);
WriteLine($"): Promise<{returnType}> {{");
Write($" return this._promise.then(obj => obj.{methodName}(");
Write(argsString);
WriteLine("));");
WriteLine(" }");
}
WriteLine();
}
WriteLine("}");
WriteLine();
}
// ============================================================================
// Builder Model Helpers (replaces AtsBuilderModelFactory)
// ============================================================================
/// <summary>
/// Groups capabilities by ExpandedTargetTypes to create builder models.
/// Uses expansion to map interface targets to their concrete implementations.
/// Also creates builders for interface types (for use as return type wrappers).
/// </summary>
private static List<BuilderModel> CreateBuilderModels(IReadOnlyList<AtsCapabilityInfo> capabilities)
{
// Group capabilities by expanded target type IDs
// A capability targeting IResource with ExpandedTargetTypes = [RedisResource]
// will be assigned to Aspire.Hosting.Redis/RedisResource (the concrete type)
var capabilitiesByTypeId = new Dictionary<string, List<AtsCapabilityInfo>>();
// Track the AtsTypeRef for each typeId (from ExpandedTargetTypes or TargetType metadata)
var typeRefsByTypeId = new Dictionary<string, AtsTypeRef>();
// Also track interface types and their capabilities (for interface wrapper classes)
var interfaceCapabilities = new Dictionary<string, List<AtsCapabilityInfo>>();
foreach (var cap in capabilities)
{
var targetTypeRef = cap.TargetType;
var targetTypeId = cap.TargetTypeId;
if (targetTypeRef == null || string.IsNullOrEmpty(targetTypeId))
{
// Entry point methods - handled separately
continue;
}
// Use category-based check instead of string parsing
if (targetTypeRef.Category != AtsTypeCategory.Handle)
{
continue;
}
// Use expanded types if available, otherwise fall back to the original target
var expandedTypes = cap.ExpandedTargetTypes;
if (expandedTypes is { Count: > 0 })
{
// Flatten to concrete types
foreach (var expandedType in expandedTypes)
{
if (!capabilitiesByTypeId.TryGetValue(expandedType.TypeId, out var list))
{
list = [];
capabilitiesByTypeId[expandedType.TypeId] = list;
// Store the type ref for this expanded type
typeRefsByTypeId[expandedType.TypeId] = expandedType;
}
list.Add(cap);
}
// Also track the original interface type for wrapper class generation
if (targetTypeRef.IsInterface)
{
if (!interfaceCapabilities.TryGetValue(targetTypeId, out var interfaceList))
{
interfaceList = [];
interfaceCapabilities[targetTypeId] = interfaceList;
// Store the type ref for the interface
typeRefsByTypeId[targetTypeId] = targetTypeRef;
}
interfaceList.Add(cap);
}
}
else
{
// No expansion - use original target (concrete type)
if (!capabilitiesByTypeId.TryGetValue(targetTypeId, out var list))
{
list = [];
capabilitiesByTypeId[targetTypeId] = list;
// Store the type ref for this target type
typeRefsByTypeId[targetTypeId] = targetTypeRef;
}
list.Add(cap);
}
}
// Create a builder for each concrete type with its specific capabilities
var builders = new List<BuilderModel>();
foreach (var (typeId, typeCapabilities) in capabilitiesByTypeId)
{
var builderClassName = DeriveClassName(typeId);
// Get the type ref from tracked metadata (based on target type, not return type)
var typeRef = typeRefsByTypeId.GetValueOrDefault(typeId);
// Deduplicate capabilities by CapabilityId to avoid duplicate methods
var uniqueCapabilities = typeCapabilities
.GroupBy(c => c.CapabilityId)
.Select(g => g.First())
.ToList();
var builder = new BuilderModel
{
TypeId = typeId,
BuilderClassName = builderClassName,
Capabilities = uniqueCapabilities,
IsInterface = typeRef?.IsInterface ?? false,
TargetType = typeRef
};
builders.Add(builder);
}
// Also create builders for interface types (for use as return type wrappers)
// These are needed when methods return interface types like IResourceWithConnectionString
foreach (var (interfaceTypeId, caps) in interfaceCapabilities)
{
// Skip if already added (shouldn't happen, but be safe)
if (capabilitiesByTypeId.ContainsKey(interfaceTypeId))
{
continue;
}
var builderClassName = DeriveClassName(interfaceTypeId);
// Get the type ref from tracked metadata
var typeRef = typeRefsByTypeId.GetValueOrDefault(interfaceTypeId);
// Deduplicate capabilities
var uniqueCapabilities = caps
.GroupBy(c => c.CapabilityId)
.Select(g => g.First())
.ToList();
var builder = new BuilderModel
{
TypeId = interfaceTypeId,
BuilderClassName = builderClassName,
Capabilities = uniqueCapabilities,
IsInterface = true,
TargetType = typeRef
};
builders.Add(builder);
}
// Also create builders for resource types referenced anywhere in capabilities
// This handles types like RedisCommanderResource that appear in callback signatures,
// return types, or parameter types but aren't capability targets
var allReferencedTypeRefs = CollectAllReferencedTypes(capabilities);
// Track all types we already have builders for (concrete + interface)
var existingBuilderTypeIds = new HashSet<string>(capabilitiesByTypeId.Keys);
foreach (var (interfaceTypeId, _) in interfaceCapabilities)
{
existingBuilderTypeIds.Add(interfaceTypeId);
}
foreach (var (typeId, typeRef) in allReferencedTypeRefs)
{
// Skip types we already have builders for (from concrete or interface lists)
if (existingBuilderTypeIds.Contains(typeId))
{
continue;
}
// Only create builders for resource types (using metadata instead of string parsing)
if (!typeRef.IsResourceBuilder)
{
continue;
}
var builderClassName = DeriveClassName(typeId);
var builder = new BuilderModel
{
TypeId = typeId,
BuilderClassName = builderClassName,
Capabilities = [], // No specific capabilities - uses base type methods
IsInterface = typeRef.IsInterface,
TargetType = typeRef
};
builders.Add(builder);
}
// Sort: concrete types first, then interfaces
return builders
.OrderBy(b => b.IsInterface)
.ThenBy(b => b.BuilderClassName)
.ToList();
}
/// <summary>
/// Collects all type refs referenced in capabilities (return types, parameter types, callback types, etc.)
/// Returns a dictionary mapping typeId to AtsTypeRef for use in builder creation.
/// </summary>
private static Dictionary<string, AtsTypeRef> CollectAllReferencedTypes(IReadOnlyList<AtsCapabilityInfo> capabilities)
{
var typeRefs = new Dictionary<string, AtsTypeRef>();
void CollectFromTypeRef(AtsTypeRef? typeRef)
{
if (typeRef == null)
{
return;
}
if (!string.IsNullOrEmpty(typeRef.TypeId) && typeRef.Category == AtsTypeCategory.Handle)
{
typeRefs.TryAdd(typeRef.TypeId, typeRef);
}
// Also check nested types (generics, arrays, etc.)
CollectFromTypeRef(typeRef.ElementType);
CollectFromTypeRef(typeRef.KeyType);
CollectFromTypeRef(typeRef.ValueType);
if (typeRef.UnionTypes != null)
{
foreach (var unionType in typeRef.UnionTypes)
{
CollectFromTypeRef(unionType);
}
}
}
foreach (var cap in capabilities)
{
// Check return type
CollectFromTypeRef(cap.ReturnType);
// Check parameter types
foreach (var param in cap.Parameters)
{
CollectFromTypeRef(param.Type);
// Check callback parameter types and return type
if (param.IsCallback)
{
if (param.CallbackParameters != null)
{
foreach (var cbParam in param.CallbackParameters)
{
CollectFromTypeRef(cbParam.Type);
}
}
CollectFromTypeRef(param.CallbackReturnType);
}
}
}
return typeRefs;
}
/// <summary>
/// Gets entry point capabilities (those without TargetTypeId).
/// </summary>
private static List<AtsCapabilityInfo> GetEntryPointCapabilities(IReadOnlyList<AtsCapabilityInfo> capabilities)
{
return capabilities.Where(c => string.IsNullOrEmpty(c.TargetTypeId)).ToList();
}
/// <summary>
/// Derives the class name from an ATS type ID.
/// For interfaces like IResource, strips the leading 'I'.
/// </summary>
private static string DeriveClassName(string typeId)
{
var typeName = ExtractSimpleTypeName(typeId);
// Strip leading 'I' from interface types
if (typeName.StartsWith('I') && typeName.Length > 1 && char.IsUpper(typeName[1]))
{
return typeName[1..];
}
return typeName;
}
/// <summary>
/// Gets the handle type alias name for a type ID.
/// </summary>
private static string GetHandleTypeName(string typeId)
{
var typeName = ExtractSimpleTypeName(typeId);
// Sanitize generic types like "Dict<String,Object>" -> "DictStringObject"
// and array types like "string[]" -> "stringArray"
typeName = typeName
.Replace("[]", "Array", StringComparison.Ordinal)
.Replace("<", "", StringComparison.Ordinal)
.Replace(">", "", StringComparison.Ordinal)
.Replace(",", "", StringComparison.Ordinal);
return $"{typeName}Handle";
}
/// <summary>
/// Extracts the simple type name from a type ID.
/// </summary>
/// <example>
/// "Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource" → "IResource"
/// "Aspire.Hosting/Aspire.Hosting.DistributedApplication" → "DistributedApplication"
/// </example>
private static string ExtractSimpleTypeName(string typeId)
{
var slashIndex = typeId.LastIndexOf('/');
var fullTypeName = slashIndex >= 0 ? typeId[(slashIndex + 1)..] : typeId;
var dotIndex = fullTypeName.LastIndexOf('.');
return dotIndex >= 0 ? fullTypeName[(dotIndex + 1)..] : fullTypeName;
}
/// <summary>
/// Determines if a type has chainable methods and should have a Promise wrapper.
/// Types with instance methods or wrapper methods get Promise wrappers.
/// </summary>
private static bool HasChainableMethods(BuilderModel model)
{
// Check for instance methods (from ExposeMethods=true) or wrapper methods
return model.Capabilities.Any(c =>
c.CapabilityKind == AtsCapabilityKind.InstanceMethod ||
c.CapabilityKind == AtsCapabilityKind.Method);
}
/// <summary>
/// Gets the Promise wrapper class name for a return type, if one exists.
/// Returns null if the return type doesn't have a Promise wrapper.
/// </summary>
private string? GetPromiseWrapperForReturnType(AtsTypeRef? returnType)
{
if (returnType == null)
{
return null;
}
// Check if the return type has a Promise wrapper
if (_typesWithPromiseWrappers.Contains(returnType.TypeId))
{
var className = _wrapperClassNames.GetValueOrDefault(returnType.TypeId)
?? DeriveClassName(returnType.TypeId);
return $"{className}Promise";
}
return null;
}
}
|