File: Rpc\NativeProviderMetadata.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.Text.Json.Nodes;
 
namespace Aspire.Hosting.Native.Rpc;
 
internal enum NativeProviderTypeCategory { Primitive, Handle, Callback, Unsupported }
internal sealed record NativeProviderMetadataType(string TypeId, NativeProviderTypeCategory Category);
internal sealed record NativeProviderMetadataCallback(NativeProviderMetadataType[] Parameters, NativeProviderMetadataType ReturnType);
internal sealed record NativeProviderMetadataParameter(string Name, NativeProviderMetadataType Type, bool IsOptional, bool IsNullable,
    NativeProviderMetadataCallback? Callback);
internal sealed record NativeProviderMetadataCapability(string Id, string Method, NativeProviderMetadataParameter[] Parameters,
    NativeProviderMetadataType ReturnType, string? TargetTypeId, string? TargetParameterName, string? LifecyclePhase, JsonObject Wire);
internal sealed record NativeProviderCoreContract(IReadOnlySet<string> Capabilities, IReadOnlySet<string> Handles);
 
/// <summary>Reads the case-sensitive provider wire schema once, before semantic projection and authority resolution.</summary>
internal static class NativeProviderMetadata
{
    public static NativeProviderCoreContract ReadCoreContract(JsonObject contract)
    {
        // The embedded ATS contract uses {"Capabilities":[{"CapabilityId":"..."}],
        // "HandleTypes":[{"AtsTypeId":"..."}]}, unlike the portable provider envelope.
        return new(
            contract["Capabilities"]!.AsArray().Select(value => value!["CapabilityId"]!.GetValue<string>()).ToHashSet(StringComparer.Ordinal),
            contract["HandleTypes"]!.AsArray().Select(value => value!["AtsTypeId"]!.GetValue<string>()).ToHashSet(StringComparer.Ordinal));
    }
 
    public static NativeProviderMetadataCapability[] Read(JsonNode? manifest)
    {
        // {"protocolVersion":2,"capabilities":[{"id":"package/method","method":"method",
        // "parameters":[{"name":"value","type":{"typeId":"string","category":"Primitive"}}],
        // "returnType":{"typeId":"void","category":"Primitive"}}]}.
        // Categories and primitive IDs are exact protocol identifiers, not CLR names.
        if (manifest is not JsonObject envelope || envelope["protocolVersion"] is not JsonValue version ||
            !version.TryGetValue<int>(out var number) || number != 2 ||
            envelope["capabilities"] is not JsonArray { Count: > 0 and <= 128 } capabilities)
        {
            throw RpcFault.ProviderMetadataInvalid("Provider metadata requires protocol version 2 and bounded capabilities.");
        }
        var result = new NativeProviderMetadataCapability[capabilities.Count];
        for (var index = 0; index < result.Length; index++)
        {
            if (capabilities[index] is not JsonObject metadata || metadata["id"] is not JsonValue ||
                metadata["parameters"] is not JsonArray { Count: <= 32 } parameters ||
                metadata["returnType"] is not JsonObject returnType)
            {
                throw RpcFault.ProviderMetadataInvalid("Provider capability projection is incomplete.");
            }
            result[index] = new(RpcArguments.String(metadata["id"]), RpcArguments.String(metadata["method"]),
                parameters.Select(ReadParameter).ToArray(), ReadType(returnType), OptionalString(metadata, "targetTypeId"),
                OptionalString(metadata, "targetParameterName"), OptionalString(metadata, "lifecyclePhase"), metadata);
        }
 
        return result;
    }
 
    private static NativeProviderMetadataParameter ReadParameter(JsonNode? parameter)
    {
        if (parameter is not JsonObject argument || argument["type"] is not JsonObject type)
        {
            throw RpcFault.ProviderMetadataInvalid("Provider arguments require distinct required names and types.");
        }
        var name = RpcArguments.String(argument["name"]);
        var optional = ReadFlag(argument, "isOptional");
        var nullable = ReadFlag(argument, "isNullable");
        if (!ReadFlag(argument, "isCallback"))
        {
            return new(name, ReadType(type), optional, nullable, null);
        }
        if (type["category"] is not JsonValue category || !category.TryGetValue<string>(out var categoryName) || categoryName != "Callback" ||
            argument["callbackParameters"] is not JsonArray { Count: <= 16 } callbackParameters ||
            argument["callbackReturnType"] is not JsonObject callbackReturn)
        {
            throw RpcFault.ProviderMetadataInvalid("Callback projection is incomplete.");
        }
        var callbackTypes = new NativeProviderMetadataType[callbackParameters.Count];
        for (var index = 0; index < callbackTypes.Length; index++)
        {
            if (callbackParameters[index] is not JsonObject projectedParameter || projectedParameter["type"] is not JsonObject projectedType)
            {
                throw RpcFault.ProviderMetadataInvalid("Callback projection is incomplete.");
            }
            callbackTypes[index] = ReadType(projectedType);
        }
 
        return new(name, new("callback", NativeProviderTypeCategory.Callback), optional, nullable, new(callbackTypes, ReadType(callbackReturn)));
    }
 
    private static NativeProviderMetadataType ReadType(JsonObject type) =>
        new(RpcArguments.String(type["typeId"]), RpcArguments.String(type["category"]) switch
        {
            "Primitive" => NativeProviderTypeCategory.Primitive,
            "Handle" => NativeProviderTypeCategory.Handle,
            "Callback" => NativeProviderTypeCategory.Callback,
            _ => NativeProviderTypeCategory.Unsupported
        });
 
    private static string? OptionalString(JsonObject metadata, string name) =>
        metadata[name] is { } value ? RpcArguments.String(value) : null;
 
    private static bool ReadFlag(JsonObject metadata, string name)
    {
        if (metadata[name] is null)
        {
            return false;
        }
        if (metadata[name] is JsonValue value && value.TryGetValue<bool>(out var flag))
        {
            return flag;
        }
 
        throw RpcFault.ProviderMetadataInvalid("Provider parameter flags must be Boolean values.");
    }
}