File: DistributedApplicationEntryPointInvoker.cs
Web Access
Project: src\src\Aspire.Hosting.Testing\Aspire.Hosting.Testing.csproj (Aspire.Hosting.Testing)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Reflection;
using Microsoft.Extensions.Hosting;
 
namespace Aspire.Hosting.Testing;
 
internal static class DistributedApplicationEntryPointInvoker
{
    // This helpers encapsulates all of the complex logic required to:
    // 1. Execute the entry point of the specified assembly in a different thread.
    // 2. Wait for the diagnostic source events to fire
    // 3. Give the caller a chance to execute logic to mutate the IDistributedApplicationBuilder
    // 4. Resolve the instance of the DistributedApplication
    // 5. Allow the caller to determine if the entry point has completed
    public static Func<string[], CancellationToken, Task<DistributedApplication>>? ResolveEntryPoint(
        Assembly assembly,
        Action<DistributedApplicationOptions, HostApplicationBuilderSettings>? onConstructing = null,
        Action<DistributedApplicationBuilder>? onConstructed = null,
        Action<DistributedApplicationBuilder>? onBuilding = null,
        Action<Exception?>? entryPointCompleted = null)
    {
        if (assembly.EntryPoint is null)
        {
            return null;
        }
 
        return async (args, ct) =>
        {
            var invoker = new EntryPointInvoker(
                args,
                assembly.EntryPoint,
                onConstructing,
                onConstructed,
                onBuilding,
                entryPointCompleted);
            return await invoker.InvokeAsync(ct).ConfigureAwait(false);
        };
    }
 
    private sealed class EntryPointInvoker : IObserver<DiagnosticListener>
    {
        private static readonly AsyncLocal<EntryPointInvoker> s_currentListener = new();
        private readonly string[] _args;
        private readonly MethodInfo _entryPoint;
        private readonly TaskCompletionSource<DistributedApplication> _appTcs = new();
        private readonly ApplicationBuilderDiagnosticListener _applicationBuilderListener;
        private readonly Action<DistributedApplicationOptions, HostApplicationBuilderSettings>? _onConstructing;
        private readonly Action<DistributedApplicationBuilder>? _onConstructed;
        private readonly Action<DistributedApplicationBuilder>? _onBuilding;
        private readonly Action<Exception?>? _entryPointCompleted;
 
        public EntryPointInvoker(
            string[] args,
            MethodInfo entryPoint,
            Action<DistributedApplicationOptions, HostApplicationBuilderSettings>? onConstructing,
            Action<DistributedApplicationBuilder>? onConstructed,
            Action<DistributedApplicationBuilder>? onBuilding,
            Action<Exception?>? entryPointCompleted)
        {
            _args = args;
            _entryPoint = entryPoint;
            _onConstructing = onConstructing;
            _onConstructed = onConstructed;
            _onBuilding = onBuilding;
            _entryPointCompleted = entryPointCompleted;
            _applicationBuilderListener = new(this);
        }
 
        public async Task<DistributedApplication> InvokeAsync(CancellationToken cancellationToken)
        {
            using var subscription = DiagnosticListener.AllListeners.Subscribe(this);
 
            // Kick off the entry point on a new thread so we don't block the current one
            // in case we need to timeout the execution
            var thread = new Thread(() =>
            {
                Exception? exception = null;
 
                try
                {
                    // Set the async local to the instance of the HostingListener so we can filter events that
                    // aren't scoped to this execution of the entry point.
                    s_currentListener.Value = this;
 
                    var parameters = _entryPoint.GetParameters();
                    object? result;
                    if (parameters.Length == 0)
                    {
                        result = _entryPoint.Invoke(null, []);
                    }
                    else
                    {
                        result = _entryPoint.Invoke(null, [_args]);
                    }
 
                    // Try to set an exception if the entry point returns gracefully, this will force
                    // build to throw
                    _appTcs.TrySetException(new InvalidOperationException($"The entry point exited without building a {nameof(DistributedApplication)}."));
                }
                catch (TargetInvocationException tie)
                {
                    exception = tie.InnerException ?? tie;
 
                    // Another exception happened, propagate that to the caller
                    _appTcs.TrySetException(exception);
                }
                catch (Exception ex)
                {
                    exception = ex;
 
                    // Another exception happened, propagate that to the caller
                    _appTcs.TrySetException(exception);
                }
                finally
                {
                    // Signal that the entry point is completed
                    _entryPointCompleted?.Invoke(exception);
                }
            })
            {
                // Make sure this doesn't hang the process
                IsBackground = true,
                Name = $"{_entryPoint.DeclaringType?.Assembly.GetName().Name ?? "Unknown"}.EntryPoint"
            };
 
            // Start the thread
            thread.Start();
 
            return await _appTcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
        }
 
        public void OnCompleted()
        {
        }
 
        public void OnError(Exception error)
        {
 
        }
 
        public void OnNext(DiagnosticListener value)
        {
            if (s_currentListener.Value != this)
            {
                // Ignore events that aren't for this listener
                return;
            }
 
            if (value.Name == "Aspire.Hosting")
            {
                _applicationBuilderListener.Subscribe(value);
            }
        }
 
        private sealed class ApplicationBuilderDiagnosticListener(EntryPointInvoker owner) : IObserver<KeyValuePair<string, object?>>
        {
            private IDisposable? _disposable;
 
            public void Subscribe(DiagnosticListener listener)
            {
                _disposable = listener.Subscribe(this);
            }
 
            public void OnCompleted()
            {
                _disposable?.Dispose();
            }
 
            public void OnError(Exception error)
            {
            }
 
            public void OnNext(KeyValuePair<string, object?> value)
            {
                if (s_currentListener.Value != owner)
                {
                    // Ignore events that aren't for this listener
                    return;
                }
 
                if (value.Key == "DistributedApplicationBuilderConstructing")
                {
                    var args = ((DistributedApplicationOptions Options, HostApplicationBuilderSettings InnerBuilderOptions))value.Value!;
                    owner._onConstructing?.Invoke(args.Options, args.InnerBuilderOptions);
                }
 
                if (value.Key == "DistributedApplicationBuilderConstructed")
                {
                    owner._onConstructed?.Invoke((DistributedApplicationBuilder)value.Value!);
                }
 
                if (value.Key == "DistributedApplicationBuilding")
                {
                    owner._onBuilding?.Invoke((DistributedApplicationBuilder)value.Value!);
                }
 
                if (value.Key == "DistributedApplicationBuilt")
                {
                    owner._appTcs.TrySetResult((DistributedApplication)value.Value!);
                }
            }
        }
    }
}