File: Ats\HandleRegistry.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.Collections.Concurrent;
using System.Globalization;
using System.Text.Json.Nodes;
 
namespace Aspire.Hosting.RemoteHost.Ats;
 
/// <summary>
/// Manages registration and lookup of ATS handles for capability dispatch.
/// Handles are opaque typed references with IDs in the format: {typeId}:{instanceId}
/// </summary>
internal sealed class HandleRegistry : IAsyncDisposable
{
    private readonly ConcurrentDictionary<string, HandleEntry> _handles = new();
    private long _idCounter;
 
    /// <summary>
    /// Represents a registered handle entry.
    /// </summary>
    private sealed class HandleEntry
    {
        public required object Object { get; init; }
        public required string TypeId { get; init; }
        public required long InstanceId { get; init; }
    }
 
    /// <summary>
    /// Registers an object as a handle with the specified ATS type ID.
    /// </summary>
    /// <param name="obj">The object to register.</param>
    /// <param name="typeId">The ATS type ID (e.g., "aspire.redis/RedisBuilder").</param>
    /// <returns>The handle ID (just the instance number).</returns>
    public string Register(object obj, string typeId)
    {
        var instanceId = Interlocked.Increment(ref _idCounter);
        var handleId = instanceId.ToString(CultureInfo.InvariantCulture);
 
        _handles[handleId] = new HandleEntry
        {
            Object = obj,
            TypeId = typeId,
            InstanceId = instanceId
        };
 
        return handleId;
    }
 
    /// <summary>
    /// Tries to get a handle entry by its ID.
    /// </summary>
    /// <param name="handleId">The handle ID.</param>
    /// <param name="obj">The underlying object if found.</param>
    /// <param name="typeId">The ATS type ID if found.</param>
    /// <returns>True if the handle was found, false otherwise.</returns>
    public bool TryGet(string handleId, out object? obj, out string? typeId)
    {
        if (_handles.TryGetValue(handleId, out var entry))
        {
            obj = entry.Object;
            typeId = entry.TypeId;
            return true;
        }
 
        obj = null;
        typeId = null;
        return false;
    }
 
    /// <summary>
    /// Gets the underlying object for a handle.
    /// </summary>
    /// <param name="handleId">The handle ID.</param>
    /// <returns>The underlying object.</returns>
    /// <exception cref="InvalidOperationException">Thrown if the handle is not found.</exception>
    public object GetObject(string handleId)
    {
        if (!_handles.TryGetValue(handleId, out var entry))
        {
            throw new InvalidOperationException($"Handle '{handleId}' not found in registry");
        }
        return entry.Object;
    }
 
    /// <summary>
    /// Gets the underlying object for a handle, cast to the specified type.
    /// </summary>
    /// <typeparam name="T">The expected type.</typeparam>
    /// <param name="handleId">The handle ID.</param>
    /// <returns>The underlying object.</returns>
    /// <exception cref="InvalidOperationException">Thrown if the handle is not found or type doesn't match.</exception>
    public T GetObject<T>(string handleId) where T : class
    {
        var obj = GetObject(handleId);
        if (obj is not T typed)
        {
            throw new InvalidOperationException(
                $"Handle '{handleId}' contains {obj.GetType().FullName}, expected {typeof(T).FullName}");
        }
        return typed;
    }
 
    /// <summary>
    /// Gets the ATS type ID for a handle.
    /// </summary>
    /// <param name="handleId">The handle ID.</param>
    /// <returns>The ATS type ID.</returns>
    /// <exception cref="InvalidOperationException">Thrown if the handle is not found.</exception>
    public string GetTypeId(string handleId)
    {
        if (!_handles.TryGetValue(handleId, out var entry))
        {
            throw new InvalidOperationException($"Handle '{handleId}' not found in registry");
        }
        return entry.TypeId;
    }
 
    /// <summary>
    /// Checks if a handle exists in the registry.
    /// </summary>
    /// <param name="handleId">The handle ID.</param>
    /// <returns>True if the handle exists, false otherwise.</returns>
    public bool Contains(string handleId)
    {
        return _handles.ContainsKey(handleId);
    }
 
    /// <summary>
    /// Unregisters a handle from the registry.
    /// </summary>
    /// <param name="handleId">The handle ID to unregister.</param>
    /// <returns>True if the handle was removed, false if it wasn't found.</returns>
    public bool Unregister(string handleId)
    {
        return _handles.TryRemove(handleId, out _);
    }
 
    /// <summary>
    /// Marshals an object to a JSON handle reference.
    /// </summary>
    /// <param name="obj">The object to marshal.</param>
    /// <param name="typeId">The ATS type ID.</param>
    /// <returns>A JsonObject containing the handle reference.</returns>
    public JsonObject Marshal(object obj, string typeId)
    {
        var handleId = Register(obj, typeId);
        return new JsonObject
        {
            ["$handle"] = handleId,
            ["$type"] = typeId
        };
    }
 
    /// <summary>
    /// Gets the count of registered handles.
    /// </summary>
    public int Count => _handles.Count;
 
    /// <summary>
    /// Disposes all disposable objects in the registry and clears it.
    /// </summary>
    public async ValueTask DisposeAsync()
    {
        var entries = _handles.Values.ToList();
        _handles.Clear();
 
        foreach (var entry in entries)
        {
            if (entry.Object is IAsyncDisposable asyncDisposable)
            {
                await asyncDisposable.DisposeAsync().ConfigureAwait(false);
            }
            else if (entry.Object is IDisposable disposable)
            {
                disposable.Dispose();
            }
        }
    }
}
 
/// <summary>
/// Reference to an ATS handle. Used when passing handles as arguments.
/// JSON shape: { "$handle": "42", "$type": "Aspire.Hosting.Redis/..." }
/// </summary>
internal sealed class HandleRef
{
    /// <summary>
    /// The handle identifier.
    /// </summary>
    public required string HandleId { get; init; }
 
    /// <summary>
    /// Creates a HandleRef from a JSON node if it contains a $handle property.
    /// </summary>
    public static HandleRef? FromJsonNode(JsonNode? node)
    {
        if (node is JsonObject obj && obj.TryGetPropertyValue("$handle", out var handleNode))
        {
            var handleId = handleNode?.GetValue<string>();
            if (!string.IsNullOrEmpty(handleId))
            {
                return new HandleRef { HandleId = handleId };
            }
        }
        return null;
    }
 
    /// <summary>
    /// Checks if a JSON node is a handle reference.
    /// </summary>
    public static bool IsHandleRef(JsonNode? node)
    {
        return node is JsonObject obj && obj.ContainsKey("$handle");
    }
}