diff --git a/src/TickerQ.Utilities/TickerFunctionProvider.cs b/src/TickerQ.Utilities/TickerFunctionProvider.cs index 9da5321f..61bc847b 100644 --- a/src/TickerQ.Utilities/TickerFunctionProvider.cs +++ b/src/TickerQ.Utilities/TickerFunctionProvider.cs @@ -20,15 +20,19 @@ namespace TickerQ.Utilities /// public static class TickerFunctionProvider { + private static readonly object _buildLock = new(); + // Callback actions to collect registrations private static Action> _requestTypeRegistrations; private static Action> _requestInfoRegistrations; private static Action> _functionRegistrations; - + // Final frozen dictionaries - public static FrozenDictionary TickerFunctionRequestTypes; - public static FrozenDictionary TickerFunctionRequestInfos; - public static FrozenDictionary TickerFunctions; + public static FrozenDictionary TickerFunctionRequestTypes = FrozenDictionary.Empty; + public static FrozenDictionary TickerFunctionRequestInfos = FrozenDictionary.Empty; + public static FrozenDictionary TickerFunctions = FrozenDictionary.Empty; + + public static bool IsBuilt { get; private set; } /// /// Registers ticker functions during application startup by adding to the callback chain. @@ -158,57 +162,36 @@ internal static void UpdateCronExpressionsFromIConfiguration(IConfiguration conf /// public static void Build() { - // Build functions dictionary - if (_functionRegistrations != null) + lock (_buildLock) { - // Single pass: execute callbacks directly on final dictionary - var functionsDict = new Dictionary(); - _functionRegistrations(functionsDict); - TickerFunctions = functionsDict.ToFrozenDictionary(); - _functionRegistrations = null; // Release callback chain - } - else - { - if (TickerFunctions == null) + // Build functions dictionary + if (_functionRegistrations != null) { - TickerFunctions = new Dictionary() - .ToFrozenDictionary(); + var functionsDict = new Dictionary(); + _functionRegistrations(functionsDict); + TickerFunctions = functionsDict.ToFrozenDictionary(); + _functionRegistrations = null; } - } - // Build request types dictionary - if (_requestTypeRegistrations != null) - { - // Single pass: execute callbacks directly on final dictionary - var requestTypesDict = new Dictionary(); - _requestTypeRegistrations(requestTypesDict); - TickerFunctionRequestTypes = requestTypesDict.ToFrozenDictionary(); - _requestTypeRegistrations = null; // Release callback chain - } - else - { - if (TickerFunctionRequestTypes == null) + // Build request types dictionary + if (_requestTypeRegistrations != null) { - TickerFunctionRequestTypes = new Dictionary() - .ToFrozenDictionary(); + var requestTypesDict = new Dictionary(); + _requestTypeRegistrations(requestTypesDict); + TickerFunctionRequestTypes = requestTypesDict.ToFrozenDictionary(); + _requestTypeRegistrations = null; } - } - // Build request info dictionary (string type + example JSON) - if (_requestInfoRegistrations != null) - { - var requestInfoDict = new Dictionary(); - _requestInfoRegistrations(requestInfoDict); - TickerFunctionRequestInfos = requestInfoDict.ToFrozenDictionary(); - _requestInfoRegistrations = null; - } - else - { - if (TickerFunctionRequestInfos == null) + // Build request info dictionary (string type + example JSON) + if (_requestInfoRegistrations != null) { - TickerFunctionRequestInfos = new Dictionary() - .ToFrozenDictionary(); + var requestInfoDict = new Dictionary(); + _requestInfoRegistrations(requestInfoDict); + TickerFunctionRequestInfos = requestInfoDict.ToFrozenDictionary(); + _requestInfoRegistrations = null; } + + IsBuilt = true; } } } diff --git a/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs b/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs index f6a07195..3cc261ad 100644 --- a/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs +++ b/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs @@ -96,6 +96,10 @@ public static IServiceCollection AddTickerQ(this IServ services.AddSingleton(_ => optionInstance); services.AddSingleton(_ => tickerExecutionContext); services.AddSingleton(_ => schedulerOptionsBuilder); + + // Register AFTER initializer and scheduler to ensure it runs last + services.AddHostedService(); + return services; } diff --git a/src/TickerQ/Src/BackgroundServices/TickerQStartupValidator.cs b/src/TickerQ/Src/BackgroundServices/TickerQStartupValidator.cs new file mode 100644 index 00000000..f671749d --- /dev/null +++ b/src/TickerQ/Src/BackgroundServices/TickerQStartupValidator.cs @@ -0,0 +1,45 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TickerQ.Utilities; +using TickerQ.Utilities.Enums; + +namespace TickerQ.BackgroundServices; + +internal class TickerQStartupValidator : IHostedService +{ + private readonly TickerExecutionContext _executionContext; + private readonly TickerQInitializerHostedService _initializer; + private readonly ILogger _logger; + + public TickerQStartupValidator( + TickerExecutionContext executionContext, + TickerQInitializerHostedService initializer, + ILogger logger) + { + _executionContext = executionContext; + _initializer = initializer; + _logger = logger; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + if (!_initializer.InitializationRequested) + { + const string message = "TickerQ — UseTickerQ() was not called. Call app.UseTickerQ() before app.Run() to initialize the scheduler."; + _logger.LogWarning(message); + _executionContext.NotifyCoreAction?.Invoke(message, CoreNotifyActionType.NotifyHostExceptionMessage); + } + else if (TickerFunctionProvider.TickerFunctions.Count == 0) + { + const string message = "TickerQ — No ticker functions registered. Ensure you have methods decorated with [TickerFunction]."; + _logger.LogWarning(message); + _executionContext.NotifyCoreAction?.Invoke(message, CoreNotifyActionType.NotifyHostExceptionMessage); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/tests/TickerQ.Tests/TickerFunctionProviderTests.cs b/tests/TickerQ.Tests/TickerFunctionProviderTests.cs index ba3eaafc..f67fb801 100644 --- a/tests/TickerQ.Tests/TickerFunctionProviderTests.cs +++ b/tests/TickerQ.Tests/TickerFunctionProviderTests.cs @@ -37,12 +37,16 @@ private static void ResetProvider() var type = typeof(TickerFunctionProvider); const BindingFlags flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public; - type.GetField("TickerFunctions", flags)!.SetValue(null, null); - type.GetField("TickerFunctionRequestTypes", flags)!.SetValue(null, null); - type.GetField("TickerFunctionRequestInfos", flags)!.SetValue(null, null); + type.GetField("TickerFunctions", flags)!.SetValue(null, + System.Collections.Frozen.FrozenDictionary.Empty); + type.GetField("TickerFunctionRequestTypes", flags)!.SetValue(null, + System.Collections.Frozen.FrozenDictionary.Empty); + type.GetField("TickerFunctionRequestInfos", flags)!.SetValue(null, + System.Collections.Frozen.FrozenDictionary.Empty); type.GetField("_functionRegistrations", flags)!.SetValue(null, null); type.GetField("_requestTypeRegistrations", flags)!.SetValue(null, null); type.GetField("_requestInfoRegistrations", flags)!.SetValue(null, null); + type.GetProperty("IsBuilt", flags)!.SetValue(null, false); } private static Task NoOpDelegate(CancellationToken ct, IServiceProvider sp, TickerQ.Utilities.Base.TickerFunctionContext ctx)