From 103830f3255f56a06573e94228825e419cf41740 Mon Sep 17 00:00:00 2001 From: John Korsnes Date: Sat, 11 Sep 2021 12:57:39 +0200 Subject: [PATCH] Simplify servicecollection extensions (#3) - Breaking change: removes DI builder pattern - Breaking change: drop netcoreapp3.1 support Other: - Update to C# 10 features and use of .NET 6 SDK +semver:major --- .editorconfig | 3 + .github/workflows/CI.yml | 15 ++- .github/workflows/PreRelease.yml | 5 +- .github/workflows/Release.yml | 5 +- README.md | 32 +++++-- Samples/ConsoleApp/ConsoleApp.csproj | 2 + Samples/ConsoleApp/Program.cs | 39 ++++---- .../CronBackgroundService.cs | 92 +++++++++---------- .../CronBackgroundServices.csproj | 17 ++-- .../Hosting/RecurringActionsBuilder.cs | 48 ---------- .../Hosting/ServiceCollectionExtensions.cs | 33 ++++--- .../IRecurringAction.cs | 63 +++++++------ .../IRecurringActionsBuilder.cs | 13 --- src/CronBackgroundServices/Timing.cs | 78 ++++++++-------- 14 files changed, 197 insertions(+), 248 deletions(-) delete mode 100644 src/CronBackgroundServices/Hosting/RecurringActionsBuilder.cs delete mode 100644 src/CronBackgroundServices/IRecurringActionsBuilder.cs diff --git a/.editorconfig b/.editorconfig index e98711d..22ecf73 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,9 @@ # Remove the line below if you want to inherit .editorconfig settings from higher directories root = true +[*] +charset = utf-8 + # C# files [*.cs] diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4fd3ae1..23378e2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,7 +2,8 @@ name: CI on: push: - branches: [main] + branches: + - "*" pull_request: branches: [main] @@ -10,23 +11,21 @@ jobs: ci-build: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v2 - uses: actions/setup-dotnet@v1 with: - dotnet-version: "3.1.x" - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "5.0.x" - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "6.0.x" + dotnet-version: "6.0.100-rc.2.21458.9" include-prerelease: true + - run: dotnet --info - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore - name: Test run: dotnet test --no-build --verbosity normal + - name: PackTest + run: dotnet pack diff --git a/.github/workflows/PreRelease.yml b/.github/workflows/PreRelease.yml index e815030..f1a9ad1 100644 --- a/.github/workflows/PreRelease.yml +++ b/.github/workflows/PreRelease.yml @@ -11,15 +11,12 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "3.1.x" - uses: actions/setup-dotnet@v1 with: dotnet-version: "5.0.x" - uses: actions/setup-dotnet@v1 with: - dotnet-version: "6.0.x" + dotnet-version: "6.0.100-rc.2.21458.9" include-prerelease: true - name: Restore dependencies run: dotnet restore diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index f90c2ae..94b3a31 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -11,15 +11,12 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - - uses: actions/setup-dotnet@v1 - with: - dotnet-version: "3.1.x" - uses: actions/setup-dotnet@v1 with: dotnet-version: "5.0.x" - uses: actions/setup-dotnet@v1 with: - dotnet-version: "6.0.x" + dotnet-version: "6.0.100-rc.2.21458.9" include-prerelease: true - name: Restore dependencies run: dotnet restore diff --git a/README.md b/README.md index e54498d..ece33a3 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ [![main](https://github.com/slackbot-net/CronBackgroundServices/workflows/CI/badge.svg)](https://github.com/slackbot-net/CronBackgroundServices/actions) [![NuGet](https://img.shields.io/nuget/v/CronBackgroundServices.svg)](https://www.nuget.org/packages/CronBackgroundServices/) [![NuGet](https://img.shields.io/nuget/vpre/CronBackgroundServices.svg)](https://www.nuget.org/packages/CronBackgroundServices/) - - ### CronBackgroundServices .NET BackgroundService jobs triggered by configured Cron Expressions - ### Installation ```bash @@ -18,10 +15,9 @@ $ dotnet add package CronBackgroundServices Jobs are configured during DI registration: ```csharp -services.AddRecurringActions() +services .AddRecurrer() .AddRecurrer() -.Build(); ``` Each job has to implement `IRecurringAction`. If you want a different TimeZone than UTC you have to override the default interface method `GetTimeZoneId`. @@ -29,9 +25,33 @@ Each job has to implement `IRecurringAction`. If you want a different TimeZone t ```csharp public interface IRecurringAction { - + + /// + /// The job to be executed at intervals defined by the Cron expression + /// + /// Task Process(CancellationToken stoppingToken); + + /// + /// The cron expression (including seconds) as defined by the Cronos library: + /// See https://github.com/HangfireIO/Cronos#cron-format + /// Ex: Every second: */1 * * * * * + /// Ex: Every minute: 0 */1 * * * * + /// Ex: Every midnight: 0 0 */1 * * * + /// Ex: First of every month 0 0 0 1 * * + /// + /// A valid Cron Expression string Cron { get; } + + /// + /// Optional: The TimeZone in which the Cron expression should be based on. + /// Defaults to UTC (Europe/London or GMT Standard Time) + /// + /// NB! When overriding this and targeting versions below .NET 6, use platform specific identifiers + /// If your runtime is .NET 6 or above, it's not required. It will handles the conversion: + /// See https://github.com/dotnet/runtime/pull/49412 + /// + /// timezoneId string GetTimeZoneId() { return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Europe/London" : "GMT Standard Time"; diff --git a/Samples/ConsoleApp/ConsoleApp.csproj b/Samples/ConsoleApp/ConsoleApp.csproj index 85b6807..afa57b3 100644 --- a/Samples/ConsoleApp/ConsoleApp.csproj +++ b/Samples/ConsoleApp/ConsoleApp.csproj @@ -2,6 +2,8 @@ net6.0 + enable + preview diff --git a/Samples/ConsoleApp/Program.cs b/Samples/ConsoleApp/Program.cs index 5b67ba3..4db3d1f 100644 --- a/Samples/ConsoleApp/Program.cs +++ b/Samples/ConsoleApp/Program.cs @@ -1,31 +1,32 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using CronBackgroundServices; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using CronBackgroundServices; Host.CreateDefaultBuilder(args) - .ConfigureLogging(logging => logging.SetMinimumLevel(LogLevel.Trace)) - .ConfigureServices((_, services) => services.AddRecurringActions().AddRecurrer().Build()) + .ConfigureServices(services => services + .AddRecurrer() + .AddRecurrer()) .Build() .Run(); -public class MyCustomRecurringJob : IRecurringAction +public class EveryFiveSeconds : IRecurringAction { - private readonly ILogger _logger; + public string Cron => "*/5 * * * * *"; - public MyCustomRecurringJob(ILogger logger) - { - _logger = logger; - } - public Task Process(CancellationToken stoppingToken) + public Task Process(CancellationToken stoppingToken) => Logger.Log("🕔 Tick 5th second 5️⃣ 🖐"); +} + +public class EveryThreeSeconds : IRecurringAction +{ + public string Cron => "*/3 * * * * *"; + + public Task Process(CancellationToken stoppingToken) => Logger.Log("🕒 Tick 3rd second 3️⃣ 🥉"); +} + +static class Logger +{ + public static Task Log(string msg) { - _logger.LogInformation("Tick"); + Console.WriteLine(msg); return Task.CompletedTask; } - - public string Cron => "* * * * * *"; // Every 30 seconds, in the zero-th minute, every hour, https://github.com/HangfireIO/Cronos#usage } - diff --git a/src/CronBackgroundServices/CronBackgroundService.cs b/src/CronBackgroundServices/CronBackgroundService.cs index 0808be6..8b309c0 100644 --- a/src/CronBackgroundServices/CronBackgroundService.cs +++ b/src/CronBackgroundServices/CronBackgroundService.cs @@ -1,69 +1,63 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace CronBackgroundServices +namespace CronBackgroundServices; + +internal class CronBackgroundService : BackgroundService { - internal class CronBackgroundService : BackgroundService + protected readonly IRecurringAction Action; + private readonly ILogger _logger; + private readonly Timing _timing; + + public CronBackgroundService(IRecurringAction action, ILogger logger) { - protected readonly IRecurringAction Action; - private readonly ILogger _logger; - private readonly Timing _timing; + _timing = new Timing(action.GetTimeZoneId()); + Action = action; + _logger = logger; + Cron = action.Cron; + _logger.LogTrace($"Using {Cron} and timezone '{_timing.TimeZoneInfo.Id}. The time in this timezone: {_timing.RelativeNow()}'"); + } - public CronBackgroundService(IRecurringAction action, ILogger logger) - { - _timing = new Timing(action.GetTimeZoneId()); - Action = action; - _logger = logger; - Cron = action.Cron; - _logger.LogTrace($"Using {Cron} and timezone '{_timing.TimeZoneInfo.Id}. The time in this timezone: {_timing.RelativeNow()}'"); - } + private string Cron { get; } - private string Cron { get; } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + DateTimeOffset? next = null; - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + do { - DateTimeOffset? next = null; + var now = _timing.RelativeNow(); - do + if (next == null) { - var now = _timing.RelativeNow(); + 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); + } - if (next == null) - { - 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); - } - - if (now > next) + if (now > next) + { + try { - try - { - await Action.Process(stoppingToken); - } - catch (Exception e) - { - _logger.LogError(e, e.Message); - } - - next = _timing.GetNextOccurenceInRelativeTime(Cron); - _logger.LogTrace($"Next at {next.Value.DateTime.ToLongDateString()} {next.Value.DateTime.ToLongTimeString()}"); + await Action.Process(stoppingToken); } - else + catch (Exception e) { - // needed for graceful shutdown for some reason. - // 100ms chosen so it doesn't affect calculating the next - // cron occurence (lowest possible: every second) - await Task.Delay(100); + _logger.LogError(e, e.Message); } - } while (!stoppingToken.IsCancellationRequested); - } + next = _timing.GetNextOccurenceInRelativeTime(Cron); + _logger.LogTrace($"Next at {next.Value.DateTime.ToLongDateString()} {next.Value.DateTime.ToLongTimeString()}"); + } + else + { + // needed for graceful shutdown for some reason. + // 100ms chosen so it doesn't affect calculating the next + // cron occurence (lowest possible: every second) + await Task.Delay(100); + } + } while (!stoppingToken.IsCancellationRequested); } } diff --git a/src/CronBackgroundServices/CronBackgroundServices.csproj b/src/CronBackgroundServices/CronBackgroundServices.csproj index f8c474e..efaa4cd 100644 --- a/src/CronBackgroundServices/CronBackgroundServices.csproj +++ b/src/CronBackgroundServices/CronBackgroundServices.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0;net6.0 + net5.0;net6.0 CronBackgroundServices CronBackgroundServices John Korsnes @@ -16,26 +16,21 @@ images/cron.png cron.png git - 6.0.0-preview.4.21253.7 + enable + latest - + - - + + - - - - - - diff --git a/src/CronBackgroundServices/Hosting/RecurringActionsBuilder.cs b/src/CronBackgroundServices/Hosting/RecurringActionsBuilder.cs deleted file mode 100644 index f35f47d..0000000 --- a/src/CronBackgroundServices/Hosting/RecurringActionsBuilder.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -namespace CronBackgroundServices.Hosting -{ - public class RecurringActionsBuilder : IRecurringActionsBuilder - { - public RecurringActionsBuilder(IServiceCollection services) - { - Services = services; - } - - public IServiceCollection Services - { - get; - } - - public IRecurringActionsBuilder Build() - { - var recurrers = Services.Where(s => s.ServiceType == typeof(IRecurringAction)).ToList(); - if(!recurrers.Any()) - throw new Exception("No recurrers added. Missing"); - - foreach(var recurrer in recurrers) - { - Services.AddSingleton(s => - { - var allRecurrers = s.GetServices(); - var single = allRecurrers.First(r => r.GetType() == recurrer.ImplementationType); - var loggerFactory = s.GetService(); - var logger = loggerFactory.CreateLogger(single.GetType()); - return new CronBackgroundService(single, logger); - }); - } - - return this; - } - - public IRecurringActionsBuilder AddRecurrer() where T : class, IRecurringAction - { - Services.AddSingleton(); - return this; - } - } -} \ No newline at end of file diff --git a/src/CronBackgroundServices/Hosting/ServiceCollectionExtensions.cs b/src/CronBackgroundServices/Hosting/ServiceCollectionExtensions.cs index 7b9eec9..824ea97 100644 --- a/src/CronBackgroundServices/Hosting/ServiceCollectionExtensions.cs +++ b/src/CronBackgroundServices/Hosting/ServiceCollectionExtensions.cs @@ -1,18 +1,25 @@ -using CronBackgroundServices; -using CronBackgroundServices.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; -// namespace on purpose: -// ReSharper disable once CheckNamespace -namespace Microsoft.Extensions.DependencyInjection +namespace CronBackgroundServices; + +public static class SlackbotWorkerBuilderExtensions { - public static class SlackbotWorkerBuilderExtensions + /// + /// For distributed apps + /// + public static IServiceCollection AddRecurrer(this IServiceCollection services) where T : class, IRecurringAction { - /// - /// For distributed apps - /// - public static IRecurringActionsBuilder AddRecurringActions(this IServiceCollection services) + services.AddSingleton(); + services.AddSingleton(s => { - return new RecurringActionsBuilder(services); - } + var allRecurrers = s.GetServices(); + var single = allRecurrers.First(r => r is T); + var loggerFactory = s.GetService(); + var logger = loggerFactory.CreateLogger(); + return new CronBackgroundService(single, logger); + }); + return services; } -} \ No newline at end of file +} diff --git a/src/CronBackgroundServices/IRecurringAction.cs b/src/CronBackgroundServices/IRecurringAction.cs index f1b6be3..a5353d0 100644 --- a/src/CronBackgroundServices/IRecurringAction.cs +++ b/src/CronBackgroundServices/IRecurringAction.cs @@ -1,38 +1,37 @@ using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -namespace CronBackgroundServices +namespace CronBackgroundServices; + +public interface IRecurringAction { - public interface IRecurringAction - { - /// - /// The job to be executed at intervals defined by the Cron expression - /// - /// - Task Process(CancellationToken stoppingToken); + /// + /// The job to be executed at intervals defined by the Cron expression + /// + /// + Task Process(CancellationToken stoppingToken); - /// - /// The cron expression (including seconds) as defined by the Cronos library: - /// See https://github.com/HangfireIO/Cronos#cron-format - /// Ex: Every second: */1 * * * * * - /// Ex: Every minute: 0 */1 * * * * - /// Ex: Every midnight: 0 0 */1 * * * - /// Ex: First of every month 0 0 0 1 * * - /// - /// A valid Cron Expression - string Cron { get; } + /// + /// The cron expression (including seconds) as defined by the Cronos library: + /// See https://github.com/HangfireIO/Cronos#cron-format + /// Ex: Every second: */1 * * * * * + /// Ex: Every minute: 0 */1 * * * * + /// Ex: Every midnight: 0 0 */1 * * * + /// Ex: First of every month 0 0 0 1 * * + /// + /// A valid Cron Expression + string Cron { get; } - /// - /// The TimeZone in which the Cron expression should be based on. - /// Defaults to UTC. - /// - /// NB! Platform specific, so make sure it returns a valid timezoneId for the platform you're targeting! - /// - /// timezoneId - string GetTimeZoneId() - { - return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Europe/London" : "GMT Standard Time"; - } + /// + /// Optional: The TimeZone in which the Cron expression should be based on. + /// Defaults to UTC (Europe/London or GMT Standard Time) + /// + /// NB! When overriding this and targeting versions below .NET 6, use platform specific identifiers + /// If your runtime is .NET 6 or above, it's not required. It will handles the conversion: + /// See https://github.com/dotnet/runtime/pull/49412 + /// + /// timezoneId + string GetTimeZoneId() + { + return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "Europe/London" : "GMT Standard Time"; } -} \ No newline at end of file +} diff --git a/src/CronBackgroundServices/IRecurringActionsBuilder.cs b/src/CronBackgroundServices/IRecurringActionsBuilder.cs deleted file mode 100644 index b08e0dd..0000000 --- a/src/CronBackgroundServices/IRecurringActionsBuilder.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace CronBackgroundServices -{ - public interface IRecurringActionsBuilder - { - IServiceCollection Services { get; } - IRecurringActionsBuilder AddRecurrer() where T : class, IRecurringAction; - - IRecurringActionsBuilder Build(); - - } -} \ No newline at end of file diff --git a/src/CronBackgroundServices/Timing.cs b/src/CronBackgroundServices/Timing.cs index c5739b1..8082ae0 100644 --- a/src/CronBackgroundServices/Timing.cs +++ b/src/CronBackgroundServices/Timing.cs @@ -1,55 +1,51 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Cronos; -namespace CronBackgroundServices +namespace CronBackgroundServices; + +internal class Timing { - internal class Timing + public readonly TimeZoneInfo TimeZoneInfo; + + public Timing(string timeZoneId) { - public readonly TimeZoneInfo TimeZoneInfo; + TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); + } - public Timing(string timeZoneId) - { - TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); - } + public DateTimeOffset RelativeNow(DateTimeOffset? nowutc = null) + { + return TimeZoneInfo.ConvertTime(nowutc ?? DateTimeOffset.UtcNow, TimeZoneInfo); + } - public DateTimeOffset RelativeNow(DateTimeOffset? nowutc = null) - { - return TimeZoneInfo.ConvertTime(nowutc ?? DateTimeOffset.UtcNow, TimeZoneInfo); - } + public DateTimeOffset? GetNextOccurenceInRelativeTime(string cron) + { + var expression = CronExpression.Parse(cron, CronFormat.IncludeSeconds); + return expression.GetNextOccurrence(DateTimeOffset.UtcNow, TimeZoneInfo); + } - public DateTimeOffset? GetNextOccurenceInRelativeTime(string cron) - { - var expression = CronExpression.Parse(cron, CronFormat.IncludeSeconds); - return expression.GetNextOccurrence(DateTimeOffset.UtcNow, TimeZoneInfo); - } + public IEnumerable Get10NextOccurrences(string cron) + { + var expression = CronExpression.Parse(cron, CronFormat.IncludeSeconds); + var fromUtc = DateTime.UtcNow; + var upcoming = new List(); + upcoming.AddRange(Get10Occurrences(upcoming, expression, fromUtc, fromUtc.AddMonths(1))); + return upcoming.Select(u => $"{u.ToLongDateString()} {u.ToLongTimeString()}"); + } - public IEnumerable Get10NextOccurrences(string cron) + private IEnumerable Get10Occurrences(List upcoming, CronExpression expression, DateTime fromUtc, DateTime toUtc) + { + while (true) { - var expression = CronExpression.Parse(cron, CronFormat.IncludeSeconds); - var fromUtc = DateTime.UtcNow; - var upcoming = new List(); - upcoming.AddRange(Get10Occurrences(upcoming, expression, fromUtc, fromUtc.AddMonths(1))); - return upcoming.Select(u => $"{u.ToLongDateString()} {u.ToLongTimeString()}"); - } + toUtc = toUtc.AddMonths(1); + var occurrences = expression.GetOccurrences(fromUtc, toUtc); + upcoming = occurrences.ToList(); - private IEnumerable Get10Occurrences(List upcoming, CronExpression expression, DateTime fromUtc, DateTime toUtc) - { - while (true) + if (upcoming.Count < 10) { - toUtc = toUtc.AddMonths(1); - var occurrences = expression.GetOccurrences(fromUtc, toUtc); - upcoming = occurrences.ToList(); - - if (upcoming.Count < 10) - { - continue; - } - break; + continue; } - return upcoming.Take(10); - + break; } + return upcoming.Take(10); + } -} \ No newline at end of file +}