diff --git a/src/TickerQ.Dashboard/DashboardOptionsBuilder.cs b/src/TickerQ.Dashboard/DashboardOptionsBuilder.cs
index 46639c95..5a14ca63 100644
--- a/src/TickerQ.Dashboard/DashboardOptionsBuilder.cs
+++ b/src/TickerQ.Dashboard/DashboardOptionsBuilder.cs
@@ -27,6 +27,9 @@ public class DashboardOptionsBuilder
/// Separate from request serialization options to prevent user configuration from breaking dashboard APIs.
///
internal JsonSerializerOptions DashboardJsonOptions { get; set; }
+
+ /// Tracks whether dashboard middleware has been applied to prevent double registration.
+ internal bool MiddlewareApplied { get; set; }
public void SetCorsPolicy(Action corsPolicyBuilder)
=> CorsPolicyBuilder = corsPolicyBuilder;
diff --git a/src/TickerQ.Dashboard/DependencyInjection/ServiceExtensions.cs b/src/TickerQ.Dashboard/DependencyInjection/ServiceExtensions.cs
index 8c940fc0..3b2f7231 100644
--- a/src/TickerQ.Dashboard/DependencyInjection/ServiceExtensions.cs
+++ b/src/TickerQ.Dashboard/DependencyInjection/ServiceExtensions.cs
@@ -1,6 +1,7 @@
using Microsoft.Extensions.DependencyInjection;
using TickerQ.Dashboard.Endpoints;
using TickerQ.Dashboard.Hubs;
+using TickerQ.Dashboard.Infrastructure;
using TickerQ.Dashboard.Infrastructure.Dashboard;
using TickerQ.Dashboard.Authentication;
using TickerQ.Utilities;
@@ -8,6 +9,7 @@
using System;
using System.Linq;
using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection.Extensions;
using TickerQ.Utilities.Entities;
@@ -59,6 +61,10 @@ public static TickerOptionsBuilder AddDashboard(dashboardConfig);
services.AddSingleton(_ => dashboardConfig);
+
+ // Register IStartupFilter for old Startup.cs pattern where IHost != IApplicationBuilder.
+ // In the new WebApplication pattern, UseDashboardDelegate handles it directly.
+ services.AddSingleton(new DashboardStartupFilter(dashboardConfig));
};
UseDashboardDelegate(tickerConfiguration, dashboardConfig);
@@ -72,16 +78,15 @@ private static void UseDashboardDelegate(this TickerOp
{
tickerConfiguration.UseDashboardApplication((appObj) =>
{
- if (appObj is not IApplicationBuilder app)
- throw new InvalidOperationException(
- "TickerQ Dashboard can only be used in ASP.NET Core applications. " +
- "The current host does not provide an HTTP application pipeline " +
- "(IApplicationBuilder is not available). " +
- "If you are running a Worker Service, Console app, or background node, " +
- "remove the dashboard configuration or move it to a WebApplication."
- );
- // Configure static files and middleware with endpoints
- app.UseDashboardWithEndpoints(dashboardConfig);
+ if (appObj is IApplicationBuilder app)
+ {
+ // New WebApplication pattern: WebApplication implements both IHost and IApplicationBuilder.
+ // Mark as applied so DashboardStartupFilter skips duplicate registration.
+ dashboardConfig.MiddlewareApplied = true;
+ app.UseDashboardWithEndpoints(dashboardConfig);
+ }
+ // Old Startup.cs pattern: IHost is not IApplicationBuilder.
+ // Dashboard middleware is injected via IStartupFilter registered in AddDashboard.
});
}
}
diff --git a/src/TickerQ.Dashboard/Infrastructure/DashboardStartupFilter.cs b/src/TickerQ.Dashboard/Infrastructure/DashboardStartupFilter.cs
new file mode 100644
index 00000000..27f9cf92
--- /dev/null
+++ b/src/TickerQ.Dashboard/Infrastructure/DashboardStartupFilter.cs
@@ -0,0 +1,34 @@
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using TickerQ.Dashboard.DependencyInjection;
+using TickerQ.Utilities.Entities;
+
+namespace TickerQ.Dashboard.Infrastructure;
+
+internal class DashboardStartupFilter : IStartupFilter
+ where TTimeTicker : TimeTickerEntity, new()
+ where TCronTicker : CronTickerEntity, new()
+{
+ private readonly DashboardOptionsBuilder _config;
+
+ public DashboardStartupFilter(DashboardOptionsBuilder config)
+ {
+ _config = config;
+ }
+
+ public Action Configure(Action next)
+ {
+ return app =>
+ {
+ // Only apply if not already applied by UseDashboardDelegate (new WebApplication pattern)
+ if (!_config.MiddlewareApplied)
+ {
+ _config.MiddlewareApplied = true;
+ app.UseDashboardWithEndpoints(_config);
+ }
+
+ next(app);
+ };
+ }
+}