diff --git a/src/TickerQ.Utilities/Enums/TickerQStartMode.cs b/src/TickerQ.Utilities/Enums/TickerQStartMode.cs
index e650325a..16ead1fe 100644
--- a/src/TickerQ.Utilities/Enums/TickerQStartMode.cs
+++ b/src/TickerQ.Utilities/Enums/TickerQStartMode.cs
@@ -2,7 +2,16 @@ namespace TickerQ.Utilities.Enums
{
public enum TickerQStartMode
{
+ ///
+ /// Start job processing immediately when UseTickerQ is called.
+ /// Background services are registered and start automatically.
+ ///
Immediate,
+
+ ///
+ /// Background services are registered but skip the first run.
+ /// Job processing needs to be started manually via ITickerQHostScheduler.
+ ///
Manual
}
}
\ No newline at end of file
diff --git a/src/TickerQ.Utilities/Interfaces/ITickerQDispatcher.cs b/src/TickerQ.Utilities/Interfaces/ITickerQDispatcher.cs
index c31df0e3..12ec1817 100644
--- a/src/TickerQ.Utilities/Interfaces/ITickerQDispatcher.cs
+++ b/src/TickerQ.Utilities/Interfaces/ITickerQDispatcher.cs
@@ -6,6 +6,12 @@ namespace TickerQ.Utilities.Interfaces
{
public interface ITickerQDispatcher
{
+ ///
+ /// Indicates whether the dispatcher is functional (background services enabled).
+ /// When false, DispatchAsync will be a no-op.
+ ///
+ bool IsEnabled { get; }
+
Task DispatchAsync(InternalFunctionContext[] contexts, CancellationToken cancellationToken = default);
}
}
diff --git a/src/TickerQ.Utilities/Managers/TickerManager.cs b/src/TickerQ.Utilities/Managers/TickerManager.cs
index 99ccdfd0..cf6649d9 100644
--- a/src/TickerQ.Utilities/Managers/TickerManager.cs
+++ b/src/TickerQ.Utilities/Managers/TickerManager.cs
@@ -103,7 +103,8 @@ private async Task> AddTimeTickerAsync(TTimeTicker ent
// Persist first
await _persistenceProvider.AddTimeTickers([entity], cancellationToken: cancellationToken);
- if (executionTime <= now.AddSeconds(1))
+ // Only try to dispatch immediately if dispatcher is enabled (background services running)
+ if (_dispatcher.IsEnabled && executionTime <= now.AddSeconds(1))
{
// Acquire and mark InProgress in one provider call
var acquired = await _persistenceProvider
@@ -347,7 +348,8 @@ private async Task>> AddTimeTickersBatchAsync(Lis
await _notificationHubSender.AddTimeTickersBatchNotifyAsync().ConfigureAwait(false);
- if (immediateTickers.Count > 0)
+ // Only try to dispatch immediately if dispatcher is enabled (background services running)
+ if (_dispatcher.IsEnabled && immediateTickers.Count > 0)
{
var acquired = await _persistenceProvider
.AcquireImmediateTimeTickersAsync(immediateTickers.ToArray(), cancellationToken)
diff --git a/src/TickerQ.Utilities/Temps/NoOpTickerQDispatcher.cs b/src/TickerQ.Utilities/Temps/NoOpTickerQDispatcher.cs
new file mode 100644
index 00000000..68273f4f
--- /dev/null
+++ b/src/TickerQ.Utilities/Temps/NoOpTickerQDispatcher.cs
@@ -0,0 +1,21 @@
+using System.Threading;
+using System.Threading.Tasks;
+using TickerQ.Utilities.Interfaces;
+using TickerQ.Utilities.Models;
+
+namespace TickerQ.Utilities.Temps;
+
+///
+/// No-operation implementation of ITickerQDispatcher.
+/// Used when background services are disabled (queue-only mode).
+///
+internal class NoOpTickerQDispatcher : ITickerQDispatcher
+{
+ public bool IsEnabled => false;
+
+ public Task DispatchAsync(InternalFunctionContext[] contexts, CancellationToken cancellationToken = default)
+ {
+ // No-op: dispatcher not available in queue-only mode
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/TickerQ.Utilities/Temps/NoOpTickerQHostScheduler.cs b/src/TickerQ.Utilities/Temps/NoOpTickerQHostScheduler.cs
new file mode 100644
index 00000000..0195a286
--- /dev/null
+++ b/src/TickerQ.Utilities/Temps/NoOpTickerQHostScheduler.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using TickerQ.Utilities.Interfaces;
+
+namespace TickerQ.Utilities.Temps;
+
+///
+/// No-operation implementation of ITickerQHostScheduler.
+/// Used when background services are disabled (queue-only mode).
+///
+internal class NoOpTickerQHostScheduler : ITickerQHostScheduler
+{
+ public bool IsRunning => false;
+
+ public Task StartAsync(CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+
+ public Task StopAsync(CancellationToken cancellationToken = default)
+ {
+ return Task.CompletedTask;
+ }
+
+ public void RestartIfNeeded(DateTime? dateTime)
+ {
+ // No-op: scheduler not running
+ }
+
+ public void Restart()
+ {
+ // No-op: scheduler not running
+ }
+}
diff --git a/src/TickerQ.Utilities/TickerOptionsBuilder.cs b/src/TickerQ.Utilities/TickerOptionsBuilder.cs
index fc782fb3..bf51fe32 100644
--- a/src/TickerQ.Utilities/TickerOptionsBuilder.cs
+++ b/src/TickerQ.Utilities/TickerOptionsBuilder.cs
@@ -34,6 +34,12 @@ internal TickerOptionsBuilder(TickerExecutionContext tickerExecutionContext, Sch
/// Defaults to true.
///
internal bool SeedDefinedCronTickers { get; set; } = true;
+
+ ///
+ /// Controls whether background services (job processors) should be registered.
+ /// Defaults to true. Set to false to only register managers for queuing jobs.
+ ///
+ internal bool RegisterBackgroundServices { get; set; } = true;
///
/// Seeding delegate for time tickers, executed with the application's service provider.
@@ -98,6 +104,17 @@ public TickerOptionsBuilder IgnoreSeedDefinedCronTicke
SeedDefinedCronTickers = false;
return this;
}
+
+ ///
+ /// Disables background services registration.
+ /// Use this when you only want to queue jobs without processing them in this application.
+ /// Only the managers (ITimeTickerManager, ICronTickerManager) will be available for queuing jobs.
+ ///
+ public TickerOptionsBuilder DisableBackgroundServices()
+ {
+ RegisterBackgroundServices = false;
+ return this;
+ }
///
/// Configure a custom seeder for time tickers, executed on application startup.
diff --git a/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs b/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs
index cd57b22b..916c5fd7 100644
--- a/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs
+++ b/src/TickerQ/DependencyInjection/TickerQServiceExtensions.cs
@@ -49,22 +49,34 @@ public static IServiceCollection AddTickerQ(this IServ
services.AddSingleton, TickerInMemoryPersistenceProvider>();
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton(provider =>
- provider.GetRequiredService());
- services.AddHostedService(provider =>
- provider.GetRequiredService());
- services.AddHostedService(provider => provider.GetRequiredService());
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton();
- services.AddSingleton(sp =>
+
+ // Only register background services if enabled (default is true)
+ if (optionInstance.RegisterBackgroundServices)
+ {
+ services.AddSingleton();
+ services.AddSingleton(provider =>
+ provider.GetRequiredService());
+ services.AddHostedService(provider =>
+ provider.GetRequiredService());
+ services.AddHostedService(provider => provider.GetRequiredService());
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(sp =>
+ {
+ var notification = sp.GetRequiredService();
+ var notifyDebounce = new SoftSchedulerNotifyDebounce((value) => notification.UpdateActiveThreads(value));
+ return new TickerQTaskScheduler(schedulerOptionsBuilder.MaxConcurrency, schedulerOptionsBuilder.IdleWorkerTimeOut, notifyDebounce);
+ });
+ }
+ else
{
- var notification = sp.GetRequiredService();
- var notifyDebounce = new SoftSchedulerNotifyDebounce((value) => notification.UpdateActiveThreads(value));
- return new TickerQTaskScheduler(schedulerOptionsBuilder.MaxConcurrency, schedulerOptionsBuilder.IdleWorkerTimeOut, notifyDebounce);
- });
+ // Register NoOp implementations when background services are disabled
+ services.AddSingleton();
+ services.AddSingleton();
+ }
+
+ services.AddSingleton();
optionInstance.ExternalProviderConfigServiceAction?.Invoke(services);
optionInstance.DashboardServiceAction?.Invoke(services);
@@ -85,25 +97,32 @@ public static IApplicationBuilder UseTickerQ(this IApplicationBuilder app, Ticke
var configuration = serviceProvider.GetService();
var notificationHubSender = serviceProvider.GetService();
var backgroundScheduler = serviceProvider.GetService();
- backgroundScheduler.SkipFirstRun = qStartMode == TickerQStartMode.Manual;
- TickerFunctionProvider.UpdateCronExpressionsFromIConfiguration(configuration);
- TickerFunctionProvider.Build();
-
- tickerExecutionContext.NotifyCoreAction += (value, type) =>
+ // If background services are registered, configure them
+ if (backgroundScheduler != null)
{
- if (type == CoreNotifyActionType.NotifyHostExceptionMessage)
+ backgroundScheduler.SkipFirstRun = qStartMode == TickerQStartMode.Manual;
+
+ tickerExecutionContext.NotifyCoreAction += (value, type) =>
{
- notificationHubSender.UpdateHostException(value);
- tickerExecutionContext.LastHostExceptionMessage = (string)value;
- }
- else if (type == CoreNotifyActionType.NotifyNextOccurence)
- notificationHubSender.UpdateNextOccurrence(value);
- else if (type == CoreNotifyActionType.NotifyHostStatus)
- notificationHubSender.UpdateHostStatus(value);
- else if (type == CoreNotifyActionType.NotifyThreadCount)
- notificationHubSender.UpdateActiveThreads(value);
- };
+ if (type == CoreNotifyActionType.NotifyHostExceptionMessage)
+ {
+ notificationHubSender.UpdateHostException(value);
+ tickerExecutionContext.LastHostExceptionMessage = (string)value;
+ }
+ else if (type == CoreNotifyActionType.NotifyNextOccurence)
+ notificationHubSender.UpdateNextOccurrence(value);
+ else if (type == CoreNotifyActionType.NotifyHostStatus)
+ notificationHubSender.UpdateHostStatus(value);
+ else if (type == CoreNotifyActionType.NotifyThreadCount)
+ notificationHubSender.UpdateActiveThreads(value);
+ };
+ }
+ // If background services are not registered (due to DisableBackgroundServices()),
+ // silently skip background service configuration. This is expected behavior.
+
+ TickerFunctionProvider.UpdateCronExpressionsFromIConfiguration(configuration);
+ TickerFunctionProvider.Build();
// Run core seeding pipeline based on main options (works for both in-memory and EF providers).
var options = tickerExecutionContext.OptionsSeeding;
diff --git a/src/TickerQ/Src/Dispatcher/TickerQDispatcher.cs b/src/TickerQ/Src/Dispatcher/TickerQDispatcher.cs
index ff83f183..95b36f2f 100644
--- a/src/TickerQ/Src/Dispatcher/TickerQDispatcher.cs
+++ b/src/TickerQ/Src/Dispatcher/TickerQDispatcher.cs
@@ -12,6 +12,8 @@ internal class TickerQDispatcher : ITickerQDispatcher
private readonly TickerQTaskScheduler _taskScheduler;
private readonly TickerExecutionTaskHandler _taskHandler;
+ public bool IsEnabled => true;
+
public TickerQDispatcher(TickerQTaskScheduler taskScheduler, TickerExecutionTaskHandler taskHandler)
{
_taskScheduler = taskScheduler ?? throw new ArgumentNullException(nameof(taskScheduler));
diff --git a/tests/TickerQ.Tests/TickerOptionsBuilderTests.cs b/tests/TickerQ.Tests/TickerOptionsBuilderTests.cs
index 195086c3..e8d3b781 100644
--- a/tests/TickerQ.Tests/TickerOptionsBuilderTests.cs
+++ b/tests/TickerQ.Tests/TickerOptionsBuilderTests.cs
@@ -151,4 +151,28 @@ public void ConfigureScheduler_Invokes_Delegate()
schedulerOptions.MaxConcurrency.Should().Be(42);
schedulerOptions.NodeIdentifier.Should().Be("test-node");
}
+
+ [Fact]
+ public void DisableBackgroundServices_Sets_Flag_To_False()
+ {
+ var executionContext = new TickerExecutionContext();
+ var schedulerOptions = new SchedulerOptionsBuilder();
+
+ var builder = new TickerOptionsBuilder(executionContext, schedulerOptions);
+
+ // Default should be true
+ var defaultFlag = typeof(TickerOptionsBuilder)
+ .GetProperty("RegisterBackgroundServices", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
+ .GetValue(builder);
+ defaultFlag.Should().BeOfType().Which.Should().BeTrue();
+
+ // After calling DisableBackgroundServices, should be false
+ builder.DisableBackgroundServices();
+
+ var flag = typeof(TickerOptionsBuilder)
+ .GetProperty("RegisterBackgroundServices", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)!
+ .GetValue(builder);
+
+ flag.Should().BeOfType().Which.Should().BeFalse();
+ }
}