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); + }; + } +}