diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs index 2b71319141c..c8c7add7f63 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs @@ -1251,7 +1251,6 @@ private void GenerateTypeClassInterface(BuilderModel model) { var className = DeriveClassName(model.TypeId); var interfaceName = GetInterfaceName(className); - var hasMethods = HasChainableMethods(model); WriteLine("// ============================================================================"); WriteLine($"// {interfaceName}"); @@ -1264,6 +1263,8 @@ private void GenerateTypeClassInterface(BuilderModel model) 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(); + var standardMethods = contextMethods.Concat(otherMethods).ToList(); + var hasMethods = standardMethods.Count > 0; var properties = GroupPropertiesByName(getters, setters); foreach (var prop in properties) @@ -1271,7 +1272,7 @@ private void GenerateTypeClassInterface(BuilderModel model) GenerateInterfaceProperty(prop.PropertyName, prop.Getter, prop.Setter); } - foreach (var method in contextMethods.Concat(otherMethods)) + foreach (var method in standardMethods) { GenerateTypeClassInterfaceMethod(className, method); } @@ -1286,7 +1287,7 @@ private void GenerateTypeClassInterface(BuilderModel model) var promiseInterfaceName = GetPromiseInterfaceName(className); WriteLine($"export interface {promiseInterfaceName} extends PromiseLike<{interfaceName}> {{"); - foreach (var method in contextMethods.Concat(otherMethods)) + foreach (var method in standardMethods) { GenerateTypeClassInterfaceMethod(className, method); } @@ -1518,7 +1519,8 @@ private void GenerateBuilderMethod(BuilderModel builder, AtsCapabilityInfo capab private void GenerateArgsObjectWithConditionals( string targetParamName, List requiredParams, - List optionalParams) + List optionalParams, + string indent = " ") { // Build the required args inline var requiredArgs = new List { $"{targetParamName}: this._handle" }; @@ -1527,13 +1529,13 @@ private void GenerateArgsObjectWithConditionals( requiredArgs.Add(GetRpcArgumentEntry(param)); } - WriteLine($" const rpcArgs: Record = {{ {string.Join(", ", requiredArgs)} }};"); + WriteLine($"{indent}const rpcArgs: Record = {{ {string.Join(", ", requiredArgs)} }};"); // Conditionally add optional params foreach (var param in optionalParams) { var rpcExpression = GetRpcArgumentExpression(param); - WriteLine($" if ({param.Name} !== undefined) rpcArgs.{param.Name} = {rpcExpression};"); + WriteLine($"{indent}if ({param.Name} !== undefined) rpcArgs.{param.Name} = {rpcExpression};"); } } @@ -1795,7 +1797,7 @@ private string GenerateCallbackTypeSignature(IReadOnlyList Promise<{returnType}>"; } - private void GenerateCallbackRegistration(AtsParameterInfo callbackParam) + private void GenerateCallbackRegistration(AtsParameterInfo callbackParam, string indent = " ") { var callbackParameters = callbackParam.CallbackParameters; var isOptional = callbackParam.IsOptional || callbackParam.IsNullable; @@ -1819,33 +1821,34 @@ private void GenerateCallbackRegistration(AtsParameterInfo callbackParam) // For optional callbacks, wrap the registration in a conditional if (isOptional) { - WriteLine($" const {callbackName}Id = {callbackName} ? registerCallback(async ({paramSignature}) => {{"); + WriteLine($"{indent}const {callbackName}Id = {callbackName} ? registerCallback(async ({paramSignature}) => {{"); } else { - WriteLine($" const {callbackName}Id = registerCallback(async ({paramSignature}) => {{"); + WriteLine($"{indent}const {callbackName}Id = registerCallback(async ({paramSignature}) => {{"); } // Generate the callback body - GenerateCallbackBody(callbackParam, callbackParameters); + GenerateCallbackBody(callbackParam, callbackParameters, indent); // Close the callback registration if (isOptional) { - WriteLine(" }) : undefined;"); + WriteLine(indent + "}) : undefined;"); } else { - WriteLine(" });"); + WriteLine(indent + "});"); } } /// /// Generates the body of a callback function. /// - private void GenerateCallbackBody(AtsParameterInfo callbackParam, IReadOnlyList? callbackParameters) + private void GenerateCallbackBody(AtsParameterInfo callbackParam, IReadOnlyList? callbackParameters, string indent) { var callbackName = callbackParam.Name; + var bodyIndent = $"{indent} "; // Check if callback has a return type - if so, we need to return the value var hasReturnType = callbackParam.CallbackReturnType != null @@ -1855,7 +1858,7 @@ private void GenerateCallbackBody(AtsParameterInfo callbackParam, IReadOnlyList< if (callbackParameters is null || callbackParameters.Count == 0) { // No parameters - just call the callback - WriteLine($" {returnPrefix}await {callbackName}();"); + WriteLine($"{bodyIndent}{returnPrefix}await {callbackName}();"); } else if (callbackParameters.Count == 1) { @@ -1866,22 +1869,22 @@ private void GenerateCallbackBody(AtsParameterInfo callbackParam, IReadOnlyList< if (cbTypeId == AtsConstants.CancellationToken) { - WriteLine($" const {cbParam.Name} = CancellationToken.fromValue({cbParam.Name}Data);"); + WriteLine($"{bodyIndent}const {cbParam.Name} = CancellationToken.fromValue({cbParam.Name}Data);"); } else 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 {GetImplementationClassName(wrapperClassName)}({cbParam.Name}Handle, this._client);"); + WriteLine($"{bodyIndent}const {cbParam.Name}Handle = wrapIfHandle({cbParam.Name}Data) as {handleType};"); + WriteLine($"{bodyIndent}const {cbParam.Name} = new {GetImplementationClassName(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($"{bodyIndent}const {cbParam.Name} = wrapIfHandle({cbParam.Name}Data) as {tsType};"); } - WriteLine($" {returnPrefix}await {callbackName}({cbParam.Name});"); + WriteLine($"{bodyIndent}{returnPrefix}await {callbackName}({cbParam.Name});"); } else { @@ -1895,24 +1898,24 @@ private void GenerateCallbackBody(AtsParameterInfo callbackParam, IReadOnlyList< if (cbTypeId == AtsConstants.CancellationToken) { - WriteLine($" const {cbParam.Name} = CancellationToken.fromValue(args.p{i});"); + WriteLine($"{bodyIndent}const {cbParam.Name} = CancellationToken.fromValue({callbackArgName});"); } else 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({callbackArgName}) as {handleType};"); - WriteLine($" const {cbParam.Name} = new {GetImplementationClassName(wrapperClassName)}({cbParam.Name}Handle, this._client);"); + WriteLine($"{bodyIndent}const {cbParam.Name}Handle = wrapIfHandle({callbackArgName}) as {handleType};"); + WriteLine($"{bodyIndent}const {cbParam.Name} = new {GetImplementationClassName(wrapperClassName)}({cbParam.Name}Handle, this._client);"); } else { // For raw handle types, just wrap and cast - WriteLine($" const {cbParam.Name} = wrapIfHandle({callbackArgName}) as {tsType};"); + WriteLine($"{bodyIndent}const {cbParam.Name} = wrapIfHandle({callbackArgName}) as {tsType};"); } callArgs.Add(cbParam.Name); } - WriteLine($" {returnPrefix}await {callbackName}({string.Join(", ", callArgs)});"); + WriteLine($"{bodyIndent}{returnPrefix}await {callbackName}({string.Join(", ", callArgs)});"); } } @@ -2079,7 +2082,6 @@ private void GenerateTypeClass(BuilderModel model) var handleType = GetHandleTypeName(model.TypeId); var className = DeriveClassName(model.TypeId); var implementationClassName = GetImplementationClassName(className); - var hasMethods = HasChainableMethods(model); GenerateTypeClassInterface(model); @@ -2093,9 +2095,8 @@ private void GenerateTypeClass(BuilderModel model) 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(); + var hasMethods = allMethods.Count > 0; WriteLine($"/**"); WriteLine($" * Type class for {className}."); diff --git a/src/Aspire.Hosting/Ats/AtsTypeMappings.cs b/src/Aspire.Hosting/Ats/AtsTypeMappings.cs index 681ccefbc0d..8ae5adef4cf 100644 --- a/src/Aspire.Hosting/Ats/AtsTypeMappings.cs +++ b/src/Aspire.Hosting/Ats/AtsTypeMappings.cs @@ -19,6 +19,7 @@ // Core types (from Aspire.Hosting namespace) [assembly: AspireExport(typeof(IDistributedApplicationBuilder))] +[assembly: AspireExport(typeof(IDistributedApplicationPipeline))] [assembly: AspireExport(typeof(DistributedApplication))] // Note: DistributedApplicationExecutionContext has [AspireExport(ExposeProperties = true)] on the type itself diff --git a/src/Aspire.Hosting/Ats/PipelineExports.cs b/src/Aspire.Hosting/Ats/PipelineExports.cs index 16453357cac..1bac7cd8ac0 100644 --- a/src/Aspire.Hosting/Ats/PipelineExports.cs +++ b/src/Aspire.Hosting/Ats/PipelineExports.cs @@ -13,6 +13,45 @@ namespace Aspire.Hosting.Ats; /// internal static class PipelineExports { + /// + /// Adds an application-level pipeline step in a TypeScript-friendly shape. + /// + /// The distributed application pipeline. + /// The unique name of the pipeline step. + /// The callback to execute when the step runs. + /// Optional step names that this step depends on. + /// Optional step names that require this step. + [AspireExport(Description = "Adds a pipeline step to the application")] + public static void AddStep( + this global::Aspire.Hosting.Pipelines.IDistributedApplicationPipeline pipeline, + string stepName, + Func callback, + string[]? dependsOn = null, + string[]? requiredBy = null) + { + ArgumentNullException.ThrowIfNull(pipeline); + ArgumentException.ThrowIfNullOrEmpty(stepName); + ArgumentNullException.ThrowIfNull(callback); + + pipeline.AddStep(stepName, callback, dependsOn, requiredBy); + } + + /// + /// Registers a pipeline configuration callback in a TypeScript-friendly shape. + /// + /// The distributed application pipeline. + /// The callback to execute during pipeline configuration. + [AspireExport(Description = "Configures the application pipeline via a callback")] + public static void Configure( + this global::Aspire.Hosting.Pipelines.IDistributedApplicationPipeline pipeline, + Func callback) + { + ArgumentNullException.ThrowIfNull(pipeline); + ArgumentNullException.ThrowIfNull(callback); + + pipeline.AddPipelineConfiguration(callback); + } + /// /// Adds a key-value pair to the pipeline summary with a Markdown-formatted value. /// diff --git a/src/Aspire.Hosting/Publishing/ManifestPublishingExtensions.cs b/src/Aspire.Hosting/Publishing/ManifestPublishingExtensions.cs index 05df047d31a..d72b939fdc1 100644 --- a/src/Aspire.Hosting/Publishing/ManifestPublishingExtensions.cs +++ b/src/Aspire.Hosting/Publishing/ManifestPublishingExtensions.cs @@ -21,6 +21,7 @@ internal static class ManifestPublishingExtensions /// /// The pipeline to add the manifest publishing step to. /// The pipeline for chaining. + [AspireExportIgnore(Reason = "Manifest publishing is an internal pipeline step and not part of the polyglot AppHost surface.")] public static IDistributedApplicationPipeline AddManifestPublishing(this IDistributedApplicationPipeline pipeline) { var step = new PipelineStep diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 7890993632f..66eebb8b898 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -9332,6 +9332,18 @@ func (s *IDistributedApplicationBuilder) ExecutionContext() (*DistributedApplica return result.(*DistributedApplicationExecutionContext), nil } +// Pipeline gets the Pipeline property +func (s *IDistributedApplicationBuilder) Pipeline() (*IDistributedApplicationPipeline, error) { + reqArgs := map[string]any{ + "context": SerializeValue(s.Handle()), + } + result, err := s.Client().InvokeCapability("Aspire.Hosting/IDistributedApplicationBuilder.pipeline", reqArgs) + if err != nil { + return nil, err + } + return result.(*IDistributedApplicationPipeline), nil +} + // UserSecretsManager gets the UserSecretsManager property func (s *IDistributedApplicationBuilder) UserSecretsManager() (*IUserSecretsManager, error) { reqArgs := map[string]any{ @@ -9593,6 +9605,49 @@ func (s *IDistributedApplicationEventing) Unsubscribe(subscription *DistributedA return err } +// IDistributedApplicationPipeline wraps a handle for Aspire.Hosting/Aspire.Hosting.Pipelines.IDistributedApplicationPipeline. +type IDistributedApplicationPipeline struct { + HandleWrapperBase +} + +// NewIDistributedApplicationPipeline creates a new IDistributedApplicationPipeline. +func NewIDistributedApplicationPipeline(handle *Handle, client *AspireClient) *IDistributedApplicationPipeline { + return &IDistributedApplicationPipeline{ + HandleWrapperBase: NewHandleWrapperBase(handle, client), + } +} + +// AddStep adds a pipeline step to the application +func (s *IDistributedApplicationPipeline) AddStep(stepName string, callback func(...any) any, dependsOn []string, requiredBy []string) error { + reqArgs := map[string]any{ + "pipeline": SerializeValue(s.Handle()), + } + reqArgs["stepName"] = SerializeValue(stepName) + if callback != nil { + reqArgs["callback"] = RegisterCallback(callback) + } + if dependsOn != nil { + reqArgs["dependsOn"] = SerializeValue(dependsOn) + } + if requiredBy != nil { + reqArgs["requiredBy"] = SerializeValue(requiredBy) + } + _, err := s.Client().InvokeCapability("Aspire.Hosting/addStep", reqArgs) + return err +} + +// Configure configures the application pipeline via a callback +func (s *IDistributedApplicationPipeline) Configure(callback func(...any) any) error { + reqArgs := map[string]any{ + "pipeline": SerializeValue(s.Handle()), + } + if callback != nil { + reqArgs["callback"] = RegisterCallback(callback) + } + _, err := s.Client().InvokeCapability("Aspire.Hosting/configure", reqArgs) + return err +} + // IDistributedApplicationResourceEvent wraps a handle for Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationResourceEvent. type IDistributedApplicationResourceEvent struct { HandleWrapperBase @@ -18560,6 +18615,9 @@ func init() { RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.IDistributedApplicationBuilder", func(h *Handle, c *AspireClient) any { return NewIDistributedApplicationBuilder(h, c) }) + RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.Pipelines.IDistributedApplicationPipeline", func(h *Handle, c *AspireClient) any { + return NewIDistributedApplicationPipeline(h, c) + }) RegisterHandleWrapper("Aspire.Hosting/Aspire.Hosting.DistributedApplication", func(h *Handle, c *AspireClient) any { return NewDistributedApplication(h, c) }) diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index 9c19bdd9406..38d72cc31c8 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -1,4 +1,4 @@ -// ===== AddDockerfileOptions.java ===== +// ===== AddDockerfileOptions.java ===== // AddDockerfileOptions.java - GENERATED CODE - DO NOT EDIT package aspire; @@ -52,6 +52,33 @@ public AddParameterWithValueOptions secret(Boolean value) { } +// ===== AddStepOptions.java ===== +// AddStepOptions.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Options for AddStep. */ +public final class AddStepOptions { + private String[] dependsOn; + private String[] requiredBy; + + public String[] getDependsOn() { return dependsOn; } + public AddStepOptions dependsOn(String[] value) { + this.dependsOn = value; + return this; + } + + public String[] getRequiredBy() { return requiredBy; } + public AddStepOptions requiredBy(String[] value) { + this.requiredBy = value; + return this; + } + +} + // ===== AfterResourcesCreatedEvent.java ===== // AfterResourcesCreatedEvent.java - GENERATED CODE - DO NOT EDIT @@ -1033,6 +1060,7 @@ private Handle ensureHandle() { public class AspireRegistrations { static { AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.IDistributedApplicationBuilder", (h, c) -> new IDistributedApplicationBuilder(h, c)); + AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.Pipelines.IDistributedApplicationPipeline", (h, c) -> new IDistributedApplicationPipeline(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.DistributedApplication", (h, c) -> new DistributedApplication(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.EndpointReference", (h, c) -> new EndpointReference(h, c)); AspireClient.registerHandleWrapper("Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource", (h, c) -> new IResource(h, c)); @@ -10528,6 +10556,13 @@ public DistributedApplicationExecutionContext executionContext() { return (DistributedApplicationExecutionContext) getClient().invokeCapability("Aspire.Hosting/IDistributedApplicationBuilder.executionContext", reqArgs); } + /** Gets the Pipeline property */ + public IDistributedApplicationPipeline pipeline() { + Map reqArgs = new HashMap<>(); + reqArgs.put("context", AspireClient.serializeValue(getHandle())); + return (IDistributedApplicationPipeline) getClient().invokeCapability("Aspire.Hosting/IDistributedApplicationBuilder.pipeline", reqArgs); + } + /** Gets the UserSecretsManager property */ public IUserSecretsManager userSecretsManager() { Map reqArgs = new HashMap<>(); @@ -10769,6 +10804,70 @@ public void unsubscribe(DistributedApplicationEventSubscription subscription) { } +// ===== IDistributedApplicationPipeline.java ===== +// IDistributedApplicationPipeline.java - GENERATED CODE - DO NOT EDIT + +package aspire; + +import java.util.*; +import java.util.function.*; + +/** Wrapper for Aspire.Hosting/Aspire.Hosting.Pipelines.IDistributedApplicationPipeline. */ +public class IDistributedApplicationPipeline extends HandleWrapperBase { + IDistributedApplicationPipeline(Handle handle, AspireClient client) { + super(handle, client); + } + + /** Adds a pipeline step to the application */ + public void addStep(String stepName, AspireAction1 callback, AddStepOptions options) { + var dependsOn = options == null ? null : options.getDependsOn(); + var requiredBy = options == null ? null : options.getRequiredBy(); + addStepImpl(stepName, callback, dependsOn, requiredBy); + } + + public void addStep(String stepName, AspireAction1 callback) { + addStep(stepName, callback, null); + } + + /** Adds a pipeline step to the application */ + private void addStepImpl(String stepName, AspireAction1 callback, String[] dependsOn, String[] requiredBy) { + Map reqArgs = new HashMap<>(); + reqArgs.put("pipeline", AspireClient.serializeValue(getHandle())); + reqArgs.put("stepName", AspireClient.serializeValue(stepName)); + var callbackId = getClient().registerCallback(args -> { + var arg = (PipelineStepContext) args[0]; + callback.invoke(arg); + return null; + }); + if (callbackId != null) { + reqArgs.put("callback", callbackId); + } + if (dependsOn != null) { + reqArgs.put("dependsOn", AspireClient.serializeValue(dependsOn)); + } + if (requiredBy != null) { + reqArgs.put("requiredBy", AspireClient.serializeValue(requiredBy)); + } + getClient().invokeCapability("Aspire.Hosting/addStep", reqArgs); + } + + /** Configures the application pipeline via a callback */ + public void configure(AspireAction1 callback) { + Map reqArgs = new HashMap<>(); + reqArgs.put("pipeline", AspireClient.serializeValue(getHandle())); + var callbackId = getClient().registerCallback(args -> { + var arg = (PipelineConfigurationContext) args[0]; + callback.invoke(arg); + return null; + }); + if (callbackId != null) { + reqArgs.put("callback", callbackId); + } + getClient().invokeCapability("Aspire.Hosting/configure", reqArgs); + } + +} + // ===== IDistributedApplicationResourceEvent.java ===== // IDistributedApplicationResourceEvent.java - GENERATED CODE - DO NOT EDIT @@ -20682,6 +20781,7 @@ public WithVolumeOptions isReadOnly(Boolean value) { // ===== sources.txt ===== .modules/AddDockerfileOptions.java .modules/AddParameterWithValueOptions.java +.modules/AddStepOptions.java .modules/AfterResourcesCreatedEvent.java .modules/Aspire.java .modules/AspireAction0.java @@ -20744,6 +20844,7 @@ public WithVolumeOptions isReadOnly(Boolean value) { .modules/IDistributedApplicationBuilder.java .modules/IDistributedApplicationEvent.java .modules/IDistributedApplicationEventing.java +.modules/IDistributedApplicationPipeline.java .modules/IDistributedApplicationResourceEvent.java .modules/IExpressionValue.java .modules/IHostEnvironment.java diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index fcc8f4e2a7a..f12c92d50d4 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -1860,6 +1860,15 @@ def execution_context(self) -> DistributedApplicationExecutionContext: ) return typing.cast(DistributedApplicationExecutionContext, result) + @_cached_property + def pipeline(self) -> AbstractDistributedApplicationPipeline: + """Gets the Pipeline property""" + result = self._client.invoke_capability( + 'Aspire.Hosting/IDistributedApplicationBuilder.pipeline', + {'context': self._handle} + ) + return typing.cast(AbstractDistributedApplicationPipeline, result) + @_cached_property def user_secrets_manager(self) -> AbstractUserSecretsManager: """Gets the UserSecretsManager property""" @@ -2208,6 +2217,45 @@ def unsubscribe(self, subscription: DistributedApplicationEventSubscription) -> ) +class AbstractDistributedApplicationPipeline: + """Type class for AbstractDistributedApplicationPipeline.""" + + def __init__(self, handle: Handle, client: AspireClient) -> None: + self._handle = handle + self._client = client + + def __repr__(self) -> str: + return f"AbstractDistributedApplicationPipeline(handle={self._handle.handle_id})" + + @_uncached_property + def handle(self) -> Handle: + """The underlying object reference handle.""" + return self._handle + + def add_step(self, step_name: str, callback: typing.Callable[[PipelineStepContext], None], *, depends_on: typing.Iterable[str] | None = None, required_by: typing.Iterable[str] | None = None) -> None: + """Adds a pipeline step to the application""" + rpc_args: dict[str, typing.Any] = {'pipeline': self._handle} + rpc_args['stepName'] = step_name + rpc_args['callback'] = self._client.register_callback(callback) + if depends_on is not None: + rpc_args['dependsOn'] = depends_on + if required_by is not None: + rpc_args['requiredBy'] = required_by + self._client.invoke_capability( + 'Aspire.Hosting/addStep', + rpc_args + ) + + def configure(self, callback: typing.Callable[[PipelineConfigurationContext], None]) -> None: + """Configures the application pipeline via a callback""" + rpc_args: dict[str, typing.Any] = {'pipeline': self._handle} + rpc_args['callback'] = self._client.register_callback(callback) + self._client.invoke_capability( + 'Aspire.Hosting/configure', + rpc_args + ) + + class AbstractDistributedApplicationResourceEvent(abc.ABC): """Abstract base class for AbstractDistributedApplicationResourceEvent.""" @@ -9720,6 +9768,7 @@ def create_builder( _register_handle_wrapper("Aspire.Hosting/Dict", AspireDict) _register_handle_wrapper("Microsoft.Extensions.Configuration.Abstractions/Microsoft.Extensions.Configuration.IConfiguration", AbstractConfiguration) _register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationEventing", AbstractDistributedApplicationEventing) +_register_handle_wrapper("Aspire.Hosting/Aspire.Hosting.Pipelines.IDistributedApplicationPipeline", AbstractDistributedApplicationPipeline) _register_handle_wrapper("Microsoft.Extensions.Hosting.Abstractions/Microsoft.Extensions.Hosting.IHostEnvironment", AbstractHostEnvironment) _register_handle_wrapper("Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger", AbstractLogger) _register_handle_wrapper("Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILoggerFactory", AbstractLoggerFactory) diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 8a31aa3be48..23432b7af3e 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -8105,6 +8105,15 @@ impl IDistributedApplicationBuilder { Ok(DistributedApplicationExecutionContext::new(handle, self.client.clone())) } + /// Gets the Pipeline property + pub fn pipeline(&self) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("context".to_string(), self.handle.to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/IDistributedApplicationBuilder.pipeline", args)?; + let handle: Handle = serde_json::from_value(result)?; + Ok(IDistributedApplicationPipeline::new(handle, self.client.clone())) + } + /// Gets the UserSecretsManager property pub fn user_secrets_manager(&self) -> Result> { let mut args: HashMap = HashMap::new(); @@ -8345,6 +8354,59 @@ impl IDistributedApplicationEventing { } } +/// Wrapper for Aspire.Hosting/Aspire.Hosting.Pipelines.IDistributedApplicationPipeline +pub struct IDistributedApplicationPipeline { + handle: Handle, + client: Arc, +} + +impl HasHandle for IDistributedApplicationPipeline { + fn handle(&self) -> &Handle { + &self.handle + } +} + +impl IDistributedApplicationPipeline { + pub fn new(handle: Handle, client: Arc) -> Self { + Self { handle, client } + } + + pub fn handle(&self) -> &Handle { + &self.handle + } + + pub fn client(&self) -> &Arc { + &self.client + } + + /// Adds a pipeline step to the application + pub fn add_step(&self, step_name: &str, callback: impl Fn(Vec) -> Value + Send + Sync + 'static, depends_on: Option>, required_by: Option>) -> Result<(), Box> { + let mut args: HashMap = HashMap::new(); + args.insert("pipeline".to_string(), self.handle.to_json()); + args.insert("stepName".to_string(), serde_json::to_value(&step_name).unwrap_or(Value::Null)); + let callback_id = register_callback(callback); + args.insert("callback".to_string(), Value::String(callback_id)); + if let Some(ref v) = depends_on { + args.insert("dependsOn".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + if let Some(ref v) = required_by { + args.insert("requiredBy".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); + } + let result = self.client.invoke_capability("Aspire.Hosting/addStep", args)?; + Ok(()) + } + + /// Configures the application pipeline via a callback + pub fn configure(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result<(), Box> { + let mut args: HashMap = HashMap::new(); + args.insert("pipeline".to_string(), self.handle.to_json()); + let callback_id = register_callback(callback); + args.insert("callback".to_string(), Value::String(callback_id)); + let result = self.client.invoke_capability("Aspire.Hosting/configure", args)?; + Ok(()) + } +} + /// Wrapper for Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationResourceEvent pub struct IDistributedApplicationResourceEvent { handle: Handle, diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index 03c76b21116..e35c2e1f896 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -187,6 +187,9 @@ type IResourceWithContainerFilesHandle = Handle<'Aspire.Hosting/Aspire.Hosting.I /** Handle to IUserSecretsManager */ type IUserSecretsManagerHandle = Handle<'Aspire.Hosting/Aspire.Hosting.IUserSecretsManager'>; +/** Handle to IDistributedApplicationPipeline */ +type IDistributedApplicationPipelineHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Pipelines.IDistributedApplicationPipeline'>; + /** Handle to IReportingStep */ type IReportingStepHandle = Handle<'Aspire.Hosting/Aspire.Hosting.Pipelines.IReportingStep'>; @@ -477,6 +480,11 @@ export interface AddParameterWithValueOptions { secret?: boolean; } +export interface AddStepOptions { + dependsOn?: string[]; + requiredBy?: string[]; +} + export interface AddTestChildDatabaseOptions { databaseName?: string; } @@ -3876,6 +3884,9 @@ export interface DistributedApplicationBuilder { executionContext: { get: () => Promise; }; + pipeline: { + get: () => Promise; + }; userSecretsManager: { get: () => Promise; }; @@ -3990,6 +4001,17 @@ class DistributedApplicationBuilderImpl implements DistributedApplicationBuilder }, }; + /** Gets the Pipeline property */ + pipeline = { + get: async (): Promise => { + const handle = await this._client.invokeCapability( + 'Aspire.Hosting/IDistributedApplicationBuilder.pipeline', + { context: this._handle } + ); + return new DistributedApplicationPipelineImpl(handle, this._client); + }, + }; + /** Gets the UserSecretsManager property */ userSecretsManager = { get: async (): Promise => { @@ -4614,6 +4636,105 @@ class DistributedApplicationEventingPromiseImpl implements DistributedApplicatio } +// ============================================================================ +// DistributedApplicationPipeline +// ============================================================================ + +export interface DistributedApplicationPipeline { + toJSON(): MarshalledHandle; + addStep(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: AddStepOptions): DistributedApplicationPipelinePromise; + configure(callback: (arg: PipelineConfigurationContext) => Promise): DistributedApplicationPipelinePromise; +} + +export interface DistributedApplicationPipelinePromise extends PromiseLike { + addStep(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: AddStepOptions): DistributedApplicationPipelinePromise; + configure(callback: (arg: PipelineConfigurationContext) => Promise): DistributedApplicationPipelinePromise; +} + +// ============================================================================ +// DistributedApplicationPipelineImpl +// ============================================================================ + +/** + * Type class for DistributedApplicationPipeline. + */ +class DistributedApplicationPipelineImpl implements DistributedApplicationPipeline { + constructor(private _handle: IDistributedApplicationPipelineHandle, private _client: AspireClientRpc) {} + + /** Serialize for JSON-RPC transport */ + toJSON(): MarshalledHandle { return this._handle.toJSON(); } + + /** Adds a pipeline step to the application */ + /** @internal */ + async _addStepInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[]): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as PipelineStepContextHandle; + const arg = new PipelineStepContextImpl(argHandle, this._client); + await callback(arg); + }); + const rpcArgs: Record = { pipeline: this._handle, stepName, callback: callbackId }; + if (dependsOn !== undefined) rpcArgs.dependsOn = dependsOn; + if (requiredBy !== undefined) rpcArgs.requiredBy = requiredBy; + await this._client.invokeCapability( + 'Aspire.Hosting/addStep', + rpcArgs + ); + return this; + } + + addStep(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: AddStepOptions): DistributedApplicationPipelinePromise { + const dependsOn = options?.dependsOn; + const requiredBy = options?.requiredBy; + return new DistributedApplicationPipelinePromiseImpl(this._addStepInternal(stepName, callback, dependsOn, requiredBy)); + } + + /** Configures the application pipeline via a callback */ + /** @internal */ + async _configureInternal(callback: (arg: PipelineConfigurationContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as PipelineConfigurationContextHandle; + const arg = new PipelineConfigurationContextImpl(argHandle, this._client); + await callback(arg); + }); + const rpcArgs: Record = { pipeline: this._handle, callback: callbackId }; + await this._client.invokeCapability( + 'Aspire.Hosting/configure', + rpcArgs + ); + return this; + } + + configure(callback: (arg: PipelineConfigurationContext) => Promise): DistributedApplicationPipelinePromise { + return new DistributedApplicationPipelinePromiseImpl(this._configureInternal(callback)); + } + +} + +/** + * Thenable wrapper for DistributedApplicationPipeline that enables fluent chaining. + */ +class DistributedApplicationPipelinePromiseImpl implements DistributedApplicationPipelinePromise { + constructor(private _promise: Promise) {} + + then( + onfulfilled?: ((value: DistributedApplicationPipeline) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): PromiseLike { + return this._promise.then(onfulfilled, onrejected); + } + + /** Adds a pipeline step to the application */ + addStep(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: AddStepOptions): DistributedApplicationPipelinePromise { + return new DistributedApplicationPipelinePromiseImpl(this._promise.then(obj => obj.addStep(stepName, callback, options))); + } + + /** Configures the application pipeline via a callback */ + configure(callback: (arg: PipelineConfigurationContext) => Promise): DistributedApplicationPipelinePromise { + return new DistributedApplicationPipelinePromiseImpl(this._promise.then(obj => obj.configure(callback))); + } + +} + // ============================================================================ // HostEnvironment // ============================================================================ @@ -31791,6 +31912,7 @@ registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.UpdateComm registerHandleWrapper('Microsoft.Extensions.Configuration.Abstractions/Microsoft.Extensions.Configuration.IConfiguration', (handle, client) => new ConfigurationImpl(handle as IConfigurationHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.IDistributedApplicationBuilder', (handle, client) => new DistributedApplicationBuilderImpl(handle as IDistributedApplicationBuilderHandle, client)); registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Eventing.IDistributedApplicationEventing', (handle, client) => new DistributedApplicationEventingImpl(handle as IDistributedApplicationEventingHandle, client)); +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.Pipelines.IDistributedApplicationPipeline', (handle, client) => new DistributedApplicationPipelineImpl(handle as IDistributedApplicationPipelineHandle, client)); registerHandleWrapper('Microsoft.Extensions.Hosting.Abstractions/Microsoft.Extensions.Hosting.IHostEnvironment', (handle, client) => new HostEnvironmentImpl(handle as IHostEnvironmentHandle, client)); registerHandleWrapper('Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILogger', (handle, client) => new LoggerImpl(handle as ILoggerHandle, client)); registerHandleWrapper('Microsoft.Extensions.Logging.Abstractions/Microsoft.Extensions.Logging.ILoggerFactory', (handle, client) => new LoggerFactoryImpl(handle as ILoggerFactoryHandle, client)); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java index dc0b4f3e247..f5fdfcac7f9 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Java/AppHost.java @@ -19,6 +19,9 @@ void main() throws Exception { var builtConnectionString = builder.addConnectionStringBuilder("customcs", (connectionStringBuilder) -> { var _isEmpty = connectionStringBuilder.isEmpty(); connectionStringBuilder.appendLiteral("Host="); connectionStringBuilder.appendValueProvider(endpoint); connectionStringBuilder.appendLiteral(";Key="); connectionStringBuilder.appendValueProvider(secretParam); var _builtExpression = connectionStringBuilder.build(); }); builtConnectionString.withConnectionProperty("Host", expr); builtConnectionString.withConnectionPropertyValue("Mode", "Development"); + var pipeline = builder.pipeline(); + pipeline.addStep("custom-builder-step", (stepContext) -> { var builderSummary = stepContext.summary(); builderSummary.add("BuilderPipelineStep", "Validated"); }, new AddStepOptions().dependsOn(new String[] { "build" }).requiredBy(new String[] { "publish" })); + pipeline.configure((configContext) -> { var _allSteps = configContext.steps(); var _builderTaggedSteps = configContext.getStepsByTag("custom-build"); }); container.withEnvironment("MY_ENDPOINT", endpoint); container.withEnvironment("MY_PARAM", configParam); container.withEnvironment("MY_CONN", builtConnectionString); diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py index 6517d69641b..870a61c87ac 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py +++ b/tests/PolyglotAppHosts/Aspire.Hosting/Python/apphost.py @@ -33,6 +33,24 @@ built_connection_string = builder.add_connection_string_builder("connection-string", lambda *_args, **_kwargs: None) built_connection_string.with_connection_property("Key", "Value") built_connection_string.with_connection_property_value("Key", "Value") + # builder-level pipeline APIs + pipeline = builder.pipeline + + def configure_builder_step(step_context): + step_context.summary.add("BuilderPipelineStep", "Validated") + + pipeline.add_step( + "custom-builder-step", + configure_builder_step, + depends_on=["build"], + required_by=["publish"], + ) + + def configure_builder_pipeline(config_context): + _all_steps = config_context.steps + _builder_tagged_steps = config_context.get_steps_by_tag("custom-build") + + pipeline.configure(configure_builder_pipeline) # withEnvironment - EndpointReference container.with_environment("KEY", endpoint) # withEnvironment - ParameterResource diff --git a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.ts b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.ts index 5275ce502b4..d7bd5022d73 100644 --- a/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.ts +++ b/tests/PolyglotAppHosts/Aspire.Hosting/TypeScript/apphost.ts @@ -71,6 +71,25 @@ await builtConnectionString.withConnectionPropertyValue("Mode", "Development"); const envConnectionString = await builder.addConnectionString("envcs"); +// =================================================================== +// Application pipeline on builder +// =================================================================== + +const pipeline = await builder.pipeline.get(); + +await pipeline.addStep("custom-builder-step", async (stepContext) => { + const summary = await stepContext.summary.get(); + await summary.add("BuilderPipelineStep", "Validated"); +}, { + dependsOn: ["build"], + requiredBy: ["publish"], +}); + +await pipeline.configure(async (configContext) => { + const _allSteps = await configContext.steps.get(); + const _builderTaggedSteps = await configContext.getStepsByTag("custom-build"); +}); + // =================================================================== // ResourceBuilderExtensions.cs — NEW exports on ContainerResource // ===================================================================