File: TestServices\LifecycleProviderTestIntegration.cs
Web Access
Project: src\tests\Aspire.Hosting.Native.Core.Tests\Aspire.Hosting.Native.Core.Tests.csproj (Aspire.Hosting.Native.Core.Tests)
// 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.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Hosting.Native.Client;
 
namespace Aspire.Hosting.Native.Core.Tests.TestServices;
 
/// <summary>Exercises the portable provider contract over actual sockets, independently of native runtime implementation types.</summary>
internal sealed class LifecycleProviderTestIntegration
{
    private const string Package = "example.lifecycle";
    private const string ResourceType = Package + "/Resource";
    private const string CoreType = "Aspire.Hosting.Native.Server/Aspire.Hosting.Native.Api.";
    public ConcurrentQueue<LifecycleProviderTestResource> Resources { get; } = new();
    public ConcurrentQueue<(LifecycleProviderTestResource Resource, string Phase)> Phases { get; } = new();
    public ConcurrentQueue<ResourceLifecycleScope> Scopes { get; } = new();
    public Func<string, ResourceLifecycleScope, CancellationToken, Task>? OnPhase { get; set; }
    public Func<LifecycleProviderTestResource, LifecycleProviderTestResource, CancellationToken, Task>? OnInvocation { get; set; }
    public NativeProviderContext? Context { get; private set; }
 
    public NativeProviderDefinition Definition
    {
        get
        {
            static JsonObject Type(string id, string category = "Handle") => new() { ["typeId"] = id, ["category"] = category };
            static JsonObject Parameter(string name, JsonObject type) => new() { ["name"] = name, ["type"] = type };
            var methods = new JsonArray
            {
                new JsonObject
                {
                    ["id"] = Package + "/add", ["method"] = "add",
                    ["parameters"] = new JsonArray(Parameter("composition", Type(CoreType + "Composition")),
                        Parameter("password", Type(CoreType + "DeclaredValue")), Parameter("image", Type("string", "Primitive"))),
                    ["returnType"] = Type(ResourceType)
                }
            };
            foreach (var phase in new[] { "initialize", "allocated", "ready", "retiring" })
            {
                methods.Add(new JsonObject
                {
                    ["id"] = Package + "/" + phase,
                    ["method"] = phase,
                    ["lifecyclePhase"] = phase,
                    ["targetTypeId"] = ResourceType,
                    ["targetParameterName"] = "context",
                    ["parameters"] = new JsonArray(Parameter("context", Type(ResourceType)), Parameter("scope", Type(CoreType + "ResourceLifecycleScope"))),
                    ["returnType"] = Type("void", "Primitive")
                });
            }
            methods.Add(new JsonObject
            {
                ["id"] = Package + "/invoke",
                ["method"] = "invoke",
                ["targetTypeId"] = ResourceType,
                ["targetParameterName"] = "context",
                ["parameters"] = new JsonArray(Parameter("context", Type(ResourceType)), Parameter("other", Type(ResourceType))),
                ["returnType"] = Type("void", "Primitive")
            });
            var manifest = new JsonObject { ["protocolVersion"] = 2, ["capabilities"] = methods };
 
            return new(JsonSerializer.SerializeToElement(manifest), DispatchAsync);
        }
    }
 
    private async Task<JsonElement> DispatchAsync(NativeProviderContext context, string capability, JsonElement arguments)
    {
        Context = context;
        if (capability == Package + "/add")
        {
            var composition = new Composition(context.Client, arguments.GetProperty("composition"));
            var declaration = await composition.AddContainerAsync("service", arguments.GetProperty("image").GetString()!);
            await declaration.WithEnvironmentAsync("TOKEN", new DeclaredValue(context.Client, arguments.GetProperty("password")));
            var resource = new LifecycleProviderTestResource(declaration);
            Resources.Enqueue(resource);
 
            return context.PublishResource(resource, ResourceType);
        }
        var target = context.ReadResource<LifecycleProviderTestResource>(arguments.GetProperty("context"));
        if (capability == Package + "/invoke")
        {
            var other = context.ReadResource<LifecycleProviderTestResource>(arguments.GetProperty("other"));
            if (OnInvocation is { } invocation)
            {
                await invocation(target, other, context.CancellationToken);
            }
 
            return JsonSerializer.SerializeToElement((string?)null);
        }
        var scope = new ResourceLifecycleScope(context.Client, arguments.GetProperty("scope"));
        var phase = await scope.ReadLifecyclePhaseAsync(context.CancellationToken);
        Phases.Enqueue((target, phase));
        Scopes.Enqueue(scope);
        if (OnPhase is { } handler)
        {
            await handler(phase, scope, context.CancellationToken);
        }
 
        return JsonSerializer.SerializeToElement((string?)null);
    }
 
    public static Task<JsonElement> AddAsync(Composition composition, DeclaredValue password, string image) =>
        composition.Client.InvokeAsync(Package + "/add", new()
        {
            ["composition"] = composition.Handle,
            ["password"] = password.Handle,
            ["image"] = JsonSerializer.SerializeToElement(image)
        }, CancellationToken.None);
}
 
internal sealed class LifecycleProviderTestResource(DeclaredResource declaration) : NativeIntegrationResource(declaration)
{
    private int _disposeCount;
    public int DisposeCount => Volatile.Read(ref _disposeCount);
    public TaskCompletionSource Disposed { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
    public override ValueTask DisposeAsync()
    {
        Interlocked.Increment(ref _disposeCount);
        Disposed.TrySetResult();
 
        return ValueTask.CompletedTask;
    }
}