diff --git a/.gitignore b/.gitignore index 67be427..0c9dd05 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,6 @@ BenchmarkDotNet.Artifacts/ project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json # StyleCop StyleCopReport.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a16aba..3e375f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,28 @@ Change Log =============================================================================== +Version 3.1.0 (6/12/2022) + +* `SchedulerBuilder.UnobservedTaskExceptionHandler` marked as `Obsolete`; + +* Adds a function to create an event handler that handles unobserved task exceptions during the lifetime of the CRON job. Thanks to @stijnmoreels + +```csharp + builder.AddUnobservedTaskExceptionHandler(sp => + { + var logger = sp.GetRequiredService().CreateLogger("CronJobs"); + + return + (sender, args) => + { + logger?.LogError(args.Exception?.Message); + args.SetObserved(); + }; + }); +``` + Version 3.0.1 + * Fixed issue with Scheduled jobs that are added on the fly to the execution engine issue #43 * Upgraded to the latest nuget packages @@ -30,6 +51,7 @@ Version 1.0.9 (2019-02-18) * Removed `HostedServiceBase` class in favor of built-in `BackgroundService` class. Version 1.0.7 (2018-01-16) + * Resolved issue #5 "Add support for SourceLink", to make use of this feature in Visual Studio.NET please deselect `Enable Just My Code` and select `Enable Source Link support` as shown per this image: ![enable](img/source_link_enable.JPG) diff --git a/LICENSE b/LICENSE index 3350274..8cb9556 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 King David Consulting LLC +Copyright (c) 2017-2022 King David Consulting LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7afd665..3f1be60 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ The goal of this library was to design a simple Cron Scheduling engine that could be used with DotNetCore `IHost` or with AspNetCore `IWebHost`. -It is much lighter than Quartz schedular or its alternatives. In the heart of its design was `KISS` principle. +It is much lighter than Quartz schedular or its alternatives. The `KISS` principle was at the heart of the development of this library. The `CronScheduler` can operate inside of any .NET Core GenericHost `IHost` thus makes it simpler to setup and configure but it always allow to be run inside of Kubernetes. @@ -162,8 +162,19 @@ The sample uses `Microsoft.Extensions.Http.Polly` extension library to make http builder.Services.AddHttpClient() .AddTransientHttpErrorPolicy(p => p.RetryAsync()); builder.AddJob(); + + // register a custom error processing for internal errors + builder.AddUnobservedTaskExceptionHandler(sp => + { + var logger = sp.GetRequiredService().CreateLogger("CronJobs"); - builder.UnobservedTaskExceptionHandler = UnobservedHandler; + return + (sender, args) => + { + logger?.LogError(args.Exception?.Message); + args.SetObserved(); + }; + }); }); ``` @@ -209,7 +220,18 @@ Then register this service within the `Startup.cs` builder.Services.AddScoped(); builder.AddJob(); - builder.UnobservedTaskExceptionHandler = UnobservedHandler; + // register a custom error processing for internal errors + builder.AddUnobservedTaskExceptionHandler(sp => + { + var logger = sp.GetRequiredService().CreateLogger("CronJobs"); + + return + (sender, args) => + { + logger?.LogError(args.Exception?.Message); + args.SetObserved(); + }; + }); }); ``` @@ -284,22 +306,6 @@ Then add sample async task to be executed by the Queued Hosted Service. } ``` -## Docker build - -Utilizes [King David Consulting LLC DotNet Docker Image](https://github.com/kdcllc/docker/tree/master/dotnet) - -```bash - docker-compose -f "docker-compose.yml" -f "docker-compose.override.yml" up -d --build -``` - -### Note - -Workaround for `Retrying 'FindPackagesByIdAsync' for source` in Docker containers restore. - -```bash - dotnet restore --disable-parallel -``` - ## License -[MIT License Copyright (c) 2017 King David Consulting LLC](./LICENSE) +[MIT License Copyright (c) 2017-2022 King David Consulting LLC](./LICENSE) diff --git a/appveyor.yml b/appveyor.yml index 7f7b284..d633e4f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,13 @@ -version: 3.0.{build} +version: 3.1.{build} branches: only: - master pull_requests: do_not_increment_build_number: true -image: Visual Studio 2017 -## temporary until 5.0.100 sdk is installed +image: Visual Studio 2022 +## temporary until 6.0.300 sdk is installed install: - - ps: $urlCurrent = "https://dotnetcli.blob.core.windows.net/dotnet/Sdk/5.0.100/dotnet-sdk-5.0.100-win-x64.zip" + - ps: $urlCurrent = "https://dotnetcli.blob.core.windows.net/dotnet/Sdk/6.0.300/dotnet-sdk-6.0.300-win-x64.zip" - ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetsdk" - ps: mkdir $env:DOTNET_INSTALL_DIR -Force | Out-Null - ps: $tempFileCurrent = [System.IO.Path]::GetTempFileName() @@ -33,6 +33,6 @@ deploy: - provider: NuGet artifact: /NuGet/ api_key: - secure: hs4f+3xdpI1ANqvOB7J9BZx+aBdbZYzHmoYymDFA7YCt5AWLJSdNyv2nkrBn1V9q + secure: a8sCawSwgb2kYDJAN+xTUvy+MH5jdJR+DmKakUmc/Xom1c+uxyvV+yvpSTJs+ypF on: branch: master diff --git a/build/dependencies.props b/build/dependencies.props index 01a9176..15e7e16 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,84 +1,67 @@ - - 2.1.1 - 2.1.1 - $(AspNetCoreVersion) - 3.1.11 - + + [6.*, ) + [6.*, ) + $(AspNetCoreVersion) + [4.*, ) + - - 5.0.* - 5.0.* - $(AspNetCoreVersion) - + + + + + + + + + + - - 2.1.1 - 2.1.1 - $(AspNetCoreVersion) - + + + + - - [2.2.*, ) - [2.2.*, ) - [2.2.*, ) - + + + + - - 3.1.10 - 3.1.10 - $(AspNetCoreVersion) - + + + + + - - - - - - - + + + - - - - + + + + + + + - - - - + + + + - - - - - + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/build/settings.props b/build/settings.props index ea3e792..df2241c 100644 --- a/build/settings.props +++ b/build/settings.props @@ -1,12 +1,10 @@ - 3.0.1-preview1 + 3.1.0-preview1 false true $(NoWarn);CS1591;NU1605; - true - inprocess @@ -28,7 +26,7 @@ git https://github.com/kdcllc/CronScheduler.AspNetCore/blob/master/LICENSE icon.png - Cron, Quatz, HangFire, AspNetCore, DotNet, DotNetCore CronNET, Scheduler, Worker, Hosting, Kubernetes, Docker + CronScheduler.AspNetCore, CronScheduler.Extensions, Cron, Quatz, HangFire, AspNetCore, DotNet, DotNetCore CronNET, Scheduler, Worker, Hosting, Kubernetes, Docker true diff --git a/build/sources.props b/build/sources.props index 425770a..a54fa28 100644 --- a/build/sources.props +++ b/build/sources.props @@ -5,12 +5,6 @@ $(DotNetRestoreSources) $(RestoreSources); - https://dotnetfeed.blob.core.windows.net/aspnet-aspnetcore/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-blazor/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-entityframeworkcore/index.json; - https://dotnetfeed.blob.core.windows.net/aspnet-extensions/index.json; - https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json; - https://www.myget.org/F/kdcllc/api/v3/index.json; https://api.nuget.org/v3/index.json; diff --git a/clean.ps1 b/clean.ps1 new file mode 100644 index 0000000..3ba7e3e --- /dev/null +++ b/clean.ps1 @@ -0,0 +1 @@ +Get-ChildItem .\ -include bin,obj -Recurse | foreach ($_) { remove-item $_.fullname -Force -Recurse } \ No newline at end of file diff --git a/src/CronScheduler.AspNetCore/CronScheduler.AspNetCore.csproj b/src/CronScheduler.AspNetCore/CronScheduler.AspNetCore.csproj index adbfe1a..bf2797d 100644 --- a/src/CronScheduler.AspNetCore/CronScheduler.AspNetCore.csproj +++ b/src/CronScheduler.AspNetCore/CronScheduler.AspNetCore.csproj @@ -1,11 +1,11 @@  - netstandard2.0;netcoreapp3.0;netcoreapp3.1;net5.0 + netstandard2.0;net6.0 - The Cron based Scheduler for AspNetCore 2.x/3.x Applications in Kubernetes/Docker. + The Cron based Scheduler for AspNetCore 2.x/3.x/5.x/6.x Applications in Kubernetes/Docker. This is a lightweight alternative to Quarts Scheduler or HangFire. @@ -15,8 +15,9 @@ - + + diff --git a/src/CronScheduler.AspNetCore/DependencyInjection/StartupJobWebHostExtensions.cs b/src/CronScheduler.AspNetCore/DependencyInjection/StartupJobWebHostExtensions.cs index b0ef280..333ba89 100644 --- a/src/CronScheduler.AspNetCore/DependencyInjection/StartupJobWebHostExtensions.cs +++ b/src/CronScheduler.AspNetCore/DependencyInjection/StartupJobWebHostExtensions.cs @@ -5,23 +5,20 @@ using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.AspNetCore.Hosting +namespace Microsoft.AspNetCore.Hosting; + +public static class StartupJobWebHostExtensions { - public static class StartupJobWebHostExtensions + /// + /// Runs async all of the registered jobs. + /// + /// + /// + /// + public static async Task RunStartupJobsAync(this IWebHost host, CancellationToken cancellationToken = default) { - /// - /// Runs async all of the registered jobs. - /// - /// - /// - /// - public static async Task RunStartupJobsAync(this IWebHost host, CancellationToken cancellationToken = default) - { - using (var scope = host.Services.CreateScope()) - { - var jobInitializer = scope.ServiceProvider.GetRequiredService(); - await jobInitializer.StartJobsAsync(cancellationToken); - } - } + using var scope = host.Services.CreateScope(); + var jobInitializer = scope.ServiceProvider.GetRequiredService(); + await jobInitializer.StartJobsAsync(cancellationToken); } } diff --git a/src/CronScheduler.Extensions/BackgroundTask/BackgroundTaskContext.cs b/src/CronScheduler.Extensions/BackgroundTask/BackgroundTaskContext.cs index 2dbfeef..1b8f8cc 100644 --- a/src/CronScheduler.Extensions/BackgroundTask/BackgroundTaskContext.cs +++ b/src/CronScheduler.Extensions/BackgroundTask/BackgroundTaskContext.cs @@ -1,23 +1,22 @@ using System.Threading; -namespace CronScheduler.Extensions.BackgroundTask +namespace CronScheduler.Extensions.BackgroundTask; + +public class BackgroundTaskContext { - public class BackgroundTaskContext - { - private int _outstandingTaskCount = 0; + private int _outstandingTaskCount = 0; - public bool IsComplete => _outstandingTaskCount == 0; + public bool IsComplete => _outstandingTaskCount == 0; - public int Count => _outstandingTaskCount; + public int Count => _outstandingTaskCount; - public void RegisterTask() - { - Interlocked.Increment(ref _outstandingTaskCount); - } + public void RegisterTask() + { + Interlocked.Increment(ref _outstandingTaskCount); + } - public void MarkAsComplete() - { - Interlocked.Decrement(ref _outstandingTaskCount); - } + public void MarkAsComplete() + { + Interlocked.Decrement(ref _outstandingTaskCount); } } diff --git a/src/CronScheduler.Extensions/BackgroundTask/BackgroundTaskQueue.cs b/src/CronScheduler.Extensions/BackgroundTask/BackgroundTaskQueue.cs index b65df74..80092b3 100644 --- a/src/CronScheduler.Extensions/BackgroundTask/BackgroundTaskQueue.cs +++ b/src/CronScheduler.Extensions/BackgroundTask/BackgroundTaskQueue.cs @@ -3,68 +3,67 @@ using System.Threading; using System.Threading.Tasks; -namespace CronScheduler.Extensions.BackgroundTask +namespace CronScheduler.Extensions.BackgroundTask; + +public class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable { - public class BackgroundTaskQueue : IBackgroundTaskQueue, IDisposable - { - private readonly ConcurrentQueue<(Func workItem, string workItemName, Action onExeption)> - _workItems = new ConcurrentQueue<(Func workItem, string workItemName, Action onException)>(); + private readonly ConcurrentQueue<(Func workItem, string workItemName, Action onExeption)> + _workItems = new ConcurrentQueue<(Func workItem, string workItemName, Action onException)>(); - private readonly SemaphoreSlim _signal = new SemaphoreSlim(0); - private readonly BackgroundTaskContext _context; + private readonly SemaphoreSlim _signal = new SemaphoreSlim(0); + private readonly BackgroundTaskContext _context; - public BackgroundTaskQueue(BackgroundTaskContext context) - { - _context = context; - } + public BackgroundTaskQueue(BackgroundTaskContext context) + { + _context = context; + } - /// - public async Task<(Func workItem, string workItemName, Action onException)> - DequeueAsync(CancellationToken cancellationToken) - { - await _signal.WaitAsync(cancellationToken); - _workItems.TryDequeue(out var workItem); - return workItem; - } + /// + public async Task<(Func workItem, string workItemName, Action onException)> + DequeueAsync(CancellationToken cancellationToken) + { + await _signal.WaitAsync(cancellationToken); + _workItems.TryDequeue(out var workItem); + return workItem; + } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - /// - public void QueueBackgroundWorkItem( - Func workItem, - string workItemName, - Action onException) + /// + public void QueueBackgroundWorkItem( + Func workItem, + string workItemName, + Action onException) + { + if (workItem == null) { - if (workItem == null) - { - throw new ArgumentNullException(nameof(workItem)); - } - - _workItems.Enqueue((workItem, workItemName, onException)); - _signal.Release(); - _context.RegisterTask(); + throw new ArgumentNullException(nameof(workItem)); } - public void QueueBackgroundWorkItem(Func workItem, Action onException) - { - QueueBackgroundWorkItem(workItem, string.Empty, onException); - } + _workItems.Enqueue((workItem, workItemName, onException)); + _signal.Release(); + _context.RegisterTask(); + } - public void QueueBackgroundWorkItem(Func workItem, string workItemName = "") - { - QueueBackgroundWorkItem(workItem, workItemName, (x) => { }); - } + public void QueueBackgroundWorkItem(Func workItem, Action onException) + { + QueueBackgroundWorkItem(workItem, string.Empty, onException); + } + + public void QueueBackgroundWorkItem(Func workItem, string workItemName = "") + { + QueueBackgroundWorkItem(workItem, workItemName, (x) => { }); + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - _signal?.Dispose(); - } + _signal?.Dispose(); } } } diff --git a/src/CronScheduler.Extensions/BackgroundTask/IBackgroundTaskQueue.cs b/src/CronScheduler.Extensions/BackgroundTask/IBackgroundTaskQueue.cs index e30ea01..cf40df7 100644 --- a/src/CronScheduler.Extensions/BackgroundTask/IBackgroundTaskQueue.cs +++ b/src/CronScheduler.Extensions/BackgroundTask/IBackgroundTaskQueue.cs @@ -2,37 +2,36 @@ using System.Threading; using System.Threading.Tasks; -namespace CronScheduler.Extensions.BackgroundTask +namespace CronScheduler.Extensions.BackgroundTask; + +/// +/// Background Task Queue. +/// +public interface IBackgroundTaskQueue { /// - /// Background Task Queue. + /// Adds Task into the queue. /// - public interface IBackgroundTaskQueue - { - /// - /// Adds Task into the queue. - /// - /// The Task item to queue. - /// The name for the executing task. - /// The delegate for exception handling of the task. - void QueueBackgroundWorkItem( - Func workItem, - string workItemName, - Action onException); + /// The Task item to queue. + /// The name for the executing task. + /// The delegate for exception handling of the task. + void QueueBackgroundWorkItem( + Func workItem, + string workItemName, + Action onException); - void QueueBackgroundWorkItem(Func workItem, Action onException); + void QueueBackgroundWorkItem(Func workItem, Action onException); - void QueueBackgroundWorkItem(Func workItem, string workItemName = ""); + void QueueBackgroundWorkItem(Func workItem, string workItemName = ""); - /// - /// - /// Dequeues Task from the Queue and adds wait lock from the thread of the based on the - /// . - /// - /// - /// - /// - Task<(Func workItem, string workItemName, Action onException)> - DequeueAsync(CancellationToken cancellationToken); - } + /// + /// + /// Dequeues Task from the Queue and adds wait lock from the thread of the based on the + /// . + /// + /// + /// + /// + Task<(Func workItem, string workItemName, Action onException)> + DequeueAsync(CancellationToken cancellationToken); } diff --git a/src/CronScheduler.Extensions/BackgroundTask/QueuedHostedService.cs b/src/CronScheduler.Extensions/BackgroundTask/QueuedHostedService.cs index c841f6d..6c359fc 100644 --- a/src/CronScheduler.Extensions/BackgroundTask/QueuedHostedService.cs +++ b/src/CronScheduler.Extensions/BackgroundTask/QueuedHostedService.cs @@ -6,99 +6,84 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace CronScheduler.Extensions.BackgroundTask +namespace CronScheduler.Extensions.BackgroundTask; + +public class QueuedHostedService : BackgroundService { - public class QueuedHostedService : BackgroundService + private readonly ILogger _logger; + private readonly BackgroundTaskContext _context; + private readonly IHostApplicationLifetime _applicationLifetime; + + private readonly QueuedHostedServiceOptions _options; + + public QueuedHostedService( + IBackgroundTaskQueue taskQueued, + ILoggerFactory loggerFactory, + BackgroundTaskContext context, + IHostApplicationLifetime applicationLifetime, + IOptionsMonitor options) { - private readonly ILogger _logger; - private readonly BackgroundTaskContext _context; - - // https://github.com/aspnet/AspNetCore/issues/7749 -#if NETCOREAPP3_0 || NETSTANDARD2_1 - private readonly IHostApplicationLifetime _applicationLifetime; -#else - private readonly IApplicationLifetime _applicationLifetime; -#endif - private readonly QueuedHostedServiceOptions _options; - - public QueuedHostedService( - IBackgroundTaskQueue taskQueued, - ILoggerFactory loggerFactory, - BackgroundTaskContext context, -#if NETCOREAPP3_0 || NETSTANDARD2_1 - IHostApplicationLifetime applicationLifetime, -#else - IApplicationLifetime applicationLifetime, -#endif - IOptionsMonitor options) - { - TaskQueued = taskQueued; - _logger = loggerFactory.CreateLogger(); - _context = context; - _options = options.CurrentValue; - -#if NETCOREAPP3_0 || NETSTANDARD2_1 - _applicationLifetime = applicationLifetime; - _applicationLifetime.ApplicationStopping.Register(OnApplicationStopping); -#else - _applicationLifetime = applicationLifetime; - _applicationLifetime.ApplicationStopping.Register(OnApplicationStopping); -#endif - } + TaskQueued = taskQueued; + _logger = loggerFactory.CreateLogger(); + _context = context; + _options = options.CurrentValue; - public IBackgroundTaskQueue TaskQueued { get; } + _applicationLifetime = applicationLifetime; + _applicationLifetime.ApplicationStopping.Register(OnApplicationStopping); + } - public override Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogDebug("{ServiceName} is starting.", nameof(QueuedHostedService)); + public IBackgroundTaskQueue TaskQueued { get; } - return base.StartAsync(cancellationToken); - } + public override Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("{ServiceName} is starting.", nameof(QueuedHostedService)); - public override Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogDebug("{ServiceName} is stopping.", nameof(QueuedHostedService)); - return base.StopAsync(cancellationToken); - } + return base.StartAsync(cancellationToken); + } + + public override Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogDebug("{ServiceName} is stopping.", nameof(QueuedHostedService)); + return base.StopAsync(cancellationToken); + } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) { - while (!stoppingToken.IsCancellationRequested) - { - var (workItem, workItemName, onException) = await TaskQueued.DequeueAsync(stoppingToken); + var (workItem, workItemName, onException) = await TaskQueued.DequeueAsync(stoppingToken); - try - { - await workItem(stoppingToken).ConfigureAwait(false); - _context.MarkAsComplete(); - } - catch (Exception ex) - { - var message = $"{nameof(QueuedHostedService)} encountered error while executing {workItemName} task."; + try + { + await workItem(stoppingToken).ConfigureAwait(false); + _context.MarkAsComplete(); + } + catch (Exception ex) + { + var message = $"{nameof(QueuedHostedService)} encountered error while executing {workItemName} task."; - onException(new Exception(message, ex)); + onException(new Exception(message, ex)); - _logger.LogError(ex, message); - } + _logger.LogError(ex, message); } } + } - private void OnApplicationStopping() + private void OnApplicationStopping() + { + if (_options.EnableApplicationOnStopWait) { - if (_options.EnableApplicationOnStopWait) + _logger.LogDebug("{ServiceName} is entered {MethodName}.", nameof(QueuedHostedService), nameof(OnApplicationStopping)); + + while (!_context.IsComplete) { - _logger.LogDebug("{ServiceName} is entered {MethodName}.", nameof(QueuedHostedService), nameof(OnApplicationStopping)); - - while (!_context.IsComplete) - { - _logger.LogDebug( - "{ServiceName} is waiting: {Timespan} seconds for the number of tasks: {TaskCount} to complete.", - nameof(QueuedHostedService), - _options.ApplicationOnStopWaitTimeout.TotalSeconds, - _context.Count); - - Thread.Sleep(_options.ApplicationOnStopWaitTimeout); - } + _logger.LogDebug( + "{ServiceName} is waiting: {Timespan} seconds for the number of tasks: {TaskCount} to complete.", + nameof(QueuedHostedService), + _options.ApplicationOnStopWaitTimeout.TotalSeconds, + _context.Count); + + Thread.Sleep(_options.ApplicationOnStopWaitTimeout); } } } diff --git a/src/CronScheduler.Extensions/BackgroundTask/QueuedHostedServiceOptions.cs b/src/CronScheduler.Extensions/BackgroundTask/QueuedHostedServiceOptions.cs index 7ed4e89..31d2251 100644 --- a/src/CronScheduler.Extensions/BackgroundTask/QueuedHostedServiceOptions.cs +++ b/src/CronScheduler.Extensions/BackgroundTask/QueuedHostedServiceOptions.cs @@ -1,20 +1,19 @@ using System; -namespace CronScheduler.Extensions.BackgroundTask +namespace CronScheduler.Extensions.BackgroundTask; + +/// +/// The basic options for the . +/// +public class QueuedHostedServiceOptions { /// - /// The basic options for the . + /// The to wait before gracefully shutdown the application. /// - public class QueuedHostedServiceOptions - { - /// - /// The to wait before gracefully shutdown the application. - /// - public TimeSpan ApplicationOnStopWaitTimeout { get; set; } + public TimeSpan ApplicationOnStopWaitTimeout { get; set; } - /// - /// The flag to enable or disable the wait for the BackgroundTasks to be completed before allowing the application to shutdown. - /// - public bool EnableApplicationOnStopWait { get; set; } - } + /// + /// The flag to enable or disable the wait for the BackgroundTasks to be completed before allowing the application to shutdown. + /// + public bool EnableApplicationOnStopWait { get; set; } } diff --git a/src/CronScheduler.Extensions/CronScheduler.Extensions.csproj b/src/CronScheduler.Extensions/CronScheduler.Extensions.csproj index 5ca0229..2099247 100644 --- a/src/CronScheduler.Extensions/CronScheduler.Extensions.csproj +++ b/src/CronScheduler.Extensions/CronScheduler.Extensions.csproj @@ -1,19 +1,22 @@  - - netstandard2.0;netstandard2.1; - + + netstandard2.0;net6.0 + - - - The Cron based Scheduler for DotNetCore 2.x/3.x Self-hosted Applications in Kubernetes/Docker or as WindowsService on Windows Machine. - This is a lightweight alternative to Quarts Scheduler or HangFire. - - + + + The Cron based Scheduler for DotNetCore 2.x/3.x/5.x/6.x Self-hosted Applications in Kubernetes/Docker or as WindowsService on Windows Machine. + This is a lightweight alternative to Quarts Scheduler or HangFire. + + - - - - - - + + + + + + + + + diff --git a/src/CronScheduler.Extensions/DependencyInjection/BackgroundQueuedServiceCollectionExtensions.cs b/src/CronScheduler.Extensions/DependencyInjection/BackgroundQueuedServiceCollectionExtensions.cs index 414eddf..538b245 100644 --- a/src/CronScheduler.Extensions/DependencyInjection/BackgroundQueuedServiceCollectionExtensions.cs +++ b/src/CronScheduler.Extensions/DependencyInjection/BackgroundQueuedServiceCollectionExtensions.cs @@ -3,42 +3,41 @@ using CronScheduler.Extensions.BackgroundTask; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class BackgroundQueuedServiceCollectionExtensions { - public static class BackgroundQueuedServiceCollectionExtensions + private static readonly BackgroundTaskContext _sharedContext = new BackgroundTaskContext(); + + /// + /// Adds Background Queued Hosted service. + /// + /// The instance of . + /// The flag to enable or disable wait for the background queued tasks to complete before application shutdowns. Default is false. + /// The timeout to wait for the background queued tasks to complete. Default is 10 seconds. + /// + public static IServiceCollection AddBackgroundQueuedService( + this IServiceCollection services, + bool applicationOnStopWaitForTasksToComplete = false, + TimeSpan applicationOnStopTimeoutWait = default) { - private static readonly BackgroundTaskContext _sharedContext = new BackgroundTaskContext(); - - /// - /// Adds Background Queued Hosted service. - /// - /// The instance of . - /// The flag to enable or disable wait for the background queued tasks to complete before application shutdowns. Default is false. - /// The timeout to wait for the background queued tasks to complete. Default is 10 seconds. - /// - public static IServiceCollection AddBackgroundQueuedService( - this IServiceCollection services, - bool applicationOnStopWaitForTasksToComplete = false, - TimeSpan applicationOnStopTimeoutWait = default) - { - applicationOnStopTimeoutWait = applicationOnStopTimeoutWait == default ? TimeSpan.FromSeconds(10) : applicationOnStopTimeoutWait; + applicationOnStopTimeoutWait = applicationOnStopTimeoutWait == default ? TimeSpan.FromSeconds(10) : applicationOnStopTimeoutWait; - services.Configure(opt => - { - opt.EnableApplicationOnStopWait = applicationOnStopWaitForTasksToComplete; - opt.ApplicationOnStopWaitTimeout = applicationOnStopTimeoutWait; - }); + services.Configure(opt => + { + opt.EnableApplicationOnStopWait = applicationOnStopWaitForTasksToComplete; + opt.ApplicationOnStopWaitTimeout = applicationOnStopTimeoutWait; + }); - // Add the singleton StartupTaskContext only once - if (services.Any(x => x.ServiceType != typeof(BackgroundTaskContext))) - { - services.AddSingleton(_sharedContext); - } + // Add the singleton StartupTaskContext only once + if (services.Any(x => x.ServiceType != typeof(BackgroundTaskContext))) + { + services.AddSingleton(_sharedContext); + } - services.AddHostedService(); - services.AddSingleton(); + services.AddHostedService(); + services.AddSingleton(); - return services; - } + return services; } } diff --git a/src/CronScheduler.Extensions/DependencyInjection/OptionsConfigurationServiceCollectionExtensions.cs b/src/CronScheduler.Extensions/DependencyInjection/OptionsConfigurationServiceCollectionExtensions.cs deleted file mode 100644 index 5ac013f..0000000 --- a/src/CronScheduler.Extensions/DependencyInjection/OptionsConfigurationServiceCollectionExtensions.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection -{ - public static class OptionsConfigurationServiceCollectionExtensions - { - /// - /// Registers a configuration instance which TOptions will bind against without passing into registration. - /// - /// - /// - /// - /// The Configuration Section Name from where to retrieve the values from. - /// - public static IServiceCollection Configure( - this IServiceCollection services, - string sectionName) - where TConfigureType : class - where TOptions : class, new() - { - return services.Configure(sectionName, Options.Options.DefaultName, _ => { }); - } - - /// - /// Registers a configuration instance which TOptions will bind against without passing into registration. - /// In addition adds the singelton of the {TOptions}. - /// https://github.com/aspnet/Extensions/blob/299af9e32ba790dbfe8cfdf99b441766d7b0f6b6/src/Options/ConfigurationExtensions/src/OptionsConfigurationServiceCollectionExtensions.cs#L58. - /// - /// The type of the object that configuration provider has entry for. - /// The type of the option object. - /// The instance. - /// The Configuration Section Name from where to retrieve the values from. - /// The named option name. - /// The . - /// - public static IServiceCollection Configure( - this IServiceCollection services, - string sectionName, - string optionName, - Action configureBinder) - where TConfigureType : class - where TOptions : class, new() - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - if (string.IsNullOrEmpty(sectionName)) - { - throw new ArgumentNullException(nameof(sectionName)); - } - - services.AddOptions(); - - services.AddSingleton>((sp) => - { - var config = sp.GetRequiredService(); - var section = config.GetSection(sectionName).GetSection(typeof(TConfigureType).Name); - - return new ConfigurationChangeTokenSource(optionName, section); - }); - - services.AddSingleton>(sp => - { - var config = sp.GetRequiredService(); - var section = config.GetSection(sectionName).GetSection(typeof(TConfigureType).Name); - - return new NamedConfigureFromConfigurationOptions(optionName, section, configureBinder); - }); - - services.AddSingleton(resolver => resolver.GetRequiredService>().Value); - - return services; - } - - /// - /// Adds Options that support being updated during application run. - /// - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddChangeTokenOptions( - this IServiceCollection services, - string sectionName, - string? optionName = default, - Action? configureAction = default) where TOptions : class, new() - { - // configure changeable configurations - services.RegisterInternal(sectionName, optionName); - - // create options instance from the configuration - services.AddTransient((Func>)(sp => - { - return new ConfigureNamedOptions(optionName, options => - { - var configuration = sp.GetRequiredService(); - configuration.Bind(sectionName, options); - - configureAction?.Invoke(options, sp); - }); - })); - - // Registers an IConfigureOptions action configurator. Being last it will bind from config source first - // and run the customization afterwards - services - .AddOptions(optionName) - .PostConfigure((options, sp) => configureAction?.Invoke(options, sp)); - - return services; - } - - /// - /// Adds Options that support being updated during application run. - /// - /// - /// - /// - /// - /// - /// - public static IServiceCollection AddChangeTokenOptions( - this IServiceCollection services, - string sectionName, - string? optionName = default, - Action? configureAction = default) where TOptions : class, new() - { - // configure changeable configurations - services.RegisterInternal(sectionName, optionName); - - // create options instance from the configuration - services.AddTransient((Func>)(sp => - { - return new ConfigureNamedOptions(optionName, options => - { - var configuration = sp.GetRequiredService(); - configuration.Bind(sectionName, options); - - configureAction?.Invoke(options); - }); - })); - - // Registers an IConfigureOptions action configurator. Being last it will bind from config source first - // and run the customization afterwards - services - .AddOptions(optionName) - .Configure(options => configureAction?.Invoke(options)); - - return services; - } - - private static void RegisterInternal( - this IServiceCollection services, - string sectionName, - string? optionName = null) - where TOptions : class, new() - { - if (optionName == null) - { - optionName = Options.Options.DefaultName; - } - - services.AddSingleton((Func>)((sp) => - { - var config = sp.GetRequiredService().GetSection(sectionName); - return new ConfigurationChangeTokenSource(optionName, config); - })); - - services.AddSingleton(resolver => resolver.GetRequiredService>().Value); - - services.AddSingleton((Func>)(sp => - { - var config = sp.GetRequiredService().GetSection(sectionName); - return new NamedConfigureFromConfigurationOptions(optionName, config); - })); - } - } -} diff --git a/src/CronScheduler.Extensions/DependencyInjection/SchedulerBuilder.cs b/src/CronScheduler.Extensions/DependencyInjection/SchedulerBuilder.cs index 908c083..0e6e7d7 100644 --- a/src/CronScheduler.Extensions/DependencyInjection/SchedulerBuilder.cs +++ b/src/CronScheduler.Extensions/DependencyInjection/SchedulerBuilder.cs @@ -1,22 +1,18 @@ using System; using System.Threading.Tasks; -using CronScheduler.Extensions.Internal; using CronScheduler.Extensions.Scheduler; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection { public class SchedulerBuilder { - /// - /// EventHanlder for Startup for Hosted Apps. - /// #pragma warning disable CA1051 // Do not declare visible instance fields #pragma warning disable SA1401 // Fields should be private + [Obsolete("User AddUnobservedTaskExceptionHandler() instead")] public EventHandler? UnobservedTaskExceptionHandler; #pragma warning restore SA1401 // Fields should be private #pragma warning restore CA1051 // Do not declare visible instance fields @@ -35,6 +31,19 @@ public SchedulerBuilder(IServiceCollection services) /// public IServiceCollection Services { get; } + internal Func>? CreateUnobservedTaskExceptionHandler { get; private set; } + + /// + /// Adds a function to create an event handler that handles unobserved task exceptions during the lifetime of the CRON job. + /// + /// The function to create the event handler. + public IServiceCollection AddUnobservedTaskExceptionHandler( + Func> createUnobservedTaskExceptionHandler) + { + CreateUnobservedTaskExceptionHandler = createUnobservedTaskExceptionHandler; + return Services; + } + /// /// Add Custom Scheduler Job with the Default options type. /// The options are accessible based on the job name that is being specified. diff --git a/src/CronScheduler.Extensions/DependencyInjection/SchedulerServiceCollectionExtensions.cs b/src/CronScheduler.Extensions/DependencyInjection/SchedulerServiceCollectionExtensions.cs index 8cfaf56..68d9534 100644 --- a/src/CronScheduler.Extensions/DependencyInjection/SchedulerServiceCollectionExtensions.cs +++ b/src/CronScheduler.Extensions/DependencyInjection/SchedulerServiceCollectionExtensions.cs @@ -7,106 +7,126 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// An Extension method to register . +/// https://github.com/aspnet/Hosting/blob/a3dd609ae667adcb6eb062125d76f9a76a82f7b4/src/Microsoft.Extensions.Hosting.Abstractions/ServiceCollectionHostedServiceExtensions.cs#L17. +/// +public static class SchedulerServiceCollectionExtensions { /// - /// An Extension method to register . - /// https://github.com/aspnet/Hosting/blob/a3dd609ae667adcb6eb062125d76f9a76a82f7b4/src/Microsoft.Extensions.Hosting.Abstractions/ServiceCollectionHostedServiceExtensions.cs#L17. + /// Adds service without global error handler . + /// Manually register jobs. /// - public static class SchedulerServiceCollectionExtensions + /// + /// + public static IServiceCollection AddScheduler(this IServiceCollection services) { - /// - /// Adds service without global error handler . - /// Manually register jobs. - /// - /// - /// - public static IServiceCollection AddScheduler(this IServiceCollection services) - { - CreateInstance(services); - return services; - } - - /// - /// Adds service with global error handler . - /// Manually register jobs. - /// - /// - /// - /// - public static IServiceCollection AddScheduler( - this IServiceCollection services, - EventHandler unobservedTaskExceptionHandler) - { - CreateInstance(services, unobservedTaskExceptionHandler); - return services; - } - - /// - /// Adds service with ability to register all of the cron job inside the context with - /// global error handler . - /// - /// - /// - /// - public static IServiceCollection AddScheduler(this IServiceCollection services, Action config) - { - var builder = new SchedulerBuilder(services); - config(builder); - - CreateInstance(builder.Services, builder.UnobservedTaskExceptionHandler); - - return builder.Services; - } - - /// - /// Adds job to DI without support for delegate. - /// - /// The type of the schedule job. - /// The options to be using for the job. - /// The DI services. - /// - /// The section name of the configuration for the job. - /// The name for the schedule job. - /// - public static IServiceCollection AddSchedulerJob( - this IServiceCollection services, - Action? configure = default, - string sectionName = "SchedulerJobs", - string? jobName = default) - where TJob : class, IScheduledJob - where TJobOptions : SchedulerOptions, new() - { - var builder = new SchedulerBuilder(services); + CreateInstance(services); + return services; + } - // add job with configuration settings - builder.AddJob(configure, sectionName, jobName); + /// + /// Adds service with global error handler . + /// Manually register jobs. + /// + /// + /// + /// + public static IServiceCollection AddScheduler( + this IServiceCollection services, + EventHandler unobservedTaskExceptionHandler) + { + CreateInstance(services, unobservedTaskExceptionHandler); + return services; + } - CreateInstance(builder.Services); + /// + /// Adds service with ability to register all of the cron job inside the context with + /// global error handler . + /// + /// The available registered application services. + /// The function to create an event handler that handles unobserved exceptions during the lifetime of the cron job. + public static IServiceCollection AddScheduler( + this IServiceCollection services, + Func> createUnobservedTaskExceptionHandler) + { + CreateInstance(services, createUnobservedTaskExceptionHandler); + return services; + } - return builder.Services; - } + /// + /// Adds service with ability to register all of the cron job inside the context with + /// global error handler . + /// + /// The available registered application services. + /// The function to configure this scheduler job. + public static IServiceCollection AddScheduler(this IServiceCollection services, Action config) + { + var builder = new SchedulerBuilder(services); + config(builder); - private static void CreateInstance( - IServiceCollection services, - EventHandler? unobservedTaskExceptionHandler = default) - { - services.TryAddSingleton(); + CreateInstance(builder.Services, sp => builder.UnobservedTaskExceptionHandler ?? builder.CreateUnobservedTaskExceptionHandler?.Invoke(sp)); - // should prevent from double registrations. - services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => - { - var registry = sp.GetRequiredService(); + return builder.Services; + } + + /// + /// Adds job to DI without support for delegate. + /// + /// The type of the schedule job. + /// The options to be using for the job. + /// The DI services. + /// + /// The section name of the configuration for the job. + /// The name for the schedule job. + /// + public static IServiceCollection AddSchedulerJob( + this IServiceCollection services, + Action? configure = default, + string sectionName = "SchedulerJobs", + string? jobName = default) + where TJob : class, IScheduledJob + where TJobOptions : SchedulerOptions, new() + { + var builder = new SchedulerBuilder(services); + + // add job with configuration settings + builder.AddJob(configure, sectionName, jobName); + + CreateInstance(builder.Services); + + return builder.Services; + } - var instance = new SchedulerHostedService(registry); + private static void CreateInstance( + IServiceCollection services, + EventHandler? unobservedTaskExceptionHandler = default) + { + CreateInstance(services, _ => unobservedTaskExceptionHandler); + } + + private static void CreateInstance( + IServiceCollection services, + Func?> createUnobservedTaskExceptionHandler) + { + services.TryAddSingleton(); - if (unobservedTaskExceptionHandler != null) - { - instance.UnobservedTaskException += unobservedTaskExceptionHandler; - } + // should prevent from double registrations. + services.TryAddEnumerable(ServiceDescriptor.Singleton(sp => + { + var registry = sp.GetRequiredService(); + + var instance = new SchedulerHostedService(registry); + + var unobservedTaskExceptionHandler = createUnobservedTaskExceptionHandler(sp); + if (unobservedTaskExceptionHandler != null) + { + instance.UnobservedTaskException += unobservedTaskExceptionHandler; + } - return instance; - })); - } + return instance; + })); } } diff --git a/src/CronScheduler.Extensions/DependencyInjection/StartupJobsServiceCollectionExtensions.cs b/src/CronScheduler.Extensions/DependencyInjection/StartupJobsServiceCollectionExtensions.cs index 1396227..f4f6803 100644 --- a/src/CronScheduler.Extensions/DependencyInjection/StartupJobsServiceCollectionExtensions.cs +++ b/src/CronScheduler.Extensions/DependencyInjection/StartupJobsServiceCollectionExtensions.cs @@ -6,60 +6,59 @@ using Microsoft.Extensions.DependencyInjection.Extensions; -namespace Microsoft.Extensions.DependencyInjection +namespace Microsoft.Extensions.DependencyInjection; + +public static class StartupJobsServiceCollectionExtensions { - public static class StartupJobsServiceCollectionExtensions + /// + /// Adds task to run as async job before the rest of the application launches. + /// + /// + /// + /// + public static IServiceCollection AddStartupJobInitializer(this IServiceCollection services, Func job) { - /// - /// Adds task to run as async job before the rest of the application launches. - /// - /// - /// - /// - public static IServiceCollection AddStartupJobInitializer(this IServiceCollection services, Func job) - { - return services.AddStartupJobInitializer() - .AddSingleton(new DelegateStartupJobInitializer(job)); - } + return services.AddStartupJobInitializer() + .AddSingleton(new DelegateStartupJobInitializer(job)); + } - /// - /// Adds to DI registration. - /// - /// - /// - public static IServiceCollection AddStartupJobInitializer(this IServiceCollection services) - { - services.TryAddTransient(); - return services; - } + /// + /// Adds to DI registration. + /// + /// + /// + public static IServiceCollection AddStartupJobInitializer(this IServiceCollection services) + { + services.TryAddTransient(); + return services; + } - /// - /// Adds job to the DI registration. - /// - /// - /// - /// - public static IServiceCollection AddStartupJob(this IServiceCollection services) - where TStartupJob : class, IStartupJob + /// + /// Adds job to the DI registration. + /// + /// + /// + /// + public static IServiceCollection AddStartupJob(this IServiceCollection services) + where TStartupJob : class, IStartupJob + { + return services + .AddStartupJobInitializer() + .AddTransient(); + } + + private class DelegateStartupJobInitializer : IStartupJob + { + private readonly Func _startupJob; + + public DelegateStartupJobInitializer(Func startupJob) { - return services - .AddStartupJobInitializer() - .AddTransient(); + _startupJob = startupJob; } - private class DelegateStartupJobInitializer : IStartupJob + public Task ExecuteAsync(CancellationToken cancellationToken = default) { - private readonly Func _startupJob; - - public DelegateStartupJobInitializer(Func startupJob) - { - _startupJob = startupJob; - } - - public Task ExecuteAsync(CancellationToken cancellationToken = default) - { - return _startupJob(); - } + return _startupJob(); } } } diff --git a/src/CronScheduler.Extensions/Internal/LoggerExtensions.cs b/src/CronScheduler.Extensions/Internal/LoggerExtensions.cs index f39f778..34a092e 100644 --- a/src/CronScheduler.Extensions/Internal/LoggerExtensions.cs +++ b/src/CronScheduler.Extensions/Internal/LoggerExtensions.cs @@ -1,63 +1,62 @@ using System; -namespace Microsoft.Extensions.Logging -{ +namespace Microsoft.Extensions.Logging; + #nullable disable - internal static class LoggerExtensions +internal static class LoggerExtensions +{ + public static class EventIds { - public static class EventIds - { - public static readonly EventId UnhandledException = new EventId(100, nameof(UnhandledException)); - public static readonly EventId JobNotRunning = new EventId(101, nameof(JobNotRunning)); - public static readonly EventId JobTimeZone = new EventId(102, nameof(JobTimeZone)); - public static readonly EventId JobTimeZoneFailedParsing = new EventId(103, nameof(JobTimeZoneFailedParsing)); - } + public static readonly EventId UnhandledException = new EventId(100, nameof(UnhandledException)); + public static readonly EventId JobNotRunning = new EventId(101, nameof(JobNotRunning)); + public static readonly EventId JobTimeZone = new EventId(102, nameof(JobTimeZone)); + public static readonly EventId JobTimeZoneFailedParsing = new EventId(103, nameof(JobTimeZoneFailedParsing)); + } #pragma warning disable SA1201 // Elements should appear in the correct order - private static readonly Action _unhandledException = + private static readonly Action _unhandledException = #pragma warning restore SA1201 // Elements should appear in the correct order - LoggerMessage.Define( - LogLevel.Error, - EventIds.UnhandledException, - "An unhandled exception has occurred while executing custom task: {jobName}."); - - private static readonly Action _jobNotRunning = - LoggerMessage.Define( - LogLevel.Warning, - EventIds.JobNotRunning, - "[Job][{jobName}] does not have CRON: {cron}. Task will not run."); - - private static readonly Action _jobTimeZone = - LoggerMessage.Define( - LogLevel.Information, - EventIds.JobTimeZone, - "[Job][{jobName}] is running under time zone: {timeZone}"); - - private static readonly Action _jobTimeZoneFailedParsing = - LoggerMessage.Define( - LogLevel.Error, - EventIds.JobTimeZoneFailedParsing, - "[Job][{jobName}] tried parse {zone} but failed with {ex} running under local time zone"); - - public static void UnhandledException(this ILogger logger, string jobName, Exception exception) - { - _unhandledException(logger, jobName, exception); - } - - public static void MissingCron(this ILogger logger, string jobName, string cron) - { - _jobNotRunning(logger, jobName, cron, null); - } - - public static void TimeZone(this ILogger logger, string jobName, string timeZone) - { - _jobTimeZone(logger, jobName, timeZone, null); - } - - public static void TimeZoneParseFailure(this ILogger logger, string jobName, string cronTime, Exception ex) - { - _jobTimeZoneFailedParsing(logger, jobName, cronTime, ex.Message, null); - } + LoggerMessage.Define( + LogLevel.Error, + EventIds.UnhandledException, + "An unhandled exception has occurred while executing custom task: {jobName}."); + + private static readonly Action _jobNotRunning = + LoggerMessage.Define( + LogLevel.Warning, + EventIds.JobNotRunning, + "[Job][{jobName}] does not have CRON: {cron}. Task will not run."); + + private static readonly Action _jobTimeZone = + LoggerMessage.Define( + LogLevel.Information, + EventIds.JobTimeZone, + "[Job][{jobName}] is running under time zone: {timeZone}"); + + private static readonly Action _jobTimeZoneFailedParsing = + LoggerMessage.Define( + LogLevel.Error, + EventIds.JobTimeZoneFailedParsing, + "[Job][{jobName}] tried parse {zone} but failed with {ex} running under local time zone"); + + public static void UnhandledException(this ILogger logger, string jobName, Exception exception) + { + _unhandledException(logger, jobName, exception); + } + + public static void MissingCron(this ILogger logger, string jobName, string cron) + { + _jobNotRunning(logger, jobName, cron, null); + } + + public static void TimeZone(this ILogger logger, string jobName, string timeZone) + { + _jobTimeZone(logger, jobName, timeZone, null); + } + + public static void TimeZoneParseFailure(this ILogger logger, string jobName, string cronTime, Exception ex) + { + _jobTimeZoneFailedParsing(logger, jobName, cronTime, ex.Message, null); } -#nullable restore } +#nullable restore diff --git a/src/CronScheduler.Extensions/Internal/SchedulerHostedService.cs b/src/CronScheduler.Extensions/Internal/SchedulerHostedService.cs index fc50fdd..4dc9957 100644 --- a/src/CronScheduler.Extensions/Internal/SchedulerHostedService.cs +++ b/src/CronScheduler.Extensions/Internal/SchedulerHostedService.cs @@ -7,72 +7,71 @@ using Microsoft.Extensions.Hosting; -namespace CronScheduler.Extensions.Internal +namespace CronScheduler.Extensions.Internal; + +/// +/// The implementation for service. +/// +internal class SchedulerHostedService : BackgroundService { + private readonly TaskFactory _taskFactory = new TaskFactory(TaskScheduler.Current); + private readonly ISchedulerRegistration _registrations; + /// - /// The implementation for service. + /// Initializes a new instance of the class. /// - internal class SchedulerHostedService : BackgroundService + /// + public SchedulerHostedService(ISchedulerRegistration registrations) { - private readonly TaskFactory _taskFactory = new TaskFactory(TaskScheduler.Current); - private readonly ISchedulerRegistration _registrations; - - /// - /// Initializes a new instance of the class. - /// - /// - public SchedulerHostedService(ISchedulerRegistration registrations) - { - _registrations = registrations ?? throw new ArgumentNullException(nameof(registrations)); - } + _registrations = registrations ?? throw new ArgumentNullException(nameof(registrations)); + } - public event EventHandler? UnobservedTaskException; + public event EventHandler? UnobservedTaskException; - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) { - while (!stoppingToken.IsCancellationRequested) - { - await ExecuteOnceAsync(stoppingToken); + await ExecuteOnceAsync(stoppingToken); - await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); - } + await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken); } + } - private async Task ExecuteOnceAsync(CancellationToken stoppingToken) - { - var referenceTime = DateTimeOffset.UtcNow; + private async Task ExecuteOnceAsync(CancellationToken stoppingToken) + { + var referenceTime = DateTimeOffset.UtcNow; - var scheduledTasks = _registrations.Jobs.Values; - var tasksThatShouldRun = scheduledTasks.Where(t => t.ShouldRun(referenceTime)).ToList(); + var scheduledTasks = _registrations.Jobs.Values; + var tasksThatShouldRun = scheduledTasks.Where(t => t.ShouldRun(referenceTime)).ToList(); - foreach (var taskThatShouldRun in tasksThatShouldRun) - { - taskThatShouldRun.Increment(); + foreach (var taskThatShouldRun in tasksThatShouldRun) + { + taskThatShouldRun.Increment(); #pragma warning disable CA2008 // Do not create tasks without passing a TaskScheduler - await _taskFactory.StartNew( - async () => + await _taskFactory.StartNew( + async () => + { + try { - try - { - await taskThatShouldRun.ScheduledJob.ExecuteAsync(stoppingToken); - } - catch (Exception ex) - { - var args = new UnobservedTaskExceptionEventArgs( - ex as AggregateException ?? new AggregateException(ex)); + await taskThatShouldRun.ScheduledJob.ExecuteAsync(stoppingToken); + } + catch (Exception ex) + { + var args = new UnobservedTaskExceptionEventArgs( + ex as AggregateException ?? new AggregateException(ex)); - UnobservedTaskException?.Invoke(this, args); + UnobservedTaskException?.Invoke(this, args); - if (!args.Observed) - { - throw; - } + if (!args.Observed) + { + throw; } - }, - stoppingToken); + } + }, + stoppingToken); #pragma warning restore CA2008 // Do not create tasks without passing a TaskScheduler - } } } } diff --git a/src/CronScheduler.Extensions/Internal/SchedulerRegistration.cs b/src/CronScheduler.Extensions/Internal/SchedulerRegistration.cs index 58441d3..0a4faaf 100644 --- a/src/CronScheduler.Extensions/Internal/SchedulerRegistration.cs +++ b/src/CronScheduler.Extensions/Internal/SchedulerRegistration.cs @@ -9,125 +9,124 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace CronScheduler.Extensions.Internal +namespace CronScheduler.Extensions.Internal; + +internal class SchedulerRegistration : ISchedulerRegistration { - internal class SchedulerRegistration : ISchedulerRegistration + private readonly IOptionsMonitor _optionsMonitor; + private readonly ILogger _logger; + private readonly ConcurrentDictionary _jobs = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _wrappedJobs = new ConcurrentDictionary(); + + public SchedulerRegistration( + IOptionsMonitor optionsMonitor, + IEnumerable scheduledJobs, + ILogger logger) { - private readonly IOptionsMonitor _optionsMonitor; - private readonly ILogger _logger; - private readonly ConcurrentDictionary _jobs = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _wrappedJobs = new ConcurrentDictionary(); - - public SchedulerRegistration( - IOptionsMonitor optionsMonitor, - IEnumerable scheduledJobs, - ILogger logger) + _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + foreach (var job in scheduledJobs) { - _optionsMonitor = optionsMonitor ?? throw new ArgumentNullException(nameof(optionsMonitor)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + AddOrUpdate(job.Name, job); + } - foreach (var job in scheduledJobs) + _optionsMonitor.OnChange((o, n) => + { + if (_jobs.TryGetValue(n, out var job) + && _wrappedJobs.ContainsKey(n)) { - AddOrUpdate(job.Name, job); + AddJob(n, job, o); } + }); + } - _optionsMonitor.OnChange((o, n) => - { - if (_jobs.TryGetValue(n, out var job) - && _wrappedJobs.ContainsKey(n)) - { - AddJob(n, job, o); - } - }); - } + public IReadOnlyDictionary Jobs => _wrappedJobs; - public IReadOnlyDictionary Jobs => _wrappedJobs; + public bool AddOrUpdate(IScheduledJob job, SchedulerOptions options) + { + return AddOrUpdate(job.Name, job, options); + } - public bool AddOrUpdate(IScheduledJob job, SchedulerOptions options) - { - return AddOrUpdate(job.Name, job, options); - } + public bool AddOrUpdate(string jobName, IScheduledJob job, SchedulerOptions options) + { + return AddJob(jobName, job, options); + } - public bool AddOrUpdate(string jobName, IScheduledJob job, SchedulerOptions options) - { - return AddJob(jobName, job, options); - } + public bool AddOrUpdate(IScheduledJob job) + { + return AddOrUpdate(job.Name, job); + } - public bool AddOrUpdate(IScheduledJob job) - { - return AddOrUpdate(job.Name, job); - } + public bool AddOrUpdate(string jobName, IScheduledJob job) + { + var options = _optionsMonitor.Get(jobName); - public bool AddOrUpdate(string jobName, IScheduledJob job) - { - var options = _optionsMonitor.Get(jobName); + return AddJob(jobName, job, options); + } - return AddJob(jobName, job, options); - } + public bool Remove(string jobName) + { + return _jobs.TryRemove(jobName, out var job) + && _wrappedJobs.TryRemove(jobName, out var wrap); + } - public bool Remove(string jobName) - { - return _jobs.TryRemove(jobName, out var job) - && _wrappedJobs.TryRemove(jobName, out var wrap); - } + private bool AddJob(string name, IScheduledJob job, SchedulerOptions options) + { + _jobs.AddOrUpdate(name, job, (n, j) => job); - private bool AddJob(string name, IScheduledJob job, SchedulerOptions options) + var wrapped = Create(name, job, options); + + if (wrapped != null) { - _jobs.AddOrUpdate(name, job, (n, j) => job); + _wrappedJobs.AddOrUpdate(name, wrapped, (n, v) => wrapped); - var wrapped = Create(name, job, options); + return true; + } - if (wrapped != null) - { - _wrappedJobs.AddOrUpdate(name, wrapped, (n, v) => wrapped); + return false; + } - return true; - } + private SchedulerTaskWrapper? Create(string jobName, IScheduledJob job, SchedulerOptions options) + { + var currentTimeUtc = DateTimeOffset.UtcNow; + var timeZone = TimeZoneInfo.Local; - return false; + if (string.IsNullOrEmpty(options.CronSchedule)) + { + _logger.MissingCron(jobName, options.CronSchedule); + + return null; } - private SchedulerTaskWrapper? Create(string jobName, IScheduledJob job, SchedulerOptions options) + if (!string.IsNullOrEmpty(options.CronTimeZone)) { - var currentTimeUtc = DateTimeOffset.UtcNow; - var timeZone = TimeZoneInfo.Local; - - if (string.IsNullOrEmpty(options.CronSchedule)) + try { - _logger.MissingCron(jobName, options.CronSchedule); - - return null; + timeZone = TimeZoneInfo.FindSystemTimeZoneById(options.CronTimeZone); } - - if (!string.IsNullOrEmpty(options.CronTimeZone)) + catch (Exception ex) { - try - { - timeZone = TimeZoneInfo.FindSystemTimeZoneById(options.CronTimeZone); - } - catch (Exception ex) - { - _logger.TimeZoneParseFailure(jobName, options.CronTimeZone, ex); - timeZone = TimeZoneInfo.Local; - } + _logger.TimeZoneParseFailure(jobName, options.CronTimeZone, ex); + timeZone = TimeZoneInfo.Local; } + } - _logger.TimeZone(jobName, timeZone.Id); + _logger.TimeZone(jobName, timeZone.Id); - CronExpression crontabSchedule; + CronExpression crontabSchedule; - if (options.CronSchedule.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries).Length == 6) - { - crontabSchedule = CronExpression.Parse(options.CronSchedule, CronFormat.IncludeSeconds); - } - else - { - crontabSchedule = CronExpression.Parse(options.CronSchedule, CronFormat.Standard); - } + if (options.CronSchedule.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries).Length == 6) + { + crontabSchedule = CronExpression.Parse(options.CronSchedule, CronFormat.IncludeSeconds); + } + else + { + crontabSchedule = CronExpression.Parse(options.CronSchedule, CronFormat.Standard); + } - var nextRunTime = options.RunImmediately ? currentTimeUtc : crontabSchedule.GetNextOccurrence(currentTimeUtc, timeZone) !.Value; + var nextRunTime = options.RunImmediately ? currentTimeUtc : crontabSchedule.GetNextOccurrence(currentTimeUtc, timeZone)!.Value; - return new SchedulerTaskWrapper(crontabSchedule, job, nextRunTime, timeZone); - } + return new SchedulerTaskWrapper(crontabSchedule, job, nextRunTime, timeZone); } } diff --git a/src/CronScheduler.Extensions/Internal/SchedulerTaskWrapper.cs b/src/CronScheduler.Extensions/Internal/SchedulerTaskWrapper.cs index c4a1b0d..9ff5d48 100644 --- a/src/CronScheduler.Extensions/Internal/SchedulerTaskWrapper.cs +++ b/src/CronScheduler.Extensions/Internal/SchedulerTaskWrapper.cs @@ -4,48 +4,47 @@ using CronScheduler.Extensions.Scheduler; -namespace CronScheduler.Extensions.Internal +namespace CronScheduler.Extensions.Internal; + +public sealed class SchedulerTaskWrapper { - public sealed class SchedulerTaskWrapper + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + public SchedulerTaskWrapper( + CronExpression cronExpression, + IScheduledJob scheduledJob, + DateTimeOffset nextRunTime, + TimeZoneInfo timeZoneInfo) + { + Schedule = cronExpression; + ScheduledJob = scheduledJob; + NextRunTime = nextRunTime; + TimeZoneInfo = timeZoneInfo; + } + + public CronExpression Schedule { get; set; } + + public IScheduledJob ScheduledJob { get; set; } + + public DateTimeOffset LastRunTime { get; set; } + + public DateTimeOffset NextRunTime { get; set; } + + public TimeZoneInfo TimeZoneInfo { get; set; } + + public void Increment() + { + LastRunTime = NextRunTime; + NextRunTime = Schedule.GetNextOccurrence(NextRunTime, TimeZoneInfo)!.Value; + } + + public bool ShouldRun(DateTimeOffset currentTime) { - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - /// - public SchedulerTaskWrapper( - CronExpression cronExpression, - IScheduledJob scheduledJob, - DateTimeOffset nextRunTime, - TimeZoneInfo timeZoneInfo) - { - Schedule = cronExpression; - ScheduledJob = scheduledJob; - NextRunTime = nextRunTime; - TimeZoneInfo = timeZoneInfo; - } - - public CronExpression Schedule { get; set; } - - public IScheduledJob ScheduledJob { get; set; } - - public DateTimeOffset LastRunTime { get; set; } - - public DateTimeOffset NextRunTime { get; set; } - - public TimeZoneInfo TimeZoneInfo { get; set; } - - public void Increment() - { - LastRunTime = NextRunTime; - NextRunTime = Schedule.GetNextOccurrence(NextRunTime, TimeZoneInfo) !.Value; - } - - public bool ShouldRun(DateTimeOffset currentTime) - { - return NextRunTime < currentTime && LastRunTime != NextRunTime; - } + return NextRunTime < currentTime && LastRunTime != NextRunTime; } } diff --git a/src/CronScheduler.Extensions/Scheduler/IScheduledJob.cs b/src/CronScheduler.Extensions/Scheduler/IScheduledJob.cs index 3c80d4b..3c93924 100644 --- a/src/CronScheduler.Extensions/Scheduler/IScheduledJob.cs +++ b/src/CronScheduler.Extensions/Scheduler/IScheduledJob.cs @@ -1,25 +1,24 @@ using System.Threading; using System.Threading.Tasks; -namespace CronScheduler.Extensions.Scheduler +namespace CronScheduler.Extensions.Scheduler; + +/// +/// Forces the implementation of the required methods for the job. +/// +public interface IScheduledJob { /// - /// Forces the implementation of the required methods for the job. + /// The name of the executing job. + /// In order for the options to work correctly make sure that the name is matched + /// between the job and the named job options. /// - public interface IScheduledJob - { - /// - /// The name of the executing job. - /// In order for the options to work correctly make sure that the name is matched - /// between the job and the named job options. - /// - string Name { get; } + string Name { get; } - /// - /// Job that will be executing on this schedule. - /// - /// - /// - Task ExecuteAsync(CancellationToken cancellationToken); - } + /// + /// Job that will be executing on this schedule. + /// + /// + /// + Task ExecuteAsync(CancellationToken cancellationToken); } diff --git a/src/CronScheduler.Extensions/Scheduler/ISchedulerRegistration.cs b/src/CronScheduler.Extensions/Scheduler/ISchedulerRegistration.cs index e1729ae..3d035cb 100644 --- a/src/CronScheduler.Extensions/Scheduler/ISchedulerRegistration.cs +++ b/src/CronScheduler.Extensions/Scheduler/ISchedulerRegistration.cs @@ -2,52 +2,51 @@ using CronScheduler.Extensions.Internal; -namespace CronScheduler.Extensions.Scheduler +namespace CronScheduler.Extensions.Scheduler; + +/// +/// Scheduled Jobs Registration. +/// +public interface ISchedulerRegistration { + IReadOnlyDictionary Jobs { get; } + + /// + /// Add or Update existing job. This methods requires that Options are registered for the job. + /// + /// The name of the job. + /// The instance of the custom job. + /// + bool AddOrUpdate(string jobName, IScheduledJob job); + + /// + /// Add or Update existing job. This methods requires that Options are registered for the job. + /// + /// The instance of the custom job. + /// + bool AddOrUpdate(IScheduledJob job); + + /// + /// Add or Update an existing custom job. This method doesn't rely on the registered options. + /// + /// The name of the job. + /// The instance of the custom job. + /// The options to be configured for the named job. + /// + bool AddOrUpdate(string jobName, IScheduledJob job, SchedulerOptions options); + + /// + /// Add or Update an existing custom job. This method doesn't rely on the registered options. + /// + /// The instance of the custom job. + /// + /// + bool AddOrUpdate(IScheduledJob job, SchedulerOptions options); + /// - /// Scheduled Jobs Registration. + /// Remove job by name. /// - public interface ISchedulerRegistration - { - IReadOnlyDictionary Jobs { get; } - - /// - /// Add or Update existing job. This methods requires that Options are registered for the job. - /// - /// The name of the job. - /// The instance of the custom job. - /// - bool AddOrUpdate(string jobName, IScheduledJob job); - - /// - /// Add or Update existing job. This methods requires that Options are registered for the job. - /// - /// The instance of the custom job. - /// - bool AddOrUpdate(IScheduledJob job); - - /// - /// Add or Update an existing custom job. This method doesn't rely on the registered options. - /// - /// The name of the job. - /// The instance of the custom job. - /// The options to be configured for the named job. - /// - bool AddOrUpdate(string jobName, IScheduledJob job, SchedulerOptions options); - - /// - /// Add or Update an existing custom job. This method doesn't rely on the registered options. - /// - /// The instance of the custom job. - /// - /// - bool AddOrUpdate(IScheduledJob job, SchedulerOptions options); - - /// - /// Remove job by name. - /// - /// - /// - bool Remove(string jobName); - } + /// + /// + bool Remove(string jobName); } diff --git a/src/CronScheduler.Extensions/Scheduler/SchedulerOptions.cs b/src/CronScheduler.Extensions/Scheduler/SchedulerOptions.cs index 17c4b20..986173a 100644 --- a/src/CronScheduler.Extensions/Scheduler/SchedulerOptions.cs +++ b/src/CronScheduler.Extensions/Scheduler/SchedulerOptions.cs @@ -1,29 +1,26 @@ -using System; +namespace CronScheduler.Extensions.Scheduler; -namespace CronScheduler.Extensions.Scheduler +public class SchedulerOptions { - public class SchedulerOptions - { - /// - /// Specify the CRON schedule. - /// . - /// - public string CronSchedule { get; set; } = string.Empty; + /// + /// Specify the CRON schedule. + /// . + /// + public string CronSchedule { get; set; } = string.Empty; - /// - /// Time Zone for the Scheduler to run. Default is null, sets it to local time zone. - /// Can be set to "Eastern Standard Time" or "Pacific Standard Time" etc. - /// - public string? CronTimeZone { get; set; } + /// + /// Time Zone for the Scheduler to run. Default is null, sets it to local time zone. + /// Can be set to "Eastern Standard Time" or "Pacific Standard Time" etc. + /// + public string? CronTimeZone { get; set; } - /// - /// Specify if the Job to be run immediately. Default value is false. - /// - public bool RunImmediately { get; set; } = false; + /// + /// Specify if the Job to be run immediately. Default value is false. + /// + public bool RunImmediately { get; set; } = false; - /// - /// The name of the job that this options is associated with. - /// - internal string JobName { get; set; } = string.Empty; - } + /// + /// The name of the job that this options is associated with. + /// + internal string JobName { get; set; } = string.Empty; } diff --git a/src/CronScheduler.Extensions/StartupInitializer/IStartupJob.cs b/src/CronScheduler.Extensions/StartupInitializer/IStartupJob.cs index ebd34ee..b48b331 100644 --- a/src/CronScheduler.Extensions/StartupInitializer/IStartupJob.cs +++ b/src/CronScheduler.Extensions/StartupInitializer/IStartupJob.cs @@ -3,18 +3,17 @@ using Microsoft.Extensions.Hosting; -namespace CronScheduler.Extensions.StartupInitializer +namespace CronScheduler.Extensions.StartupInitializer; + +/// +/// Allows to run async jobs on Program.cs. +/// +public interface IStartupJob { /// - /// Allows to run async jobs on Program.cs. + /// Starts async job for . /// - public interface IStartupJob - { - /// - /// Starts async job for . - /// - /// - /// - Task ExecuteAsync(CancellationToken cancellationToken = default); - } + /// + /// + Task ExecuteAsync(CancellationToken cancellationToken = default); } diff --git a/src/CronScheduler.Extensions/StartupInitializer/StartupJobHostExtensions.cs b/src/CronScheduler.Extensions/StartupInitializer/StartupJobHostExtensions.cs index cb28ac6..ea4903a 100644 --- a/src/CronScheduler.Extensions/StartupInitializer/StartupJobHostExtensions.cs +++ b/src/CronScheduler.Extensions/StartupInitializer/StartupJobHostExtensions.cs @@ -5,23 +5,22 @@ using Microsoft.Extensions.DependencyInjection; -namespace Microsoft.Extensions.Hosting +namespace Microsoft.Extensions.Hosting; + +public static class StartupJobHostExtensions { - public static class StartupJobHostExtensions + /// + /// Runs async all of the registered jobs. + /// + /// + /// + /// + public static async Task RunStartupJobsAync(this IHost host, CancellationToken cancellationToken = default) { - /// - /// Runs async all of the registered jobs. - /// - /// - /// - /// - public static async Task RunStartupJobsAync(this IHost host, CancellationToken cancellationToken = default) + using (var scope = host.Services.CreateScope()) { - using (var scope = host.Services.CreateScope()) - { - var jobInitializer = scope.ServiceProvider.GetRequiredService(); - await jobInitializer.StartJobsAsync(cancellationToken); - } + var jobInitializer = scope.ServiceProvider.GetRequiredService(); + await jobInitializer.StartJobsAsync(cancellationToken); } } } diff --git a/src/CronScheduler.Extensions/StartupInitializer/StartupJobInitializer.cs b/src/CronScheduler.Extensions/StartupInitializer/StartupJobInitializer.cs index f4834b8..f95cf30 100644 --- a/src/CronScheduler.Extensions/StartupInitializer/StartupJobInitializer.cs +++ b/src/CronScheduler.Extensions/StartupInitializer/StartupJobInitializer.cs @@ -6,60 +6,59 @@ using Microsoft.Extensions.Logging; -namespace CronScheduler.Extensions.StartupInitializer +namespace CronScheduler.Extensions.StartupInitializer; + +public class StartupJobInitializer { - public class StartupJobInitializer + private readonly ILogger _logger; + private readonly IEnumerable _startupJobs; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public StartupJobInitializer(IEnumerable startupJobs, ILoggerFactory loggerFactory) { - private readonly ILogger _logger; - private readonly IEnumerable _startupJobs; + _logger = loggerFactory.CreateLogger(); - /// - /// Initializes a new instance of the class. - /// - /// - /// - public StartupJobInitializer(IEnumerable startupJobs, ILoggerFactory loggerFactory) + _startupJobs = startupJobs; + } + + /// + /// Starts jobs for all of the registered instances. + /// + /// + /// + public async Task StartJobsAsync(CancellationToken cancellationToken) + { + try { - _logger = loggerFactory.CreateLogger(); + var jobCount = _startupJobs.ToList().Count; - _startupJobs = startupJobs; - } + _logger.LogInformation("{name} start queuing {count} jobs", nameof(StartupJobInitializer), jobCount); - /// - /// Starts jobs for all of the registered instances. - /// - /// - /// - public async Task StartJobsAsync(CancellationToken cancellationToken) - { - try + foreach (var job in _startupJobs) { - var jobCount = _startupJobs.ToList().Count; + cancellationToken.ThrowIfCancellationRequested(); - _logger.LogInformation("{name} start queuing {count} jobs", nameof(StartupJobInitializer), jobCount); - - foreach (var job in _startupJobs) + try { - cancellationToken.ThrowIfCancellationRequested(); - - try - { - await job.ExecuteAsync(cancellationToken); - _logger.LogInformation("{jobName} completed", job.GetType()); - } - catch (Exception ex) - { - _logger.LogError("{jobName} failed with the following message {message}", job.GetType(), ex.Message); - } + await job.ExecuteAsync(cancellationToken); + _logger.LogInformation("{jobName} completed", job.GetType()); + } + catch (Exception ex) + { + _logger.LogError("{jobName} failed with the following message {message}", job.GetType(), ex.Message); } - - _logger.LogInformation("{name} completed queuing {count} jobs", nameof(StartupJobInitializer), jobCount); - } - catch (Exception ex) - { - _logger.LogError("{name} initialization failed with {ex}", nameof(StartupJobInitializer), ex.Message); - throw; } + + _logger.LogInformation("{name} completed queuing {count} jobs", nameof(StartupJobInitializer), jobCount); + } + catch (Exception ex) + { + _logger.LogError("{name} initialization failed with {ex}", nameof(StartupJobInitializer), ex.Message); + throw; } } } diff --git a/src/CronSchedulerApp/Controllers/HomeController.cs b/src/CronSchedulerApp/Controllers/HomeController.cs index 689c33e..b5dec77 100644 --- a/src/CronSchedulerApp/Controllers/HomeController.cs +++ b/src/CronSchedulerApp/Controllers/HomeController.cs @@ -6,6 +6,7 @@ using CronScheduler.Extensions.BackgroundTask; using CronScheduler.Extensions.Scheduler; + using CronSchedulerApp.Jobs; using CronSchedulerApp.Models; using CronSchedulerApp.Services; @@ -78,6 +79,8 @@ public IActionResult Index() public IActionResult About() { ViewData["Message"] = "Your application description page."; + ViewData["winTimeZone"] = TimeZoneInfo.FindSystemTimeZoneById("AUS Eastern Standard Time"); + ViewData["linuxTimeZone"] = TimeZoneInfo.FindSystemTimeZoneById("Australia/Sydney"); return View(); } diff --git a/src/CronSchedulerApp/CronSchedulerApp.csproj b/src/CronSchedulerApp/CronSchedulerApp.csproj index 49164a4..a867b6c 100644 --- a/src/CronSchedulerApp/CronSchedulerApp.csproj +++ b/src/CronSchedulerApp/CronSchedulerApp.csproj @@ -1,31 +1,36 @@  - - netcoreapp3.1 - b790ca5d-c09d-4e9c-af99-c7b8c6b6210a - $(AspNetCoreHostingModel) - $(DockerDefaultTargetOS) - ..\..\docker-compose.dcproj - false - true - true - true - Bootstrap4 - + + net6.0 + b790ca5d-c09d-4e9c-af99-c7b8c6b6210a + $(AspNetCoreHostingModel) + Linux + ..\..\docker-compose.dcproj + false + true + true + true + Bootstrap4 + inprocess + true + ..\.. + - - - - - - - - - - + + + + + + + + + + + + - - - + + + diff --git a/src/CronSchedulerApp/Data/ApplicationDbContext.cs b/src/CronSchedulerApp/Data/ApplicationDbContext.cs index 01612d7..21f21e5 100644 --- a/src/CronSchedulerApp/Data/ApplicationDbContext.cs +++ b/src/CronSchedulerApp/Data/ApplicationDbContext.cs @@ -1,13 +1,12 @@ using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; -namespace CronSchedulerApp.Data +namespace CronSchedulerApp.Data; + +public class ApplicationDbContext : IdentityDbContext { - public class ApplicationDbContext : IdentityDbContext + public ApplicationDbContext(DbContextOptions options) + : base(options) { - public ApplicationDbContext(DbContextOptions options) - : base(options) - { - } } } diff --git a/src/CronSchedulerApp/Dockerfile b/src/CronSchedulerApp/Dockerfile index 1ff5266..2221952 100644 --- a/src/CronSchedulerApp/Dockerfile +++ b/src/CronSchedulerApp/Dockerfile @@ -1,12 +1,32 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. +# docker build --pull --rm -f "src\CronSchedulerApp\Dockerfile" -t cronscheduler:latest . +# docker run --rm -d -p 4443:443/tcp -p 8080:80/tcp cronscheduler:latest -# Build image -FROM kdcllc/dotnet-sdk:3.1-buster as builder -WORKDIR ./src/CronSchedulerApp -RUN dotnet publish --no-build -c Release -o /app +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +WORKDIR /src + +COPY ["build/", "build/"] + +COPY ["Directory.Build.props", "Directory.Build.props"] +COPY ["Directory.Build.targets", "Directory.Build.targets"] -# App image -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS final +COPY ["src/CronSchedulerApp/CronSchedulerApp.csproj", "src/CronSchedulerApp/"] +COPY ["src/CronScheduler.AspNetCore/CronScheduler.AspNetCore.csproj", "src/CronScheduler.AspNetCore/"] +COPY ["src/CronScheduler.Extensions/CronScheduler.Extensions.csproj", "src/CronScheduler.Extensions/"] +RUN dotnet restore "src/CronSchedulerApp/CronSchedulerApp.csproj" +COPY . . +WORKDIR "/src/src/CronSchedulerApp" +RUN dotnet build "CronSchedulerApp.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "CronSchedulerApp.csproj" -c Release -o /app/publish + +FROM base AS final WORKDIR /app -COPY --from=builder /app . +COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "CronSchedulerApp.dll"] - diff --git a/src/CronSchedulerApp/Jobs/TestJob.cs b/src/CronSchedulerApp/Jobs/TestJob.cs index 49da313..6bbcf71 100644 --- a/src/CronSchedulerApp/Jobs/TestJob.cs +++ b/src/CronSchedulerApp/Jobs/TestJob.cs @@ -6,29 +6,28 @@ using Microsoft.Extensions.Logging; -namespace CronSchedulerApp.Jobs +namespace CronSchedulerApp.Jobs; + +public class TestJob : IScheduledJob { - public class TestJob : IScheduledJob + private readonly ILogger _logger; + private SchedulerOptions _options; + + public TestJob( + SchedulerOptions options, + ILogger logger) { - private readonly ILogger _logger; - private SchedulerOptions _options; - - public TestJob( - SchedulerOptions options, - ILogger logger) - { - _options = options; - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public string Name { get; } = nameof(TestJob); - - // will be removed in the next release - public Task ExecuteAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("{cronSchedule} - {id}", _options.CronSchedule, Guid.NewGuid()); - - return Task.CompletedTask; - } + _options = options; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Name { get; } = nameof(TestJob); + + // will be removed in the next release + public Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("{cronSchedule} - {id}", _options.CronSchedule, Guid.NewGuid()); + + return Task.CompletedTask; } } diff --git a/src/CronSchedulerApp/Jobs/TorahQuoteJob.cs b/src/CronSchedulerApp/Jobs/TorahQuoteJob.cs index 2f77cc3..4d6b951 100644 --- a/src/CronSchedulerApp/Jobs/TorahQuoteJob.cs +++ b/src/CronSchedulerApp/Jobs/TorahQuoteJob.cs @@ -8,39 +8,38 @@ using Microsoft.Extensions.Options; -namespace CronSchedulerApp.Jobs +namespace CronSchedulerApp.Jobs; + +public class TorahQuoteJob : IScheduledJob { - public class TorahQuoteJob : IScheduledJob + private readonly TorahQuoteJobOptions _options; + private readonly TorahVerses _torahVerses; + private readonly TorahService _service; + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public TorahQuoteJob( + IOptionsMonitor options, + TorahService service, + TorahVerses torahVerses) + { + _options = options.Get(Name); + _service = service ?? throw new ArgumentNullException(nameof(service)); + _torahVerses = torahVerses ?? throw new ArgumentNullException(nameof(torahVerses)); + } + + // job name and options name must match. + public string Name { get; } = nameof(TorahQuoteJob); + + public async Task ExecuteAsync(CancellationToken cancellationToken) { - private readonly TorahQuoteJobOptions _options; - private readonly TorahVerses _torahVerses; - private readonly TorahService _service; - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public TorahQuoteJob( - IOptionsMonitor options, - TorahService service, - TorahVerses torahVerses) - { - _options = options.Get(Name); - _service = service ?? throw new ArgumentNullException(nameof(service)); - _torahVerses = torahVerses ?? throw new ArgumentNullException(nameof(torahVerses)); - } - - // job name and options name must match. - public string Name { get; } = nameof(TorahQuoteJob); - - public async Task ExecuteAsync(CancellationToken cancellationToken) - { - var index = new Random().Next(_options.Verses.Length); - var exp = _options.Verses[index]; - - _torahVerses.Current = await _service.GetVersesAsync(exp, cancellationToken); - } + var index = new Random().Next(_options.Verses.Length); + var exp = _options.Verses[index]; + + _torahVerses.Current = await _service.GetVersesAsync(exp, cancellationToken); } } diff --git a/src/CronSchedulerApp/Jobs/TorahQuoteJobOptions.cs b/src/CronSchedulerApp/Jobs/TorahQuoteJobOptions.cs index 1b7c7c5..3fad165 100644 --- a/src/CronSchedulerApp/Jobs/TorahQuoteJobOptions.cs +++ b/src/CronSchedulerApp/Jobs/TorahQuoteJobOptions.cs @@ -2,14 +2,13 @@ using CronScheduler.Extensions.Scheduler; -namespace CronSchedulerApp.Jobs +namespace CronSchedulerApp.Jobs; + +public class TorahQuoteJobOptions : SchedulerOptions { - public class TorahQuoteJobOptions : SchedulerOptions - { - public string ApiUrl { get; set; } = string.Empty; + public string ApiUrl { get; set; } = string.Empty; - public string WebsiteUrl { get; set; } = string.Empty; + public string WebsiteUrl { get; set; } = string.Empty; - public string[] Verses { get; set; } = Array.Empty(); - } + public string[] Verses { get; set; } = Array.Empty(); } diff --git a/src/CronSchedulerApp/Jobs/UserJob.cs b/src/CronSchedulerApp/Jobs/UserJob.cs index 05964fe..b8b1201 100644 --- a/src/CronSchedulerApp/Jobs/UserJob.cs +++ b/src/CronSchedulerApp/Jobs/UserJob.cs @@ -10,35 +10,34 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -namespace CronSchedulerApp.Jobs +namespace CronSchedulerApp.Jobs; + +public class UserJob : IScheduledJob { - public class UserJob : IScheduledJob - { - private readonly UserJobOptions _options; - private readonly IServiceProvider _provider; + private readonly UserJobOptions _options; + private readonly IServiceProvider _provider; - public UserJob( - IServiceProvider provider, - IOptionsMonitor options) - { - _options = options.Get(Name); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - } + public UserJob( + IServiceProvider provider, + IOptionsMonitor options) + { + _options = options.Get(Name); + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } - public string Name { get; } = nameof(UserJob); + public string Name { get; } = nameof(UserJob); - public async Task ExecuteAsync(CancellationToken cancellationToken) - { - // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2&tabs=visual-studio#consuming-a-scoped-service-in-a-background-task - using var scope = _provider.CreateScope(); - var userService = scope.ServiceProvider.GetRequiredService(); + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-2.2&tabs=visual-studio#consuming-a-scoped-service-in-a-background-task + using var scope = _provider.CreateScope(); + var userService = scope.ServiceProvider.GetRequiredService(); - var users = userService.GetUsers(); + var users = userService.GetUsers(); - foreach (var user in users) - { - await userService.AddClaimAsync(user, new Claim(_options.ClaimName, DateTime.UtcNow.ToString())); - } + foreach (var user in users) + { + await userService.AddClaimAsync(user, new Claim(_options.ClaimName, DateTime.UtcNow.ToString())); } } } diff --git a/src/CronSchedulerApp/Jobs/UserJobOptions.cs b/src/CronSchedulerApp/Jobs/UserJobOptions.cs index 9960273..b2e45d7 100644 --- a/src/CronSchedulerApp/Jobs/UserJobOptions.cs +++ b/src/CronSchedulerApp/Jobs/UserJobOptions.cs @@ -1,9 +1,8 @@ using CronScheduler.Extensions.Scheduler; -namespace CronSchedulerApp.Jobs +namespace CronSchedulerApp.Jobs; + +public class UserJobOptions : SchedulerOptions { - public class UserJobOptions : SchedulerOptions - { - public string ClaimName { get; set; } = string.Empty; - } + public string ClaimName { get; set; } = string.Empty; } diff --git a/src/CronSchedulerApp/Models/ErrorViewModel.cs b/src/CronSchedulerApp/Models/ErrorViewModel.cs index a3721f6..b391122 100644 --- a/src/CronSchedulerApp/Models/ErrorViewModel.cs +++ b/src/CronSchedulerApp/Models/ErrorViewModel.cs @@ -1,9 +1,8 @@ -namespace CronSchedulerApp.Models +namespace CronSchedulerApp.Models; + +public class ErrorViewModel { - public class ErrorViewModel - { - public string RequestId { get; set; } = string.Empty; + public string RequestId { get; set; } = string.Empty; - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); } diff --git a/src/CronSchedulerApp/Program.cs b/src/CronSchedulerApp/Program.cs index 49c4f70..ef56729 100644 --- a/src/CronSchedulerApp/Program.cs +++ b/src/CronSchedulerApp/Program.cs @@ -10,64 +10,63 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace CronSchedulerApp +namespace CronSchedulerApp; + +public sealed class Program { - public sealed class Program + public static async Task Main(string[] args) { - public static async Task Main(string[] args) - { - // run async jobs before the IWebHost run - // AspNetCore 2.x syntax of the registration. - // var host = CreateWebHostBuilder(args).Build(); - var host = CreateHostBuilder(args).Build(); - - await host.RunStartupJobsAync(); + // run async jobs before the IWebHost run + // AspNetCore 2.x syntax of the registration. + // var host = CreateWebHostBuilder(args).Build(); + var host = CreateHostBuilder(args).Build(); - await host.RunAsync(); - } + await host.RunStartupJobsAync(); - public static IHostBuilder CreateHostBuilder(string[] args) - { - return Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); + await host.RunAsync(); + } - webBuilder.ConfigureServices(services => - { - services.AddStartupJob(); - services.AddStartupJob(); - }); - }) - .ConfigureLogging((context, logger) => - { - logger.AddConsole(); - logger.AddDebug(); - logger.AddConfiguration(context.Configuration.GetSection("Logging")); - }); - } + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); - /// - /// AspNetCore 2.x syntax of the registration. - /// - /// - /// - public static IWebHostBuilder CreateWebHostBuilder(string[] args) - { - return WebHost.CreateDefaultBuilder(args) - .ConfigureServices(services => + webBuilder.ConfigureServices(services => { services.AddStartupJob(); services.AddStartupJob(); - }) - .ConfigureLogging((context, logger) => - { - logger.AddConsole(); - logger.AddDebug(); - logger.AddConfiguration(context.Configuration.GetSection("Logging")); - }) - .UseShutdownTimeout(TimeSpan.FromSeconds(10)) // default is 5 seconds. - .UseStartup(); - } + }); + }) + .ConfigureLogging((context, logger) => + { + logger.AddConsole(); + logger.AddDebug(); + logger.AddConfiguration(context.Configuration.GetSection("Logging")); + }); + } + + /// + /// AspNetCore 2.x syntax of the registration. + /// + /// + /// + public static IWebHostBuilder CreateWebHostBuilder(string[] args) + { + return WebHost.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddStartupJob(); + services.AddStartupJob(); + }) + .ConfigureLogging((context, logger) => + { + logger.AddConsole(); + logger.AddDebug(); + logger.AddConfiguration(context.Configuration.GetSection("Logging")); + }) + .UseShutdownTimeout(TimeSpan.FromSeconds(10)) // default is 5 seconds. + .UseStartup(); } } diff --git a/src/CronSchedulerApp/Properties/launchSettings.json b/src/CronSchedulerApp/Properties/launchSettings.json new file mode 100644 index 0000000..8da8933 --- /dev/null +++ b/src/CronSchedulerApp/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "profiles": { + "CronSchedulerApp": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:51440;http://localhost:51441" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "publishAllPorts": true, + "useSSL": true + }, + "WSL": { + "commandName": "WSL2", + "launchBrowser": true, + "launchUrl": "https://localhost:51440", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "https://localhost:51440;http://localhost:51441" + }, + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/src/CronSchedulerApp/Services/TorahService.cs b/src/CronSchedulerApp/Services/TorahService.cs index 3fc2f83..d9800d7 100644 --- a/src/CronSchedulerApp/Services/TorahService.cs +++ b/src/CronSchedulerApp/Services/TorahService.cs @@ -11,69 +11,66 @@ using Newtonsoft.Json; -namespace CronSchedulerApp.Services +namespace CronSchedulerApp.Services; + +/// +/// typed version of the service to access http://labs.bible.org/api_web_service. +/// +public class TorahService { + private TorahQuoteJobOptions _options; + /// - /// typed version of the service to access http://labs.bible.org/api_web_service. + /// Initializes a new instance of the class. /// - public class TorahService + /// + /// + public TorahService( + HttpClient httpClient, + IOptionsMonitor options) { - private TorahQuoteJobOptions _options; + _options = options.Get(nameof(TorahQuoteJob)); - /// - /// Initializes a new instance of the class. - /// - /// - /// - public TorahService( - HttpClient httpClient, - IOptionsMonitor options) + // updates on providers change + options.OnChange((opt, n) => { - _options = options.Get(nameof(TorahQuoteJob)); - - // updates on providers change - options.OnChange((opt, n) => + if (n == nameof(TorahQuoteJob)) { - if (n == nameof(TorahQuoteJob)) - { - _options = opt; - } - }); + _options = opt; + } + }); - httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); - httpClient.DefaultRequestHeaders.Add("User-Agent", nameof(TorahService)); + httpClient.DefaultRequestHeaders.Add("Accept", "application/json"); + httpClient.DefaultRequestHeaders.Add("User-Agent", nameof(TorahService)); - Client = httpClient; - } + Client = httpClient; + } - public HttpClient Client { get; } + public HttpClient Client { get; } - /// - /// Returns verses from the quotation. - /// Utilizes QqueryHelpers: https://rehansaeed.com/asp-net-core-hidden-gem-queryhelpers/. - /// - /// - /// - /// - public async Task> GetVersesAsync(string exp, CancellationToken cancellationToken) + /// + /// Returns verses from the quotation. + /// Utilizes QqueryHelpers: https://rehansaeed.com/asp-net-core-hidden-gem-queryhelpers/. + /// + /// + /// + /// + public async Task> GetVersesAsync(string exp, CancellationToken cancellationToken) + { + // create query parameters + var args = new Dictionary { - // create query parameters - var args = new Dictionary - { - { "type", "json" }, - { "passage", Uri.EscapeDataString(exp) } - }; + { "type", "json" }, + { "passage", Uri.EscapeDataString(exp) } + }; - var url = QueryHelpers.AddQueryString(_options.ApiUrl, args); + var url = QueryHelpers.AddQueryString(_options.ApiUrl, args); - using (var request = new HttpRequestMessage(HttpMethod.Get, url)) - { - var response = await Client.SendAsync(request, cancellationToken).ConfigureAwait(false); - response.EnsureSuccessStatusCode(); + using var request = new HttpRequestMessage(HttpMethod.Get, url); + var response = await Client.SendAsync(request, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); - var result = await response.Content.ReadAsStringAsync(); - return JsonConvert.DeserializeObject>(result); - } - } + var result = await response.Content.ReadAsStringAsync(cancellationToken); + return JsonConvert.DeserializeObject>(result)!; } } diff --git a/src/CronSchedulerApp/Services/TorahVerses.cs b/src/CronSchedulerApp/Services/TorahVerses.cs index 41f9aea..19e1575 100644 --- a/src/CronSchedulerApp/Services/TorahVerses.cs +++ b/src/CronSchedulerApp/Services/TorahVerses.cs @@ -1,22 +1,21 @@ using System; using System.Collections.Generic; -namespace CronSchedulerApp.Services +namespace CronSchedulerApp.Services; + +public class TorahVerses { - public class TorahVerses - { - public IList Current { get; set; } = new List(); + public IList Current { get; set; } = new List(); - public string Bookname { get; set; } = string.Empty; + public string Bookname { get; set; } = string.Empty; - public string Chapter { get; set; } = string.Empty; + public string Chapter { get; set; } = string.Empty; - public string Verse { get; set; } = string.Empty; + public string Verse { get; set; } = string.Empty; - public string Text { get; set; } = string.Empty; + public string Text { get; set; } = string.Empty; - public string Title { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; - public string[] Titles { get; set; } = Array.Empty(); - } + public string[] Titles { get; set; } = Array.Empty(); } diff --git a/src/CronSchedulerApp/Services/UserService.cs b/src/CronSchedulerApp/Services/UserService.cs index b1dad22..32bc36d 100644 --- a/src/CronSchedulerApp/Services/UserService.cs +++ b/src/CronSchedulerApp/Services/UserService.cs @@ -8,37 +8,36 @@ using Microsoft.AspNetCore.Identity; -namespace CronSchedulerApp.Services +namespace CronSchedulerApp.Services; + +/// +/// Demonstrates utilization of the scoped/transient objects inside of the jobs. +/// +public class UserService { - /// - /// Demonstrates utilization of the scoped/transient objects inside of the jobs. - /// - public class UserService + private readonly UserManager _userManager; + private readonly ApplicationDbContext _dbContext; + + public UserService( + ApplicationDbContext dbContext, + UserManager userManager) + { + _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); + } + + public async Task GetUserByEmail(string email) + { + return await _userManager.FindByEmailAsync(email); + } + + public async Task AddClaimAsync(IdentityUser user, Claim claim) + { + return await _userManager.AddClaimAsync(user, claim); + } + + public IEnumerable GetUsers() { - private readonly UserManager _userManager; - private readonly ApplicationDbContext _dbContext; - - public UserService( - ApplicationDbContext dbContext, - UserManager userManager) - { - _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); - _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); - } - - public async Task GetUserByEmail(string email) - { - return await _userManager.FindByEmailAsync(email); - } - - public async Task AddClaimAsync(IdentityUser user, Claim claim) - { - return await _userManager.AddClaimAsync(user, claim); - } - - public IEnumerable GetUsers() - { - return _dbContext.Users.ToList(); - } + return _dbContext.Users.ToList(); } } diff --git a/src/CronSchedulerApp/Startup.cs b/src/CronSchedulerApp/Startup.cs index 0eafacb..dc7cc63 100644 --- a/src/CronSchedulerApp/Startup.cs +++ b/src/CronSchedulerApp/Startup.cs @@ -1,6 +1,4 @@ -using System.Threading.Tasks; - -using CronSchedulerApp.Data; +using CronSchedulerApp.Data; using CronSchedulerApp.Jobs; using CronSchedulerApp.Services; @@ -16,113 +14,110 @@ using Polly; -namespace CronSchedulerApp +namespace CronSchedulerApp; + +public class Startup { - public class Startup + public Startup(IConfiguration configuration) { - private ILogger? _logger; + Configuration = configuration; + } - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + public IConfiguration Configuration { get; } - public IConfiguration Configuration { get; } + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.Configure(options => + { + // This lambda determines whether user consent for non-essential cookies is needed for a given request. + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = SameSiteMode.None; + }); - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) + services.AddDbContext(options => { - services.Configure(options => + if (Configuration["DatabaseProvider:Type"] == "Sqlite") { - // This lambda determines whether user consent for non-essential cookies is needed for a given request. - options.CheckConsentNeeded = context => true; - options.MinimumSameSitePolicy = SameSiteMode.None; - }); + options.UseSqlite(Configuration.GetConnectionString("SqliteConnection")); + } - services.AddDbContext(options => + if (Configuration["DatabaseProvider:Type"] == "SqlServer") { - if (Configuration["DatabaseProvider:Type"] == "Sqlite") - { - options.UseSqlite(Configuration.GetConnectionString("SqliteConnection")); - } - - if (Configuration["DatabaseProvider:Type"] == "SqlServer") - { - options.UseSqlServer(Configuration.GetConnectionString("SqlConnection")); - } - }); + options.UseSqlServer(Configuration.GetConnectionString("SqlConnection")); + } + }); - services.AddDefaultIdentity() - .AddEntityFrameworkStores(); + services.AddDefaultIdentity() + .AddEntityFrameworkStores(); - services.AddControllersWithViews(); + services.AddControllersWithViews(); - services.AddRazorPages(); + services.AddRazorPages(); - services.AddScheduler(builder => - { - // 1. Add Torah Quote Service and Job. - builder.Services.AddSingleton(); + services.AddScheduler(builder => + { + // 1. Add Torah Quote Service and Job. + builder.Services.AddSingleton(); - // Build a policy that will handle exceptions, 408s, and 500s from the remote server - builder.Services - .AddHttpClient() - .AddTransientHttpErrorPolicy(p => p.RetryAsync()); + // Build a policy that will handle exceptions, 408s, and 500s from the remote server + builder.Services + .AddHttpClient() + .AddTransientHttpErrorPolicy(p => p.RetryAsync()); - builder.AddJob(); + builder.AddJob(); - // 2. Add User Service and Job - builder.Services.AddScoped(); - builder.AddJob(); + // 2. Add User Service and Job + builder.Services.AddScoped(); + builder.AddJob(); - builder.UnobservedTaskExceptionHandler = UnobservedHandler; + // register a custom error processing for internal errors + builder.AddUnobservedTaskExceptionHandler(sp => + { + var logger = sp.GetRequiredService().CreateLogger("CronJobs"); + + return + (sender, args) => + { + logger?.LogError(args.Exception?.Message); + args.SetObserved(); + }; }); + }); - // services.AddScheduler((sender, args) => - // { - // _logger.LogError(args.Exception.Message); - // args.SetObserved(); - // }); - services.AddBackgroundQueuedService(applicationOnStopWaitForTasksToComplete: true); - } + services.AddBackgroundQueuedService(applicationOnStopWaitForTasksToComplete: true); - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) - { - _logger = loggerFactory.CreateLogger(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseDatabaseErrorPage(); - } - else - { - app.UseExceptionHandler("/Home/Error"); - app.UseHsts(); - } + services.AddDatabaseDeveloperPageExceptionFilter(); + } - app.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseCookiePolicy(); + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseMigrationsEndPoint(); + } + else + { + app.UseExceptionHandler("/Home/Error"); + app.UseHsts(); + } - app.UseAuthentication(); + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseCookiePolicy(); - app.UseRouting(); + app.UseAuthentication(); - // https://devblogs.microsoft.com/aspnet/blazor-now-in-official-preview/ - app.UseEndpoints(routes => - { - routes.MapControllers(); - routes.MapDefaultControllerRoute(); - routes.MapRazorPages(); - }); - } + app.UseRouting(); - private void UnobservedHandler(object sender, UnobservedTaskExceptionEventArgs args) + // https://devblogs.microsoft.com/aspnet/blazor-now-in-official-preview/ + app.UseEndpoints(routes => { - _logger?.LogError(args.Exception?.Message); - args.SetObserved(); - } + routes.MapControllers(); + routes.MapDefaultControllerRoute(); + routes.MapRazorPages(); + }); } } diff --git a/src/CronSchedulerApp/Views/Home/About.cshtml b/src/CronSchedulerApp/Views/Home/About.cshtml index 3674e37..9e2a421 100644 --- a/src/CronSchedulerApp/Views/Home/About.cshtml +++ b/src/CronSchedulerApp/Views/Home/About.cshtml @@ -5,3 +5,6 @@

