File: Infrastructure\ReadinessNotificationService.cs
Web Access
Project: src\src\Components\Testing\src\Microsoft.AspNetCore.Components.Testing.csproj (Microsoft.AspNetCore.Components.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.Net.Http;
using Microsoft.Extensions.Hosting;
 
namespace Microsoft.AspNetCore.Components.Testing.Infrastructure;
 
// Hosted service that signals the test harness when the app is fully started.
// Listens to IHostApplicationLifetime.ApplicationStarted; when it fires, POSTs
// to the callback URL provided via the E2E_READY_URL environment variable.
// This replaces health-check polling with a one-shot push signal.
internal class ReadinessNotificationService : IHostedService
{
    private readonly IHostApplicationLifetime _lifetime;
 
    public ReadinessNotificationService(IHostApplicationLifetime lifetime)
    {
        _lifetime = lifetime;
    }
 
    public Task StartAsync(CancellationToken cancellationToken)
    {
        var readyUrl = Environment.GetEnvironmentVariable("E2E_READY_URL");
        if (string.IsNullOrEmpty(readyUrl))
        {
            return Task.CompletedTask;
        }
 
        _lifetime.ApplicationStarted.Register(() =>
        {
            // Fire-and-forget: notify the test fixture that this app is listening.
            // Errors are intentionally swallowed — the test fixture has a timeout fallback.
            _ = NotifyReadyAsync(readyUrl);
        });
 
        return Task.CompletedTask;
    }
 
    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
 
    private static async Task NotifyReadyAsync(string url)
    {
        const int maxAttempts = 5;
        var delay = TimeSpan.FromMilliseconds(200);
 
        for (var attempt = 1; attempt <= maxAttempts; attempt++)
        {
            try
            {
                using var client = new HttpClient();
                client.Timeout = TimeSpan.FromSeconds(5);
                var response = await client.PostAsync(url, content: null).ConfigureAwait(false);
                if (response.IsSuccessStatusCode)
                {
                    return;
                }
            }
            catch
            {
                // Retry on any error
            }
 
            if (attempt < maxAttempts)
            {
                await Task.Delay(delay).ConfigureAwait(false);
                delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2);
            }
        }
    }
}