File: Rpc\NativeProviderContractProjection.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;
using Aspire.Hosting.Native.Runtime;
 
namespace Aspire.Hosting.Native.Rpc;
 
/// <summary>Validates parsed contracts against native type, receiver and lifecycle rules before registration.</summary>
internal sealed class NativeProviderContractProjection(NativeProviderCoreContract coreContract)
{
    public IReadOnlySet<string> CoreCapabilities { get; } = coreContract.Capabilities;
    public IReadOnlySet<string> CoreHandles { get; } = coreContract.Handles;
 
    public static void ValidatePackageName(string packageName)
    {
        if (string.IsNullOrWhiteSpace(packageName) || packageName.Length > 128 ||
            packageName.Any(character => !(char.IsAsciiLetterOrDigit(character) || character is '.' or '-' or '_')))
        {
            throw RpcFault.InvalidArgument("A portable provider package identity is required.");
        }
    }
 
    public NativeProviderManifest Project(string packageName, IReadOnlyList<NativeProviderMetadataCapability> capabilities)
    {
        var projected = new Dictionary<string, NativeProviderCapability>(StringComparer.Ordinal);
        var ownTypes = new HashSet<string>(StringComparer.Ordinal);
        foreach (var metadata in capabilities)
        {
            var id = metadata.Id;
            if (!id.StartsWith(packageName + "/", StringComparison.Ordinal) || id.Length > 256 ||
                string.IsNullOrWhiteSpace(metadata.Method) || projected.ContainsKey(id))
            {
                throw RpcFault.ProviderConflict("The provider capability identity is duplicate or outside its package.");
            }
            var resultType = ProjectType(metadata.ReturnType);
            var arguments = new List<NativeProviderParameter>();
            var names = new HashSet<string>(StringComparer.Ordinal);
            foreach (var parameter in metadata.Parameters)
            {
                if (!names.Add(parameter.Name) || string.IsNullOrWhiteSpace(parameter.Name) || parameter.IsOptional || parameter.IsNullable)
                {
                    throw RpcFault.ProviderMetadataInvalid("Provider arguments require distinct required names and types.");
                }
                if (parameter.Callback is { } callback)
                {
                    var callbackResult = ProjectType(callback.ReturnType);
                    if (callbackResult.Kind == NativeProviderValueKind.Void)
                    {
                        throw RpcFault.ProviderMetadataUnsupported("Provider callbacks require a result type for writeback.");
                    }
                    arguments.Add(new(parameter.Name, new("callback", NativeProviderValueKind.Callback),
                        new(callback.Parameters.Select(ProjectType).ToArray(), callbackResult)));
                }
                else
                {
                    arguments.Add(new(parameter.Name, ProjectType(parameter.Type), null));
                }
            }
            var targetType = metadata.TargetTypeId;
            var receiver = metadata.TargetParameterName;
            if (targetType is not null)
            {
                var target = arguments.SingleOrDefault(parameter => parameter.Name == receiver);
                if (target is null || target.Type.TypeId != targetType || target.Type.Kind != NativeProviderValueKind.Handle)
                {
                    throw RpcFault.ProviderMetadataInvalid("The provider receiver must match its projected handle parameter.");
                }
            }
            var phase = metadata.LifecyclePhase;
            if (phase is not null && (phase is not (ProviderLifecyclePhases.Initialize or ProviderLifecyclePhases.Allocated or ProviderLifecyclePhases.Ready or ProviderLifecyclePhases.Retiring) ||
                targetType is null || CoreHandles.Contains(targetType) || arguments.Count != 2 ||
                arguments.Any(parameter => parameter.Callback is not null) ||
                arguments.SingleOrDefault(parameter => parameter.Name == "scope")?.Type.TypeId != NativeRpcContract.LifecycleScopeTypeId ||
                resultType.Kind != NativeProviderValueKind.Void))
            {
                throw RpcFault.ProviderMetadataInvalid("Lifecycle metadata requires an owned receiver, one native scope and a void result.");
            }
            // Preserve opaque discovery extensions for clients without granting
            // authority to any types mentioned outside the validated signature.
            projected.Add(id, new(id, arguments.ToArray(), resultType, targetType, receiver, phase, metadata.Wire.DeepClone().AsObject()));
        }
        if (projected.Values.Where(capability => capability.LifecyclePhase is not null)
            .GroupBy(capability => (capability.TargetTypeId, capability.LifecyclePhase)).Any(group => group.Count() > 1))
        {
            throw RpcFault.ProviderMetadataInvalid("A provider resource phase cannot have duplicate capabilities.");
        }
 
        return new(projected, ownTypes);
 
        NativeProviderType ProjectType(NativeProviderMetadataType metadataType)
        {
            var type = ProjectSupportedType(metadataType, packageName);
            if (type.Kind == NativeProviderValueKind.Handle && !CoreHandles.Contains(type.TypeId))
            {
                ownTypes.Add(type.TypeId);
            }
 
            return type;
        }
    }
 
    private NativeProviderType ProjectSupportedType(NativeProviderMetadataType type, string packageName)
    {
        var id = type.TypeId;
        if (type.Category == NativeProviderTypeCategory.Handle && (CoreHandles.Contains(id) ||
            id.StartsWith(packageName + "/", StringComparison.Ordinal) && id.Length <= 256))
        {
            return new(id, NativeProviderValueKind.Handle);
        }
        var kind = type.Category == NativeProviderTypeCategory.Primitive ? id switch
        {
            "void" => NativeProviderValueKind.Void,
            "string" => NativeProviderValueKind.String,
            "bool" => NativeProviderValueKind.Boolean,
            "int" => NativeProviderValueKind.Int32,
            "long" => NativeProviderValueKind.Int64,
            _ => (NativeProviderValueKind?)null
        } : null;
        if (kind is null)
        {
            throw RpcFault.ProviderMetadataUnsupported("Provider contracts support primitive values, typed handles and projected callbacks only.");
        }
 
        return new(id, kind.Value);
    }
}
 
internal enum NativeProviderValueKind
{
    Void,
    String,
    Boolean,
    Int32,
    Int64,
    Handle,
    Callback
}
internal sealed record NativeProviderType(string TypeId, NativeProviderValueKind Kind);
internal sealed record NativeProviderCallback(NativeProviderType[] Parameters, NativeProviderType ReturnType);
internal sealed record NativeProviderParameter(string Name, NativeProviderType Type, NativeProviderCallback? Callback);
internal sealed record NativeProviderCapability(string Id, NativeProviderParameter[] Parameters, NativeProviderType ReturnType,
    string? TargetTypeId, string? TargetParameterName, string? LifecyclePhase, JsonObject Metadata);
internal sealed record NativeProviderManifest(Dictionary<string, NativeProviderCapability> Capabilities, HashSet<string> OwnedTypes);
internal sealed record NativeProviderHandle(string Id, JsonObject Wire, JsonObject? Lifetime);