using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; namespace Prefab.Tests.Infrastructure.Aspire; internal sealed class AppHost(DistributedApplication application, EnvironmentVariableScope? environmentScope) : IAsyncDisposable { public DistributedApplication Application => application; public static async Task StartAsync(IReadOnlyDictionary? environmentOverrides, CancellationToken cancellationToken) { EnvironmentVariableScope? scope = null; if (environmentOverrides is not null && environmentOverrides.Count > 0) { scope = new EnvironmentVariableScope(environmentOverrides); } DistributedApplication? application = null; try { string[] defaultArgs = [ "SqlServer:UseDataVolume=false", "SqlServer:HostPort=", "Parameters:prefab-sql-password=PrefabTests!1234", ]; var builder = await DistributedApplicationTestingBuilder .CreateAsync(defaultArgs, static (_, hostOptions) => { hostOptions ??= new HostApplicationBuilderSettings(); hostOptions.Configuration ??= new ConfigurationManager(); }, cancellationToken); application = await builder.BuildAsync(cancellationToken); await application.StartAsync(cancellationToken); var notifications = application.ResourceNotifications; await notifications.WaitForResourceHealthyAsync("prefab-sql", cancellationToken); await notifications.WaitForResourceHealthyAsync("prefab-catalog", cancellationToken); await notifications.WaitForResourceHealthyAsync("prefab-web", cancellationToken); return new AppHost(application, scope); } catch { if (application is not null) { await application.DisposeAsync(); } scope?.Dispose(); throw; } } public HttpClient CreateHttpClient(string resourceName) => application.CreateHttpClient(resourceName); public async Task GetSqlConnectionStringAsync(string resourceName, CancellationToken cancellationToken) { var connectionString = await application.GetConnectionStringAsync(resourceName, cancellationToken).ConfigureAwait(false); if (string.IsNullOrWhiteSpace(connectionString)) { throw new InvalidOperationException($"SQL resource '{resourceName}' did not provide a connection string."); } return connectionString; } public async ValueTask DisposeAsync() { try { await application.StopAsync(CancellationToken.None); } catch { // best effort } await application.DisposeAsync(); environmentScope?.Dispose(); } } internal sealed class EnvironmentVariableScope : IDisposable { private readonly IReadOnlyDictionary _overrides; private readonly Dictionary _originalValues = new(StringComparer.OrdinalIgnoreCase); public EnvironmentVariableScope(IReadOnlyDictionary overrides) { _overrides = overrides; foreach (var pair in overrides) { _originalValues[pair.Key] = Environment.GetEnvironmentVariable(pair.Key); Environment.SetEnvironmentVariable(pair.Key, pair.Value); } } public void Dispose() { foreach (var pair in _overrides) { _originalValues.TryGetValue(pair.Key, out var value); Environment.SetEnvironmentVariable(pair.Key, value); } } }