From 469793770977fbb6f8c58e09c8c7a30781056d0b Mon Sep 17 00:00:00 2001 From: Hadi Khazaee Asl Date: Wed, 19 Aug 2026 12:33:57 +0330 Subject: [PATCH] add Support for Background Services ... --- Extensions/BackgroundServiceExtensions.cs | 25 +++ Providers/XBackgroundService.cs | 26 +++ Providers/XResumableBackgroundService.cs | 93 ++++++++++ Providers/XTimeBasedBackgroundService.cs | 215 ++++++++++++++++++++++ 4 files changed, 359 insertions(+) create mode 100644 Extensions/BackgroundServiceExtensions.cs create mode 100644 Providers/XBackgroundService.cs create mode 100644 Providers/XResumableBackgroundService.cs create mode 100644 Providers/XTimeBasedBackgroundService.cs diff --git a/Extensions/BackgroundServiceExtensions.cs b/Extensions/BackgroundServiceExtensions.cs new file mode 100644 index 0000000..c716fb5 --- /dev/null +++ b/Extensions/BackgroundServiceExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using xCommons.Providers; + +namespace xCommons.Extensions +{ + public static class BackgroundServiceExtensions + { + /// + /// Register Background Services ... + /// + /// + /// + /// + public static void AddXBackgroundService( + this IServiceCollection source, + TService service + ) + where TService : XBackgroundService + { + // + source.AddSingleton(service); + source.AddHostedService(); + } + } +} \ No newline at end of file diff --git a/Providers/XBackgroundService.cs b/Providers/XBackgroundService.cs new file mode 100644 index 0000000..d8e35b7 --- /dev/null +++ b/Providers/XBackgroundService.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace xCommons.Providers +{ + /// + /// Default Background Service ... + /// + public abstract class XBackgroundService : BackgroundService + { + // + protected readonly IServiceProvider serviceProvider; + protected readonly ILogger logger; + + protected XBackgroundService( + IServiceProvider serviceProvider, + ILogger logger + ) + { + // + this.logger = logger; + this.serviceProvider = serviceProvider; + } + } +} \ No newline at end of file diff --git a/Providers/XResumableBackgroundService.cs b/Providers/XResumableBackgroundService.cs new file mode 100644 index 0000000..3d6bc45 --- /dev/null +++ b/Providers/XResumableBackgroundService.cs @@ -0,0 +1,93 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace xCommons.Providers +{ + /// + /// Resumable Background Task ... + /// + public abstract class XResumableBackgroundService : XBackgroundService + { + /// + /// Check is Paused ... + /// + protected bool paused = false; + + protected XResumableBackgroundService( + IServiceProvider serviceProvider, + ILogger logger + ) : base( + logger: logger, + serviceProvider: serviceProvider + ) + { } + + protected override async Task ExecuteAsync( + CancellationToken stoppingToken = default + ) + { + // + if (paused) + { + return; + } + + // + await DoWorkAsync( + payload: null, + cancellationToken: stoppingToken + ); + } + + /// + /// do all required works here ... + /// + /// + /// + /// + public abstract Task DoWorkAsync( + object payload = null, + CancellationToken cancellationToken = default + ); + + /// + /// Pause Consuming ... + /// + /// + public bool Pause() + { + // + var result = !paused; + if (!result) + { + return result; + } + + // + paused = true; + result = paused; + return result; + } + + /// + /// Resume Consuming ... + /// + /// + public bool Resume() + { + // + var result = paused; + if (!result) + { + return result; + } + + // + paused = false; + result = !paused; + return result; + } + } +} \ No newline at end of file diff --git a/Providers/XTimeBasedBackgroundService.cs b/Providers/XTimeBasedBackgroundService.cs new file mode 100644 index 0000000..2527cbc --- /dev/null +++ b/Providers/XTimeBasedBackgroundService.cs @@ -0,0 +1,215 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using xCommons.Extensions; +using xExceptions.Constants; + +namespace xCommons.Providers +{ + /// + /// Do Background Jobs based on Time Intervals ... + /// + public abstract class XTimeBasedBackgroundService : XBackgroundService + { + // + private bool isWorking = false; + private readonly TimeSpan restartInterval; + private readonly SemaphoreSlim mlock = new(1, 1); + public DateTime? LastRunTime { get; private set; } + private CancellationTokenSource cancellationTokenSource; + + protected XTimeBasedBackgroundService( + IServiceProvider serviceProvider, + ILogger logger, + int intervalInMinutes = 10 + ) : base( + logger: logger, + serviceProvider: serviceProvider + ) + { + // + // Validate Interval ... + if (intervalInMinutes <= 0) + { + XException.InvalidArgs.Throw(); + } + + // + restartInterval = TimeSpan.FromMinutes(intervalInMinutes); + } + + /// + /// Override base action to Implement timing ... + /// + /// + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // + while (!stoppingToken.IsCancellationRequested) + { + try + { + if (!isWorking) + { + // + // Enable Working ... + await mlock.WaitAsync(stoppingToken); + try + { + isWorking = true; + } + finally + { + mlock.Release(); + } + + // + // Start a new work task ... + cancellationTokenSource = CancellationTokenSource + .CreateLinkedTokenSource(stoppingToken); + + // + // Execute the work ... + await ExecuteWorkAsync(cancellationTokenSource.Token); + + // + // Disable Working ... + await mlock.WaitAsync(stoppingToken); + try + { + isWorking = false; + } + finally + { + mlock.Release(); + } + + // + // Wait for the restart interval ... + await Task.Delay(restartInterval, stoppingToken); + } + else + { + // + // Prevent tight error loops ... + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + } + } + catch (OperationCanceledException) + { + // + // Disable Working ... + await mlock.WaitAsync(stoppingToken); + try + { + isWorking = false; + } + finally + { + mlock.Release(); + } + break; + } + catch (Exception) + { + // + // Disable Working ... + await mlock.WaitAsync(stoppingToken); + try + { + isWorking = false; + } + finally + { + mlock.Release(); + } + + // + // Prevent tight error loops ... + await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); + } + } + } + + /// + /// Override Stop Background Worker ... + /// + /// + /// + public override async Task StopAsync(CancellationToken cancellationToken) + { + // + // Cancel the current work session + cancellationTokenSource.Cancel(); + await base.StopAsync(cancellationToken); + } + + /// + /// Dispose all Objects ... + /// + public override void Dispose() + { + mlock.Dispose(); + cancellationTokenSource.Dispose(); + GC.SuppressFinalize(this); + base.Dispose(); + } + + /// + /// do all required works here ... + /// + /// + /// + public abstract Task DoWorkAsync( + CancellationToken cancellationToken = default + ); + + /// + /// Refresh Worker ... + /// + /// + /// + public async Task Refresh( + CancellationToken cancellationToken = default + ) + { + // + // Cancel current work if running + cancellationTokenSource.Cancel(); + + // + // Wait a moment for cancellation + await Task.Delay(100, cancellationToken); + + // + // Start new work immediately + cancellationTokenSource = CancellationTokenSource + .CreateLinkedTokenSource(cancellationToken); + await ExecuteWorkAsync(cancellationTokenSource.Token); + } + + // + #region Private ... + private async Task ExecuteWorkAsync(CancellationToken cancellationToken) + { + // + LastRunTime = DateTime.UtcNow; + try + { + await DoWorkAsync(cancellationToken); + } + catch (OperationCanceledException) + { + } + catch (Exception) + { + } + finally + { + } + } + #endregion + } +} \ No newline at end of file