diff --git a/src/Aspire.Dashboard/DashboardWebApplication.cs b/src/Aspire.Dashboard/DashboardWebApplication.cs index c233b13c8aa..8271e4b5a34 100644 --- a/src/Aspire.Dashboard/DashboardWebApplication.cs +++ b/src/Aspire.Dashboard/DashboardWebApplication.cs @@ -232,6 +232,7 @@ public DashboardWebApplication( // Data from the server. builder.Services.TryAddScoped(); + builder.Services.TryAddSingleton(); // OTLP services. builder.Services.AddGrpc(); @@ -340,7 +341,7 @@ public DashboardWebApplication( { if (context.Request.Path.Equals(TargetLocationInterceptor.ResourcesPath, StringComparisons.UrlPath)) { - var client = context.RequestServices.GetRequiredService(); + var client = context.RequestServices.GetRequiredService(); if (!client.IsEnabled) { context.Response.Redirect(TargetLocationInterceptor.StructuredLogsPath); diff --git a/src/Aspire.Dashboard/ResourceService/DashboardClient.cs b/src/Aspire.Dashboard/ResourceService/DashboardClient.cs index 3a399664ebe..63637f06396 100644 --- a/src/Aspire.Dashboard/ResourceService/DashboardClient.cs +++ b/src/Aspire.Dashboard/ResourceService/DashboardClient.cs @@ -31,7 +31,7 @@ namespace Aspire.Dashboard.Model; /// If the DOTNET_RESOURCE_SERVICE_ENDPOINT_URL environment variable is not specified, then there's /// no known endpoint to connect to, and this dashboard client will be disabled. Calls to /// and -/// will throw if is . Callers should +/// will throw if is . Callers should /// check this property first, before calling these methods. /// /// @@ -47,6 +47,7 @@ internal sealed class DashboardClient : IDashboardClient private readonly object _lock = new(); private readonly ILoggerFactory _loggerFactory; + private readonly IDashboardClientStatus _dashboardClientStatus; private readonly BrowserTimeProvider _timeProvider; private readonly IKnownPropertyLookup _knownPropertyLookup; private readonly DashboardOptions _dashboardOptions; @@ -71,11 +72,13 @@ public DashboardClient( ILoggerFactory loggerFactory, IConfiguration configuration, IOptions dashboardOptions, + IDashboardClientStatus dashboardClientStatus, BrowserTimeProvider timeProvider, IKnownPropertyLookup knownPropertyLookup, Action? configureHttpHandler = null) { _loggerFactory = loggerFactory; + _dashboardClientStatus = dashboardClientStatus; _timeProvider = timeProvider; _knownPropertyLookup = knownPropertyLookup; _dashboardOptions = dashboardOptions.Value; @@ -85,9 +88,7 @@ public DashboardClient( _logger = loggerFactory.CreateLogger(); - var address = _dashboardOptions.ResourceServiceClient.GetUri(); - - if (address is null) + if (!_dashboardClientStatus.IsEnabled) { _state = StateDisabled; _logger.LogDebug($"{DashboardConfigNames.ResourceServiceUrlName.ConfigKey} is not specified. Dashboard client services are unavailable."); @@ -96,6 +97,7 @@ public DashboardClient( return; } + var address = _dashboardOptions.ResourceServiceClient.GetUri()!; _logger.LogDebug("Dashboard configured to connect to: {Address}", address); // Create the gRPC channel. This channel performs automatic reconnects. diff --git a/src/Aspire.Dashboard/ResourceService/DashboardClientStatus.cs b/src/Aspire.Dashboard/ResourceService/DashboardClientStatus.cs new file mode 100644 index 00000000000..643316cf2c0 --- /dev/null +++ b/src/Aspire.Dashboard/ResourceService/DashboardClientStatus.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Dashboard.Configuration; +using Microsoft.Extensions.Options; + +namespace Aspire.Dashboard.Model; + +internal sealed class DashboardClientStatus(IOptions dashboardOptions) : IDashboardClientStatus +{ + public bool IsEnabled => dashboardOptions.Value.ResourceServiceClient.GetUri() is not null; +} diff --git a/src/Aspire.Dashboard/ResourceService/IDashboardClient.cs b/src/Aspire.Dashboard/ResourceService/IDashboardClient.cs index 2065c47fa53..b036f3a28a2 100644 --- a/src/Aspire.Dashboard/ResourceService/IDashboardClient.cs +++ b/src/Aspire.Dashboard/ResourceService/IDashboardClient.cs @@ -8,17 +8,8 @@ namespace Aspire.Dashboard.Model; /// /// Provides data about active resources to external components, such as the dashboard. /// -public interface IDashboardClient : IAsyncDisposable +public interface IDashboardClient : IDashboardClientStatus, IAsyncDisposable { - /// - /// Gets whether this client object is enabled for use. - /// - /// - /// Users of this client should check before calling - /// any other members of this interface, to avoid exceptions. - /// - bool IsEnabled { get; } - Task WhenConnected { get; } /// diff --git a/src/Aspire.Dashboard/ResourceService/IDashboardClientStatus.cs b/src/Aspire.Dashboard/ResourceService/IDashboardClientStatus.cs new file mode 100644 index 00000000000..faedafd2ceb --- /dev/null +++ b/src/Aspire.Dashboard/ResourceService/IDashboardClientStatus.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Dashboard.Model; + +public interface IDashboardClientStatus +{ + /// + /// Gets whether the client object is enabled for use. + /// + /// + /// Users of client should check before calling + /// any other members of this interface, to avoid exceptions. + /// + bool IsEnabled { get; } +} diff --git a/tests/Aspire.Dashboard.Components.Tests/Controls/ApplicationNameTests.cs b/tests/Aspire.Dashboard.Components.Tests/Controls/ApplicationNameTests.cs index 14ca0f71c02..13e0e1f3547 100644 --- a/tests/Aspire.Dashboard.Components.Tests/Controls/ApplicationNameTests.cs +++ b/tests/Aspire.Dashboard.Components.Tests/Controls/ApplicationNameTests.cs @@ -21,6 +21,7 @@ public void Render_DashboardClientDisabled_Success() Services.AddSingleton(new ConfigurationManager()); Services.AddSingleton(NullLoggerFactory.Instance); Services.AddSingleton(); + Services.AddSingleton(); Services.AddSingleton(); Services.AddSingleton(new MockKnownPropertyLookup()); diff --git a/tests/Aspire.Dashboard.Tests/Integration/DashboardClientAuthTests.cs b/tests/Aspire.Dashboard.Tests/Integration/DashboardClientAuthTests.cs index 7dd1488a6d6..73a841cd15e 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/DashboardClientAuthTests.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/DashboardClientAuthTests.cs @@ -130,6 +130,7 @@ private static async Task CreateDashboardClientAsync( loggerFactory: loggerFactory, configuration: new ConfigurationManager(), dashboardOptions: Options.Create(options), + dashboardClientStatus: new TestDashboardClientStatus(), timeProvider: new BrowserTimeProvider(NullLoggerFactory.Instance), knownPropertyLookup: new MockKnownPropertyLookup(), configureHttpHandler: handler => handler.SslOptions.RemoteCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true); @@ -159,6 +160,11 @@ private sealed class TestCalls public Channel> ApplicationInformationCallsChannel { get; } = Channel.CreateUnbounded>(); } + private sealed class TestDashboardClientStatus : IDashboardClientStatus + { + public bool IsEnabled => true; + } + private sealed class MockDashboardService(TestCalls testCalls) : DashboardServiceBase { public override Task GetApplicationInformation( diff --git a/tests/Aspire.Dashboard.Tests/Integration/Playwright/Infrastructure/DashboardServerFixture.cs b/tests/Aspire.Dashboard.Tests/Integration/Playwright/Infrastructure/DashboardServerFixture.cs index 5b3674850cf..78772f3fe85 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/Playwright/Infrastructure/DashboardServerFixture.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/Playwright/Infrastructure/DashboardServerFixture.cs @@ -57,6 +57,7 @@ public async Task InitializeAsync() preConfigureBuilder: builder => { builder.Configuration.AddConfiguration(config); + builder.Services.AddSingleton(); builder.Services.AddScoped(); }); diff --git a/tests/Aspire.Dashboard.Tests/Integration/Playwright/Infrastructure/MockDashboardClient.cs b/tests/Aspire.Dashboard.Tests/Integration/Playwright/Infrastructure/MockDashboardClient.cs index 24797dd46a2..4544675fe91 100644 --- a/tests/Aspire.Dashboard.Tests/Integration/Playwright/Infrastructure/MockDashboardClient.cs +++ b/tests/Aspire.Dashboard.Tests/Integration/Playwright/Infrastructure/MockDashboardClient.cs @@ -8,10 +8,20 @@ namespace Aspire.Dashboard.Tests.Integration.Playwright.Infrastructure; +public sealed class MockDashboardClientStatus : IDashboardClientStatus +{ + public bool IsEnabled => true; +} + public sealed class MockDashboardClient : IDashboardClient { private static readonly BrowserTimeProvider s_timeProvider = new(NullLoggerFactory.Instance); + public MockDashboardClient(IDashboardClientStatus dashboardClientStatus) + { + _dashboardClientStatus = dashboardClientStatus; + } + public static readonly ResourceViewModel TestResource1 = ModelTestHelpers.CreateResource( appName: "TestResource", resourceType: KnownResourceTypes.Project, @@ -32,7 +42,9 @@ public sealed class MockDashboardClient : IDashboardClient }.ToDictionary(), state: KnownResourceState.Running); - public bool IsEnabled => true; + private readonly IDashboardClientStatus _dashboardClientStatus; + + public bool IsEnabled => _dashboardClientStatus.IsEnabled; public Task WhenConnected => Task.CompletedTask; public string ApplicationName => "IntegrationTestApplication"; public ValueTask DisposeAsync() => ValueTask.CompletedTask; diff --git a/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs b/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs index 5000cad4401..3e5c76cefce 100644 --- a/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/DashboardClientTests.cs @@ -151,6 +151,11 @@ public async Task SubscribeResources_HasInitialData_InitialDataReturned() private DashboardClient CreateResourceServiceClient() { - return new DashboardClient(NullLoggerFactory.Instance, _configuration, _dashboardOptions, s_timeProvider, new MockKnownPropertyLookup()); + return new DashboardClient(NullLoggerFactory.Instance, _configuration, _dashboardOptions, new TestDashboardClientStatus(), s_timeProvider, new MockKnownPropertyLookup()); + } + + private sealed class TestDashboardClientStatus : IDashboardClientStatus + { + public bool IsEnabled => true; } }