File: TestProgram.cs
Web Access
Project: src\tests\testproject\TestProject.AppHost\TestProject.AppHost.csproj (TestProject.AppHost)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Testing;
using Aspire.TestProject;
using Microsoft.Extensions.DependencyInjection;
 
public class TestProgram : IDisposable
{
    private const string AspireTestContainerRegistry = "netaspireci.azurecr.io";
 
    private TestProgram(
        string[] args,
        string assemblyName,
        bool disableDashboard,
        bool includeIntegrationServices,
        bool allowUnsecuredTransport,
        bool randomizePorts)
    {
        TestResourceNames resourcesToSkip = TestResourceNames.None;
        for (int i = 0; i < args.Length; i++)
        {
            if (args[i].StartsWith("--skip-resources", StringComparison.InvariantCultureIgnoreCase))
            {
                if (args.Length > i + 1)
                {
                    resourcesToSkip = TestResourceNamesExtensions.Parse(args[i + 1].Split(','));
                    break;
                }
                else
                {
                    throw new ArgumentException("Missing argument to --skip-resources option.");
                }
            }
        }
        if (resourcesToSkip.HasFlag(TestResourceNames.dashboard))
        {
            disableDashboard = true;
        }
 
        var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions()
        {
            Args = args,
            DisableDashboard = disableDashboard,
            AssemblyName = assemblyName,
            AllowUnsecuredTransport = allowUnsecuredTransport,
        });
 
        builder.Configuration["DcpPublisher:DeleteResourcesOnShutdown"] = "true";
        builder.Configuration["DcpPublisher:ResourceNameSuffix"] = $"{Random.Shared.Next():x}";
        builder.Configuration["DcpPublisher:RandomizePorts"] = randomizePorts.ToString(CultureInfo.InvariantCulture);
 
        AppBuilder = builder;
 
        var serviceAPath = Path.Combine(Projects.TestProject_AppHost.ProjectPath, @"..\TestProject.ServiceA\TestProject.ServiceA.csproj");
 
        ServiceABuilder = AppBuilder.AddProject("servicea", serviceAPath, launchProfileName: "http");
        ServiceBBuilder = AppBuilder.AddProject<Projects.ServiceB>("serviceb", launchProfileName: "http");
        ServiceCBuilder = AppBuilder.AddProject<Projects.ServiceC>("servicec", launchProfileName: "http");
        WorkerABuilder = AppBuilder.AddProject<Projects.WorkerA>("workera");
 
        if (includeIntegrationServices)
        {
            IntegrationServiceABuilder = AppBuilder.AddProject<Projects.IntegrationServiceA>("integrationservicea");
            IntegrationServiceABuilder = IntegrationServiceABuilder.WithEnvironment("SKIP_RESOURCES", string.Join(',', resourcesToSkip));
 
            if (!resourcesToSkip.HasFlag(TestResourceNames.redis))
            {
                var redis = AppBuilder.AddRedis("redis")
                    .WithImageRegistry(AspireTestContainerRegistry);
                IntegrationServiceABuilder = IntegrationServiceABuilder.WithReference(redis);
            }
            if (!resourcesToSkip.HasFlag(TestResourceNames.postgres) || !resourcesToSkip.HasFlag(TestResourceNames.efnpgsql))
            {
                var postgresDbName = "postgresdb";
                var postgres = AppBuilder.AddPostgres("postgres")
                    .WithImageRegistry(AspireTestContainerRegistry)
                    .WithEnvironment("POSTGRES_DB", postgresDbName)
                    .AddDatabase(postgresDbName);
                IntegrationServiceABuilder = IntegrationServiceABuilder.WithReference(postgres);
            }
        }
 
        AppBuilder.Services.AddHostedService<ResourceLoggerForwarderService>();
        AppBuilder.Services.AddLifecycleHook<EndPointWriterHook>();
        AppBuilder.Services.AddHttpClient();
    }
 
    public static TestProgram Create<T>(
        string[]? args = null,
        bool includeIntegrationServices = false,
        bool disableDashboard = true,
        bool allowUnsecuredTransport = true,
        bool randomizePorts = true)
    {
        return new TestProgram(
            args ?? [],
            assemblyName: typeof(T).Assembly.FullName!,
            disableDashboard: disableDashboard,
            includeIntegrationServices: includeIntegrationServices,
            allowUnsecuredTransport: allowUnsecuredTransport,
            randomizePorts: randomizePorts);
    }
 
    public IDistributedApplicationBuilder AppBuilder { get; private set; }
    public IResourceBuilder<ProjectResource> ServiceABuilder { get; private set; }
    public IResourceBuilder<ProjectResource> ServiceBBuilder { get; private set; }
    public IResourceBuilder<ProjectResource> ServiceCBuilder { get; private set; }
    public IResourceBuilder<ProjectResource> WorkerABuilder { get; private set; }
    public IResourceBuilder<ProjectResource>? IntegrationServiceABuilder { get; private set; }
    public DistributedApplication? App { get; private set; }
 
    public List<IResourceBuilder<ProjectResource>> ServiceProjectBuilders => [ServiceABuilder, ServiceBBuilder, ServiceCBuilder];
 
    public async Task RunAsync(CancellationToken cancellationToken = default)
    {
        var app = Build();
        await app.RunAsync(cancellationToken);
    }
 
    public DistributedApplication Build()
    {
        return App ??= AppBuilder.Build();
    }
 
    public void Run()
    {
        var app = Build();
        app.Run();
    }
 
    public void Dispose() => App?.Dispose();
 
    /// <summary>
    /// Writes the allocated endpoints to the console in JSON format.
    /// This allows for easier consumption by the external test process.
    /// </summary>
    private sealed class EndPointWriterHook : IDistributedApplicationLifecycleHook
    {
        public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken)
        {
            var root = new JsonObject();
            foreach (var project in appModel.Resources.OfType<ProjectResource>())
            {
                var projectJson = new JsonObject();
                root[project.Name] = projectJson;
 
                var endpointsJsonArray = new JsonArray();
                projectJson["Endpoints"] = endpointsJsonArray;
 
                foreach (var endpoint in project.Annotations.OfType<EndpointAnnotation>())
                {
                    var allocatedEndpoint = endpoint.AllocatedEndpoint;
                    if (allocatedEndpoint is null)
                    {
                        continue;
                    }
 
                    var endpointJsonObject = new JsonObject
                    {
                        ["Name"] = endpoint.Name,
                        ["Uri"] = allocatedEndpoint.UriString
                    };
                    endpointsJsonArray.Add(endpointJsonObject);
                }
            }
 
            // write the whole json in a single line so it's easier to parse by the external process
            await Console.Out.WriteLineAsync("$ENDPOINTS: " + JsonSerializer.Serialize(root, JsonSerializerOptions.Default));
        }
    }
}