From 82d1cdde9bc2c7409b9a5723a8a55973a0f4682d Mon Sep 17 00:00:00 2001 From: John Korsnes Date: Mon, 21 Sep 2026 18:04:29 +0200 Subject: [PATCH] Make recurring actions scoped instead of singleton AddRecurrer now registers T with AddScoped instead of forcing every IRecurringAction into a DI singleton. CronBackgroundService creates a fresh scope per cron tick and resolves T from it, so actions can take scoped dependencies (e.g. MassTransit's IPublishEndpoint) directly in their constructor instead of injecting IServiceScopeFactory and opening a scope by hand. IRecurringAction is no longer registered as an open multi-service (GetServices() no longer resolves anything); only the concrete T is registered now. --- .../CronBackgroundService.cs | 39 +++++++++++++------ .../Hosting/ServiceCollectionExtensions.cs | 7 ++-- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/CronBackgroundServices/CronBackgroundService.cs b/src/CronBackgroundServices/CronBackgroundService.cs index f871340..93d25a2 100644 --- a/src/CronBackgroundServices/CronBackgroundService.cs +++ b/src/CronBackgroundServices/CronBackgroundService.cs @@ -1,18 +1,31 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace CronBackgroundServices; -internal class CronBackgroundService(IRecurringAction Action, ILogger logger) : BackgroundService +internal class CronBackgroundService : BackgroundService where T : IRecurringAction { - private readonly Timing _timing = new(Action.GetTimeZoneId()); + private readonly IServiceScopeFactory _scopeFactory; + private readonly ILogger _logger; + private readonly Timing _timing; + private readonly string _cron; - private string Cron { get; } = Action.Cron; + public CronBackgroundService(IServiceScopeFactory scopeFactory, ILogger logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + + using var scope = scopeFactory.CreateScope(); + var action = scope.ServiceProvider.GetRequiredService(); + _cron = action.Cron; + _timing = new Timing(action.GetTimeZoneId()); + } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - logger.LogTrace( - $"Using {Cron} and timezone '{_timing.TimeZoneInfo.Id}. The time in this timezone: {_timing.RelativeNow()}'"); + _logger.LogTrace( + $"Using {_cron} and timezone '{_timing.TimeZoneInfo.Id}. The time in this timezone: {_timing.RelativeNow()}'"); DateTimeOffset? next = null; do @@ -21,25 +34,27 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (next == null) { - next = _timing.GetNextOccurenceInRelativeTime(Cron); - var uText = _timing.Get10NextOccurrences(Cron); + next = _timing.GetNextOccurenceInRelativeTime(_cron); + var uText = _timing.Get10NextOccurrences(_cron); var logText = $"Ten next occurrences :\n{uText.Aggregate((x, y) => x + "\n" + y)}"; - logger.LogTrace(logText); + _logger.LogTrace(logText); } if (now > next) { try { - await Action.Process(stoppingToken); + using var scope = _scopeFactory.CreateScope(); + var action = scope.ServiceProvider.GetRequiredService(); + await action.Process(stoppingToken); } catch (Exception e) { - logger.LogError(e, e.Message); + _logger.LogError(e, e.Message); } - next = _timing.GetNextOccurenceInRelativeTime(Cron); - logger.LogTrace(next is not null + next = _timing.GetNextOccurenceInRelativeTime(_cron); + _logger.LogTrace(next is not null ? $"Next at {next.Value.DateTime.ToLongDateString()} {next.Value.DateTime.ToLongTimeString()}" : "No more occurences."); } diff --git a/src/CronBackgroundServices/Hosting/ServiceCollectionExtensions.cs b/src/CronBackgroundServices/Hosting/ServiceCollectionExtensions.cs index 5fcd1c9..71fae10 100644 --- a/src/CronBackgroundServices/Hosting/ServiceCollectionExtensions.cs +++ b/src/CronBackgroundServices/Hosting/ServiceCollectionExtensions.cs @@ -11,14 +11,13 @@ public static class ServiceCollectionExtensions /// public static IServiceCollection AddRecurrer(this IServiceCollection services) where T : class, IRecurringAction { - services.AddSingleton(); + services.AddScoped(); services.AddSingleton(s => { - var allRecurrers = s.GetServices(); - var single = allRecurrers.First(r => r is T); + var scopeFactory = s.GetRequiredService(); var loggerFactory = s.GetRequiredService(); var logger = loggerFactory.CreateLogger(); - return new CronBackgroundService(single, logger); + return new CronBackgroundService(scopeFactory, logger); }); return services; }