Aspire Resource for Aspire–part 2–code

So those are the challenges

Challenge 1: Extracting the Dashboard URL

The Aspire dashboard doesn’t expose its URL directly through a simple API. Instead, the URL is logged to the application’s output. To capture it, I had to hook into the logging infrastructure. I created a FakeLoggerProvider(https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.testing.fakeloggerprovider?view=net-9.0-pp) that collects log messages, then scanned those messages for lines like "Now listening on:" to extract the actual URL. This required careful timing and retries, since the log message might not appear immediately after startup.

    static AspireResource resource = new();
    static bool added = false;
    public static IResourceBuilder<AspireResource>? AddAspireResource(this IDistributedApplicationBuilder builder)
    {
        if (added)
        {
            return null;
        }
        added = true;
        var res = builder.AddResource(resource);
        builder.Services.AddFakeLogging();
        return res;
    }

Challenge 2: Obtaining the Login Token

Similarly, the login token (or login URL) is also only available via log output, typically in a message like "Login to the dashboard at". I had to repeatedly poll the collected log messages, searching for this specific phrase. To avoid race conditions and ensure reliability, I implemented retries and delays, making sure I didn’t miss the message if it appeared later than expected.

internal static async Task<string?> ViewData(DistributedApplication da)
{
    await Task.Delay(1000);
    var fake = typeof(FakeLoggerProvider);
    var logger = da.Services.GetServices<ILoggerProvider>();
    FakeLoggerProvider? fakeLogger = null;
    foreach (var service in logger)
    {
        if (service.GetType() == fake)
        {
            fakeLogger = service as FakeLoggerProvider;
        }
    }
    ArgumentNullException.ThrowIfNull(fakeLogger);
    var tsWait = TimeSpan.FromSeconds(5);
    string? url = null;
    while (url == null)
    {

        var messages = fakeLogger.Collector.GetSnapshot().Select(x => x.Message).ToArray();
        //Console.WriteLine("wating"+string.Join(',',messages));
        await Task.Delay(tsWait);
        url = FindUrl(messages);
    }
    string? login = null;
    var nrRetry = 10;
    while (url != null && login == null && nrRetry > 0)
    {
        nrRetry--;
        var messages = fakeLogger.Collector.GetSnapshot().Select(x => x.Message).ToArray();
        login = FindLogin(messages);
        await Task.Delay(tsWait);
    }
    LoginUrl = login ?? url;
    return LoginUrl;
}
private static string? FindLogin(string[] messages)
{
    var mes = messages.FirstOrDefault(it => it.Contains("Login to the dashboard at"));
    if (mes == null) return null;
    var url = mes.Replace("Login to the dashboard at", "");
    return url.Trim();
}
private static string? FindUrl(string[] messages)
{
    var mes = messages.FirstOrDefault(it => it.Contains("Now listening on:"));
    if (mes == null) return null;
    var url = mes.Replace("Now listening on:", "");
    return url.Trim();

}

Challenge 3: Sharing Environment Variables with Other Resources

Once I had the URL and login token, the next step was to make them available to other resources in the distributed application. I achieved this by injecting them as environment variables. This allows test projects, NodeJS apps, or any other resource to access the Aspire dashboard endpoints automatically, without manual configuration.

public async Task<string> StartParsing(DistributedApplication da)
{
    var ret = await AddAspire.ViewData(da);
    if (ret != null)
    {
        var env = await this.GetEnvironmentVariableValuesAsync();
        await da.ResourceNotifications.PublishUpdateAsync(this, mainState => {
            EnvironmentVariableSnapshot login = new("ASPIRE_LOGIN_URL", ret, true);

            var urlSha = new UrlSnapshot("ASPIRE_LOGIN_URL", ret, false)
            {
                DisplayProperties = new("LoginUrl")
            };

            _baseUrl = ret;
            if (ret.IndexOf("login?") > 0)
            {
                _baseUrl = ret.Substring(0, ret.IndexOf("login?"));
            }

            UrlSnapshot baseUrlSnap = new UrlSnapshot("ASPIRE_BASE_URL", _baseUrl, false)
            {
                DisplayProperties = new("BaseUrl")
            };
            var urls = mainState.Urls.AddRange(baseUrlSnap, urlSha);

            EnvironmentVariableSnapshot baseEnv = new EnvironmentVariableSnapshot("ASPIRE_BASE_URL", _baseUrl, true);
            var env = mainState.EnvironmentVariables.AddRange(login, baseEnv);

            foreach (var r in resources)
            {
                da.ResourceNotifications.PublishUpdateAsync(r.Resource, s =>
                {

                    var envRes = s.EnvironmentVariables.AddRange(
                        new EnvironmentVariableSnapshot("ASPIRE_LOGIN_URL", ret, true),
                        new EnvironmentVariableSnapshot("ASPIRE_BASE_URL", _baseUrl, true)

                        );
                    return s with
                    {
                        EnvironmentVariables = envRes
                    };

                });
            }
            ;



            return mainState with
            {
                State = KnownResourceStates.Running,
                EnvironmentVariables = env,
                Urls = urls,
            };
        });
    }
    _loginUrl = ret;
    return _loginUrl ?? "";
}

Now you can use https://www.nuget.org/packages/AspireExtensionsResource/ !


Posted

in

, ,

by

Tags: