File: Rpc\NativeHandles.cs
Web Access
Project: src\src\Aspire.Hosting.Native.Server\Aspire.Hosting.Native.Server.csproj (Aspire.Hosting.Native.Server)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Security.Cryptography;
using System.Text.Json.Nodes;
using Aspire.Hosting.Native.Api;
 
namespace Aspire.Hosting.Native.Rpc;
 
/// <summary>Owns opaque, typed capability handles within one authenticated connection.</summary>
internal sealed class NativeHandles(int capacity) : IDisposable
{
    private readonly Dictionary<string, Entry> _entries = new(StringComparer.Ordinal);
    private readonly Dictionary<object, string> _identities = new(ReferenceEqualityComparer.Instance);
 
    public int Count => _entries.Count;
 
    public void EnsureCapacity()
    {
        if (_entries.Count >= capacity)
        {
            throw RpcFault.HandleLimit("The connection handle limit was reached.");
        }
    }
 
    public JsonObject Add(object value, string typeId)
    {
        if (!_identities.TryGetValue(value, out var id))
        {
            if (_entries.Count >= capacity)
            {
                // A newly constructed session has no handle yet. Release it
                // rather than leaking ownership when publication is rejected.
                (value as ICapabilityOwner)?.Close();
                throw RpcFault.HandleLimit("The connection handle limit was reached.");
            }
            id = $"{typeId}:{RandomNumberGenerator.GetHexString(32, lowercase: true)}";
            _entries.Add(id, new Entry(value, typeId));
            _identities.Add(value, id);
        }
 
        return new JsonObject { ["$handle"] = id, ["$type"] = typeId };
    }
 
    public T Get<T>(JsonNode? value, string expectedTypeId) where T : class
    {
        if (value is not JsonObject reference)
        {
            throw RpcFault.InvalidHandle("A capability handle is required.");
        }
        RpcArguments.Validate(reference, ["$handle", "$type"]);
        var id = RpcArguments.String(reference["$handle"]);
        var typeId = RpcArguments.String(reference["$type"]);
        if (!_entries.TryGetValue(id, out var entry) || entry.Value is ICapabilityLifetime { IsRevoked: true })
        {
            throw RpcFault.HandleNotFound("The capability handle is unknown or retired.");
        }
        if (typeId != expectedTypeId || entry.TypeId != expectedTypeId || entry.Value is not T typed)
        {
            throw RpcFault.TypeMismatch("The capability handle is not applicable.");
        }
 
        return typed;
    }
 
    public object ReadReference(JsonObject reference, string? expectedTypeId = null)
    {
        RpcArguments.Validate(reference, ["$handle", "$type"]);
        var id = RpcArguments.String(reference["$handle"]);
        var typeId = RpcArguments.String(reference["$type"]);
        if (!_entries.TryGetValue(id, out var entry) || entry.Value is ICapabilityLifetime { IsRevoked: true })
        {
            throw RpcFault.HandleNotFound("The capability handle is unknown or retired.");
        }
        if (typeId != entry.TypeId || expectedTypeId is not null && typeId != expectedTypeId)
        {
            throw RpcFault.TypeMismatch("The capability handle is not applicable.");
        }
 
        return entry.Value;
    }
 
    public void RemoveRevoked()
    {
        // Dictionary.Remove does not invalidate enumeration on modern .NET.
        // Sweep under the connection gate without copying every live handle.
        // https://learn.microsoft.com/dotnet/api/system.collections.generic.dictionary-2.enumerator#remarks
        foreach (var (id, entry) in _entries)
        {
            if (entry.Value is ICapabilityLifetime { IsRevoked: true })
            {
                _entries.Remove(id);
                _identities.Remove(entry.Value);
            }
        }
    }
 
    public void Dispose()
    {
        foreach (var entry in _entries.Values)
        {
            (entry.Value as ICapabilityOwner)?.Close();
        }
        _entries.Clear();
        _identities.Clear();
    }
 
    private sealed record Entry(object Value, string TypeId);
}