diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..9830e70 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "gitversion.tool": { + "version": "6.0.0", + "commands": [ + "dotnet-gitversion" + ], + "rollForward": false + } + } +} diff --git a/.editorconfig b/.editorconfig index da52519..63f7593 100644 --- a/.editorconfig +++ b/.editorconfig @@ -305,3 +305,6 @@ dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = warning dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i + +# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.SpacingRules' +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.SpacingRules.severity = none diff --git a/.env b/.env index 892ba52..b1ba3a5 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -DOCKER_REGISTRY= \ No newline at end of file +DOCKER_REGISTRY=kdcllc diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml new file mode 100644 index 0000000..d5b00c1 --- /dev/null +++ b/.github/workflows/manual.yml @@ -0,0 +1,54 @@ +name: manual + +on: + pull_request: + branches: + - master + - feat/* + - feature/* + - release/* + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install GitVersion + uses: gittools/actions/gitversion/setup@v3.0.0 + with: + versionSpec: '6.x' + + - name: Determine Version + id: version # step id used as reference for output values + uses: gittools/actions/gitversion/execute@v3.0.0 + + - name: Print GitVersion_FullSemVer + run: echo "The GitVersion_FullSemVer is ${{ env.GitVersion_FullSemVer }}" + + - name: Print GitVersion_SemVer + run: echo "The GitVersion_SemVer is ${{ env.GitVersion_SemVer }}" + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '8.0.x' + + - name: Install dependencies + run: dotnet restore CronScheduler.sln + + - name: Build + run: dotnet build CronScheduler.sln --configuration Release --no-restore /p:Version=${{ env.GitVersion_FullSemVer }} + + - name: Test + run: dotnet test + + - name: Publish NuGet package + run: | + dotnet pack CronScheduler.sln --configuration Release --no-build /p:PackageVersion=${{ env.GitVersion_FullSemVer }} /p:Version=${{ env.GitVersion_FullSemVer }} + dotnet nuget push **/*.nupkg --source https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/nuget/index.json --api-key ${{ secrets.FEEDZ_API_KEY }} --skip-duplicate diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml new file mode 100644 index 0000000..e51cd29 --- /dev/null +++ b/.github/workflows/master.yml @@ -0,0 +1,45 @@ +name: master + +on: + push: + branches: + - master +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install GitVersion + uses: gittools/actions/gitversion/setup@v3.0.0 + with: + versionSpec: '6.x' + + - name: Determine Version + id: version # step id used as reference for output values + uses: gittools/actions/gitversion/execute@v3.0.0 + + - name: Print GitVersion_SemVer + run: echo "The GitVersion_SemVer is ${{ env.GitVersion_SemVer }}" + - name: Setup .NET SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '8.0.x' + + - name: Install dependencies + run: dotnet restore CronScheduler.sln + + - name: Build + run: dotnet build CronScheduler.sln --configuration Release --no-restore /p:Version=${{ env.GitVersion_SemVer }} + + - name: Test + run: dotnet test + + - name: Publish NuGet package + run: | + dotnet pack src/YourProject/YourProject.csproj --configuration Release --output ./nupkg /p:Version=${{ env.GitVersion_SemVer }} /p:Version=${{ env.GitVersion_SemVer }} + dotnet nuget push ./nupkg/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/.gitignore b/.gitignore index 0c9dd05..08f8707 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore *.db +app.db # User-specific files *.suo @@ -330,3 +331,6 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ +src/CronSchedulerApp/app.db-shm +src/CronSchedulerApp/app.db-wal +.aider* diff --git a/.vscode/launch.json b/.vscode/launch.json index 014d707..70e717f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,21 +4,28 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Worker Debug", + "type": "dotnet", + "request": "launch", + "projectPath": "${workspaceFolder}/src/CronSchedulerWorker/CronSchedulerWorker.csproj" + }, { "name": ".NET Core Launch Web", "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceFolder}/src/CronSchedulerApp/bin/Debug/netcoreapp3.0/CronSchedulerApp.dll", + "program": "${workspaceFolder}/src/CronSchedulerApp/bin/Debug/net8.0/CronSchedulerApp.dll", "args": [], - "cwd": "${workspaceFolder}/src/", + "cwd": "${workspaceFolder}/src/CronSchedulerApp/", "console": "internalConsole", "stopAtEntry": false, + "justMyCode": true, - // "env": { - // "ASPNETCORE_ENVIRONMENT": "Development", - // "ASPNETCORE_URLS": "https://localhost:50001" - // }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "https://localhost:51440" + }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" diff --git a/.vscode/settings.json b/.vscode/settings.json index 6b5742e..3523c1e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,4 @@ { - "dotnet-test-explorer.testProjectPath" : "test/**/*.csproj", - "files.associations": { "Dockerfile*": "dockerfile" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e375f5..5044f4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ Change Log =============================================================================== +Version 3.2.0 (07/26/2024) + +- removed obsolete methods +- fixed RunStartupJobsAsync + Version 3.1.0 (6/12/2022) * `SchedulerBuilder.UnobservedTaskExceptionHandler` marked as `Obsolete`; diff --git a/CronScheduler.sln b/CronScheduler.sln index fac8746..2ce1566 100644 --- a/CronScheduler.sln +++ b/CronScheduler.sln @@ -13,15 +13,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{8C00CEBF EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CronSchedulerApp", "src\CronSchedulerApp\CronSchedulerApp.csproj", "{71B7EDED-F26D-4545-9E9B-1F6500AA589C}" EndProject -Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{CFB990F3-58B7-4C9A-AF62-CC13454162B2}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solutions Items", "Solutions Items", "{998E5783-A784-44C5-88BF-06884943BD6C}" ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore .editorconfig = .editorconfig .env = .env .gitignore = .gitignore - appveyor.yml = appveyor.yml CHANGELOG.md = CHANGELOG.md clean.sh = clean.sh Directory.Build.props = Directory.Build.props diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..b7dc626 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,25 @@ +mode: ContinuousDelivery +branches: + main: + regex: ^main$ + label: '' + increment: Patch + is-release-branch: true + feature: + regex: ^(feature|feat)[/-] + label: alpha + increment: Minor + bugfix: + regex: ^bugfix[/-] + label: alpha + increment: Patch + chore: + regex: ^chore[/-] + label: beta + increment: None + pull-request: + regex: ^(pull|pr)[/-] + label: alpha + increment: Inherit +commit-message-incrementing: Enabled +major-version-bump-message: '\\+semver:\\s?(breaking|major)' diff --git a/LICENSE b/LICENSE index 8cb9556..b10b441 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2022 King David Consulting LLC +Copyright (c) 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 3f1be60..5675dc9 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,43 @@ # CronScheduler.AspNetCore -[![Build status](https://ci.appveyor.com/api/projects/status/wrme1wr6kgjp3a0o?svg=true)](https://ci.appveyor.com/project/kdcllc/cronscheduler-aspnetcore) +![master workflow](https://github.com/github/docs/actions/workflows/master.yml/badge.svg) [![NuGet](https://img.shields.io/nuget/v/CronScheduler.AspNetCore.svg)](https://www.nuget.org/packages?q=CronScheduler.AspNetCore) ![Nuget](https://img.shields.io/nuget/dt/CronScheduler.AspNetCore) [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/shield/CronScheduler.AspNetCore/latest)](https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/packages/CronScheduler.AspNetCore/latest/download) *Note: Pre-release packages are distributed via [feedz.io](https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/nuget/index.json).* +![I Stand With Israel](./img/IStandWithIsrael.png) + ## Summary -The goal of this library was to design a simple Cron Scheduling engine that could be used with DotNetCore `IHost` or with AspNetCore `IWebHost`. +**Unlock the Power of Simplified Cron Scheduling in Your .NET Core Apps** + +Are you tired of complex scheduling libraries holding you back from building scalable and efficient applications? Look no further! Introducing **CronScheduler**, a lightweight and easy-to-use library designed specifically for .NET Core `IHost` or `IWebHost`. + +Built with the KISS principle in mind, CronScheduler is a simplified alternative to Quartz Scheduler and its alternatives. With CronScheduler, you can easily schedule tasks using cron syntax and operate within any .NET Core GenericHost `IHost`, making setup and configuration a breeze. + +But that's not all! We've also introduced **IStartupJob**, allowing for async initialization of critical processes before the host is ready to start. This means you can ensure your application is properly initialized and running smoothly, even in complex Kubernetes environments. -It is much lighter than Quartz schedular or its alternatives. The `KISS` principle was at the heart of the development of this library. +**Benefits:** -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. +* Lightweight and easy-to-use library +* Simplified scheduling with cron syntax +* Operates within .NET Core GenericHost `IHost` or `IWebHost` +* Async initialization support for critical processes with IStartupJob -In addition `IStartupJob` was added to support async initialization of critical process before the `IHost` is ready to start. +**Join the CronScheduler community today and start simplifying your application's scheduling needs!** -> +> > **Please refer to [Migration Guide](./Migration.md) for the upgrade.** > +[![buymeacoffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/vyve0og) + +## Give a Star! :star: + +If you like or are using this project to learn or start your solution, please give it a star. Thanks! + +## Installation - Install package for `AspNetCore` hosting .NET CLI @@ -39,6 +57,8 @@ This library supports up to 5 seconds job intervals in the Crontab format thank You can use [https://crontab-generator.org/](https://crontab-generator.org/) to generated needed job/task schedule. +### Cron format + Cron expression is a mask to define fixed times, dates and intervals. The mask consists of second (optional), minute, hour, day-of-month, month and day-of-week fields. All of the fields allow you to specify multiple values, and any given date/time will satisfy the specified Cron expression, if all the fields contain a matching value. Allowed values Allowed special characters Comment @@ -52,12 +72,6 @@ Cron expression is a mask to define fixed times, dates and intervals. The mask c │ │ │ │ │ │ * * * * * * -[![buymeacoffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/vyve0og) - -## Give a Star! :star: - -If you like or are using this project to learn or start your solution, please give it a star. Thanks! - ## Demo Applications - [CronSchedulerWorker](./src/CronSchedulerWorker/) - this example demonstrates how to use `CronScheduler` with new Microsoft .NET Core Workers Template @@ -150,130 +164,93 @@ This job registration is assuming that the name of the job and options name are } ``` -Then register this service within the `Startup.cs` -The sample uses `Microsoft.Extensions.Http.Polly` extension library to make http calls every 10 seconds. +Then register this service within the `Program.cs`: ```csharp - services.AddScheduler(builder => - { - builder.Services.AddSingleton(); - - // Build a policy that will handle exceptions, 408s, and 500s from the remote server - 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"); +var builder = WebApplication.CreateBuilder(args); - return - (sender, args) => - { - logger?.LogError(args.Exception?.Message); - args.SetObserved(); - }; - }); - }); -``` +// Add services to the container. +builder.Services.AddScheduler(builder => +{ + builder.Services.AddSingleton(); + builder.Services + .AddHttpClient() + .AddTransientHttpErrorPolicy(p => p.RetryAsync()); -## Sample code for Scoped or Transient Schedule Job and its dependencies + builder.AddJob(); + builder.Services.AddScoped(); + builder.AddJob(); -```csharp - public class UserJob : IScheduledJob + builder.AddUnobservedTaskExceptionHandler(sp => { - private readonly UserJobOptions _options; - private readonly IServiceProvider _provider; - - public UserJob( - IServiceProvider provider, - IOptionsMonitor options) + var logger = sp.GetRequiredService().CreateLogger("CronJobs"); + return (sender, args) => { - _options = options.Get(Name); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - } + logger?.LogError(args.Exception?.Message); + args.SetObserved(); + }; + }); +}); - public string Name { get; } = nameof(UserJob); +builder.Services.AddBackgroundQueuedService(applicationOnStopWaitForTasksToComplete: true); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); - 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(); +builder.Services.AddStartupJob(); +builder.Services.AddStartupJob(); - var users = userService.GetUsers(); +builder.Logging.AddConsole(); +builder.Logging.AddDebug(); +builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); - foreach (var user in users) - { - await userService.AddClaimAsync(user, new Claim(_options.ClaimName, DateTime.UtcNow.ToString())); - } - } - } -``` +var app = builder.Build(); -Then register this service within the `Startup.cs` +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); + app.UseMigrationsEndPoint(); +} +else +{ + app.UseExceptionHandler("/Home/Error"); + app.UseHsts(); +} -```csharp - services.AddScheduler(builder => - { - builder.Services.AddScoped(); - builder.AddJob(); +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseCookiePolicy(); +app.UseAuthentication(); +app.UseRouting(); - // register a custom error processing for internal errors - builder.AddUnobservedTaskExceptionHandler(sp => - { - var logger = sp.GetRequiredService().CreateLogger("CronJobs"); +app.MapControllers(); +app.MapDefaultControllerRoute(); +app.MapRazorPages(); - return - (sender, args) => - { - logger?.LogError(args.Exception?.Message); - args.SetObserved(); - }; - }); - }); +await app.RunStartupJobsAsync(); +await app.RunAsync(); ``` + ## `IStartupJobs` to assist with async jobs initialization before the application starts -There are many case scenarios to use StartupJobs for the IWebHost interface or IGenericHost. Most common case scenario is to make sure that database is created and updated. +There are many case scenarios to use StartupJobs for the `IWebHost` interface or `IGenericHost`. The most common case scenario is to make sure that the database is created and updated. This library makes it possible by simply doing the following: -- In the Program.cs file add the following: +- In the `Program.cs` file add the following: ```csharp - public static async Task Main(string[] args) - { - var host = CreateWebHostBuilder(args).Build(); - - // process any async jobs required to get the site up and running - await host.RunStartupJobsAync(); +var builder = WebApplication.CreateBuilder(args); - host.Run(); - } -``` +// Add services to the container. +builder.Services.AddStartupJob(); +builder.Services.AddStartupJob(); -- Register the startup job in `Program.cs` or in `Startup.cs` file. +var app = builder.Build(); -```csharp -public static IWebHostBuilder CreateWebHostBuilder(string[] args) -{ - return WebHost.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddStartupJob(); - }) - .ConfigureLogging((context, logger) => - { - logger.AddConsole(); - logger.AddDebug(); - logger.AddConfiguration(context.Configuration.GetSection("Logging")); - }) - .UseStartup(); -} +// Configure the HTTP request pipeline. +await app.RunStartupJobsAsync(); +await app.RunAsync(); ``` - ## Background Queues In some instances of the application the need for queuing of the tasks is required. In order to enable this add the following in `Startup.cs`. @@ -308,4 +285,4 @@ Then add sample async task to be executed by the Queued Hosted Service. ## License -[MIT License Copyright (c) 2017-2022 King David Consulting LLC](./LICENSE) +[MIT License](./LICENSE) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index d633e4f..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: 3.1.{build} -branches: - only: - - master -pull_requests: - do_not_increment_build_number: true -image: Visual Studio 2022 -## temporary until 6.0.300 sdk is installed -install: - - 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() - - ps: (New-Object System.Net.WebClient).DownloadFile($urlCurrent, $tempFileCurrent) - - ps: Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory($tempFileCurrent, $env:DOTNET_INSTALL_DIR) - - ps: $env:Path = "$env:DOTNET_INSTALL_DIR;$env:Path" -nuget: - disable_publish_on_pr: true - -build_script: - - ps: dotnet restore CronScheduler.sln -v quiet - - ps: dotnet build CronScheduler.sln /p:configuration=Release /p:Version=$($env:appveyor_build_version) - -#test: off -test_script: - - dotnet test test/CronScheduler.UnitTest/CronScheduler.UnitTest.csproj - -artifacts: -- path: .\src\**\*.nupkg - name: NuGet package - -deploy: -- provider: NuGet - artifact: /NuGet/ - api_key: - secure: a8sCawSwgb2kYDJAN+xTUvy+MH5jdJR+DmKakUmc/Xom1c+uxyvV+yvpSTJs+ypF - on: - branch: master diff --git a/build/dependencies.props b/build/dependencies.props index 15e7e16..4204f61 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,14 +1,14 @@ - [6.*, ) - [6.*, ) + [8.*, ) + [8.*, ) $(AspNetCoreVersion) [4.*, ) - + @@ -53,7 +53,7 @@ - + diff --git a/build/settings.props b/build/settings.props index df2241c..467ebfb 100644 --- a/build/settings.props +++ b/build/settings.props @@ -1,10 +1,10 @@ - 3.1.0-preview1 + 3.1.0-preview1 false true - $(NoWarn);CS1591;NU1605; + $(NoWarn);CS1591;NU1605;NU5104 diff --git a/clean.sh b/clean.sh old mode 100644 new mode 100755 index 3a0b990..f487910 --- a/clean.sh +++ b/clean.sh @@ -1,3 +1,5 @@ +# enable script +# chmod +x clean.sh #!/bin/bash find . -name 'obj' -type d -exec rm -rv {} + ; find . -name 'bin' -type d -exec rm -rv {} + ; diff --git a/docker-compose.dcproj b/docker-compose.dcproj deleted file mode 100644 index f57082e..0000000 --- a/docker-compose.dcproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 2.1 - Linux - cfb990f3-58b7-4c9a-af62-cc13454162b2 - LaunchBrowser - {Scheme}://localhost:{ServicePort} - cronscheduler.app - - - - docker-compose.yml - - - - - \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml deleted file mode 100644 index e3f7a5f..0000000 --- a/docker-compose.override.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3.4' - -services: - cronscheduler.app: - environment: - - ASPNETCORE_ENVIRONMENT=Development - - ASPNETCORE_URLS=https://+:443;http://+:80 - - ASPNETCORE_HTTPS_PORT=44369 - - ConnectionStrings:DefaultConnection=Server=cronscheduler.sql;Database=CronSchedulerApp;User Id=sa;Password=Pass@word - ports: - - "57989:80" - - "44369:443" - volumes: - - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro - - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro - - cronscheduler.sql: - ports: - - "5467:1433" - environment: - - SA_PASSWORD=Pass@word - - ACCEPT_EULA=Y diff --git a/docker-compose.vscode.yml b/docker-compose.vscode.yml deleted file mode 100644 index 0d03783..0000000 --- a/docker-compose.vscode.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: '3' - -services: - CronSchedulerApp: - image: kdcllc/dotnet:3.0-sdk-vscode-bionic - # Azure Key Vault Authentication global tool - # dotnet tool install --global appauthentication --version 1.2.2 - # run this tool first then check the configuration - # docker-compose -f ./docker-compose.vscode.yml config - environment: - - MSI_ENDPOINT=${MSI_ENDPOINT} - - MSI_SECRET=${MSI_SECRET} - volumes: - - ..:/workspace - - ~/.ssh:/root/.ssh-localhost - # Forwards the local Docker socket to the container. - - /var/run/docker.sock:/var/run/docker.sock - # kubernetes - - $HOME/.kube:/root/.kube-localhost - # Overrides default command so things don't shut down after the process ends. - command: sleep infinity - diff --git a/docker-compose.yml b/docker-compose.yml index 607afaf..761823a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,12 +6,22 @@ services: build: context: . dockerfile: src/CronSchedulerApp/Dockerfile - args: - RUNTESTS: "false" - VERBOSE: "false" - #NUGET_RESTORE: "-f" #overides the --disable-parallel - PROJECT_PATH: "/src/CronSchedulerApp/CronSchedulerApp.csproj" - SOLUTION_BASE: "false" + environment: + - ASPNETCORE_ENVIRONMENT=Development + - ASPNETCORE_URLS=https://+:443;http://+:80 + - ASPNETCORE_HTTPS_PORT=44369 + - ConnectionStrings:DefaultConnection=Server=cronscheduler.sql;Database=CronSchedulerApp;User Id=sa;Password=Pass@word + ports: + - "57989:80" + - "44369:443" + volumes: + - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro + - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro cronscheduler.sql: image: microsoft/mssql-server-linux:2017-latest + ports: + - "5467:1433" + environment: + - SA_PASSWORD=Pass@word + - ACCEPT_EULA=Y diff --git a/img/IStandWithIsrael.png b/img/IStandWithIsrael.png new file mode 100644 index 0000000..9389efb Binary files /dev/null and b/img/IStandWithIsrael.png differ diff --git a/src/CronScheduler.AspNetCore/CronScheduler.AspNetCore.csproj b/src/CronScheduler.AspNetCore/CronScheduler.AspNetCore.csproj index bf2797d..d4607b5 100644 --- a/src/CronScheduler.AspNetCore/CronScheduler.AspNetCore.csproj +++ b/src/CronScheduler.AspNetCore/CronScheduler.AspNetCore.csproj @@ -1,12 +1,12 @@  - netstandard2.0;net6.0 + netstandard2.0;net8.0 - 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. + A lightweight Cron-based Scheduler for AspNetCore applications, designed for Kubernetes/Docker environments. + An efficient alternative to Quartz Scheduler and HangFire. diff --git a/src/CronScheduler.AspNetCore/DependencyInjection/StartupJobWebHostExtensions.cs b/src/CronScheduler.AspNetCore/DependencyInjection/StartupJobWebHostExtensions.cs index 333ba89..21e73c2 100644 --- a/src/CronScheduler.AspNetCore/DependencyInjection/StartupJobWebHostExtensions.cs +++ b/src/CronScheduler.AspNetCore/DependencyInjection/StartupJobWebHostExtensions.cs @@ -15,7 +15,7 @@ public static class StartupJobWebHostExtensions /// /// /// - public static async Task RunStartupJobsAync(this IWebHost host, CancellationToken cancellationToken = default) + public static async Task RunStartupJobsAsync(this IWebHost host, CancellationToken cancellationToken = default) { using var scope = host.Services.CreateScope(); var jobInitializer = scope.ServiceProvider.GetRequiredService(); diff --git a/src/CronScheduler.Extensions/CronScheduler.Extensions.csproj b/src/CronScheduler.Extensions/CronScheduler.Extensions.csproj index 2099247..c3f620f 100644 --- a/src/CronScheduler.Extensions/CronScheduler.Extensions.csproj +++ b/src/CronScheduler.Extensions/CronScheduler.Extensions.csproj @@ -1,12 +1,12 @@  - netstandard2.0;net6.0 + netstandard2.0;net8.0 - 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. + A powerful Cron-based Scheduler for .NET Core applications, optimized for Kubernetes/Docker and Windows Services. + A lightweight alternative to Quartz Scheduler and HangFire. diff --git a/src/CronScheduler.Extensions/DependencyInjection/SchedulerBuilder.cs b/src/CronScheduler.Extensions/DependencyInjection/SchedulerBuilder.cs index 0e6e7d7..84157d2 100644 --- a/src/CronScheduler.Extensions/DependencyInjection/SchedulerBuilder.cs +++ b/src/CronScheduler.Extensions/DependencyInjection/SchedulerBuilder.cs @@ -10,13 +10,6 @@ namespace Microsoft.Extensions.DependencyInjection { public class SchedulerBuilder { -#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 - /// /// Initializes a new instance of the class. /// @@ -178,14 +171,15 @@ private void AddJobOptions( string name) where TJobOptions : SchedulerOptions, new() { // named options used within the job. - Services - .AddChangeTokenOptions( - $"{sectionName}:{name}", - name, - o => + Services.AddOptions(name) + .Configure((options, configuration) => { - configure?.Invoke(o); - o.JobName = name; + configure?.Invoke(options); + options.JobName = name; + + // print all of the configurations + // configuration.AsEnumerable().ToList().ForEach(c => Console.WriteLine($"{c.Key} : {c.Value}")); + configuration.Bind($"{sectionName}:{name}", options); }); var so = new Action( diff --git a/src/CronScheduler.Extensions/DependencyInjection/SchedulerServiceCollectionExtensions.cs b/src/CronScheduler.Extensions/DependencyInjection/SchedulerServiceCollectionExtensions.cs index 68d9534..64032d8 100644 --- a/src/CronScheduler.Extensions/DependencyInjection/SchedulerServiceCollectionExtensions.cs +++ b/src/CronScheduler.Extensions/DependencyInjection/SchedulerServiceCollectionExtensions.cs @@ -67,7 +67,7 @@ public static IServiceCollection AddScheduler(this IServiceCollection services, var builder = new SchedulerBuilder(services); config(builder); - CreateInstance(builder.Services, sp => builder.UnobservedTaskExceptionHandler ?? builder.CreateUnobservedTaskExceptionHandler?.Invoke(sp)); + CreateInstance(builder.Services, sp => builder.CreateUnobservedTaskExceptionHandler?.Invoke(sp)); return builder.Services; } diff --git a/src/CronScheduler.Extensions/Internal/SchedulerRegistration.cs b/src/CronScheduler.Extensions/Internal/SchedulerRegistration.cs index 0a4faaf..0296af9 100644 --- a/src/CronScheduler.Extensions/Internal/SchedulerRegistration.cs +++ b/src/CronScheduler.Extensions/Internal/SchedulerRegistration.cs @@ -33,10 +33,10 @@ public SchedulerRegistration( _optionsMonitor.OnChange((o, n) => { - if (_jobs.TryGetValue(n, out var job) - && _wrappedJobs.ContainsKey(n)) + if (!string.IsNullOrEmpty(n) && _jobs.TryGetValue(n!, out var job) + && _wrappedJobs.ContainsKey(n!)) { - AddJob(n, job, o); + AddJob(n!, job, o); } }); } diff --git a/src/CronScheduler.Extensions/StartupInitializer/StartupJobHostExtensions.cs b/src/CronScheduler.Extensions/StartupInitializer/StartupJobHostExtensions.cs index ea4903a..ebbb586 100644 --- a/src/CronScheduler.Extensions/StartupInitializer/StartupJobHostExtensions.cs +++ b/src/CronScheduler.Extensions/StartupInitializer/StartupJobHostExtensions.cs @@ -15,12 +15,10 @@ public static class StartupJobHostExtensions /// /// /// - public static async Task RunStartupJobsAync(this IHost host, CancellationToken cancellationToken = default) + public static async Task RunStartupJobsAsync(this IHost 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/CronSchedulerApp/Controllers/HomeController.cs b/src/CronSchedulerApp/Controllers/HomeController.cs index b5dec77..332b9be 100644 --- a/src/CronSchedulerApp/Controllers/HomeController.cs +++ b/src/CronSchedulerApp/Controllers/HomeController.cs @@ -14,138 +14,137 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -namespace CronSchedulerApp.Controllers +namespace CronSchedulerApp.Controllers; + +public class HomeController : Controller { - public class HomeController : Controller + private readonly IBackgroundTaskQueue _taskQueue; + private readonly ILogger _logger; + private readonly TorahVerses _torahVerses; + private readonly ISchedulerRegistration _schedulerRegistration; + private readonly ILoggerFactory _loggerFactory; + + public HomeController( + IBackgroundTaskQueue taskQueue, + ILogger logger, + ILoggerFactory loggerFactory, + ISchedulerRegistration schedulerRegistration, + TorahVerses torahVerses) { - private readonly IBackgroundTaskQueue _taskQueue; - private readonly ILogger _logger; - private readonly TorahVerses _torahVerses; - private readonly ISchedulerRegistration _schedulerRegistration; - private readonly ILoggerFactory _loggerFactory; - - public HomeController( - IBackgroundTaskQueue taskQueue, - ILogger logger, - ILoggerFactory loggerFactory, - ISchedulerRegistration schedulerRegistration, - TorahVerses torahVerses) - { - _taskQueue = taskQueue; - _logger = logger; - _torahVerses = torahVerses; - _schedulerRegistration = schedulerRegistration; - _loggerFactory = loggerFactory; - } + _taskQueue = taskQueue; + _logger = logger; + _torahVerses = torahVerses; + _schedulerRegistration = schedulerRegistration; + _loggerFactory = loggerFactory; + } - public IActionResult Index() + public IActionResult Index() + { + var jobOptions = new SchedulerOptions { - var jobOptions = new SchedulerOptions - { - CronSchedule = "0/1 * * * * *", - RunImmediately = true - }; + CronSchedule = "0/1 * * * * *", + RunImmediately = true + }; - _schedulerRegistration.AddOrUpdate(new TestJob(jobOptions, _loggerFactory.CreateLogger()), jobOptions); + _schedulerRegistration.AddOrUpdate(new TestJob(jobOptions, _loggerFactory.CreateLogger()), jobOptions); - if (_torahVerses.Current != null) - { - var text = _torahVerses.Current.Select(x => x.Text).Aggregate((i, j) => i + Environment.NewLine + j); - var bookName = _torahVerses.Current.Select(x => x.Bookname).Distinct().FirstOrDefault(); - var chapter = _torahVerses.Current.Select(x => x.Chapter).Distinct().FirstOrDefault(); - var versesArray = _torahVerses.Current.Select(x => x.Verse).Aggregate((i, j) => $"{i};{j}").Split(';'); - - var verses = string.Empty; + if (_torahVerses?.Current.Count > 0) + { + var text = _torahVerses.Current.Select(x => x.Text).Aggregate((i, j) => i + Environment.NewLine + j); + var bookName = _torahVerses.Current.Select(x => x.Bookname).Distinct().FirstOrDefault(); + var chapter = _torahVerses.Current.Select(x => x.Chapter).Distinct().FirstOrDefault(); + var versesArray = _torahVerses.Current.Select(x => x.Verse).Aggregate((i, j) => $"{i};{j}").Split(';'); - if (versesArray.Length > 1) - { - verses = $"{versesArray.FirstOrDefault()}-{versesArray.Reverse().FirstOrDefault()}"; - } - else - { - verses = versesArray.FirstOrDefault(); - } + var verses = string.Empty; - ViewBag.Text = text; - ViewBag.BookName = bookName; - ViewBag.Chapter = chapter; - ViewBag.Verses = verses; - ViewBag.Url = $"https://studybible.info/KJV_Strongs/{Uri.EscapeDataString($"{bookName} {chapter}:{verses}")}"; + if (versesArray.Length > 1) + { + verses = $"{versesArray.FirstOrDefault()}-{versesArray.Reverse().FirstOrDefault()}"; + } + else + { + verses = versesArray.FirstOrDefault(); } - return View(); + ViewBag.Text = text; + ViewBag.BookName = bookName; + ViewBag.Chapter = chapter; + ViewBag.Verses = verses; + ViewBag.Url = $"https://studybible.info/KJV_Strongs/{Uri.EscapeDataString($"{bookName} {chapter}:{verses}")}"; } - 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(); + } - return View(); - } + public IActionResult About() + { + ViewData["Message"] = "Your application description page."; + ViewData["winTimeZone"] = TimeZoneInfo.FindSystemTimeZoneById("AUS Eastern Standard Time"); + ViewData["linuxTimeZone"] = TimeZoneInfo.FindSystemTimeZoneById("Australia/Sydney"); - public IActionResult Contact() - { - ViewData["Message"] = "Your contact page."; + return View(); + } - return View(); - } + public IActionResult Contact() + { + ViewData["Message"] = "Your contact page."; - public IActionResult Privacy() - { - return View(); - } + return View(); + } - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] - public IActionResult Error() - { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); - } + public IActionResult Privacy() + { + return View(); + } - public IActionResult Queue() - { - ViewData["Message"] = "Background Queue Hosted Service Test"; - var processId = $" Queued-Task-{Guid.NewGuid().ToString()} "; + [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + public IActionResult Error() + { + return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } - ViewData["Process"] = processId; + public IActionResult Queue() + { + ViewData["Message"] = "Background Queue Hosted Service Test"; + var processId = $" Queued-Task-{Guid.NewGuid().ToString()} "; - _taskQueue.QueueBackgroundWorkItem( - async (token) => - { - token.Register(() => - { - _logger.LogInformation("{Task} canceled.", processId); - }); - - token.ThrowIfCancellationRequested(); - - var guid = Guid.NewGuid().ToString(); - - var repeat = 4; - var idx = 0; - - for (var delayLoop = 0; delayLoop < repeat; delayLoop++) - { - ++idx; - - // if (idx == new Random().Next(0, repeat)) - // { - // throw new AggregateException("Something went wrong"); - // } - _logger.LogInformation($"Queued Background Task {guid} is running. {delayLoop}/{idx}"); - await Task.Delay(TimeSpan.FromSeconds(10), token); - } - - _logger.LogInformation($"Queued Background Task {guid} is complete. {repeat}/{idx}"); - }, - processId, - ex => + ViewData["Process"] = processId; + + _taskQueue.QueueBackgroundWorkItem( + async (token) => + { + token.Register(() => { - _logger.LogError(ex.ToString()); + _logger.LogInformation("{Task} canceled.", processId); }); - return View(); - } + token.ThrowIfCancellationRequested(); + + var guid = Guid.NewGuid().ToString(); + + var repeat = 4; + var idx = 0; + + for (var delayLoop = 0; delayLoop < repeat; delayLoop++) + { + ++idx; + + // if (idx == new Random().Next(0, repeat)) + // { + // throw new AggregateException("Something went wrong"); + // } + _logger.LogInformation($"Queued Background Task {guid} is running. {delayLoop}/{idx}"); + await Task.Delay(TimeSpan.FromSeconds(10), token); + } + + _logger.LogInformation($"Queued Background Task {guid} is complete. {repeat}/{idx}"); + }, + processId, + ex => + { + _logger.LogError(ex.ToString()); + }); + + return View(); } } diff --git a/src/CronSchedulerApp/CronSchedulerApp.csproj b/src/CronSchedulerApp/CronSchedulerApp.csproj index a867b6c..b4385c6 100644 --- a/src/CronSchedulerApp/CronSchedulerApp.csproj +++ b/src/CronSchedulerApp/CronSchedulerApp.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 b790ca5d-c09d-4e9c-af99-c7b8c6b6210a $(AspNetCoreHostingModel) Linux diff --git a/src/CronSchedulerApp/Dockerfile b/src/CronSchedulerApp/Dockerfile index 2221952..31a1842 100644 --- a/src/CronSchedulerApp/Dockerfile +++ b/src/CronSchedulerApp/Dockerfile @@ -2,12 +2,12 @@ # 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 -FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["build/", "build/"] diff --git a/src/CronSchedulerApp/Program.cs b/src/CronSchedulerApp/Program.cs index ef56729..2a3cf48 100644 --- a/src/CronSchedulerApp/Program.cs +++ b/src/CronSchedulerApp/Program.cs @@ -1,72 +1,103 @@ -using System; -using System.Threading.Tasks; - +using CronSchedulerApp.Data; +using CronSchedulerApp.Jobs; using CronSchedulerApp.Jobs.Startup; - -using Microsoft.AspNetCore; +using CronSchedulerApp.Services; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Polly; -namespace CronSchedulerApp; +#pragma warning disable SA1516 // ElementsMustBeSeparatedByBlankLine +var builder = WebApplication.CreateBuilder(args); -public sealed class Program +// Add services to the container. +builder.Services.Configure(options => { - 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(); + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = SameSiteMode.None; +}); - await host.RunStartupJobsAync(); - - await host.RunAsync(); +builder.Services.AddDbContext(options => +{ + if (builder.Configuration["DatabaseProvider:Type"] == "Sqlite") + { + options.UseSqlite(builder.Configuration.GetConnectionString("SqliteConnection")); } - public static IHostBuilder CreateHostBuilder(string[] args) + if (builder.Configuration["DatabaseProvider:Type"] == "SqlServer") { - return Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - - webBuilder.ConfigureServices(services => - { - services.AddStartupJob(); - services.AddStartupJob(); - }); - }) - .ConfigureLogging((context, logger) => - { - logger.AddConsole(); - logger.AddDebug(); - logger.AddConfiguration(context.Configuration.GetSection("Logging")); - }); + options.UseSqlServer(builder.Configuration.GetConnectionString("SqlConnection")); } +}); - /// - /// AspNetCore 2.x syntax of the registration. - /// - /// - /// - public static IWebHostBuilder CreateWebHostBuilder(string[] args) +builder.Services.AddDefaultIdentity() + .AddEntityFrameworkStores(); + +builder.Services.AddControllersWithViews(); +builder.Services.AddRazorPages(); + +builder.Services.AddScheduler(builder => +{ + builder.Services.AddSingleton(); + builder.Services + .AddHttpClient() + .AddTransientHttpErrorPolicy(p => p.RetryAsync()); + + builder.AddJob(); + builder.Services.AddScoped(); + builder.AddJob(); + + builder.AddUnobservedTaskExceptionHandler(sp => { - 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(); - } + var logger = sp.GetRequiredService().CreateLogger("CronJobs"); + return (sender, args) => + { + logger?.LogError(args.Exception?.Message); + args.SetObserved(); + }; + }); +}); + +builder.Services.AddBackgroundQueuedService(applicationOnStopWaitForTasksToComplete: true); +builder.Services.AddDatabaseDeveloperPageExceptionFilter(); + +builder.Services.AddStartupJob(); +builder.Services.AddStartupJob(); + +builder.Logging.AddConsole(); +builder.Logging.AddDebug(); +builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseDeveloperExceptionPage(); + app.UseMigrationsEndPoint(); } +else +{ + app.UseExceptionHandler("/Home/Error"); + app.UseHsts(); +} + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseCookiePolicy(); +app.UseAuthentication(); +app.UseRouting(); + +app.MapControllers(); +app.MapDefaultControllerRoute(); +app.MapRazorPages(); + +await app.RunStartupJobsAsync(); +await app.RunAsync(); +#pragma warning restore SA1516 // ElementsMustBeSeparatedByBlankLine diff --git a/src/CronSchedulerApp/README.md b/src/CronSchedulerApp/README.md index 4110db1..915fd5a 100644 --- a/src/CronSchedulerApp/README.md +++ b/src/CronSchedulerApp/README.md @@ -1,7 +1,172 @@ # CronSchedulerApp +![master workflow](https://github.com/github/docs/actions/workflows/master.yml/badge.svg) +[![NuGet](https://img.shields.io/nuget/v/CronScheduler.AspNetCore.svg)](https://www.nuget.org/packages?q=CronScheduler.AspNetCore) +![Nuget](https://img.shields.io/nuget/dt/CronScheduler.AspNetCore) +[![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/shield/CronScheduler.AspNetCore/latest)](https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/packages/CronScheduler.AspNetCore/latest/download) -## Installing Redis Server +*Note: Pre-release packages are distributed via [feedz.io](https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/nuget/index.json).* -- [Installing on Windows with Chocolatey](https://chocolatey.org/packages/redis-64/) -- [Installing on WS2L]() +This ASP.NET Core web application demonstrates various scheduled jobs and how to use them. It includes examples of background tasks, startup jobs, and scheduled jobs using the CronScheduler library. + +![I Stand With Israel](../../img/IStandWithIsrael.png) + +[![buymeacoffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/vyve0og) + +## Give a Star! :star: + +If you like or are using this project to learn or start your solution, please give it a star. Thanks! + +## Using Scheduled Jobs in ASP.NET Core Web Application + +This application demonstrates how to use scheduled jobs in an ASP.NET Core web application. The application leverages the CronScheduler library to manage and execute scheduled tasks. Below are the steps to add and configure scheduled jobs: + +### Job Details + +#### SeedDatabaseStartupJob + +This job demonstrates how to use EF context to seed the database before any request is served. It ensures that the database exists and creates 5 users. + +#### UserJob + +This job demonstrates how to add a claim to each user in the database. It retrieves users from the database and adds a claim with the current UTC time. + +#### TestJob + +This job is a simple test job that logs its execution. It will be removed in the next release. + +#### TorahQuoteJob + +This job retrieves a random verse from the Torah and updates the current verses in the `TorahVerses` service. + +### Adding a Scheduled Job + +1. **Create a Job Class**: Implement the `IScheduledJob` interface in your job class. For example: + + ```csharp + using System; + using System.Threading; + using System.Threading.Tasks; + using CronScheduler.Extensions.Scheduler; + using Microsoft.Extensions.Logging; + + public class MyScheduledJob : IScheduledJob + { + private readonly ILogger _logger; + + public MyScheduledJob(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Name { get; } = nameof(MyScheduledJob); + + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Executing scheduled job: {jobName}", Name); + // Your job logic here + await Task.CompletedTask; + } + } + ``` + +2. **Configure Job Options**: Create a class that inherits from `SchedulerOptions` to define job-specific options. + + ```csharp + using CronScheduler.Extensions.Scheduler; + + public class MyScheduledJobOptions : SchedulerOptions + { + public string SomeOption { get; set; } = string.Empty; + } + ``` + +3. **Register the Job**: In your `Program.cs` or `Startup.cs`, register the job and its options with the scheduler. Additionally, add a custom error processing for internal errors using an unobserved task exception handler. + + ```csharp + services.AddScheduler(builder => + { + builder.AddJob(); + }); + + // Add 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(); + }; + }); + ``` + +### Adding Jobs on the Fly + +Jobs can also be added on the fly and configured from a database class where the options are stored using the following syntax: + +```csharp +var jobOptions = await _dbContext.JobOptions.FindAsync("TestJob"); +_schedulerRegistration.AddOrUpdate(new TestJob(jobOptions, _loggerFactory.CreateLogger()), jobOptions); +``` + +### Sample Job Options + +Below are sample options for configuring the jobs in the `appsettings.json` file: + +```json +{ + "SchedulerJobs": { + "TorahQuoteJob": { + "RunImmediately": true, + "ApiUrl": "http://labs.bible.org/api/", + "WebsiteUrl": "https://studybible.info/KJV_Strongs", + "CronSchedule": "0/10 * * * * *", + "Verses": [ + "Genesis 1:1-3", + "Exodus 3:14-15", + "Isaiah 53", + "Isaiah 26:18", + "Proverbs 14:23", + "Daniel 12:1-12" + ] + }, + "UserJob": { + "RunImmediately": true, + "CronSchedule": "2 9 * * *", + "ClaimName": "TestClaim" + } + } +} +``` + +### Running the Application + +#### Docker Container + +1. **Build the Docker Image**: Use the following command to build the Docker image. + + ```bash + docker build --pull --rm -f "src/CronSchedulerApp/Dockerfile" -t cronscheduler:latest . + ``` + +2. **Run the Docker Container**: Use the following command to run the Docker container. + + ```bash + docker run --rm -d -p 4443:443/tcp -p 8080:80/tcp cronscheduler:latest + ``` + +#### Run `dotnet` + +1. **Build and Run**: Build and run your application using the following commands. The scheduled jobs will be executed based on their configured schedules. + + ```bash + # Navigate to the project directory + cd src/CronSchedulerApp + + # Build the project + dotnet build + + # Run the project + dotnet run + ``` diff --git a/src/CronSchedulerApp/Services/TorahService.cs b/src/CronSchedulerApp/Services/TorahService.cs index d9800d7..fd9f12d 100644 --- a/src/CronSchedulerApp/Services/TorahService.cs +++ b/src/CronSchedulerApp/Services/TorahService.cs @@ -50,7 +50,7 @@ public TorahService( /// /// Returns verses from the quotation. - /// Utilizes QqueryHelpers: https://rehansaeed.com/asp-net-core-hidden-gem-queryhelpers/. + /// Utilizes QueryHelpers: https://rehansaeed.com/asp-net-core-hidden-gem-queryhelpers/. /// /// /// @@ -58,7 +58,7 @@ public TorahService( public async Task> GetVersesAsync(string exp, CancellationToken cancellationToken) { // create query parameters - var args = new Dictionary + var args = new Dictionary { { "type", "json" }, { "passage", Uri.EscapeDataString(exp) } diff --git a/src/CronSchedulerApp/Services/UserService.cs b/src/CronSchedulerApp/Services/UserService.cs index 32bc36d..c429b2a 100644 --- a/src/CronSchedulerApp/Services/UserService.cs +++ b/src/CronSchedulerApp/Services/UserService.cs @@ -26,7 +26,7 @@ public UserService( _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); } - public async Task GetUserByEmail(string email) + public async Task GetUserByEmail(string email) { return await _userManager.FindByEmailAsync(email); } diff --git a/src/CronSchedulerApp/Startup.cs b/src/CronSchedulerApp/Startup.cs deleted file mode 100644 index dc7cc63..0000000 --- a/src/CronSchedulerApp/Startup.cs +++ /dev/null @@ -1,123 +0,0 @@ -using CronSchedulerApp.Data; -using CronSchedulerApp.Jobs; -using CronSchedulerApp.Services; - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; - -using Polly; - -namespace CronSchedulerApp; - -public class Startup -{ - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - 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; - }); - - services.AddDbContext(options => - { - if (Configuration["DatabaseProvider:Type"] == "Sqlite") - { - options.UseSqlite(Configuration.GetConnectionString("SqliteConnection")); - } - - if (Configuration["DatabaseProvider:Type"] == "SqlServer") - { - options.UseSqlServer(Configuration.GetConnectionString("SqlConnection")); - } - }); - - services.AddDefaultIdentity() - .AddEntityFrameworkStores(); - - services.AddControllersWithViews(); - - services.AddRazorPages(); - - 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()); - - builder.AddJob(); - - // 2. Add User Service and Job - builder.Services.AddScoped(); - builder.AddJob(); - - // 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.AddBackgroundQueuedService(applicationOnStopWaitForTasksToComplete: true); - - services.AddDatabaseDeveloperPageExceptionFilter(); - } - - // 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.UseHttpsRedirection(); - app.UseStaticFiles(); - app.UseCookiePolicy(); - - app.UseAuthentication(); - - app.UseRouting(); - - // https://devblogs.microsoft.com/aspnet/blazor-now-in-official-preview/ - app.UseEndpoints(routes => - { - routes.MapControllers(); - routes.MapDefaultControllerRoute(); - routes.MapRazorPages(); - }); - } -} diff --git a/src/CronSchedulerApp/app.db b/src/CronSchedulerApp/app.db deleted file mode 100644 index b07dea1..0000000 Binary files a/src/CronSchedulerApp/app.db and /dev/null differ diff --git a/src/CronSchedulerWorker/CronSchedulerWorker.csproj b/src/CronSchedulerWorker/CronSchedulerWorker.csproj index 78c0625..3a08253 100644 --- a/src/CronSchedulerWorker/CronSchedulerWorker.csproj +++ b/src/CronSchedulerWorker/CronSchedulerWorker.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 dotnet-CronScheduler-16DA7ECC-2C2F-42B2-943E-6F8486D62575 false Linux diff --git a/src/CronSchedulerWorker/Dockerfile b/src/CronSchedulerWorker/Dockerfile index 42f8139..7305102 100644 --- a/src/CronSchedulerWorker/Dockerfile +++ b/src/CronSchedulerWorker/Dockerfile @@ -1,9 +1,9 @@ #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 +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base WORKDIR /app -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["build/", "build/"] diff --git a/src/CronSchedulerWorker/Program.cs b/src/CronSchedulerWorker/Program.cs index f667305..7331539 100644 --- a/src/CronSchedulerWorker/Program.cs +++ b/src/CronSchedulerWorker/Program.cs @@ -12,7 +12,7 @@ public static async Task Main(string[] args) { var host = CreateHostBuilder(args).Build(); - await host.RunStartupJobsAync(); + await host.RunStartupJobsAsync(); await host.RunAsync(); } @@ -20,6 +20,7 @@ public static async Task Main(string[] args) public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) + .ConfigureServices(services => { services.AddStartupJob(); diff --git a/src/CronSchedulerWorker/README.md b/src/CronSchedulerWorker/README.md index 53ad85a..5077bde 100644 --- a/src/CronSchedulerWorker/README.md +++ b/src/CronSchedulerWorker/README.md @@ -1,23 +1,149 @@ -# CronScheduler with .NET Core Workers Template +# CronSchedulerWorker -This sample application demonstrate utilization of new .NET Core Worker template and CronScheduler.AspNetCore. +![master workflow](https://github.com/github/docs/actions/workflows/master.yml/badge.svg) +[![NuGet](https://img.shields.io/nuget/v/CronScheduler.AspNetCore.svg)](https://www.nuget.org/packages?q=CronScheduler.AspNetCore) +![Nuget](https://img.shields.io/nuget/dt/CronScheduler.AspNetCore) +[![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/shield/CronScheduler.AspNetCore/latest)](https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/packages/CronScheduler.AspNetCore/latest/download) -## Hosting in Azure Container +*Note: Pre-release packages are distributed via [feedz.io](https://f.feedz.io/kdcllc/cronscheduler-aspnetcore/nuget/index.json).* -[.NET Core Workers in Azure Container Instances](https://devblogs.microsoft.com/aspnet/dotnet-core-workers-in-azure-container-instances) +This .NET Core Worker application demonstrates various scheduled jobs and how to use them. It includes examples of background tasks, startup jobs, and scheduled jobs using the CronScheduler library. -1. Build the Docker Container +![I Stand With Israel](../../img/IStandWithIsrael.png) -```bash - docker build --rm -f "src\CronSchedulerWorker\Dockerfile" -t cronschedulerworker:latest . +[![buymeacoffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/vyve0og) + +## Give a Star! :star: + +If you like or are using this project to learn or start your solution, please give it a star. Thanks! + +## Using Scheduled Jobs in .NET Core Worker Application + +This application demonstrates how to use scheduled jobs in a .NET Core Worker application. The application leverages the CronScheduler library to manage and execute scheduled tasks. Below are the steps to add and configure scheduled jobs: + +### Job Details + +#### TestJob + +This job is a simple test job that logs its execution. + +### Adding a Scheduled Job + +1. **Create a Job Class**: Implement the `IScheduledJob` interface in your job class. For example: + + ```csharp + using System; + using System.Threading; + using System.Threading.Tasks; + using CronScheduler.Extensions.Scheduler; + using Microsoft.Extensions.Logging; + + public class MyScheduledJob : IScheduledJob + { + private readonly ILogger _logger; + + public MyScheduledJob(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Name { get; } = nameof(MyScheduledJob); + + public async Task ExecuteAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Executing scheduled job: {jobName}", Name); + // Your job logic here + await Task.CompletedTask; + } + } + ``` + +2. **Configure Job Options**: Create a class that inherits from `SchedulerOptions` to define job-specific options. + + ```csharp + using CronScheduler.Extensions.Scheduler; + + public class MyScheduledJobOptions : SchedulerOptions + { + public string SomeOption { get; set; } = string.Empty; + } + ``` + +3. **Register the Job**: In your `Program.cs` or `Startup.cs`, register the job and its options with the scheduler. Additionally, add a custom error processing for internal errors using an unobserved task exception handler. + + ```csharp + services.AddScheduler(builder => + { + builder.AddJob(); + }); + + // Add 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(); + }; + }); + ``` + +### Adding Jobs on the Fly + +Jobs can also be added on the fly and configured from a database class where the options are stored using the following syntax: + +```csharp +var jobOptions = await _dbContext.JobOptions.FindAsync("TestJob"); +_schedulerRegistration.AddOrUpdate(new TestJob(jobOptions, _loggerFactory.CreateLogger()), jobOptions); ``` -2. Run the Docker Container +### Sample Job Options -```bash - docker run --rm -it cronschedulerworker:latest +Below are sample options for configuring the jobs in the `appsettings.json` file: + +```json +{ + "SchedulerJobs": { + "TestJob": { + "RunImmediately": true, + "CronSchedule": "0/10 * * * * *" + } + } +} ``` +### Running the Application + +#### Docker Container + +1. **Build the Docker Image**: Use the following command to build the Docker image. + + ```bash + docker build --pull --rm -f "src/CronSchedulerWorker/Dockerfile" -t cronschedulerworker:latest . + ``` + +2. **Run the Docker Container**: Use the following command to run the Docker container. + + ```bash + docker run --rm -d cronschedulerworker:latest + ``` + +#### Run `dotnet` + +1. **Build and Run**: Build and run your application using the following commands. The scheduled jobs will be executed based on their configured schedules. + + ```bash + # Navigate to the project directory + cd src/CronSchedulerWorker + + # Build the project + dotnet build + + # Run the project + dotnet run + ``` + ## Windows Service [.NET Core Workers as Windows Services](https://devblogs.microsoft.com/aspnet/net-core-workers-as-windows-services/) diff --git a/test/CronScheduler.UnitTest/BackgroundQueueTests.cs b/test/CronScheduler.UnitTest/BackgroundQueueTests.cs index cecda5b..bc19ac1 100644 --- a/test/CronScheduler.UnitTest/BackgroundQueueTests.cs +++ b/test/CronScheduler.UnitTest/BackgroundQueueTests.cs @@ -18,10 +18,7 @@ public async Task Dequeue_With_Successful_WorkItemName() var service = new BackgroundTaskQueue(context); service.QueueBackgroundWorkItem( - async token => - { - await Task.CompletedTask; - }, + async token => await Task.CompletedTask, workItemName); var task = await service.DequeueAsync(CancellationToken.None); diff --git a/test/CronScheduler.UnitTest/CronScheduler.UnitTest.csproj b/test/CronScheduler.UnitTest/CronScheduler.UnitTest.csproj index 0094aa6..68a3f57 100644 --- a/test/CronScheduler.UnitTest/CronScheduler.UnitTest.csproj +++ b/test/CronScheduler.UnitTest/CronScheduler.UnitTest.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 false $(NoWarn);SA1402; @@ -21,6 +21,7 @@ + diff --git a/test/CronScheduler.UnitTest/CustomTestJob.cs b/test/CronScheduler.UnitTest/CustomTestJob.cs index dd868b1..339f42f 100644 --- a/test/CronScheduler.UnitTest/CustomTestJob.cs +++ b/test/CronScheduler.UnitTest/CustomTestJob.cs @@ -7,24 +7,16 @@ namespace CronScheduler.UnitTest; -public class CustomTestJob : IScheduledJob +public class CustomTestJob( + CustomTestJobOptions options, + ILogger logger) : IScheduledJob { - private readonly CustomTestJobOptions _options; - private readonly ILogger _logger; - - public CustomTestJob(CustomTestJobOptions options, ILogger logger) - { - _options = options; - _logger = logger; - Name = options.JobName; - } - - public string Name { get; } + public string Name { get; } = options.JobName; public Task ExecuteAsync(CancellationToken cancellationToken) { - _logger.LogInformation("Running {name}", nameof(CustomTestJob)); - _logger.LogInformation(_options.DisplayText); + logger.LogInformation("Running {name}", nameof(CustomTestJob)); + logger.LogInformation(options.DisplayText); return Task.CompletedTask; } diff --git a/test/CronScheduler.UnitTest/SchedulerFuncTests.cs b/test/CronScheduler.UnitTest/SchedulerFuncTests.cs index 50a82da..a4c7213 100644 --- a/test/CronScheduler.UnitTest/SchedulerFuncTests.cs +++ b/test/CronScheduler.UnitTest/SchedulerFuncTests.cs @@ -24,19 +24,19 @@ namespace CronScheduler.UnitTest; -public class SchedulerFuncTests +public class SchedulerFuncTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; - - public SchedulerFuncTests(ITestOutputHelper output) - { - _output = output; - } - [Fact] public async Task Job_RunImmediately_Factory_Successfully() { // assign + using var logFactory = TestLoggerBuilder.Create(builder => + { + builder.AddConsole(); + builder.AddDebug(); + builder.AddXunit(output, LogLevel.Debug); + }); + var mockLoggerTestJob = new Mock>(); var host = CreateHost(services => @@ -72,8 +72,8 @@ public async Task Job_RunImmediately_Factory_Successfully() 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)); + It.Is>((v, t) => true)), + Times.Between(1, 3, Range.Inclusive)); } [Fact] @@ -133,7 +133,7 @@ public async Task Same_Job_RunImmediately_Factory_Two_Different_Options_Successf It.IsAny(), It.Is((object v, Type _) => v.ToString()!.Contains(nameof(TestJob))), It.IsAny(), - It.Is>((v, t) => true)), + It.Is>((v, t) => true)), Times.Between(2, 6, Range.Inclusive)); } @@ -169,8 +169,8 @@ public async Task Job_RunImmediately_Configuration_Successfully() 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)); + It.Is>((v, t) => true)), + Times.Between(1, 3, Range.Inclusive)); } [Fact] @@ -212,7 +212,7 @@ public async Task Job_RunDelayed_Factory_Successfully() It.IsAny(), It.Is((object v, Type _) => v.ToString()!.Contains(nameof(TestJob))), It.IsAny(), - It.Is>((v, t) => true)), + It.Is>((v, t) => true)), Times.Between(1, 2, Range.Inclusive)); } @@ -263,7 +263,7 @@ public async Task Job_RunDelayed_And_Raise_UnobservedTaskException() It.IsAny(), It.Is((object v, Type _) => v.ToString()!.Contains(nameof(Exception))), It.IsAny(), - It.Is>((v, t) => true)), + It.Is>((v, t) => true)), Times.Between(1, 2, Range.Inclusive)); } @@ -313,8 +313,8 @@ public async Task Job_RunDelayed_And_Raise_UnobservedTaskException_Via_Delegate( 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)); + It.Is>((v, t) => true)), + Times.Between(1, 3, Range.Inclusive)); } [Fact] @@ -348,7 +348,7 @@ public async Task Job_RunImmediately_Configuration_And_Raise_Exception() It.IsAny(), It.Is((object v, Type _) => v.ToString()!.Contains(nameof(Exception))), It.IsAny(), - It.Is>((v, t) => true)), + It.Is>((v, t) => true)), Times.Between(1, 4, Range.Inclusive)); } @@ -380,7 +380,7 @@ private IWebHostBuilder CreateHost( .ConfigureTestServices(services => { configServices(services); - services.AddLogging(x => x.AddXunit(_output)); + services.AddLogging(x => x.AddXunit(output)); }) .UseDefaultServiceProvider(options => options.ValidateScopes = validateScopes); } diff --git a/test/CronScheduler.UnitTest/SchedulerRegistrationTests.cs b/test/CronScheduler.UnitTest/SchedulerRegistrationTests.cs index 12ce0a7..4e9cfa9 100644 --- a/test/CronScheduler.UnitTest/SchedulerRegistrationTests.cs +++ b/test/CronScheduler.UnitTest/SchedulerRegistrationTests.cs @@ -17,15 +17,8 @@ namespace CronScheduler.UnitTest; -public class SchedulerRegistrationTests +public class SchedulerRegistrationTests(ITestOutputHelper output) { - private ITestOutputHelper _output; - - public SchedulerRegistrationTests(ITestOutputHelper output) - { - _output = output ?? throw new ArgumentNullException(nameof(output)); - } - [Fact] public async Task Successfully_Register_Two_Jobs_With_The_Same_Type() { @@ -38,7 +31,7 @@ public async Task Successfully_Register_Two_Jobs_With_The_Same_Type() services.AddLogging(builder => { builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); + builder.AddXunit(output, LogLevel.Debug); }); services.AddScheduler(); diff --git a/test/CronScheduler.UnitTest/SchedulerServiceTests.cs b/test/CronScheduler.UnitTest/SchedulerServiceTests.cs index 572e6fe..394b7dd 100644 --- a/test/CronScheduler.UnitTest/SchedulerServiceTests.cs +++ b/test/CronScheduler.UnitTest/SchedulerServiceTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Bet.Extensions.Testing.Logging; @@ -17,19 +16,12 @@ namespace CronScheduler.UnitTest; -public class SchedulerServiceTests +public class SchedulerServiceTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; - - public SchedulerServiceTests(ITestOutputHelper output) - { - _output = output ?? throw new ArgumentNullException(nameof(output)); - } - [Fact] public void Add_Job_Successfully() { - var dic = new Dictionary + var dic = new Dictionary { { "SchedulerJobs:TestJobException:CronSchedule", "*/10 * * * * *" }, { "SchedulerJobs:TestJobException:CronTimeZone", string.Empty }, @@ -56,7 +48,7 @@ public void Add_Job_Successfully() { builder.AddConsole(); builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); + builder.AddXunit(output, LogLevel.Debug); }); services.AddSingleton(); @@ -69,7 +61,7 @@ public void Add_Job_Successfully() { builder.AddConsole(); builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); + builder.AddXunit(output, LogLevel.Debug); }); var job = new TestJob(logFactory.CreateLogger()); @@ -83,7 +75,7 @@ public void Add_Job_Successfully() [Fact] public void Add_Job_Successfully_1() { - var dic = new Dictionary + var dic = new Dictionary { { "SchedulerJobs:TestJobException:CronSchedule", "*/10 * * * * *" }, { "SchedulerJobs:TestJobException:CronTimeZone", string.Empty }, @@ -106,7 +98,7 @@ public void Add_Job_Successfully_1() { builder.AddConsole(); builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); + builder.AddXunit(output, LogLevel.Debug); }); service.AddSingleton(); @@ -119,7 +111,7 @@ public void Add_Job_Successfully_1() { builder.AddConsole(); builder.AddDebug(); - builder.AddXunit(_output, LogLevel.Debug); + builder.AddXunit(output, LogLevel.Debug); }); var job = new TestJob(logFactory.CreateLogger()); @@ -127,11 +119,11 @@ public void Add_Job_Successfully_1() instance!.AddOrUpdate(job.GetType().Name, job, options); - Assert.Equal(1, instance.Jobs.Count); + Assert.Single(instance.Jobs); 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 67afa8e..1a811fb 100644 --- a/test/CronScheduler.UnitTest/StartupJobFuncTests.cs +++ b/test/CronScheduler.UnitTest/StartupJobFuncTests.cs @@ -30,7 +30,7 @@ public async Task RunJobs() using (host) { - await host.RunStartupJobsAync(cts.Token); + await host.RunStartupJobsAsync(cts.Token); cts.Cancel(); @@ -47,7 +47,7 @@ public async Task RunDelegate() var host = CreateHost(services => services.AddStartupJobInitializer(CompletedTask)); - await host.RunStartupJobsAync(); + await host.RunStartupJobsAsync(); host.Dispose(); } diff --git a/test/CronScheduler.UnitTest/TestStartup.cs b/test/CronScheduler.UnitTest/TestStartup.cs index 3bf539e..c182659 100644 --- a/test/CronScheduler.UnitTest/TestStartup.cs +++ b/test/CronScheduler.UnitTest/TestStartup.cs @@ -31,10 +31,7 @@ public void Configure( { app.Map("/hc", route => { - route.Run(async context => - { - await context.Response.WriteAsync("healthy"); - }); + route.Run(async context => await context.Response.WriteAsync("healthy")); }); } } diff --git a/test/CronScheduler.UnitTest/TestStartupJob.cs b/test/CronScheduler.UnitTest/TestStartupJob.cs index 6d61011..ab75189 100644 --- a/test/CronScheduler.UnitTest/TestStartupJob.cs +++ b/test/CronScheduler.UnitTest/TestStartupJob.cs @@ -10,7 +10,7 @@ public class TestStartupJob : IStartupJob { public async Task ExecuteAsync(CancellationToken cancellationToken = default) { - await Task.Delay(TimeSpan.FromSeconds(10)); + await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); await Task.CompletedTask; }