@ViewData["Message"]

Use this area to provide additional information.

+ +

TimeZoneInfo.FindSystemTimeZoneById("AUS Eastern Standard Time")@ViewData["winTimeZone"]

+

TimeZoneInfo.FindSystemTimeZoneById("Australia/Sydney"): @ViewData["linuxTimeZone"]

diff --git a/src/CronSchedulerApp/app.db b/src/CronSchedulerApp/app.db index 79a1b53..b07dea1 100644 Binary files a/src/CronSchedulerApp/app.db and b/src/CronSchedulerApp/app.db differ diff --git a/src/CronSchedulerWorker/CronSchedulerWorker.csproj b/src/CronSchedulerWorker/CronSchedulerWorker.csproj index 1b8bb6b..78c0625 100644 --- a/src/CronSchedulerWorker/CronSchedulerWorker.csproj +++ b/src/CronSchedulerWorker/CronSchedulerWorker.csproj @@ -1,16 +1,19 @@  - - netcoreapp3.0 - dotnet-CronScheduler-16DA7ECC-2C2F-42B2-943E-6F8486D62575 - false - + + net6.0 + dotnet-CronScheduler-16DA7ECC-2C2F-42B2-943E-6F8486D62575 + false + Linux + ..\.. + - - - + + + + - - - + + + diff --git a/src/CronSchedulerWorker/Dockerfile b/src/CronSchedulerWorker/Dockerfile index 863e3cb..42f8139 100644 --- a/src/CronSchedulerWorker/Dockerfile +++ b/src/CronSchedulerWorker/Dockerfile @@ -1,17 +1,27 @@ -FROM mcr.microsoft.com/dotnet/core/runtime:3.0 AS base +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base WORKDIR /app -FROM mcr.microsoft.com/dotnet/core/sdk:3.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build WORKDIR /src + +COPY ["build/", "build/"] + +COPY ["Directory.Build.props", "Directory.Build.props"] +COPY ["Directory.Build.targets", "Directory.Build.targets"] + COPY ["src/CronSchedulerWorker/CronSchedulerWorker.csproj", "src/CronSchedulerWorker/"] +COPY ["src/CronScheduler.Extensions/CronScheduler.Extensions.csproj", "src/CronScheduler.Extensions/"] RUN dotnet restore "src/CronSchedulerWorker/CronSchedulerWorker.csproj" - COPY . . +WORKDIR "/src/src/CronSchedulerWorker" +RUN dotnet build "CronSchedulerWorker.csproj" -c Release -o /app/build -RUN dotnet build "src/CronSchedulerWorker/CronSchedulerWorker.csproj" -c Release -o /app -RUN dotnet publish "src/CronSchedulerWorker/CronSchedulerWorker.csproj" -c Release -o /app +FROM build AS publish +RUN dotnet publish "CronSchedulerWorker.csproj" -c Release -o /app/publish FROM base AS final WORKDIR /app -COPY --from=build /app . +COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "CronSchedulerWorker.dll"] diff --git a/src/CronSchedulerWorker/Program.cs b/src/CronSchedulerWorker/Program.cs index b718a60..f667305 100644 --- a/src/CronSchedulerWorker/Program.cs +++ b/src/CronSchedulerWorker/Program.cs @@ -1,41 +1,46 @@ -using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; -namespace CronSchedulerWorker +namespace CronSchedulerWorker; + +public sealed class Program { - public sealed class Program + public static async Task Main(string[] args) { - public static async Task Main(string[] args) - { - var host = CreateHostBuilder(args).Build(); + var host = CreateHostBuilder(args).Build(); - await host.RunStartupJobsAync(); + await host.RunStartupJobsAync(); - await host.RunAsync(); - } + await host.RunAsync(); + } - public static IHostBuilder CreateHostBuilder(string[] args) + public static IHostBuilder CreateHostBuilder(string[] args) + { + return Host.CreateDefaultBuilder(args) + .ConfigureServices(services => { - return Host.CreateDefaultBuilder(args) - .ConfigureServices(services => + services.AddStartupJob(); + + services.AddScheduler(builder => { - services.AddStartupJob(); + builder.AddJob(); - services.AddScheduler(builder => + // register a custom error processing for internal errors + builder.AddUnobservedTaskExceptionHandler(sp => { - builder.AddJob(); - builder.UnobservedTaskExceptionHandler = UnobservedHandler; + var logger = sp.GetRequiredService().CreateLogger("CronJobs"); + + return + (sender, args) => + { + logger?.LogError(args.Exception?.Message); + args.SetObserved(); + }; }); }); - } - - private static void UnobservedHandler(object sender, UnobservedTaskExceptionEventArgs e) - { - Console.WriteLine(e.Exception?.GetBaseException()); - e.SetObserved(); - } + }); } } diff --git a/src/CronSchedulerWorker/Properties/launchSettings.json b/src/CronSchedulerWorker/Properties/launchSettings.json new file mode 100644 index 0000000..d4812ca --- /dev/null +++ b/src/CronSchedulerWorker/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "CronSchedulerWorker": { + "commandName": "Project" + }, + "Docker (1)": { + "commandName": "Docker" + } + } +} \ No newline at end of file diff --git a/src/CronSchedulerWorker/TestJob.cs b/src/CronSchedulerWorker/TestJob.cs index 1864763..ad7c187 100644 --- a/src/CronSchedulerWorker/TestJob.cs +++ b/src/CronSchedulerWorker/TestJob.cs @@ -8,33 +8,32 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace CronSchedulerWorker +namespace CronSchedulerWorker; + +public class TestJob : IScheduledJob { - public class TestJob : IScheduledJob - { - private readonly TestJobOptions _options; - private readonly ILogger _logger; + private readonly TestJobOptions _options; + private readonly ILogger _logger; - public TestJob( - IOptionsMonitor options, - ILogger logger, - IHostLifetime lifetime) - { - _options = options.Get(Name); + public TestJob( + IOptionsMonitor options, + ILogger logger, + IHostLifetime lifetime) + { + _options = options.Get(Name); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - // _logger.LogInformation("IsSystemd: {isSystemd}", lifetime.GetType() == typeof(SystemdLifetime)); - _logger.LogInformation("IHostLifetime: {hostLifetime}", lifetime.GetType()); - } + // _logger.LogInformation("IsSystemd: {isSystemd}", lifetime.GetType() == typeof(SystemdLifetime)); + _logger.LogInformation("IHostLifetime: {hostLifetime}", lifetime.GetType()); + } - public string Name { get; } = nameof(TestJob); + public string Name { get; } = nameof(TestJob); - public async Task ExecuteAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("{jobName} is executing with Custom Field Value: {customField}", nameof(TestJob), _options.CustomField); + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("{jobName} is executing with Custom Field Value: {customField}", nameof(TestJob), _options.CustomField); - await Task.CompletedTask; - } + await Task.CompletedTask; } } diff --git a/src/CronSchedulerWorker/TestJobOptions.cs b/src/CronSchedulerWorker/TestJobOptions.cs index 8de1154..355df41 100644 --- a/src/CronSchedulerWorker/TestJobOptions.cs +++ b/src/CronSchedulerWorker/TestJobOptions.cs @@ -1,9 +1,8 @@ using CronScheduler.Extensions.Scheduler; -namespace CronSchedulerWorker +namespace CronSchedulerWorker; + +public class TestJobOptions : SchedulerOptions { - public class TestJobOptions : SchedulerOptions - { - public string CustomField { get; set; } = string.Empty; - } + public string CustomField { get; set; } = string.Empty; } diff --git a/src/CronSchedulerWorker/TestStartupJob.cs b/src/CronSchedulerWorker/TestStartupJob.cs index 9d6be78..7348522 100644 --- a/src/CronSchedulerWorker/TestStartupJob.cs +++ b/src/CronSchedulerWorker/TestStartupJob.cs @@ -6,24 +6,23 @@ using Microsoft.Extensions.Logging; -namespace CronSchedulerWorker +namespace CronSchedulerWorker; + +public class TestStartupJob : IStartupJob { - public class TestStartupJob : IStartupJob - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public TestStartupJob(ILogger logger) - { - _logger = logger; - } + public TestStartupJob(ILogger logger) + { + _logger = logger; + } - public async Task ExecuteAsync(CancellationToken cancellationToken = default) - { - _logger.LogInformation("{job} started", nameof(TestStartupJob)); + public async Task ExecuteAsync(CancellationToken cancellationToken = default) + { + _logger.LogInformation("{job} started", nameof(TestStartupJob)); - await Task.Delay(TimeSpan.FromSeconds(3)); + await Task.Delay(TimeSpan.FromSeconds(3)); - _logger.LogInformation("{job} ended", nameof(TestStartupJob)); - } + _logger.LogInformation("{job} ended", nameof(TestStartupJob)); } } diff --git a/test/CronScheduler.UnitTest/BackgroundQueueTests.cs b/test/CronScheduler.UnitTest/BackgroundQueueTests.cs index 34127db..cecda5b 100644 --- a/test/CronScheduler.UnitTest/BackgroundQueueTests.cs +++ b/test/CronScheduler.UnitTest/BackgroundQueueTests.cs @@ -5,32 +5,31 @@ using Xunit; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class BackgroundQueueTests { - public class BackgroundQueueTests + [Fact] + public async Task Dequeue_With_Successful_WorkItemName() { - [Fact] - public async Task Dequeue_With_Successful_WorkItemName() - { - var workItemName = "TestItem"; - var context = new BackgroundTaskContext(); + var workItemName = "TestItem"; + var context = new BackgroundTaskContext(); - var service = new BackgroundTaskQueue(context); + var service = new BackgroundTaskQueue(context); - service.QueueBackgroundWorkItem( - async token => - { - await Task.CompletedTask; - }, - workItemName); + service.QueueBackgroundWorkItem( + async token => + { + await Task.CompletedTask; + }, + workItemName); - var task = await service.DequeueAsync(CancellationToken.None); + var task = await service.DequeueAsync(CancellationToken.None); - await task.workItem(CancellationToken.None); + await task.workItem(CancellationToken.None); - Assert.Equal(workItemName, task.workItemName); + Assert.Equal(workItemName, task.workItemName); - service.Dispose(); - } + service.Dispose(); } } diff --git a/test/CronScheduler.UnitTest/CronScheduler.UnitTest.csproj b/test/CronScheduler.UnitTest/CronScheduler.UnitTest.csproj index 776f192..0094aa6 100644 --- a/test/CronScheduler.UnitTest/CronScheduler.UnitTest.csproj +++ b/test/CronScheduler.UnitTest/CronScheduler.UnitTest.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 false $(NoWarn);SA1402; @@ -9,13 +9,10 @@ - + - - all - runtime; build; native; contentfiles; analyzers - + diff --git a/test/CronScheduler.UnitTest/CustomTestJob.cs b/test/CronScheduler.UnitTest/CustomTestJob.cs index a21cc38..dd868b1 100644 --- a/test/CronScheduler.UnitTest/CustomTestJob.cs +++ b/test/CronScheduler.UnitTest/CustomTestJob.cs @@ -5,28 +5,27 @@ using Microsoft.Extensions.Logging; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class CustomTestJob : IScheduledJob { - public class CustomTestJob : IScheduledJob - { - private readonly CustomTestJobOptions _options; - private readonly ILogger _logger; + private readonly CustomTestJobOptions _options; + private readonly ILogger _logger; - public CustomTestJob(CustomTestJobOptions options, ILogger logger) - { - _options = options; - _logger = logger; - Name = options.JobName; - } + public CustomTestJob(CustomTestJobOptions options, ILogger logger) + { + _options = options; + _logger = logger; + Name = options.JobName; + } - public string Name { get; } + public string Name { get; } - public Task ExecuteAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Running {name}", nameof(CustomTestJob)); - _logger.LogInformation(_options.DisplayText); + public Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Running {name}", nameof(CustomTestJob)); + _logger.LogInformation(_options.DisplayText); - return Task.CompletedTask; - } + return Task.CompletedTask; } } diff --git a/test/CronScheduler.UnitTest/CustomTestJobOptions.cs b/test/CronScheduler.UnitTest/CustomTestJobOptions.cs index 52a928d..31f64d2 100644 --- a/test/CronScheduler.UnitTest/CustomTestJobOptions.cs +++ b/test/CronScheduler.UnitTest/CustomTestJobOptions.cs @@ -1,9 +1,8 @@ using CronScheduler.Extensions.Scheduler; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class CustomTestJobOptions : SchedulerOptions { - public class CustomTestJobOptions : SchedulerOptions - { - public string DisplayText { get; set; } = string.Empty; - } + public string DisplayText { get; set; } = string.Empty; } diff --git a/test/CronScheduler.UnitTest/SchedulerFuncTests.cs b/test/CronScheduler.UnitTest/SchedulerFuncTests.cs index 0317c68..50a82da 100644 --- a/test/CronScheduler.UnitTest/SchedulerFuncTests.cs +++ b/test/CronScheduler.UnitTest/SchedulerFuncTests.cs @@ -3,6 +3,8 @@ using System.Reflection; using System.Threading.Tasks; +using Bet.Extensions.Testing.Logging; + using CronScheduler.Extensions.Scheduler; using Microsoft.AspNetCore.Hosting; @@ -20,325 +22,377 @@ using Range = Moq.Range; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class SchedulerFuncTests { - public class SchedulerFuncTests + private readonly ITestOutputHelper _output; + + public SchedulerFuncTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; + _output = output; + } - public SchedulerFuncTests(ITestOutputHelper output) - { - _output = output; - } + [Fact] + public async Task Job_RunImmediately_Factory_Successfully() + { + // assign + var mockLoggerTestJob = new Mock>(); - [Fact] - public async Task Job_RunImmediately_Factory_Successfully() + var host = CreateHost(services => { - // assign - var mockLoggerTestJob = new Mock>(); + var jobName = nameof(TestJob); - var host = CreateHost(services => + services.AddScheduler(ctx => { - var jobName = nameof(TestJob); + ctx.AddJob( + sp => new TestJob(mockLoggerTestJob.Object), + options => + { + options.CronSchedule = "*/5 * * * * *"; + options.RunImmediately = true; + }, + jobName: jobName); + }); + }); + + var client = new TestServer(host).CreateClient(); + + // act + var response = await client.GetAsync("/hc"); + response.EnsureSuccessStatusCode(); + await Task.Delay(TimeSpan.FromSeconds(6)); + + // assert + Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); + + mockLoggerTestJob.Verify( + l => l.Log( + LogLevel.Information, + It.IsAny(), + It.Is((object v, Type _) => v.ToString()!.Contains(nameof(TestJob))), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Between(1, 2, Range.Inclusive)); + } - services.AddScheduler(ctx => - { - ctx.AddJob( - sp => new TestJob(mockLoggerTestJob.Object), - options => - { - options.CronSchedule = "*/5 * * * * *"; - options.RunImmediately = true; - }, - jobName: jobName); - }); + [Fact] + public async Task Same_Job_RunImmediately_Factory_Two_Different_Options_Successfully() + { + // assign + var mockLoggerTestJob = new Mock>(); + + var host = CreateHost(services => + { + services.AddScheduler(ctx => + { + var jobName1 = "TestJob1"; + + ctx.AddJob( + sp => + { + var options = sp.GetRequiredService>().Get(jobName1); + return new TestJobDup(options, mockLoggerTestJob.Object); + }, + options => + { + options.CronSchedule = "*/5 * * * * *"; + options.RunImmediately = true; + }, + jobName: jobName1); + + var jobName2 = "TestJob2"; + + ctx.AddJob( + sp => + { + var options = sp.GetRequiredService>().Get(jobName2); + return new TestJobDup(options, mockLoggerTestJob.Object); + }, options => + { + options.CronSchedule = "*/5 * * * * *"; + options.RunImmediately = true; + }, + jobName: jobName2); }); + }); - var client = new TestServer(host).CreateClient(); + var client = new TestServer(host).CreateClient(); - // act - var response = await client.GetAsync("/hc"); - response.EnsureSuccessStatusCode(); - await Task.Delay(TimeSpan.FromSeconds(6)); + // act + var response = await client.GetAsync("/hc"); + response.EnsureSuccessStatusCode(); + await Task.Delay(TimeSpan.FromSeconds(6)); - // assert - Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); + // assert + Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); - mockLoggerTestJob.Verify( + mockLoggerTestJob.Verify( l => l.Log( LogLevel.Information, It.IsAny(), - It.Is((object v, Type _) => v.ToString() !.Contains(nameof(TestJob))), + It.Is((object v, Type _) => v.ToString()!.Contains(nameof(TestJob))), It.IsAny(), It.Is>((v, t) => true)), - Times.Between(1, 2, Range.Inclusive)); - } + Times.Between(2, 6, Range.Inclusive)); + } - [Fact] - public async Task Same_Job_RunImmediately_Factory_Two_Different_Options_Successfully() - { - // assign - var mockLoggerTestJob = new Mock>(); + [Fact] + public async Task Job_RunImmediately_Configuration_Successfully() + { + // assign + var mockLoggerTestJob = new Mock>(); - var host = CreateHost(services => + var host = CreateHost(services => + { + services.AddScheduler(ctx => { - services.AddScheduler(ctx => - { - var jobName1 = "TestJob1"; - - ctx.AddJob( - sp => - { - var options = sp.GetRequiredService>().Get(jobName1); - return new TestJobDup(options, mockLoggerTestJob.Object); - }, - options => - { - options.CronSchedule = "*/5 * * * * *"; - options.RunImmediately = true; - }, - jobName: jobName1); - - var jobName2 = "TestJob2"; - - ctx.AddJob( - sp => - { - var options = sp.GetRequiredService>().Get(jobName2); - return new TestJobDup(options, mockLoggerTestJob.Object); - }, options => - { - options.CronSchedule = "*/5 * * * * *"; - options.RunImmediately = true; - }, - jobName: jobName2); - }); + ctx.AddJob(); }); - var client = new TestServer(host).CreateClient(); + services.AddTransient(x => mockLoggerTestJob.Object); + }); - // act - var response = await client.GetAsync("/hc"); - response.EnsureSuccessStatusCode(); - await Task.Delay(TimeSpan.FromSeconds(6)); + var client = new TestServer(host).CreateClient(); - // assert - Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); + // act + var response = await client.GetAsync("/hc"); + response.EnsureSuccessStatusCode(); + await Task.Delay(TimeSpan.FromSeconds(6)); - mockLoggerTestJob.Verify( - l => l.Log( - LogLevel.Information, - It.IsAny(), - It.Is((object v, Type _) => v.ToString() !.Contains(nameof(TestJob))), - It.IsAny(), - It.Is>((v, t) => true)), - Times.Between(2, 6, Range.Inclusive)); - } + // assert + Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); - [Fact] - public async Task Job_RunImmediately_Configuration_Successfully() + mockLoggerTestJob.Verify( + l => l.Log( + LogLevel.Information, + It.IsAny(), + It.Is((object v, Type _) => v.ToString()!.Contains(nameof(TestJob))), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Between(1, 2, Range.Inclusive)); + } + + [Fact] + public async Task Job_RunDelayed_Factory_Successfully() + { + var mockLogger = new Mock>(); + + var host = CreateHost(services => { - // assign - var mockLoggerTestJob = new Mock>(); + services.AddScheduler(ctx => + { + var jobName = nameof(TestJob); + + ctx.AddJob( + sp => new TestJob(mockLogger.Object), + configure: options => + { + options.CronSchedule = "*/05 * * * * *"; + options.RunImmediately = false; + }, + jobName: jobName); + }); + }); + + var client = new TestServer(host).CreateClient(); + + // act + var response = await client.GetAsync("/hc"); + response.EnsureSuccessStatusCode(); + await Task.Delay(TimeSpan.FromSeconds(6)); + + // assert + Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); + + // fixed moq and log according to https://github.com/moq/moq4/issues/918#issuecomment-535060645 + mockLogger.Verify( + l => l.Log( + LogLevel.Information, + It.IsAny(), + It.Is((object v, Type _) => v.ToString()!.Contains(nameof(TestJob))), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Between(1, 2, Range.Inclusive)); + } - var host = CreateHost(services => + [Fact] + public async Task Job_RunDelayed_And_Raise_UnobservedTaskException() + { + // arrange + var mockLogger = new Mock>(); + + var host = CreateHost(services => + { + services.AddScheduler(ctx => { - services.AddScheduler(ctx => - { - ctx.AddJob(); - }); + var jobName = nameof(TestJobException); + + ctx.AddUnobservedTaskExceptionHandler(sp => (sender, args) => UnobservedTaskExceptionHandler(sp, args)); - services.AddTransient(x => mockLoggerTestJob.Object); + ctx.AddJob( + sp => + { + var options = sp.GetRequiredService>(); + return new TestJobException(mockLogger.Object, options); + }, + options => + { + options.CronSchedule = "*/4 * * * * *"; + options.RunImmediately = false; + options.JobName = jobName; + options.RaiseException = true; + }, + jobName: jobName); }); + }); - var client = new TestServer(host).CreateClient(); + var client = new TestServer(host).CreateClient(); - // act - var response = await client.GetAsync("/hc"); - response.EnsureSuccessStatusCode(); - await Task.Delay(TimeSpan.FromSeconds(6)); + // act + var response = await client.GetAsync("/hc"); + response.EnsureSuccessStatusCode(); + await Task.Delay(TimeSpan.FromSeconds(6)); - // assert - Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); + // assert + Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); - mockLoggerTestJob.Verify( - l => l.Log( - LogLevel.Information, + mockLogger.Verify( + l => l.Log( + LogLevel.Error, It.IsAny(), - It.Is((object v, Type _) => v.ToString() !.Contains(nameof(TestJob))), + It.Is((object v, Type _) => v.ToString()!.Contains(nameof(Exception))), It.IsAny(), It.Is>((v, t) => true)), - Times.Between(1, 2, Range.Inclusive)); - } + Times.Between(1, 2, Range.Inclusive)); + } - [Fact] - public async Task Job_RunDelayed_Factory_Successfully() + [Fact] + public async Task Job_RunDelayed_And_Raise_UnobservedTaskException_Via_Delegate() + { + // arrange + var mockLogger = new Mock>(); + var host = CreateHost(services => { - var mockLogger = new Mock>(); - - var host = CreateHost(services => + services.AddScheduler(ctx => { - services.AddScheduler(ctx => - { - var jobName = nameof(TestJob); - - ctx.AddJob( - sp => new TestJob(mockLogger.Object), - configure: options => - { - options.CronSchedule = "*/05 * * * * *"; - options.RunImmediately = false; - }, - jobName: jobName); - }); + var jobName = nameof(TestJobException); + + ctx.AddUnobservedTaskExceptionHandler(sp => (sender, args) => UnobservedTaskExceptionHandler(sp, args)); + + ctx.AddJob( + sp => + { + var options = sp.GetRequiredService>(); + return new TestJobException(mockLogger.Object, options); + }, + options => + { + options.CronSchedule = "*/4 * * * * *"; + options.RunImmediately = false; + options.JobName = jobName; + options.RaiseException = true; + }, + jobName: jobName); }); + }); - var client = new TestServer(host).CreateClient(); + var client = new TestServer(host).CreateClient(); - // act - var response = await client.GetAsync("/hc"); - response.EnsureSuccessStatusCode(); - await Task.Delay(TimeSpan.FromSeconds(6)); + // act + var response = await client.GetAsync("/hc"); + response.EnsureSuccessStatusCode(); + await Task.Delay(TimeSpan.FromSeconds(6)); - // assert - Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); + // assert + Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); - // fixed moq and log according to https://github.com/moq/moq4/issues/918#issuecomment-535060645 - mockLogger.Verify( - l => l.Log( - LogLevel.Information, + mockLogger.Verify( + l => l.Log( + LogLevel.Error, It.IsAny(), - It.Is((object v, Type _) => v.ToString() !.Contains(nameof(TestJob))), + It.Is((object v, Type _) => v.ToString()!.Contains(nameof(Exception))), It.IsAny(), It.Is>((v, t) => true)), - Times.Between(1, 2, Range.Inclusive)); - } + Times.Between(1, 2, Range.Inclusive)); + } - [Fact] - public async Task Job_RunDelayed_And_Raise_UnobservedTaskException() - { - // arrange - var mockLogger = new Mock>(); + [Fact] + public async Task Job_RunImmediately_Configuration_And_Raise_Exception() + { + // arrange + var mockLogger = new Mock>(); - var host = CreateHost(services => - { - services.AddScheduler(ctx => - { - var jobName = nameof(TestJobException); - - ctx.UnobservedTaskExceptionHandler = UnobservedTaskExceptionHandler; - - ctx.AddJob( - sp => - { - var options = sp.GetRequiredService>(); - return new TestJobException(mockLogger.Object, options); - }, - options => - { - options.CronSchedule = "*/4 * * * * *"; - options.RunImmediately = false; - options.JobName = jobName; - options.RaiseException = true; - }, - jobName: jobName); - }); - }); + var host = CreateHost(services => + { + // used for tests + services.AddTransient(x => mockLogger.Object); - var client = new TestServer(host).CreateClient(); + // short registration without UnobservedTaskExceptionHandler + services.AddSchedulerJob(); + }); - // act - var response = await client.GetAsync("/hc"); - response.EnsureSuccessStatusCode(); - await Task.Delay(TimeSpan.FromSeconds(6)); + var client = new TestServer(host).CreateClient(); - // assert - Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); + // act + var response = await client.GetAsync("/hc"); + response.EnsureSuccessStatusCode(); + await Task.Delay(TimeSpan.FromSeconds(3)); - mockLogger.Verify( - l => l.Log( - LogLevel.Error, - It.IsAny(), - It.Is((object v, Type _) => v.ToString() !.Contains(nameof(Exception))), - It.IsAny(), - It.Is>((v, t) => true)), - Times.Between(1, 2, Range.Inclusive)); - } + // assert + Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); - [Fact] - public async Task Job_RunImmediately_Configuration_And_Raise_Exception() - { - // arrange - var mockLogger = new Mock>(); + mockLogger.Verify( + l => l.Log( + LogLevel.Error, + It.IsAny(), + It.Is((object v, Type _) => v.ToString()!.Contains(nameof(Exception))), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Between(1, 4, Range.Inclusive)); + } - var host = CreateHost(services => + private IWebHostBuilder CreateHost( + Action configServices, + bool validateScopes = false) + { + return new WebHostBuilder() + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => { - // used for tests - services.AddTransient(x => mockLogger.Object); - - // short registration without UnobservedTaskExceptionHandler - services.AddSchedulerJob(); - }); - - var client = new TestServer(host).CreateClient(); + var env = hostingContext.HostingEnvironment; - // act - var response = await client.GetAsync("/hc"); - response.EnsureSuccessStatusCode(); - await Task.Delay(TimeSpan.FromSeconds(3)); + config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - // assert - Assert.Equal("healthy", await response.Content.ReadAsStringAsync()); - - mockLogger.Verify( - l => l.Log( - LogLevel.Error, - It.IsAny(), - It.Is((object v, Type _) => v.ToString() !.Contains(nameof(Exception))), - It.IsAny(), - It.Is>((v, t) => true)), - Times.Between(1, 4, Range.Inclusive)); - } - - private IWebHostBuilder CreateHost( - Action configServices, - bool validateScopes = false) - { - return new WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => + if (env.IsDevelopment()) { - var env = hostingContext.HostingEnvironment; - - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - - if (env.IsDevelopment()) + var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); + if (appAssembly != null) { - var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); - if (appAssembly != null) - { - config.AddUserSecrets(appAssembly, optional: true); - } + config.AddUserSecrets(appAssembly, optional: true); } + } - config.AddEnvironmentVariables(); - }) - .UseStartup() - .ConfigureTestServices(services => - { - configServices(services); - services.AddLogging(); - }) - .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes); - } + config.AddEnvironmentVariables(); + }) + .UseStartup() + .ConfigureTestServices(services => + { + configServices(services); + services.AddLogging(x => x.AddXunit(_output)); + }) + .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes); + } - private void UnobservedTaskExceptionHandler(object sender, UnobservedTaskExceptionEventArgs e) - { - _output.WriteLine(e.Exception?.InnerException?.Message); + private void UnobservedTaskExceptionHandler(IServiceProvider sp, UnobservedTaskExceptionEventArgs e) + { + var loggerFactory = sp.GetRequiredService(); + var logger = loggerFactory.CreateLogger("CronJobs"); + + logger.LogError(e?.Exception, "Failed"); - // if not set exception is thrown from the tasks management context - e.SetObserved(); - } + // if not set exception is thrown from the tasks management context + e?.SetObserved(); } } diff --git a/test/CronScheduler.UnitTest/SchedulerRegistrationTests.cs b/test/CronScheduler.UnitTest/SchedulerRegistrationTests.cs index 2e4efbc..12ce0a7 100644 --- a/test/CronScheduler.UnitTest/SchedulerRegistrationTests.cs +++ b/test/CronScheduler.UnitTest/SchedulerRegistrationTests.cs @@ -15,64 +15,63 @@ using Xunit; using Xunit.Abstractions; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class SchedulerRegistrationTests { - public class SchedulerRegistrationTests - { - private ITestOutputHelper _output; + private ITestOutputHelper _output; - public SchedulerRegistrationTests(ITestOutputHelper output) - { - _output = output ?? throw new ArgumentNullException(nameof(output)); - } + public SchedulerRegistrationTests(ITestOutputHelper output) + { + _output = output ?? throw new ArgumentNullException(nameof(output)); + } - [Fact] - public async Task Successfully_Register_Two_Jobs_With_The_Same_Type() - { - var configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); + [Fact] + public async Task Successfully_Register_Two_Jobs_With_The_Same_Type() + { + var configuration = new ConfigurationBuilder().AddInMemoryCollection().Build(); - var services = new ServiceCollection(); + var services = new ServiceCollection(); - services.AddSingleton(configuration); + services.AddSingleton(configuration); - services.AddLogging(builder => - { - builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); - }); + services.AddLogging(builder => + { + builder.AddDebug(); + builder.AddXunit(_output, LogLevel.Debug); + }); - services.AddScheduler(); - services.AddHostedService(); + services.AddScheduler(); + services.AddHostedService(); - var sp = services.BuildServiceProvider(); - var schedulerRegistration = sp.GetRequiredService(); - var loggerFactory = sp.GetRequiredService(); + var sp = services.BuildServiceProvider(); + var schedulerRegistration = sp.GetRequiredService(); + var loggerFactory = sp.GetRequiredService(); - var options1 = new CustomTestJobOptions - { - JobName = "Job1", - CronSchedule = "0/2 * * * * *", - RunImmediately = false, - DisplayText = "Every 2 seconds." - }; + var options1 = new CustomTestJobOptions + { + JobName = "Job1", + CronSchedule = "0/2 * * * * *", + RunImmediately = false, + DisplayText = "Every 2 seconds." + }; - var options2 = new CustomTestJobOptions - { - JobName = "Job2", - CronSchedule = "0/4 * * * * *", - RunImmediately = false, - DisplayText = "Every 4 seconds." - }; + var options2 = new CustomTestJobOptions + { + JobName = "Job2", + CronSchedule = "0/4 * * * * *", + RunImmediately = false, + DisplayText = "Every 4 seconds." + }; - schedulerRegistration.AddOrUpdate(new CustomTestJob(options1, loggerFactory.CreateLogger()), options1); - schedulerRegistration.AddOrUpdate(new CustomTestJob(options2, loggerFactory.CreateLogger()), options2); + schedulerRegistration.AddOrUpdate(new CustomTestJob(options1, loggerFactory.CreateLogger()), options1); + schedulerRegistration.AddOrUpdate(new CustomTestJob(options2, loggerFactory.CreateLogger()), options2); - var backgroundService = sp.GetService() as SchedulerHostedService; - await backgroundService.StartAsync(CancellationToken.None); + var backgroundService = sp.GetService() as SchedulerHostedService; + await backgroundService!.StartAsync(CancellationToken.None); - await Task.Delay(TimeSpan.FromSeconds(15)); + await Task.Delay(TimeSpan.FromSeconds(15)); - await backgroundService.StopAsync(CancellationToken.None); - } + await backgroundService.StopAsync(CancellationToken.None); } } diff --git a/test/CronScheduler.UnitTest/SchedulerServiceTests.cs b/test/CronScheduler.UnitTest/SchedulerServiceTests.cs index 85f6502..572e6fe 100644 --- a/test/CronScheduler.UnitTest/SchedulerServiceTests.cs +++ b/test/CronScheduler.UnitTest/SchedulerServiceTests.cs @@ -15,124 +15,123 @@ using Xunit; using Xunit.Abstractions; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class SchedulerServiceTests { - public class SchedulerServiceTests + private readonly ITestOutputHelper _output; + + public SchedulerServiceTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; + _output = output ?? throw new ArgumentNullException(nameof(output)); + } - public SchedulerServiceTests(ITestOutputHelper output) + [Fact] + public void Add_Job_Successfully() + { + var dic = new Dictionary { - _output = output ?? throw new ArgumentNullException(nameof(output)); - } + { "SchedulerJobs:TestJobException:CronSchedule", "*/10 * * * * *" }, + { "SchedulerJobs:TestJobException:CronTimeZone", string.Empty }, + { "SchedulerJobs:TestJobException:RunImmediately", "true" }, + }; - [Fact] - public void Add_Job_Successfully() - { - var dic = new Dictionary - { - { "SchedulerJobs:TestJobException:CronSchedule", "*/10 * * * * *" }, - { "SchedulerJobs:TestJobException:CronTimeZone", string.Empty }, - { "SchedulerJobs:TestJobException:RunImmediately", "true" }, - }; + var configuration = new ConfigurationBuilder().AddInMemoryCollection(dic).Build(); - var configuration = new ConfigurationBuilder().AddInMemoryCollection(dic).Build(); + var services = new ServiceCollection(); - var services = new ServiceCollection(); + services.AddSingleton(configuration); - services.AddSingleton(configuration); + services.AddOptions(); - services.AddOptions(); + var name = typeof(TestJob).Name; - var name = typeof(TestJob).Name; - - services.AddOptions(name) - .Configure((options, configuration) => - { - configuration.Bind("SchedulerJobs:TestJobException", options); - }); - - services.AddLogging(builder => + services.AddOptions(name) + .Configure((options, configuration) => { - builder.AddConsole(); - builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); + configuration.Bind("SchedulerJobs:TestJobException", options); }); - services.AddSingleton(); + services.AddLogging(builder => + { + builder.AddConsole(); + builder.AddDebug(); + builder.AddXunit(_output, LogLevel.Debug); + }); - var sp = services.BuildServiceProvider(); + services.AddSingleton(); - var instance = sp.GetService(); + var sp = services.BuildServiceProvider(); - using var logFactory = TestLoggerBuilder.Create(builder => - { - builder.AddConsole(); - builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); - }); + var instance = sp.GetService(); - var job = new TestJob(logFactory.CreateLogger()); - var options = sp.GetRequiredService>().Get(name); + using var logFactory = TestLoggerBuilder.Create(builder => + { + builder.AddConsole(); + builder.AddDebug(); + builder.AddXunit(_output, LogLevel.Debug); + }); + + var job = new TestJob(logFactory.CreateLogger()); + var options = sp.GetRequiredService>().Get(name); - instance.AddOrUpdate(job.GetType().Name, job, options); + instance!.AddOrUpdate(job.GetType().Name, job, options); - Assert.Single(instance.Jobs); - } + Assert.Single(instance.Jobs); + } - [Fact] - public void Add_Job_Successfully_1() + [Fact] + public void Add_Job_Successfully_1() + { + var dic = new Dictionary { - var dic = new Dictionary - { - { "SchedulerJobs:TestJobException:CronSchedule", "*/10 * * * * *" }, - { "SchedulerJobs:TestJobException:CronTimeZone", string.Empty }, - { "SchedulerJobs:TestJobException:RunImmediately", "true" }, - }; + { "SchedulerJobs:TestJobException:CronSchedule", "*/10 * * * * *" }, + { "SchedulerJobs:TestJobException:CronTimeZone", string.Empty }, + { "SchedulerJobs:TestJobException:RunImmediately", "true" }, + }; - var configuration = new ConfigurationBuilder().AddInMemoryCollection(dic).Build(); + var configuration = new ConfigurationBuilder().AddInMemoryCollection(dic).Build(); - var service = new ServiceCollection(); + var service = new ServiceCollection(); - service.AddSingleton(configuration); + service.AddSingleton(configuration); - service.AddOptions(); + service.AddOptions(); - var name = typeof(TestJob).Name; + var name = typeof(TestJob).Name; - service.AddChangeTokenOptions("SchedulerJobs:TestJobException", name, _ => { }); + service.AddChangeTokenOptions("SchedulerJobs:TestJobException", name, _ => { }); - service.AddLogging(builder => - { - builder.AddConsole(); - builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); - }); + service.AddLogging(builder => + { + builder.AddConsole(); + builder.AddDebug(); + builder.AddXunit(_output, LogLevel.Debug); + }); - service.AddSingleton(); + service.AddSingleton(); - var sp = service.BuildServiceProvider(); + var sp = service.BuildServiceProvider(); - var instance = sp.GetService(); + var instance = sp.GetService(); - using var logFactory = TestLoggerBuilder.Create(builder => - { - builder.AddConsole(); - builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); - }); + using var logFactory = TestLoggerBuilder.Create(builder => + { + builder.AddConsole(); + builder.AddDebug(); + builder.AddXunit(_output, LogLevel.Debug); + }); - var job = new TestJob(logFactory.CreateLogger()); - var options = sp.GetRequiredService>().Get(name); + var job = new TestJob(logFactory.CreateLogger()); + var options = sp.GetRequiredService>().Get(name); - instance.AddOrUpdate(job.GetType().Name, job, options); + instance!.AddOrUpdate(job.GetType().Name, job, options); - Assert.Equal(1, instance.Jobs.Count); + Assert.Equal(1, instance.Jobs.Count); - configuration.Providers.ToList()[0].Set("SchedulerJobs:TestJobException:CronSchedule", "*/1 * * * * *"); - configuration.Reload(); + configuration.Providers.ToList()[0].Set("SchedulerJobs:TestJobException:CronSchedule", "*/1 * * * * *"); + configuration.Reload(); - _output.WriteLine(instance.Jobs.ToArray()[0].Value.Schedule.ToString()); - } + _output.WriteLine(instance.Jobs.ToArray()[0].Value.Schedule.ToString()); } } diff --git a/test/CronScheduler.UnitTest/StartupJobFuncTests.cs b/test/CronScheduler.UnitTest/StartupJobFuncTests.cs index ea5a28c..67afa8e 100644 --- a/test/CronScheduler.UnitTest/StartupJobFuncTests.cs +++ b/test/CronScheduler.UnitTest/StartupJobFuncTests.cs @@ -8,57 +8,56 @@ using Xunit; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class StartupJobFuncTests { - public class StartupJobFuncTests + [Fact] + public async Task RunJobs() { - [Fact] - public async Task RunJobs() - { - var cts = new CancellationTokenSource(); + var cts = new CancellationTokenSource(); - var builder = WebHost.CreateDefaultBuilder() - .UseStartup() - .ConfigureServices(services => - { - services.AddLogging(); - services.AddStartupJob(); - }) - .UseDefaultServiceProvider(options => options.ValidateScopes = false); + var builder = WebHost.CreateDefaultBuilder() + .UseStartup() + .ConfigureServices(services => + { + services.AddLogging(); + services.AddStartupJob(); + }) + .UseDefaultServiceProvider(options => options.ValidateScopes = false); - var host = builder.Build(); + var host = builder.Build(); - using (host) - { - await host.RunStartupJobsAync(cts.Token); - - cts.Cancel(); + using (host) + { + await host.RunStartupJobsAync(cts.Token); - await host.WaitForShutdownAsync(cts.Token); - } + cts.Cancel(); - cts.Dispose(); + await host.WaitForShutdownAsync(cts.Token); } - [Fact] - public async Task RunDelegate() - { - async Task CompletedTask() => await Task.CompletedTask; + cts.Dispose(); + } - var host = CreateHost(services => services.AddStartupJobInitializer(CompletedTask)); + [Fact] + public async Task RunDelegate() + { + async Task CompletedTask() => await Task.CompletedTask; - await host.RunStartupJobsAync(); + var host = CreateHost(services => services.AddStartupJobInitializer(CompletedTask)); - host.Dispose(); - } + await host.RunStartupJobsAync(); - private static IWebHost CreateHost(Action configureServices, bool validateScopes = false) - { - return new WebHostBuilder() - .UseStartup() - .ConfigureServices(configureServices) - .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) - .Build(); - } + host.Dispose(); + } + + private static IWebHost CreateHost(Action configureServices, bool validateScopes = false) + { + return new WebHostBuilder() + .UseStartup() + .ConfigureServices(configureServices) + .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes) + .Build(); } } diff --git a/test/CronScheduler.UnitTest/TestJob.cs b/test/CronScheduler.UnitTest/TestJob.cs index 6415389..7784f45 100644 --- a/test/CronScheduler.UnitTest/TestJob.cs +++ b/test/CronScheduler.UnitTest/TestJob.cs @@ -5,23 +5,22 @@ using Microsoft.Extensions.Logging; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class TestJob : IScheduledJob { - public class TestJob : IScheduledJob - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public TestJob(ILogger logger) - { - _logger = logger; - } + public TestJob(ILogger logger) + { + _logger = logger; + } - public string Name { get; } = nameof(TestJob); + public string Name { get; } = nameof(TestJob); - public Task ExecuteAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Running {name}", nameof(TestJob)); - return Task.CompletedTask; - } + public Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Running {name}", nameof(TestJob)); + return Task.CompletedTask; } } diff --git a/test/CronScheduler.UnitTest/TestJobDup.cs b/test/CronScheduler.UnitTest/TestJobDup.cs index 71ab787..75f3ff7 100644 --- a/test/CronScheduler.UnitTest/TestJobDup.cs +++ b/test/CronScheduler.UnitTest/TestJobDup.cs @@ -5,28 +5,27 @@ using Microsoft.Extensions.Logging; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +/// +/// The purpose of this class to demonstrate how to utilize the same job +/// with different schedules. +/// +public class TestJobDup : IScheduledJob { - /// - /// The purpose of this class to demonstrate how to utilize the same job - /// with different schedules. - /// - public class TestJobDup : IScheduledJob - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public TestJobDup(SchedulerOptions options, ILogger logger) - { - _logger = logger; - Name = options.JobName; - } + public TestJobDup(SchedulerOptions options, ILogger logger) + { + _logger = logger; + Name = options.JobName; + } - public string Name { get; } + public string Name { get; } - public Task ExecuteAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Running {name}", nameof(TestJob)); - return Task.CompletedTask; - } + public Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Running {name}", nameof(TestJob)); + return Task.CompletedTask; } } diff --git a/test/CronScheduler.UnitTest/TestJobException.cs b/test/CronScheduler.UnitTest/TestJobException.cs index ed761cd..d894a60 100644 --- a/test/CronScheduler.UnitTest/TestJobException.cs +++ b/test/CronScheduler.UnitTest/TestJobException.cs @@ -7,40 +7,39 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class TestJobExceptionOptions : SchedulerOptions { - public class TestJobExceptionOptions : SchedulerOptions - { - public bool RaiseException { get; set; } - } + public bool RaiseException { get; set; } +} - public class TestJobException : IScheduledJob - { - private readonly ILogger _logger; - private readonly TestJobExceptionOptions _options; +public class TestJobException : IScheduledJob +{ + private readonly ILogger _logger; + private readonly TestJobExceptionOptions _options; - public TestJobException( - ILogger logger, - IOptionsMonitor optionsMonitor) - { - _logger = logger; + public TestJobException( + ILogger logger, + IOptionsMonitor optionsMonitor) + { + _logger = logger; - _options = optionsMonitor.Get(Name); - } + _options = optionsMonitor.Get(Name); + } - public string Name { get; } = nameof(TestJobException); + public string Name { get; } = nameof(TestJobException); - public Task ExecuteAsync(CancellationToken cancellationToken) + public Task ExecuteAsync(CancellationToken cancellationToken) + { + if (_options.RaiseException) { - if (_options.RaiseException) - { - var message = nameof(Exception); - _logger.LogError(message); - throw new Exception(message); - } - - _logger.LogInformation("Running {name}", nameof(TestJobException)); - return Task.CompletedTask; + var message = nameof(Exception); + _logger.LogError(message); + throw new Exception(message); } + + _logger.LogInformation("Running {name}", nameof(TestJobException)); + return Task.CompletedTask; } } diff --git a/test/CronScheduler.UnitTest/TestStartup.cs b/test/CronScheduler.UnitTest/TestStartup.cs index 1baa91a..3bf539e 100644 --- a/test/CronScheduler.UnitTest/TestStartup.cs +++ b/test/CronScheduler.UnitTest/TestStartup.cs @@ -5,37 +5,36 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class TestStartup { - public class TestStartup - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public TestStartup( - IConfiguration configuration, - ILogger logger) - { - Configuration = configuration; - _logger = logger; - } + public TestStartup( + IConfiguration configuration, + ILogger logger) + { + Configuration = configuration; + _logger = logger; + } - public IConfiguration Configuration { get; } + public IConfiguration Configuration { get; } - public void ConfigureServices(IServiceCollection services) - { - } + public void ConfigureServices(IServiceCollection services) + { + } - public void Configure( - IApplicationBuilder app, - IWebHostEnvironment env) + public void Configure( + IApplicationBuilder app, + IWebHostEnvironment env) + { + app.Map("/hc", route => { - app.Map("/hc", route => + route.Run(async context => { - route.Run(async context => - { - await context.Response.WriteAsync("healthy"); - }); + await context.Response.WriteAsync("healthy"); }); - } + }); } } diff --git a/test/CronScheduler.UnitTest/TestStartupJob.cs b/test/CronScheduler.UnitTest/TestStartupJob.cs index 1a673bb..6d61011 100644 --- a/test/CronScheduler.UnitTest/TestStartupJob.cs +++ b/test/CronScheduler.UnitTest/TestStartupJob.cs @@ -4,15 +4,14 @@ using CronScheduler.Extensions.StartupInitializer; -namespace CronScheduler.UnitTest +namespace CronScheduler.UnitTest; + +public class TestStartupJob : IStartupJob { - public class TestStartupJob : IStartupJob + public async Task ExecuteAsync(CancellationToken cancellationToken = default) { - public async Task ExecuteAsync(CancellationToken cancellationToken = default) - { - await Task.Delay(TimeSpan.FromSeconds(10)); + await Task.Delay(TimeSpan.FromSeconds(10)); - await Task.CompletedTask; - } + await Task.CompletedTask; } }