using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; namespace Prefab.Tests.Infrastructure; /// /// Starts the Aspire AppHost while keeping only infrastructure resources (e.g., SQL, cache). /// public sealed class AspireInfraHost : IAsyncLifetime { private readonly PrefabHarnessOptions _options; private readonly string[] _infraResourcesToKeep; private DistributedApplication? _app; private readonly Dictionary _projects = new(StringComparer.OrdinalIgnoreCase); public AspireInfraHost(PrefabHarnessOptions options, params string[] infraResourcesToKeep) { _options = options; _infraResourcesToKeep = infraResourcesToKeep; } public DistributedApplication App => _app ?? throw new InvalidOperationException("Aspire is not running."); public IServiceProvider Services => App.Services; public async ValueTask InitializeAsync() { var appArgs = new List(); if (_options.Mode == RunMode.EphemeralIsolated) { var containerName = $"prefab-sql-{Guid.NewGuid():N}"[..19]; appArgs.Add("SqlServer:UseDataVolume=false"); appArgs.Add("SqlServer:HostPort="); appArgs.Add($"SqlServer:ContainerName={containerName}"); } if (_options.DisablePortRandomization) { appArgs.Add("DcpPublisher:RandomizePorts=false"); } var builder = await DistributedApplicationTestingBuilder.CreateAsync( appArgs.ToArray(), configureBuilder: (appOptions, hostSettings) => { appOptions.DisableDashboard = !_options.EnableAspireDashboard; hostSettings ??= new HostApplicationBuilderSettings(); hostSettings.Configuration ??= new ConfigurationManager(); }); if (_options.DisablePortRandomization) { builder.Configuration["DcpPublisher:RandomizePorts"] = "false"; } builder.Configuration["ASPIRE_ALLOW_UNSECURED_TRANSPORT"] = "true"; foreach (var project in builder.Resources.OfType()) { _projects[project.Name] = project; } var keep = new HashSet(_infraResourcesToKeep, StringComparer.OrdinalIgnoreCase); // Ensure parent resources remain so infra dependencies stay intact. var added = 0; do { added = 0; foreach (var resource in builder.Resources) { if (keep.Contains(resource.Name)) { if (resource is IResourceWithParent withParent && withParent.Parent is { } parent && keep.Add(parent.Name)) { added++; } var relationships = resource.Annotations.OfType(); foreach (var relationship in relationships.Where(r => r.Type == "Parent")) { if (keep.Add(relationship.Resource.Name)) { added++; } } } } } while (added > 0); foreach (var resource in builder.Resources.Where(r => !keep.Contains(r.Name)).ToArray()) { builder.Resources.Remove(resource); } if (_options.Mode == RunMode.EphemeralIsolated) { foreach (var resource in builder.Resources) { var lifetime = resource.Annotations.OfType().FirstOrDefault(); if (lifetime is not null) { resource.Annotations.Remove(lifetime); } } } _app = await builder.BuildAsync(); await _app.StartAsync(); foreach (var resource in builder.Resources) { await _app.ResourceNotifications.WaitForResourceHealthyAsync(resource.Name); } } public async ValueTask DisposeAsync() { if (_app is not null) { await _app.DisposeAsync(); _app = null; } } public ProjectResource GetProject(string name) { if (_projects.TryGetValue(name, out var project)) { return project; } throw new ArgumentException($"Project '{name}' not found in AppHost model.", nameof(name)); } public async Task GetConnectionStringAsync(string resourceName, CancellationToken cancellationToken = default) => (await App.GetConnectionStringAsync(resourceName, cancellationToken)) ?? throw new InvalidOperationException($"Connection string for '{resourceName}' is not available."); public static async Task> ResolveConfigForProjectAsync( ProjectResource project, IServiceProvider services, CancellationToken cancellationToken = default) { var config = new Dictionary(StringComparer.OrdinalIgnoreCase); if (project is not IResourceWithEnvironment withEnvironment || !withEnvironment.TryGetEnvironmentVariables(out var annotations)) { return config; } var execOptions = new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run) { ServiceProvider = services }; var execContext = new DistributedApplicationExecutionContext(execOptions); var envContext = new EnvironmentCallbackContext(execContext, cancellationToken: cancellationToken); foreach (var annotation in annotations) { await annotation.Callback(envContext); } foreach (var (key, value) in envContext.EnvironmentVariables) { if (string.Equals(key, "ASPNETCORE_URLS", StringComparison.OrdinalIgnoreCase)) { continue; } var resolved = value switch { string s => s, IValueProvider provider => await SafeGet(provider, cancellationToken), _ => null }; if (resolved is not null) { config[key.Replace("__", ":")] = resolved; } } return config; static async Task SafeGet(IValueProvider provider, CancellationToken ct) { try { return await provider.GetValueAsync(ct); } catch { return null; } } } }