File: RemoteAppHostService.cs
Web Access
Project: src\src\Aspire.Hosting.RemoteHost\Aspire.Hosting.RemoteHost.csproj (Aspire.Hosting.RemoteHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json.Nodes;
using Aspire.Hosting.Ats;
using Aspire.Hosting.RemoteHost.Ats;
using Microsoft.Extensions.Logging;
using StreamJsonRpc;
 
namespace Aspire.Hosting.RemoteHost;
 
internal sealed class RemoteAppHostService
{
    private readonly JsonRpcCallbackInvoker _callbackInvoker;
    private readonly CancellationTokenRegistry _cancellationTokenRegistry;
    private readonly ILogger<RemoteAppHostService> _logger;
 
    // ATS (Aspire Type System) components
    private readonly CapabilityDispatcher _capabilityDispatcher;
 
    public RemoteAppHostService(
        JsonRpcCallbackInvoker callbackInvoker,
        CancellationTokenRegistry cancellationTokenRegistry,
        CapabilityDispatcher capabilityDispatcher,
        ILogger<RemoteAppHostService> logger)
    {
        _callbackInvoker = callbackInvoker;
        _cancellationTokenRegistry = cancellationTokenRegistry;
        _capabilityDispatcher = capabilityDispatcher;
        _logger = logger;
    }
 
    /// <summary>
    /// Sets the JSON-RPC connection for callback invocation.
    /// </summary>
    public void SetClientConnection(JsonRpc clientRpc)
    {
        _callbackInvoker.SetConnection(clientRpc);
    }
 
    [JsonRpcMethod("ping")]
#pragma warning disable CA1822 // Mark members as static - JSON-RPC methods must be instance methods
    public string Ping()
#pragma warning restore CA1822
    {
        return "pong";
    }
 
    /// <summary>
    /// Cancels a CancellationToken by its ID.
    /// Called by the guest when an AbortSignal is aborted.
    /// </summary>
    /// <param name="tokenId">The token ID returned from capability invocation.</param>
    /// <returns>True if the token was found and cancelled, false otherwise.</returns>
    [JsonRpcMethod("cancelToken")]
    public bool CancelToken(string tokenId)
    {
        _logger.LogDebug("cancelToken({TokenId})", tokenId);
        return _cancellationTokenRegistry.Cancel(tokenId);
    }
 
    #region ATS Capabilities
 
    /// <summary>
    /// Invokes an ATS capability by ID.
    /// </summary>
    /// <param name="capabilityId">The capability ID (e.g., "aspire.redis/addRedis@1").</param>
    /// <param name="args">The arguments as a JSON object.</param>
    /// <returns>The result as JSON, or an error object.</returns>
    [JsonRpcMethod("invokeCapability")]
    public async Task<JsonNode?> InvokeCapabilityAsync(string capabilityId, JsonObject? args)
    {
        _logger.LogDebug(">> invokeCapability({CapabilityId}) args: {Args}", capabilityId, args?.ToJsonString() ?? "null");
        var sw = System.Diagnostics.Stopwatch.StartNew();
        try
        {
            var result = await _capabilityDispatcher.InvokeAsync(capabilityId, args).ConfigureAwait(false);
            _logger.LogDebug("   invokeCapability({CapabilityId}) result: {Result}", capabilityId, result?.ToJsonString() ?? "null");
            return result;
        }
        catch (CapabilityException ex)
        {
            _logger.LogWarning("   invokeCapability({CapabilityId}) CapabilityException: {Code} - {Message}", capabilityId, ex.Error.Code, ex.Error.Message);
            if (ex.Error.Details != null)
            {
                _logger.LogWarning("   Details: param={Parameter}, expected={Expected}, actual={Actual}", ex.Error.Details.Parameter, ex.Error.Details.Expected, ex.Error.Details.Actual);
            }
            // Return structured error
            return new JsonObject
            {
                ["$error"] = ex.Error.ToJsonObject()
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "   invokeCapability({CapabilityId}) Exception: {ExceptionType} - {Message}", capabilityId, ex.GetType().Name, ex.Message);
            // Wrap unexpected errors
            var error = new AtsError
            {
                Code = AtsErrorCodes.InternalError,
                Message = ex.Message,
                Capability = capabilityId
            };
            return new JsonObject
            {
                ["$error"] = error.ToJsonObject()
            };
        }
        finally
        {
            _logger.LogDebug("<< invokeCapability({CapabilityId}) completed in {ElapsedMs}ms", capabilityId, sw.ElapsedMilliseconds);
        }
    }
 
    #endregion
}