diff --git a/playground/TypeScriptAppHost/.modules/aspire.ts b/playground/TypeScriptAppHost/.modules/aspire.ts index 8ade3448ce5..2a1032125cf 100644 --- a/playground/TypeScriptAppHost/.modules/aspire.ts +++ b/playground/TypeScriptAppHost/.modules/aspire.ts @@ -8,6 +8,7 @@ import { AspireClient as AspireClientRpc, Handle, MarshalledHandle, + AppHostUsageError, CapabilityError, registerCallback, wrapIfHandle, @@ -239,6 +240,7 @@ export enum EndpointProperty { Scheme = "Scheme", TargetPort = "TargetPort", HostAndPort = "HostAndPort", + TlsEnabled = "TlsEnabled", } /** Enum type for IconVariant */ @@ -876,6 +878,16 @@ export class EndpointReference { }, }; + /** Gets the TlsEnabled property */ + tlsEnabled = { + get: async (): Promise => { + return await this._client.invokeCapability( + 'Aspire.Hosting.ApplicationModel/EndpointReference.tlsEnabled', + { context: this._handle } + ); + }, + }; + /** Gets the Port property */ port = { get: async (): Promise => { @@ -937,6 +949,15 @@ export class EndpointReference { ); } + /** Gets a conditional expression that resolves to the enabledValue when TLS is enabled on the endpoint, or to the disabledValue otherwise. */ + async getTlsValue(enabledValue: ReferenceExpression, disabledValue: ReferenceExpression): Promise { + const rpcArgs: Record = { context: this._handle, enabledValue, disabledValue }; + return await this._client.invokeCapability( + 'Aspire.Hosting.ApplicationModel/EndpointReference.getTlsValue', + rpcArgs + ); + } + } /** @@ -957,6 +978,11 @@ export class EndpointReferencePromise implements PromiseLike return this._promise.then(obj => obj.getValueAsync(options)); } + /** Gets a conditional expression that resolves to the enabledValue when TLS is enabled on the endpoint, or to the disabledValue otherwise. */ + getTlsValue(enabledValue: ReferenceExpression, disabledValue: ReferenceExpression): Promise { + return this._promise.then(obj => obj.getTlsValue(enabledValue, disabledValue)); + } + } // ============================================================================ @@ -3202,36 +3228,6 @@ export class ConnectionStringResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, remoteImageName }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageName', - rpcArgs - ); - return new ConnectionStringResource(result, this._client); - } - - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ConnectionStringResourcePromise { - return new ConnectionStringResourcePromise(this._withRemoteImageNameInternal(remoteImageName)); - } - - /** @internal */ - private async _withRemoteImageTagInternal(remoteImageTag: string): Promise { - const rpcArgs: Record = { builder: this._handle, remoteImageTag }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageTag', - rpcArgs - ); - return new ConnectionStringResource(result, this._client); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ConnectionStringResourcePromise { - return new ConnectionStringResourcePromise(this._withRemoteImageTagInternal(remoteImageTag)); - } - /** @internal */ private async _withPipelineStepFactoryInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -3441,16 +3437,6 @@ export class ConnectionStringResourcePromise implements PromiseLike obj.excludeFromMcp())); } - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ConnectionStringResourcePromise { - return new ConnectionStringResourcePromise(this._promise.then(obj => obj.withRemoteImageName(remoteImageName))); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ConnectionStringResourcePromise { - return new ConnectionStringResourcePromise(this._promise.then(obj => obj.withRemoteImageTag(remoteImageTag))); - } - /** Adds a pipeline step to the resource */ withPipelineStepFactory(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: WithPipelineStepFactoryOptions): ConnectionStringResourcePromise { return new ConnectionStringResourcePromise(this._promise.then(obj => obj.withPipelineStepFactory(stepName, callback, options))); @@ -3755,36 +3741,6 @@ export class ContainerRegistryResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, remoteImageName }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageName', - rpcArgs - ); - return new ContainerRegistryResource(result, this._client); - } - - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ContainerRegistryResourcePromise { - return new ContainerRegistryResourcePromise(this._withRemoteImageNameInternal(remoteImageName)); - } - - /** @internal */ - private async _withRemoteImageTagInternal(remoteImageTag: string): Promise { - const rpcArgs: Record = { builder: this._handle, remoteImageTag }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageTag', - rpcArgs - ); - return new ContainerRegistryResource(result, this._client); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ContainerRegistryResourcePromise { - return new ContainerRegistryResourcePromise(this._withRemoteImageTagInternal(remoteImageTag)); - } - /** @internal */ private async _withPipelineStepFactoryInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -3959,16 +3915,6 @@ export class ContainerRegistryResourcePromise implements PromiseLike obj.excludeFromMcp())); } - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ContainerRegistryResourcePromise { - return new ContainerRegistryResourcePromise(this._promise.then(obj => obj.withRemoteImageName(remoteImageName))); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ContainerRegistryResourcePromise { - return new ContainerRegistryResourcePromise(this._promise.then(obj => obj.withRemoteImageTag(remoteImageTag))); - } - /** Adds a pipeline step to the resource */ withPipelineStepFactory(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: WithPipelineStepFactoryOptions): ContainerRegistryResourcePromise { return new ContainerRegistryResourcePromise(this._promise.then(obj => obj.withPipelineStepFactory(stepName, callback, options))); @@ -8974,36 +8920,6 @@ export class DockerComposeEnvironmentResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, remoteImageName }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageName', - rpcArgs - ); - return new DockerComposeEnvironmentResource(result, this._client); - } - - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): DockerComposeEnvironmentResourcePromise { - return new DockerComposeEnvironmentResourcePromise(this._withRemoteImageNameInternal(remoteImageName)); - } - - /** @internal */ - private async _withRemoteImageTagInternal(remoteImageTag: string): Promise { - const rpcArgs: Record = { builder: this._handle, remoteImageTag }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageTag', - rpcArgs - ); - return new DockerComposeEnvironmentResource(result, this._client); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): DockerComposeEnvironmentResourcePromise { - return new DockerComposeEnvironmentResourcePromise(this._withRemoteImageTagInternal(remoteImageTag)); - } - /** @internal */ private async _withPipelineStepFactoryInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -9235,16 +9151,6 @@ export class DockerComposeEnvironmentResourcePromise implements PromiseLike obj.excludeFromMcp())); } - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): DockerComposeEnvironmentResourcePromise { - return new DockerComposeEnvironmentResourcePromise(this._promise.then(obj => obj.withRemoteImageName(remoteImageName))); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): DockerComposeEnvironmentResourcePromise { - return new DockerComposeEnvironmentResourcePromise(this._promise.then(obj => obj.withRemoteImageTag(remoteImageTag))); - } - /** Adds a pipeline step to the resource */ withPipelineStepFactory(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: WithPipelineStepFactoryOptions): DockerComposeEnvironmentResourcePromise { return new DockerComposeEnvironmentResourcePromise(this._promise.then(obj => obj.withPipelineStepFactory(stepName, callback, options))); @@ -9585,36 +9491,6 @@ export class DockerComposeServiceResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, remoteImageName }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageName', - rpcArgs - ); - return new DockerComposeServiceResource(result, this._client); - } - - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): DockerComposeServiceResourcePromise { - return new DockerComposeServiceResourcePromise(this._withRemoteImageNameInternal(remoteImageName)); - } - - /** @internal */ - private async _withRemoteImageTagInternal(remoteImageTag: string): Promise { - const rpcArgs: Record = { builder: this._handle, remoteImageTag }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageTag', - rpcArgs - ); - return new DockerComposeServiceResource(result, this._client); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): DockerComposeServiceResourcePromise { - return new DockerComposeServiceResourcePromise(this._withRemoteImageTagInternal(remoteImageTag)); - } - /** @internal */ private async _withPipelineStepFactoryInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -9789,16 +9665,6 @@ export class DockerComposeServiceResourcePromise implements PromiseLike obj.excludeFromMcp())); } - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): DockerComposeServiceResourcePromise { - return new DockerComposeServiceResourcePromise(this._promise.then(obj => obj.withRemoteImageName(remoteImageName))); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): DockerComposeServiceResourcePromise { - return new DockerComposeServiceResourcePromise(this._promise.then(obj => obj.withRemoteImageTag(remoteImageTag))); - } - /** Adds a pipeline step to the resource */ withPipelineStepFactory(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: WithPipelineStepFactoryOptions): DockerComposeServiceResourcePromise { return new DockerComposeServiceResourcePromise(this._promise.then(obj => obj.withPipelineStepFactory(stepName, callback, options))); @@ -13073,36 +12939,6 @@ export class ExternalServiceResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, remoteImageName }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageName', - rpcArgs - ); - return new ExternalServiceResource(result, this._client); - } - - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ExternalServiceResourcePromise { - return new ExternalServiceResourcePromise(this._withRemoteImageNameInternal(remoteImageName)); - } - - /** @internal */ - private async _withRemoteImageTagInternal(remoteImageTag: string): Promise { - const rpcArgs: Record = { builder: this._handle, remoteImageTag }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageTag', - rpcArgs - ); - return new ExternalServiceResource(result, this._client); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ExternalServiceResourcePromise { - return new ExternalServiceResourcePromise(this._withRemoteImageTagInternal(remoteImageTag)); - } - /** @internal */ private async _withPipelineStepFactoryInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -13282,16 +13118,6 @@ export class ExternalServiceResourcePromise implements PromiseLike obj.excludeFromMcp())); } - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ExternalServiceResourcePromise { - return new ExternalServiceResourcePromise(this._promise.then(obj => obj.withRemoteImageName(remoteImageName))); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ExternalServiceResourcePromise { - return new ExternalServiceResourcePromise(this._promise.then(obj => obj.withRemoteImageTag(remoteImageTag))); - } - /** Adds a pipeline step to the resource */ withPipelineStepFactory(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: WithPipelineStepFactoryOptions): ExternalServiceResourcePromise { return new ExternalServiceResourcePromise(this._promise.then(obj => obj.withPipelineStepFactory(stepName, callback, options))); @@ -16823,36 +16649,6 @@ export class ParameterResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, remoteImageName }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageName', - rpcArgs - ); - return new ParameterResource(result, this._client); - } - - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ParameterResourcePromise { - return new ParameterResourcePromise(this._withRemoteImageNameInternal(remoteImageName)); - } - - /** @internal */ - private async _withRemoteImageTagInternal(remoteImageTag: string): Promise { - const rpcArgs: Record = { builder: this._handle, remoteImageTag }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageTag', - rpcArgs - ); - return new ParameterResource(result, this._client); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ParameterResourcePromise { - return new ParameterResourcePromise(this._withRemoteImageTagInternal(remoteImageTag)); - } - /** @internal */ private async _withPipelineStepFactoryInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -17032,16 +16828,6 @@ export class ParameterResourcePromise implements PromiseLike return new ParameterResourcePromise(this._promise.then(obj => obj.excludeFromMcp())); } - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ParameterResourcePromise { - return new ParameterResourcePromise(this._promise.then(obj => obj.withRemoteImageName(remoteImageName))); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ParameterResourcePromise { - return new ParameterResourcePromise(this._promise.then(obj => obj.withRemoteImageTag(remoteImageTag))); - } - /** Adds a pipeline step to the resource */ withPipelineStepFactory(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: WithPipelineStepFactoryOptions): ParameterResourcePromise { return new ParameterResourcePromise(this._promise.then(obj => obj.withPipelineStepFactory(stepName, callback, options))); @@ -20971,36 +20757,6 @@ export class PostgresDatabaseResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, remoteImageName }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageName', - rpcArgs - ); - return new PostgresDatabaseResource(result, this._client); - } - - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): PostgresDatabaseResourcePromise { - return new PostgresDatabaseResourcePromise(this._withRemoteImageNameInternal(remoteImageName)); - } - - /** @internal */ - private async _withRemoteImageTagInternal(remoteImageTag: string): Promise { - const rpcArgs: Record = { builder: this._handle, remoteImageTag }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageTag', - rpcArgs - ); - return new PostgresDatabaseResource(result, this._client); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): PostgresDatabaseResourcePromise { - return new PostgresDatabaseResourcePromise(this._withRemoteImageTagInternal(remoteImageTag)); - } - /** @internal */ private async _withPipelineStepFactoryInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -21224,16 +20980,6 @@ export class PostgresDatabaseResourcePromise implements PromiseLike obj.excludeFromMcp())); } - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): PostgresDatabaseResourcePromise { - return new PostgresDatabaseResourcePromise(this._promise.then(obj => obj.withRemoteImageName(remoteImageName))); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): PostgresDatabaseResourcePromise { - return new PostgresDatabaseResourcePromise(this._promise.then(obj => obj.withRemoteImageTag(remoteImageTag))); - } - /** Adds a pipeline step to the resource */ withPipelineStepFactory(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: WithPipelineStepFactoryOptions): PostgresDatabaseResourcePromise { return new PostgresDatabaseResourcePromise(this._promise.then(obj => obj.withPipelineStepFactory(stepName, callback, options))); @@ -30152,12 +29898,6 @@ export class RedisResource extends ResourceBuilderBase { { context: this._handle } ); }, - set: async (value: boolean): Promise => { - await this._client.invokeCapability( - 'Aspire.Hosting.ApplicationModel/RedisResource.setTlsEnabled', - { context: this._handle, value } - ); - } }; /** Gets the ConnectionStringExpression property */ @@ -33889,6 +33629,36 @@ export class ComputeResource extends ResourceBuilderBase super(handle, client); } + /** @internal */ + private async _withRemoteImageNameInternal(remoteImageName: string): Promise { + const rpcArgs: Record = { builder: this._handle, remoteImageName }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withRemoteImageName', + rpcArgs + ); + return new ComputeResource(result, this._client); + } + + /** Sets the remote image name for publishing */ + withRemoteImageName(remoteImageName: string): ComputeResourcePromise { + return new ComputeResourcePromise(this._withRemoteImageNameInternal(remoteImageName)); + } + + /** @internal */ + private async _withRemoteImageTagInternal(remoteImageTag: string): Promise { + const rpcArgs: Record = { builder: this._handle, remoteImageTag }; + const result = await this._client.invokeCapability( + 'Aspire.Hosting/withRemoteImageTag', + rpcArgs + ); + return new ComputeResource(result, this._client); + } + + /** Sets the remote image tag for publishing */ + withRemoteImageTag(remoteImageTag: string): ComputeResourcePromise { + return new ComputeResourcePromise(this._withRemoteImageTagInternal(remoteImageTag)); + } + /** @internal */ private async _publishAsDockerComposeServiceInternal(configure: (arg1: DockerComposeServiceResource, arg2: Service) => Promise): Promise { const configureId = registerCallback(async (argsData: unknown) => { @@ -33929,6 +33699,16 @@ export class ComputeResourcePromise implements PromiseLike { return this._promise.then(onfulfilled, onrejected); } + /** Sets the remote image name for publishing */ + withRemoteImageName(remoteImageName: string): ComputeResourcePromise { + return new ComputeResourcePromise(this._promise.then(obj => obj.withRemoteImageName(remoteImageName))); + } + + /** Sets the remote image tag for publishing */ + withRemoteImageTag(remoteImageTag: string): ComputeResourcePromise { + return new ComputeResourcePromise(this._promise.then(obj => obj.withRemoteImageTag(remoteImageTag))); + } + /** Publishes the resource as a Docker Compose service with custom service configuration */ publishAsDockerComposeService(configure: (arg1: DockerComposeServiceResource, arg2: Service) => Promise): ComputeResourcePromise { return new ComputeResourcePromise(this._promise.then(obj => obj.publishAsDockerComposeService(configure))); @@ -34266,36 +34046,6 @@ export class Resource extends ResourceBuilderBase { return new ResourcePromise(this._excludeFromMcpInternal()); } - /** @internal */ - private async _withRemoteImageNameInternal(remoteImageName: string): Promise { - const rpcArgs: Record = { builder: this._handle, remoteImageName }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageName', - rpcArgs - ); - return new Resource(result, this._client); - } - - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ResourcePromise { - return new ResourcePromise(this._withRemoteImageNameInternal(remoteImageName)); - } - - /** @internal */ - private async _withRemoteImageTagInternal(remoteImageTag: string): Promise { - const rpcArgs: Record = { builder: this._handle, remoteImageTag }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withRemoteImageTag', - rpcArgs - ); - return new Resource(result, this._client); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ResourcePromise { - return new ResourcePromise(this._withRemoteImageTagInternal(remoteImageTag)); - } - /** @internal */ private async _withPipelineStepFactoryInternal(stepName: string, callback: (arg: PipelineStepContext) => Promise, dependsOn?: string[], requiredBy?: string[], tags?: string[], description?: string): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -34470,16 +34220,6 @@ export class ResourcePromise implements PromiseLike { return new ResourcePromise(this._promise.then(obj => obj.excludeFromMcp())); } - /** Sets the remote image name for publishing */ - withRemoteImageName(remoteImageName: string): ResourcePromise { - return new ResourcePromise(this._promise.then(obj => obj.withRemoteImageName(remoteImageName))); - } - - /** Sets the remote image tag for publishing */ - withRemoteImageTag(remoteImageTag: string): ResourcePromise { - return new ResourcePromise(this._promise.then(obj => obj.withRemoteImageTag(remoteImageTag))); - } - /** Adds a pipeline step to the resource */ withPipelineStepFactory(stepName: string, callback: (arg: PipelineStepContext) => Promise, options?: WithPipelineStepFactoryOptions): ResourcePromise { return new ResourcePromise(this._promise.then(obj => obj.withPipelineStepFactory(stepName, callback, options))); @@ -35668,7 +35408,7 @@ export async function createBuilder(options?: CreateBuilderOptions): Promise { const error = reason instanceof Error ? reason : new Error(String(reason)); - if (reason instanceof CapabilityError) { + if (reason instanceof AppHostUsageError) { + console.error(`\n❌ AppHost Error: ${error.message}`); + } else if (reason instanceof CapabilityError) { console.error(`\n❌ Capability Error: ${error.message}`); console.error(` Code: ${(reason as CapabilityError).code}`); if ((reason as CapabilityError).capability) { @@ -35699,8 +35441,12 @@ process.on('unhandledRejection', (reason: unknown) => { }); process.on('uncaughtException', (error: Error) => { - console.error(`\n❌ Uncaught Exception: ${error.message}`); - if (error.stack) { + if (error instanceof AppHostUsageError) { + console.error(`\n❌ AppHost Error: ${error.message}`); + } else { + console.error(`\n❌ Uncaught Exception: ${error.message}`); + } + if (!(error instanceof AppHostUsageError) && error.stack) { console.error(error.stack); } process.exit(1); diff --git a/playground/TypeScriptAppHost/.modules/base.ts b/playground/TypeScriptAppHost/.modules/base.ts index 7778b0f1737..6256537c773 100644 --- a/playground/TypeScriptAppHost/.modules/base.ts +++ b/playground/TypeScriptAppHost/.modules/base.ts @@ -1,5 +1,5 @@ // aspire.ts - Core Aspire types: base classes, ReferenceExpression -import { Handle, AspireClient, MarshalledHandle } from './transport.js'; +import { Handle, AspireClient, MarshalledHandle, registerCancellation, registerHandleWrapper, unregisterCancellation } from './transport.js'; // Re-export transport types for convenience export { Handle, AspireClient, CapabilityError, registerCallback, unregisterCallback, registerCancellation, unregisterCancellation } from './transport.js'; @@ -43,22 +43,46 @@ export class ReferenceExpression { private readonly _format?: string; private readonly _valueProviders?: unknown[]; + // Conditional mode fields + private readonly _condition?: unknown; + private readonly _whenTrue?: ReferenceExpression; + private readonly _whenFalse?: ReferenceExpression; + private readonly _matchValue?: string; + // Handle mode fields (when wrapping a server-returned handle) private readonly _handle?: Handle; private readonly _client?: AspireClient; constructor(format: string, valueProviders: unknown[]); constructor(handle: Handle, client: AspireClient); - constructor(handleOrFormat: Handle | string, clientOrValueProviders: AspireClient | unknown[]) { - if (typeof handleOrFormat === 'string') { - this._format = handleOrFormat; - this._valueProviders = clientOrValueProviders as unknown[]; + constructor(condition: unknown, matchValue: string, whenTrue: ReferenceExpression, whenFalse: ReferenceExpression); + constructor( + handleOrFormatOrCondition: Handle | string | unknown, + clientOrValueProvidersOrMatchValue: AspireClient | unknown[] | string, + whenTrueOrWhenFalse?: ReferenceExpression, + whenFalse?: ReferenceExpression + ) { + if (typeof handleOrFormatOrCondition === 'string') { + this._format = handleOrFormatOrCondition; + this._valueProviders = clientOrValueProvidersOrMatchValue as unknown[]; + } else if (handleOrFormatOrCondition instanceof Handle) { + this._handle = handleOrFormatOrCondition; + this._client = clientOrValueProvidersOrMatchValue as AspireClient; } else { - this._handle = handleOrFormat; - this._client = clientOrValueProviders as AspireClient; + this._condition = handleOrFormatOrCondition; + this._matchValue = (clientOrValueProvidersOrMatchValue as string) ?? 'True'; + this._whenTrue = whenTrueOrWhenFalse; + this._whenFalse = whenFalse; } } + /** + * Gets whether this reference expression is conditional. + */ + get isConditional(): boolean { + return this._condition !== undefined; + } + /** * Creates a reference expression from a tagged template literal. * @@ -82,16 +106,46 @@ export class ReferenceExpression { return new ReferenceExpression(format, valueProviders); } + /** + * Creates a conditional reference expression from its constituent parts. + * + * @param condition - A value provider whose result is compared to matchValue + * @param whenTrue - The expression to use when the condition matches + * @param whenFalse - The expression to use when the condition does not match + * @param matchValue - The value to compare the condition against (defaults to "True") + * @returns A ReferenceExpression instance in conditional mode + */ + static createConditional( + condition: unknown, + matchValue: string, + whenTrue: ReferenceExpression, + whenFalse: ReferenceExpression + ): ReferenceExpression { + return new ReferenceExpression(condition, matchValue, whenTrue, whenFalse); + } + /** * Serializes the reference expression for JSON-RPC transport. - * In template-literal mode, uses the $expr format. + * In expression mode, uses the $expr format with format + valueProviders. + * In conditional mode, uses the $expr format with condition + whenTrue + whenFalse. * In handle mode, delegates to the handle's serialization. */ - toJSON(): { $expr: { format: string; valueProviders?: unknown[] } } | MarshalledHandle { + toJSON(): { $expr: { format: string; valueProviders?: unknown[] } | { condition: unknown; whenTrue: unknown; whenFalse: unknown; matchValue: string } } | MarshalledHandle { if (this._handle) { return this._handle.toJSON(); } + if (this.isConditional) { + return { + $expr: { + condition: this._condition instanceof Handle ? this._condition.toJSON() : this._condition, + whenTrue: this._whenTrue!.toJSON(), + whenFalse: this._whenFalse!.toJSON(), + matchValue: this._matchValue! + } + }; + } + return { $expr: { format: this._format!, @@ -100,6 +154,30 @@ export class ReferenceExpression { }; } + /** + * Resolves the expression to its string value on the server. + * Only available on server-returned ReferenceExpression instances (handle mode). + * + * @param cancellationToken - Optional AbortSignal for cancellation support + * @returns The resolved string value, or null if the expression resolves to null + */ + async getValue(cancellationToken?: AbortSignal): Promise { + if (!this._handle || !this._client) { + throw new Error('getValue is only available on server-returned ReferenceExpression instances'); + } + const cancellationTokenId = registerCancellation(cancellationToken); + try { + const rpcArgs: Record = { context: this._handle }; + if (cancellationTokenId !== undefined) rpcArgs.cancellationToken = cancellationTokenId; + return await this._client.invokeCapability( + 'Aspire.Hosting.ApplicationModel/getValue', + rpcArgs + ); + } finally { + unregisterCancellation(cancellationTokenId); + } + } + /** * String representation for debugging. */ @@ -107,10 +185,17 @@ export class ReferenceExpression { if (this._handle) { return `ReferenceExpression(handle)`; } + if (this.isConditional) { + return `ReferenceExpression(conditional)`; + } return `ReferenceExpression(${this._format})`; } } +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression', (handle, client) => + new ReferenceExpression(handle, client) +); + /** * Extracts a value for use in reference expressions. * Supports handles (objects) and string literals. diff --git a/playground/TypeScriptAppHost/.modules/transport.ts b/playground/TypeScriptAppHost/.modules/transport.ts index 7bddd74beff..7ee1ba87e3f 100644 --- a/playground/TypeScriptAppHost/.modules/transport.ts +++ b/playground/TypeScriptAppHost/.modules/transport.ts @@ -213,6 +213,75 @@ export class CapabilityError extends Error { } } +/** + * Error thrown when the AppHost script uses the generated SDK incorrectly. + */ +export class AppHostUsageError extends Error { + constructor(message: string) { + super(message); + this.name = 'AppHostUsageError'; + } +} + +function isPromiseLike(value: unknown): value is PromiseLike { + return ( + value !== null && + (typeof value === 'object' || typeof value === 'function') && + 'then' in value && + typeof (value as { then?: unknown }).then === 'function' + ); +} + +function validateCapabilityArgs( + capabilityId: string, + args?: Record +): void { + if (!args) { + return; + } + + const seen = new Set(); + + const validateValue = (value: unknown, path: string): void => { + if (value === null || value === undefined) { + return; + } + + if (isPromiseLike(value)) { + throw new AppHostUsageError( + `Argument '${path}' passed to capability '${capabilityId}' is a Promise-like value. ` + + `This usually means an async builder call was not awaited. ` + + `Did you forget 'await' on a call like builder.addPostgres(...) or resource.addDatabase(...)?` + ); + } + + if (typeof value !== 'object') { + return; + } + + if (seen.has(value)) { + return; + } + + seen.add(value); + + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + validateValue(value[i], `${path}[${i}]`); + } + return; + } + + for (const [key, nestedValue] of Object.entries(value)) { + validateValue(nestedValue, `${path}.${key}`); + } + }; + + for (const [key, value] of Object.entries(args)) { + validateValue(value, key); + } +} + // ============================================================================ // Callback Registry // ============================================================================ @@ -533,6 +602,8 @@ export class AspireClient { throw new Error('Not connected to AppHost'); } + validateCapabilityArgs(capabilityId, args); + // Ref counting: The vscode-jsonrpc socket keeps Node's event loop alive. // We ref() during RPC calls so the process doesn't exit mid-call, and // unref() when idle so the process can exit naturally after all work completes. diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs index c20b71ba205..c29f8ade7df 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs @@ -2490,6 +2490,13 @@ private static List CreateBuilderModels(IReadOnlyList 0 }) diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.ts b/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.ts index 9a3427e7e72..6256537c773 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.ts +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/base.ts @@ -1,5 +1,5 @@ // aspire.ts - Core Aspire types: base classes, ReferenceExpression -import { Handle, AspireClient, MarshalledHandle } from './transport.js'; +import { Handle, AspireClient, MarshalledHandle, registerCancellation, registerHandleWrapper, unregisterCancellation } from './transport.js'; // Re-export transport types for convenience export { Handle, AspireClient, CapabilityError, registerCallback, unregisterCallback, registerCancellation, unregisterCancellation } from './transport.js'; @@ -154,6 +154,30 @@ export class ReferenceExpression { }; } + /** + * Resolves the expression to its string value on the server. + * Only available on server-returned ReferenceExpression instances (handle mode). + * + * @param cancellationToken - Optional AbortSignal for cancellation support + * @returns The resolved string value, or null if the expression resolves to null + */ + async getValue(cancellationToken?: AbortSignal): Promise { + if (!this._handle || !this._client) { + throw new Error('getValue is only available on server-returned ReferenceExpression instances'); + } + const cancellationTokenId = registerCancellation(cancellationToken); + try { + const rpcArgs: Record = { context: this._handle }; + if (cancellationTokenId !== undefined) rpcArgs.cancellationToken = cancellationTokenId; + return await this._client.invokeCapability( + 'Aspire.Hosting.ApplicationModel/getValue', + rpcArgs + ); + } finally { + unregisterCancellation(cancellationTokenId); + } + } + /** * String representation for debugging. */ @@ -168,6 +192,10 @@ export class ReferenceExpression { } } +registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression', (handle, client) => + new ReferenceExpression(handle, client) +); + /** * Extracts a value for use in reference expressions. * Supports handles (objects) and string literals. diff --git a/src/Aspire.Hosting.RemoteHost/Ats/CapabilityDispatcher.cs b/src/Aspire.Hosting.RemoteHost/Ats/CapabilityDispatcher.cs index 29f64ac36f8..551d79e21eb 100644 --- a/src/Aspire.Hosting.RemoteHost/Ats/CapabilityDispatcher.cs +++ b/src/Aspire.Hosting.RemoteHost/Ats/CapabilityDispatcher.cs @@ -309,29 +309,7 @@ private void RegisterContextTypeMethod(AtsCapabilityInfo capability, MethodInfo object? result; result = await InvokeMethodAsync(methodToInvoke, invokeTarget, methodArgs, capability.RunSyncOnBackgroundThread).ConfigureAwait(false); - // Handle async methods - await instead of blocking - if (result is Task task) - { - try - { - await task.ConfigureAwait(false); - } - catch (Exception ex) - { - throw new InvalidOperationException(ex.Message, ex); - } - - var taskType = task.GetType(); - if (taskType.IsGenericType) - { - var resultProperty = taskType.GetProperty("Result"); - result = resultProperty?.GetValue(task); - } - else - { - result = null; - } - } + result = await UnwrapAsyncResultAsync(result, methodToInvoke.ReturnType).ConfigureAwait(false); return _marshaller.MarshalToJson(result, capability.ReturnType); }; @@ -393,31 +371,7 @@ private void RegisterFromCapability(AtsCapabilityInfo capability, MethodInfo met object? result; result = await InvokeMethodAsync(methodToInvoke, target: null, methodArgs, capability.RunSyncOnBackgroundThread).ConfigureAwait(false); - // Handle async methods - await instead of blocking - if (result is Task task) - { - try - { - await task.ConfigureAwait(false); - } - catch (Exception ex) - { - // Rethrow the exception - it will be caught by the outer handler - // and converted to a CapabilityException - throw new InvalidOperationException(ex.Message, ex); - } - - var taskType = task.GetType(); - if (taskType.IsGenericType) - { - var resultProperty = taskType.GetProperty("Result"); - result = resultProperty?.GetValue(task); - } - else - { - result = null; - } - } + result = await UnwrapAsyncResultAsync(result, methodToInvoke.ReturnType).ConfigureAwait(false); return _marshaller.MarshalToJson(result, capability.ReturnType); }; @@ -505,7 +459,7 @@ public void Register( private static async Task InvokeMethodAsync(MethodInfo method, object? target, object?[] methodArgs, bool runSyncOnBackgroundThread) { - if (runSyncOnBackgroundThread && !typeof(Task).IsAssignableFrom(method.ReturnType)) + if (runSyncOnBackgroundThread && !IsAsyncReturnType(method.ReturnType)) { return await Task.Run(() => InvokeMethodCore(method, target, methodArgs)).ConfigureAwait(false); } @@ -513,6 +467,63 @@ public void Register( return InvokeMethodCore(method, target, methodArgs); } + private static bool IsAsyncReturnType(Type returnType) + { + if (typeof(Task).IsAssignableFrom(returnType) || returnType == typeof(ValueTask)) + { + return true; + } + + return returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>); + } + + private static async Task UnwrapAsyncResultAsync(object? result, Type returnType) + { + try + { + if (result is Task task) + { + await task.ConfigureAwait(false); + return GetAsyncResultValue(task); + } + + if (returnType == typeof(ValueTask) && result is ValueTask valueTask) + { + await valueTask.ConfigureAwait(false); + return null; + } + + if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) + { + var asTask = returnType.GetMethod(nameof(ValueTask.AsTask), BindingFlags.Instance | BindingFlags.Public) + ?? throw new InvalidOperationException($"Unable to await ValueTask result for return type '{returnType}'."); + var boxedTask = asTask.Invoke(result, null) as Task + ?? throw new InvalidOperationException($"Unable to convert ValueTask result for return type '{returnType}' to Task."); + + await boxedTask.ConfigureAwait(false); + return GetAsyncResultValue(boxedTask); + } + + return result; + } + catch (Exception ex) when (ex is not InvalidOperationException) + { + throw new InvalidOperationException(ex.Message, ex); + } + } + + private static object? GetAsyncResultValue(Task task) + { + var taskType = task.GetType(); + if (!taskType.IsGenericType) + { + return null; + } + + var resultProperty = taskType.GetProperty("Result"); + return resultProperty?.GetValue(task); + } + private static object? InvokeMethodCore(MethodInfo method, object? target, object?[] methodArgs) { try diff --git a/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs b/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs index d879167ce38..0dfb1b7a024 100644 --- a/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs +++ b/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs @@ -221,6 +221,7 @@ IEnumerable IValueWithReferences.References /// Gets the value of the expression. The final string value after evaluating the format string and its parameters. /// /// A . + [AspireExport("getValue", Description = "Gets the resolved string value of the reference expression asynchronously")] public ValueTask GetValueAsync(CancellationToken cancellationToken) { return this.GetValueAsync(new(), cancellationToken); 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 32f5c44a83d..9b606e02034 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -4953,7 +4953,14 @@ class ReferenceExpression(HandleWrapperBase): def __init__(self, handle: Handle, client: AspireClient): super().__init__(handle, client) - pass + def get_value(self, cancellation_token: CancellationToken) -> str: + """Gets the resolved string value of the reference expression asynchronously""" + args: Dict[str, Any] = { "context": serialize_value(self._handle) } + cancellation_token_id = register_cancellation(cancellation_token, self._client) if cancellation_token is not None else None + if cancellation_token_id is not None: + args["cancellationToken"] = cancellation_token_id + return self._client.invoke_capability("Aspire.Hosting.ApplicationModel/getValue", args) + class ReferenceExpressionBuilder(HandleWrapperBase): def __init__(self, handle: Handle, client: AspireClient): diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs index 2ab0dab222d..015fa7d28f2 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs @@ -50,6 +50,17 @@ await Verify(files["aspire.ts"], extension: "ts") .UseFileName("AtsGeneratedAspire"); } + [Fact] + public void GenerateDistributedApplication_WithHostingTypes_KeepsReferenceExpressionInBaseTs() + { + var atsContext = CreateContextFromBothAssemblies(); + + var files = _generator.GenerateDistributedApplication(atsContext); + + Assert.DoesNotContain("export class ReferenceExpression {", files["aspire.ts"]); + Assert.Contains("registerHandleWrapper('Aspire.Hosting/Aspire.Hosting.ApplicationModel.ReferenceExpression'", files["base.ts"]); + } + [Fact] public void GenerateDistributedApplication_WithTestTypes_IncludesCapabilities() { @@ -965,6 +976,19 @@ public void Scanner_InstanceMethod_HasCorrectCapabilityKind() Assert.Equal(AtsCapabilityKind.InstanceMethod, getValueAsync.CapabilityKind); } + [Fact] + public void Scanner_ReferenceExpressionGetValueAsync_IsExported() + { + var capabilities = ScanCapabilitiesFromHostingAssembly(); + + var getValueAsync = capabilities.FirstOrDefault(c => + c.CapabilityId == "Aspire.Hosting.ApplicationModel/getValue" && + c.TargetTypeId == AtsConstants.ReferenceExpressionTypeId); + + Assert.NotNull(getValueAsync); + Assert.Equal(AtsCapabilityKind.InstanceMethod, getValueAsync.CapabilityKind); + } + [Fact] public void Scanner_ExtensionMethod_HasCorrectCapabilityKind() { diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/CapabilityDispatcherTests.cs b/tests/Aspire.Hosting.RemoteHost.Tests/CapabilityDispatcherTests.cs index ed748556d1d..c15546f4c77 100644 --- a/tests/Aspire.Hosting.RemoteHost.Tests/CapabilityDispatcherTests.cs +++ b/tests/Aspire.Hosting.RemoteHost.Tests/CapabilityDispatcherTests.cs @@ -543,6 +543,38 @@ public void Invoke_AsyncInstanceMethod() Assert.Equal("TEST", result.GetValue()); } + [Fact] + public void Invoke_ValueTaskInstanceMethod() + { + var handles = new HandleRegistry(); + var dispatcher = new CapabilityDispatcher(handles, CreateTestMarshaller(handles), [typeof(TestTypeWithMethods).Assembly]); + + var context = new TestTypeWithMethods(); + var handleId = handles.Register(context, "Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.TestTypeWithMethods"); + var args = new JsonObject + { + ["context"] = new JsonObject { ["$handle"] = handleId }, + ["input"] = "value-task" + }; + + var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/TestTypeWithMethods.processValueTaskAsync", args); + + Assert.NotNull(result); + Assert.Equal("VALUE-TASK", result.GetValue()); + } + + [Fact] + public void Invoke_StaticValueTaskMethod() + { + var dispatcher = CreateDispatcher(typeof(TestCapabilities).Assembly); + var args = new JsonObject { ["value"] = "value-task" }; + + var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/asyncValueTaskWithResult", args); + + Assert.NotNull(result); + Assert.Equal("VALUE-TASK", result.GetValue()); + } + [Fact] public void Constructor_SkipsNonPublicMethods() { @@ -678,6 +710,31 @@ public void Invoke_SyncCapabilityWithBackgroundThreadOptIn_RunsOnBackgroundThrea Assert.NotEqual(callerThreadId, result.GetValue()); } + [Fact] + public void Invoke_ValueTaskCapabilityWithBackgroundThreadOptIn_RunsInline() + { + var dispatcher = CreateDispatcher(typeof(TestCapabilitiesWithBackgroundThreadDispatch).Assembly); + var callerThreadId = Environment.CurrentManagedThreadId; + + var result = dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/valueTaskBackgroundThreadProbe", null); + + Assert.NotNull(result); + Assert.Equal(callerThreadId, result.GetValue()); + } + + [Fact] + public void Invoke_NonGenericValueTaskCapabilityWithBackgroundThreadOptIn_RunsInline() + { + var dispatcher = CreateDispatcher(typeof(TestCapabilitiesWithBackgroundThreadDispatch).Assembly); + var callerThreadId = Environment.CurrentManagedThreadId; + + TestCapabilitiesWithBackgroundThreadDispatch.ResetLastObservedThreadId(); + + dispatcher.Invoke("Aspire.Hosting.RemoteHost.Tests/nonGenericValueTaskBackgroundThreadProbe", null); + + Assert.Equal(callerThreadId, TestCapabilitiesWithBackgroundThreadDispatch.LastObservedThreadId); + } + [Fact] public void Invoke_MethodWithoutCallbackFactory_ThrowsForCallbackParameter() { @@ -1351,6 +1408,13 @@ public static async Task AsyncWithResult(string value) return value.ToUpperInvariant(); } + [AspireExport("asyncValueTaskWithResult", Description = "Async method returning ValueTask")] + public static async ValueTask AsyncValueTaskWithResult(string value) + { + await Task.Delay(1); + return value.ToUpperInvariant(); + } + [AspireExport("asyncThrows", Description = "Async method that throws")] public static async Task AsyncThrows(string value) { @@ -1438,6 +1502,12 @@ public async Task ProcessAsync(string input) return input.ToUpperInvariant(); } + public async ValueTask ProcessValueTaskAsync(string input) + { + await Task.Delay(1); + return input.ToUpperInvariant(); + } + // This should NOT be exposed - private method #pragma warning disable IDE0051 // Remove unused private member - testing that private methods are not exposed private void PrivateMethod() @@ -1493,6 +1563,13 @@ public static int WithAsyncCallback(Func> callback) internal static class TestCapabilitiesWithBackgroundThreadDispatch { + public static int LastObservedThreadId { get; private set; } + + public static void ResetLastObservedThreadId() + { + LastObservedThreadId = 0; + } + [AspireExport("syncInlineThreadProbe", Description = "Captures the current thread for inline sync invocation")] public static int SyncInlineThreadProbe() { @@ -1504,6 +1581,19 @@ public static int SyncBackgroundThreadProbe() { return Environment.CurrentManagedThreadId; } + + [AspireExport("valueTaskBackgroundThreadProbe", Description = "Captures the current thread for ValueTask invocation", RunSyncOnBackgroundThread = true)] + public static ValueTask ValueTaskBackgroundThreadProbe() + { + return ValueTask.FromResult(Environment.CurrentManagedThreadId); + } + + [AspireExport("nonGenericValueTaskBackgroundThreadProbe", Description = "Captures the current thread for ValueTask invocation", RunSyncOnBackgroundThread = true)] + public static ValueTask NonGenericValueTaskBackgroundThreadProbe() + { + LastObservedThreadId = Environment.CurrentManagedThreadId; + return ValueTask.CompletedTask; + } } ///