├── .gitattributes ├── .gitignore ├── .vscode └── extensions.json ├── Hangfire.RecurringJobExtensions.sln ├── LICENSE ├── README.md ├── appveyor.yml ├── samples └── Hangfire.Samples │ ├── Hangfire.Samples.csproj │ ├── LongRunningJob.cs │ ├── MyJob1.cs │ ├── MyJob2.cs │ ├── Program.cs │ ├── Project_Readme.html │ ├── Properties │ └── launchSettings.json │ ├── RecurringJobService.cs │ ├── Startup.cs │ ├── appsettings.json │ ├── recurringjob.json │ └── web.config ├── src └── Hangfire.RecurringJobExtensions │ ├── Configuration │ ├── FileConfigurationProvider.cs │ ├── IConfigurationProvider.cs │ ├── JsonConfigurationProvider.cs │ └── RecurringJobJsonOptions.cs │ ├── CronJob.cs │ ├── Hangfire.RecurringJobExtensions.csproj │ ├── HangfireExtensions.cs │ ├── IRecurringJob.cs │ ├── IRecurringJobBuilder.cs │ ├── IRecurringJobInfoStorage.cs │ ├── IRecurringJobRegistry.cs │ ├── PerformContextExtensions.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── RecurringJobAttribute.cs │ ├── RecurringJobBuilder.cs │ ├── RecurringJobInfo.cs │ ├── RecurringJobInfoStorage.cs │ ├── RecurringJobRegistry.cs │ ├── TimeZoneInfoConverter.cs │ └── TypeExtensions.cs └── test └── Hangfire.RecurringJobExtensions.Tests ├── Configuration ├── JsonConfigurationProviderTest.cs └── RecurringJobJsonOptionsTest.cs ├── Hangfire.RecurringJobExtensions.Tests.csproj ├── MyJob1.cs ├── MyJob2.cs ├── Properties └── AssemblyInfo.cs ├── RecurringJobAttributeTest.cs ├── error.json ├── job1.json └── job2.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 6 | 7 | ] 8 | } -------------------------------------------------------------------------------- /Hangfire.RecurringJobExtensions.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6E0E1A07-7311-4F8C-B1BD-B3803B6E621E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7A9F6A71-9A61-4817-AE86-9254999FA206}" 9 | ProjectSection(SolutionItems) = preProject 10 | appveyor.yml = appveyor.yml 11 | README.md = README.md 12 | EndProjectSection 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B04B5FCE-89A8-4A3E-9671-CB3070C89AAF}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{F5091E02-C3A4-407E-995C-86E07A801832}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.RecurringJobExtensions", "src\Hangfire.RecurringJobExtensions\Hangfire.RecurringJobExtensions.csproj", "{D75DFE5D-38CF-4150-8264-EB22E579237D}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.Samples", "samples\Hangfire.Samples\Hangfire.Samples.csproj", "{9DCBF5BA-414C-4E39-B264-CFD6C824AA66}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.RecurringJobExtensions.Tests", "test\Hangfire.RecurringJobExtensions.Tests\Hangfire.RecurringJobExtensions.Tests.csproj", "{B1AA8C91-161A-4A92-9B0A-B47742CDDB38}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {D75DFE5D-38CF-4150-8264-EB22E579237D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {D75DFE5D-38CF-4150-8264-EB22E579237D}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {D75DFE5D-38CF-4150-8264-EB22E579237D}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {D75DFE5D-38CF-4150-8264-EB22E579237D}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {9DCBF5BA-414C-4E39-B264-CFD6C824AA66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {9DCBF5BA-414C-4E39-B264-CFD6C824AA66}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {9DCBF5BA-414C-4E39-B264-CFD6C824AA66}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {9DCBF5BA-414C-4E39-B264-CFD6C824AA66}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {B1AA8C91-161A-4A92-9B0A-B47742CDDB38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {B1AA8C91-161A-4A92-9B0A-B47742CDDB38}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {B1AA8C91-161A-4A92-9B0A-B47742CDDB38}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {B1AA8C91-161A-4A92-9B0A-B47742CDDB38}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(NestedProjects) = preSolution 47 | {D75DFE5D-38CF-4150-8264-EB22E579237D} = {6E0E1A07-7311-4F8C-B1BD-B3803B6E621E} 48 | {9DCBF5BA-414C-4E39-B264-CFD6C824AA66} = {B04B5FCE-89A8-4A3E-9671-CB3070C89AAF} 49 | {B1AA8C91-161A-4A92-9B0A-B47742CDDB38} = {F5091E02-C3A4-407E-995C-86E07A801832} 50 | EndGlobalSection 51 | EndGlobal 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 xuxi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hangfire.RecurringJobExtensions 2 | 3 | [![Official Site](https://img.shields.io/badge/site-hangfire.io-blue.svg)](http://hangfire.io) 4 | [![NuGet](https://buildstats.info/nuget/Hangfire.RecurringJobExtensions)](https://www.nuget.org/packages/Hangfire.RecurringJobExtensions/) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/i02yxvu0mvhyv5nk?svg=true)](https://ci.appveyor.com/project/icsharp/hangfire-recurringjobextensions) 6 | [![License MIT](https://img.shields.io/badge/license-MIT-green.svg)](http://opensource.org/licenses/MIT) 7 | 8 | This repo is the extension for [Hangfire](https://github.com/HangfireIO/Hangfire) to build `RecurringJob` automatically. 9 | When app start, `RecurringJob` will be added/updated automatically. 10 | There is two ways to build `RecurringJob`. 11 | 12 | - `RecurringJobAttribute` attribute 13 | - Json Configuration 14 | 15 | ## Using RecurringJobAttribute 16 | 17 | We can use the attribute `RecurringJobAttribute` to assign the interface/instance/static method. 18 | 19 | 20 | ```csharp 21 | public class RecurringJobService 22 | { 23 | [RecurringJob("*/1 * * * *")] 24 | [Queue("jobs")] 25 | public void TestJob1(PerformContext context) 26 | { 27 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} TestJob1 Running ..."); 28 | } 29 | [RecurringJob("*/2 * * * *", RecurringJobId = "TestJob2")] 30 | [Queue("jobs")] 31 | public void TestJob2(PerformContext context) 32 | { 33 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} TestJob2 Running ..."); 34 | } 35 | [RecurringJob("*/2 * * * *", "China Standard Time", "jobs")] 36 | public void TestJob3(PerformContext context) 37 | { 38 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} TestJob3 Running ..."); 39 | } 40 | [RecurringJob("*/5 * * * *", "jobs")] 41 | public void InstanceTestJob(PerformContext context) 42 | { 43 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} InstanceTestJob Running ..."); 44 | } 45 | 46 | [RecurringJob("*/6 * * * *", "UTC", "jobs")] 47 | public static void StaticTestJob(PerformContext context) 48 | { 49 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} StaticTestJob Running ..."); 50 | } 51 | } 52 | ``` 53 | 54 | ## Json Configuration 55 | 56 | It is similar to [quartz.net](http://www.quartz-scheduler.net/), We also define the unified interface `IRecurringJob`. 57 | Recurring jobs must impl the specified interface like this. 58 | 59 | ```csharp 60 | [AutomaticRetry(Attempts = 0)] 61 | [DisableConcurrentExecution(90)] 62 | public class LongRunningJob : IRecurringJob 63 | { 64 | public void Execute(PerformContext context) 65 | { 66 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} LongRunningJob Running ..."); 67 | 68 | var runningTimes = context.GetJobData("RunningTimes"); 69 | 70 | context.WriteLine($"get job data parameter-> RunningTimes: {runningTimes}"); 71 | 72 | var progressBar = context.WriteProgressBar(); 73 | 74 | foreach (var i in Enumerable.Range(1, runningTimes).ToList().WithProgress(progressBar)) 75 | { 76 | Thread.Sleep(1000); 77 | } 78 | } 79 | } 80 | ``` 81 | 82 | Now we need to provider the json config file to assign the implemented recurring job, the json configuration samples as below. 83 | 84 | ```json 85 | [{ 86 | "job-name": "My Job1", 87 | "job-type": "Hangfire.Samples.MyJob1, Hangfire.Samples", 88 | "cron-expression": "*/1 * * * *", 89 | "timezone": "China Standard Time", 90 | "queue": "jobs" 91 | }, 92 | { 93 | "job-name": "My Job2", 94 | "job-type": "Hangfire.Samples.MyJob2, Hangfire.Samples", 95 | "cron-expression": "*/5 * * * *", 96 | "job-data": { 97 | "IntVal": 1, 98 | "StringVal": "abcdef", 99 | "BooleanVal": true, 100 | "SimpleObject": { 101 | "Name": "Foo", 102 | "Age": 100 103 | } 104 | } 105 | }, 106 | { 107 | "job-name": "Long Running Job", 108 | "job-type": "Hangfire.Samples.LongRunningJob, Hangfire.Samples", 109 | "cron-expression": "*/2 * * * *", 110 | "job-data": { 111 | "RunningTimes": 300 112 | } 113 | }] 114 | ``` 115 | 116 | The json token description to the configuration is here. 117 | 118 | JSON Token | Description 119 | ---|--- 120 | **job-name** | *[required]* The job name to `RecurringJob`. 121 | **job-type** | *[required]* The job type while impl the interface `IRecurringJob`. 122 | **cron-expression** | *[required]* Cron expressions. 123 | timezone | *[optional]* Default value is `TimeZoneInfo.Utc`. 124 | queue | *[optional]* The specified queue name , default value is `default`. 125 | job-data | *[optional]* Similar to the [quartz.net](http://www.quartz-scheduler.net/) `JobDataMap`, it is can be deserialized to the type `Dictionary`. 126 | enable | *[optional]* Whether the `RecurringJob` can be added/updated, default value is true, if false `RecurringJob` will be deleted automatically. 127 | 128 | *To the json token `job-data`, we can use extension method to get/set data with specified key from storage which associated with `BackgroundJob.Id` when recurring job running.* 129 | 130 | ```csharp 131 | var intVal = context.GetJobData("IntVal"); 132 | 133 | context.SetJobData("IntVal", ++intVal); 134 | ``` 135 | 136 | ## Building RecurringJob 137 | 138 | - Building with `CronJob`. 139 | 140 | In hangfire client, we can use the helper class `CronJob` to add or update recurringjob automatically. 141 | 142 | ```csharp 143 | //Builds within specified interface or class. 144 | CronJob.AddOrUpdate(typeof(RecurringJobService1),typeof(RecurringJobService2)); 145 | 146 | //Builds by using multiple JSON configuration files. 147 | CronJob.AddOrUpdate("recurringjob1.json","recurringjob2.json"); 148 | ``` 149 | 150 | - Building with `IGlobalConfiguration`. 151 | 152 | Use `IGlobalConfiguration` extension method `UseRecurringJob` to build `RecurringJob`, in .NET Core's Startup.cs. 153 | 154 | ``` csharp 155 | public void ConfigureServices(IServiceCollection services) 156 | { 157 | services.AddHangfire(x => 158 | { 159 | x.UseSqlServerStorage(_config.GetConnectionString("Hangfire")); 160 | 161 | x.UseConsole(); 162 | 163 | //using json config file to build RecurringJob automatically. 164 | x.UseRecurringJob("recurringjob.json"); 165 | //using RecurringJobAttribute to build RecurringJob automatically. 166 | x.UseRecurringJob(typeof(RecurringJobService)); 167 | 168 | x.UseDefaultActivator(); 169 | }); 170 | } 171 | ``` 172 | 173 | *For the json configuration file, we can monitor the file change and reload `RecurringJob` dynamically by passing the parameter `reloadOnChange = true`.* -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.{build} 2 | before_build: 3 | - cmd: dotnet restore 4 | build: 5 | verbosity: minimal 6 | test_script: 7 | - cmd: >- 8 | cd test\Hangfire.RecurringJobExtensions.Tests 9 | 10 | dotnet test -------------------------------------------------------------------------------- /samples/Hangfire.Samples/Hangfire.Samples.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp1.1 5 | true 6 | Hangfire.Samples 7 | Exe 8 | Hangfire.Samples 9 | 1.1.1 10 | $(PackageTargetFallback);dotnet5.6;portable-net45+win8 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /samples/Hangfire.Samples/LongRunningJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using Hangfire.Console; 5 | using Hangfire.RecurringJobExtensions; 6 | using Hangfire.Server; 7 | 8 | namespace Hangfire.Samples 9 | { 10 | [AutomaticRetry(Attempts = 0)] 11 | [DisableConcurrentExecution(90)] 12 | public class LongRunningJob : IRecurringJob 13 | { 14 | public void Execute(PerformContext context) 15 | { 16 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} LongRunningJob Running ..."); 17 | 18 | var runningTimes = context.GetJobData("RunningTimes"); 19 | 20 | context.WriteLine($"get job data parameter-> RunningTimes: {runningTimes}"); 21 | 22 | var progressBar = context.WriteProgressBar(); 23 | 24 | foreach (var i in Enumerable.Range(1, runningTimes).ToList().WithProgress(progressBar)) 25 | { 26 | Thread.Sleep(1000); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /samples/Hangfire.Samples/MyJob1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire.Console; 3 | using Hangfire.RecurringJobExtensions; 4 | using Hangfire.Server; 5 | 6 | namespace Hangfire.Samples 7 | { 8 | public class MyJob1 : IRecurringJob 9 | { 10 | public void Execute(PerformContext context) 11 | { 12 | context.SetJobData("NewIntVal", 99); 13 | 14 | var newIntVal = context.GetJobData("NewIntVal"); 15 | 16 | context.WriteLine($"NewIntVal:{newIntVal}"); 17 | 18 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} MyJob1 Running ..."); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/Hangfire.Samples/MyJob2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire.Common; 3 | using Hangfire.Console; 4 | using Hangfire.RecurringJobExtensions; 5 | using Hangfire.Server; 6 | 7 | namespace Hangfire.Samples 8 | { 9 | public class MyJob2 : IRecurringJob 10 | { 11 | class SimpleObject 12 | { 13 | public string Name { get; set; } 14 | public int Age { get; set; } 15 | } 16 | 17 | public void Execute(PerformContext context) 18 | { 19 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} MyJob2 Running ..."); 20 | 21 | var intVal = context.GetJobData("IntVal"); 22 | 23 | var stringVal = context.GetJobData("StringVal"); 24 | 25 | var booleanVal = context.GetJobData("BooleanVal"); 26 | 27 | var simpleObject = context.GetJobData("SimpleObject"); 28 | 29 | context.WriteLine($"IntVal:{intVal},StringVal:{stringVal},BooleanVal:{booleanVal},simpleObject:{JobHelper.ToJson(simpleObject)}"); 30 | 31 | context.SetJobData("IntVal", ++intVal); 32 | 33 | context.WriteLine($"IntVal changed to {intVal}"); 34 | 35 | context.SetJobData("NewIntVal", 99); 36 | 37 | var newIntVal = context.GetJobData("NewIntVal"); 38 | 39 | context.WriteLine($"NewIntVal:{newIntVal}"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/Hangfire.Samples/Program.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace Hangfire.Samples 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | var host = new WebHostBuilder() 11 | .UseKestrel() 12 | .UseContentRoot(Directory.GetCurrentDirectory()) 13 | .UseIISIntegration() 14 | .UseStartup() 15 | .Build(); 16 | 17 | host.Run(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/Hangfire.Samples/Project_Readme.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Welcome to ASP.NET Core 6 | 127 | 128 | 129 | 130 | 138 | 139 |
140 |
141 |

This application consists of:

142 |
    143 |
  • Sample pages using ASP.NET Core MVC
  • 144 |
  • Bower for managing client-side libraries
  • 145 |
  • Theming using Bootstrap
  • 146 |
147 |
148 | 160 | 172 |
173 |

Run & Deploy

174 | 179 |
180 | 181 | 184 |
185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /samples/Hangfire.Samples/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:38723/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Hangfire.Samples": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /samples/Hangfire.Samples/RecurringJobService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire.Console; 3 | using Hangfire.RecurringJobExtensions; 4 | using Hangfire.Server; 5 | 6 | namespace Hangfire.Samples 7 | { 8 | public class RecurringJobService 9 | { 10 | [RecurringJob("*/1 * * * *")] 11 | [Queue("jobs")] 12 | public void TestJob1(PerformContext context) 13 | { 14 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} TestJob1 Running ..."); 15 | } 16 | [RecurringJob("*/2 * * * *", RecurringJobId = "TestJob2")] 17 | [Queue("jobs")] 18 | public void TestJob2(PerformContext context) 19 | { 20 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} TestJob2 Running ..."); 21 | } 22 | [RecurringJob("*/2 * * * *", "China Standard Time", "jobs")] 23 | public void TestJob3(PerformContext context) 24 | { 25 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} TestJob3 Running ..."); 26 | } 27 | [RecurringJob("*/5 * * * *", "jobs")] 28 | public void InstanceTestJob(PerformContext context) 29 | { 30 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} InstanceTestJob Running ..."); 31 | } 32 | 33 | [RecurringJob("*/6 * * * *", "UTC", "jobs")] 34 | public static void StaticTestJob(PerformContext context) 35 | { 36 | context.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} StaticTestJob Running ..."); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /samples/Hangfire.Samples/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.Configuration; 12 | using Hangfire.Console; 13 | using Hangfire.RecurringJobExtensions; 14 | namespace Hangfire.Samples 15 | { 16 | public class Startup 17 | { 18 | private readonly IConfigurationRoot _config; 19 | 20 | public Startup(IHostingEnvironment env) 21 | { 22 | // Set up configuration providers. 23 | var builder = new ConfigurationBuilder() 24 | .SetBasePath(Directory.GetCurrentDirectory()) 25 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 26 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); 27 | 28 | if (env.IsDevelopment()) 29 | { 30 | // For more details on using the user secret store see http://go.microsoft.com/fwlink/?LinkID=532709 31 | //builder.AddUserSecrets(); 32 | } 33 | 34 | builder.AddEnvironmentVariables(); 35 | 36 | _config = builder.Build(); 37 | } 38 | // This method gets called by the runtime. Use this method to add services to the container. 39 | // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 40 | public void ConfigureServices(IServiceCollection services) 41 | { 42 | services.AddHangfire(x => 43 | { 44 | x.UseSqlServerStorage(_config.GetConnectionString("Hangfire")); 45 | 46 | x.UseConsole(); 47 | 48 | x.UseRecurringJob("recurringjob.json"); 49 | 50 | x.UseRecurringJob(typeof(RecurringJobService)); 51 | 52 | x.UseDefaultActivator(); 53 | }); 54 | } 55 | 56 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 57 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 58 | { 59 | loggerFactory.AddConsole(); 60 | 61 | if (env.IsDevelopment()) 62 | { 63 | app.UseDeveloperExceptionPage(); 64 | } 65 | 66 | app.UseHangfireServer(new BackgroundJobServerOptions 67 | { 68 | Queues = new[] { "default", "apis", "jobs" } 69 | }); 70 | 71 | app.UseHangfireDashboard(""); 72 | 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /samples/Hangfire.Samples/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Hangfire": "Server=.\\sqlexpress; Database=Hangfire; Trusted_Connection=True;" 4 | } 5 | } -------------------------------------------------------------------------------- /samples/Hangfire.Samples/recurringjob.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "job-name": "My Job1", 4 | "job-type": "Hangfire.Samples.MyJob1, Hangfire.Samples", 5 | "cron-expression": "*/1 * * * *", 6 | "timezone": "China Standard Time", 7 | "queue": "jobs" 8 | }, 9 | { 10 | "job-name": "My Job2", 11 | "job-type": "Hangfire.Samples.MyJob2, Hangfire.Samples", 12 | "cron-expression": "*/5 * * * *", 13 | "job-data": { 14 | "IntVal": 1, 15 | "StringVal": "abcdef", 16 | "BooleanVal": true, 17 | "SimpleObject": { 18 | "Name": "Foo", 19 | "Age": 100 20 | } 21 | } 22 | }, 23 | { 24 | "job-name": "Long Running Job", 25 | "job-type": "Hangfire.Samples.LongRunningJob, Hangfire.Samples", 26 | "cron-expression": "*/2 * * * *", 27 | "job-data": { 28 | "RunningTimes": 300 29 | } 30 | } 31 | ] -------------------------------------------------------------------------------- /samples/Hangfire.Samples/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/Configuration/FileConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Security; 6 | using System.Threading; 7 | using Hangfire.Logging; 8 | 9 | namespace Hangfire.RecurringJobExtensions.Configuration 10 | { 11 | /// 12 | /// Represents a base class for file based . 13 | /// 14 | public abstract class FileConfigurationProvider : IConfigurationProvider, IDisposable 15 | { 16 | private const int NumberOfRetries = 3; 17 | private const int DelayOnRetry = 1000; 18 | 19 | private static readonly ILog _logger = LogProvider.For(); 20 | private FileSystemWatcher _fileWatcher; 21 | private readonly object _fileWatcherLock = new object(); 22 | 23 | /// 24 | /// Initializes a new 25 | /// 26 | /// The source settings file. 27 | /// Whether the should be reloaded if the file changes. 28 | public FileConfigurationProvider(string configFile, bool reloadOnChange = true) 29 | : this(new FileInfo(configFile), reloadOnChange) { } 30 | 31 | /// 32 | /// Initializes a new 33 | /// 34 | /// The source settings . 35 | /// Whether the should be reloaded if the file changes. 36 | public FileConfigurationProvider(FileInfo fileInfo, bool reloadOnChange = true) 37 | { 38 | if (fileInfo == null) throw new ArgumentNullException(nameof(fileInfo)); 39 | 40 | if (!fileInfo.Exists) 41 | throw new FileNotFoundException($"The json file {fileInfo.FullName} does not exist."); 42 | 43 | ConfigFile = fileInfo; 44 | 45 | ReloadOnChange = reloadOnChange; 46 | 47 | Initialize(); 48 | } 49 | private void Initialize() 50 | { 51 | _fileWatcher = new FileSystemWatcher(ConfigFile.DirectoryName, ConfigFile.Name); 52 | _fileWatcher.EnableRaisingEvents = ReloadOnChange; 53 | _fileWatcher.Changed += OnChanged; 54 | _fileWatcher.Error += OnError; 55 | } 56 | private void OnError(object sender, ErrorEventArgs e) 57 | { 58 | _logger.InfoException($"File {ConfigFile} occurred errors.", e.GetException()); 59 | } 60 | 61 | private void OnChanged(object sender, FileSystemEventArgs e) 62 | { 63 | lock (_fileWatcherLock) 64 | { 65 | _logger.Info($"File {e.Name} changed, try to reload configuration again..."); 66 | 67 | var recurringJobInfos = Load().ToArray(); 68 | 69 | if (recurringJobInfos == null || recurringJobInfos.Length == 0) return; 70 | 71 | CronJob.AddOrUpdate(recurringJobInfos); 72 | } 73 | } 74 | 75 | /// 76 | /// configuraion file 77 | /// 78 | public virtual FileInfo ConfigFile { get; set; } 79 | 80 | /// 81 | /// Whether the should be reloaded. 82 | /// 83 | public virtual bool ReloadOnChange { get; set; } 84 | 85 | /// 86 | /// Loads the data for this provider. 87 | /// 88 | /// The list of . 89 | public abstract IEnumerable Load(); 90 | 91 | /// 92 | /// Reads from config file. 93 | /// 94 | /// The string content reading from file. 95 | protected virtual string ReadFromFile() 96 | { 97 | if (!ConfigFile.Exists) 98 | throw new FileNotFoundException($"The json file {ConfigFile.FullName} does not exist."); 99 | 100 | var content = string.Empty; 101 | 102 | for (int i = 0; i < NumberOfRetries; ++i) 103 | { 104 | try 105 | { 106 | // Do stuff with file 107 | using (var file = ConfigFile.OpenRead()) 108 | using (StreamReader reader = new StreamReader(file)) 109 | content = reader.ReadToEnd(); 110 | 111 | break; // When done we can break loop 112 | } 113 | catch (Exception ex) when ( 114 | ex is IOException || 115 | ex is SecurityException || 116 | ex is UnauthorizedAccessException) 117 | { 118 | // Swallow the exception. 119 | _logger.DebugException($"read file {ConfigFile} error.", ex); 120 | 121 | // You may check error code to filter some exceptions, not every error 122 | // can be recovered. 123 | if (i == NumberOfRetries) // Last one, (re)throw exception and exit 124 | throw; 125 | 126 | Thread.Sleep(DelayOnRetry); 127 | } 128 | } 129 | 130 | return content; 131 | } 132 | 133 | /// 134 | /// Disposes the file watcher 135 | /// 136 | public virtual void Dispose() 137 | { 138 | _fileWatcher?.Dispose(); 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/Configuration/IConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Hangfire.RecurringJobExtensions.Configuration 4 | { 5 | /// 6 | /// Provides configuration for for Hangfire. 7 | /// 8 | public interface IConfigurationProvider 9 | { 10 | /// 11 | /// Loads configuration values from the source represented by this . 12 | /// 13 | /// The list of . 14 | IEnumerable Load(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/Configuration/JsonConfigurationProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using Hangfire.Common; 6 | using Hangfire.States; 7 | 8 | namespace Hangfire.RecurringJobExtensions.Configuration 9 | { 10 | /// 11 | /// Represents a JSON file provider as an . 12 | /// 13 | public class JsonConfigurationProvider : FileConfigurationProvider 14 | { 15 | /// 16 | /// Initializes a new . 17 | /// 18 | /// The source settings file. 19 | /// Whether the should be reloaded if the file changes. 20 | public JsonConfigurationProvider(string configFile, bool reloadOnChange = true) 21 | : base(configFile, reloadOnChange) { } 22 | 23 | /// 24 | /// Loads the for this source. 25 | /// 26 | /// The list of for this provider. 27 | public override IEnumerable Load() 28 | { 29 | var jsonContent = ReadFromFile(); 30 | 31 | if (string.IsNullOrWhiteSpace(jsonContent)) throw new Exception("Json file content is empty."); 32 | 33 | var jsonOptions = JobHelper.FromJson>(jsonContent); 34 | 35 | foreach (var o in jsonOptions) 36 | yield return Convert(o); 37 | } 38 | 39 | private RecurringJobInfo Convert(RecurringJobJsonOptions option) 40 | { 41 | ValidateJsonOptions(option); 42 | 43 | return new RecurringJobInfo 44 | { 45 | RecurringJobId = option.JobName, 46 | #if NET45 47 | Method = option.JobType.GetMethod(nameof(IRecurringJob.Execute)), 48 | #else 49 | Method = option.JobType.GetTypeInfo().GetDeclaredMethod(nameof(IRecurringJob.Execute)), 50 | #endif 51 | Cron = option.Cron, 52 | Queue = option.Queue ?? EnqueuedState.DefaultQueue, 53 | TimeZone = option.TimeZone ?? TimeZoneInfo.Utc, 54 | JobData = option.JobData, 55 | Enable = option.Enable ?? true 56 | }; 57 | } 58 | 59 | private void ValidateJsonOptions(RecurringJobJsonOptions option) 60 | { 61 | if (option == null) throw new ArgumentNullException(nameof(option)); 62 | 63 | if (string.IsNullOrWhiteSpace(option.JobName)) 64 | { 65 | throw new Exception($"The json token 'job-name' is null, empty, or consists only of white-space."); 66 | } 67 | 68 | if (!option.JobType.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IRecurringJob))) 69 | { 70 | throw new Exception($"job-type: {option.JobType} must impl the interface {typeof(IRecurringJob)}."); 71 | } 72 | 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/Configuration/RecurringJobJsonOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | 5 | namespace Hangfire.RecurringJobExtensions.Configuration 6 | { 7 | /// 8 | /// json configuration settings. 9 | /// 10 | public class RecurringJobJsonOptions 11 | { 12 | /// 13 | /// The job name represents for 14 | /// 15 | [JsonProperty("job-name")] 16 | #if !NET45 17 | [JsonRequired] 18 | #endif 19 | public string JobName { get; set; } 20 | /// 21 | /// The job type while impl the interface . 22 | /// 23 | [JsonProperty("job-type")] 24 | #if !NET45 25 | [JsonRequired] 26 | #endif 27 | public Type JobType { get; set; } 28 | 29 | /// 30 | /// Cron expressions 31 | /// 32 | [JsonProperty("cron-expression")] 33 | #if !NET45 34 | [JsonRequired] 35 | #endif 36 | public string Cron { get; set; } 37 | 38 | /// 39 | /// The value of can be created by 40 | /// 41 | [JsonProperty("timezone")] 42 | [JsonConverter(typeof(TimeZoneInfoConverter))] 43 | public TimeZoneInfo TimeZone { get; set; } 44 | /// 45 | /// Whether the property can be serialized or not. 46 | /// 47 | /// true if value not null, otherwise false. 48 | public bool ShouldSerializeTimeZone() => TimeZone != null; 49 | /// 50 | /// Hangfire queue name 51 | /// 52 | [JsonProperty("queue")] 53 | public string Queue { get; set; } 54 | /// 55 | /// Whether the property can be serialized or not. 56 | /// 57 | /// true if value not null or empty, otherwise false. 58 | public bool ShouldSerializeQueue() => !string.IsNullOrEmpty(Queue); 59 | /// 60 | /// The data persisted in storage. 61 | /// 62 | [JsonProperty("job-data")] 63 | public IDictionary JobData { get; set; } 64 | /// 65 | /// Whether the property can be serialized or not. 66 | /// 67 | /// true if value not null or count is zero, otherwise false. 68 | public bool ShouldSerializeJobData() => JobData != null && JobData.Count > 0; 69 | 70 | /// 71 | /// Whether the can be added/updated, 72 | /// default value is true, if false it will be deleted automatically. 73 | /// 74 | [JsonProperty("enable")] 75 | public bool? Enable { get; set; } 76 | 77 | /// 78 | /// Whether the property can be serialized or not. 79 | /// 80 | /// true if value is not null, otherwise false. 81 | public bool ShouldSerializeEnable() => Enable.HasValue; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/CronJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using Hangfire.RecurringJobExtensions.Configuration; 6 | 7 | namespace Hangfire.RecurringJobExtensions 8 | { 9 | /// 10 | /// The helper class to build automatically. 11 | /// 12 | public class CronJob 13 | { 14 | /// 15 | /// Builds automatically within specified interface or class. 16 | /// 17 | /// Specified interface or class 18 | public static void AddOrUpdate(params Type[] types) 19 | { 20 | AddOrUpdate(() => types); 21 | } 22 | 23 | /// 24 | /// Builds automatically within specified interface or class. 25 | /// 26 | /// The provider to get specified interfaces or class. 27 | public static void AddOrUpdate(Func> typesProvider) 28 | { 29 | if (typesProvider == null) throw new ArgumentNullException(nameof(typesProvider)); 30 | 31 | IRecurringJobBuilder builder = new RecurringJobBuilder(); 32 | 33 | builder.Build(typesProvider); 34 | } 35 | 36 | /// 37 | /// Builds automatically by using multiple JSON configuration files. 38 | /// 39 | /// The array of json files. 40 | /// Whether the should be reloaded if the file changes. 41 | public static void AddOrUpdate(string[] jsonFiles, bool reloadOnChange = true) 42 | { 43 | if (jsonFiles == null) throw new ArgumentNullException(nameof(jsonFiles)); 44 | 45 | foreach (var jsonFile in jsonFiles) 46 | AddOrUpdate(jsonFile, reloadOnChange); 47 | } 48 | /// 49 | /// Builds automatically by using a JSON configuration. 50 | /// 51 | /// Json file for configuration. 52 | /// Whether the should be reloaded if the file changes. 53 | public static void AddOrUpdate(string jsonFile, bool reloadOnChange = true) 54 | { 55 | if (string.IsNullOrWhiteSpace(jsonFile)) throw new ArgumentNullException(nameof(jsonFile)); 56 | 57 | var configFile = File.Exists(jsonFile) ? jsonFile : 58 | Path.Combine( 59 | #if NET45 60 | AppDomain.CurrentDomain.BaseDirectory, 61 | #else 62 | AppContext.BaseDirectory, 63 | #endif 64 | jsonFile); 65 | 66 | 67 | if (!File.Exists(configFile)) throw new FileNotFoundException($"The json file {configFile} does not exist."); 68 | 69 | IConfigurationProvider provider = new JsonConfigurationProvider(configFile, reloadOnChange); 70 | 71 | AddOrUpdate(provider); 72 | } 73 | 74 | /// 75 | /// Builds automatically with . 76 | /// 77 | /// 78 | public static void AddOrUpdate(IConfigurationProvider provider) 79 | { 80 | if (provider == null) throw new ArgumentNullException(nameof(provider)); 81 | 82 | IRecurringJobBuilder builder = new RecurringJobBuilder(); 83 | 84 | AddOrUpdate(provider.Load()); 85 | } 86 | 87 | /// 88 | /// Builds automatically with the collection of . 89 | /// 90 | /// The collection of . 91 | public static void AddOrUpdate(IEnumerable recurringJobInfos) 92 | { 93 | if (recurringJobInfos == null) throw new ArgumentNullException(nameof(recurringJobInfos)); 94 | 95 | IRecurringJobBuilder builder = new RecurringJobBuilder(); 96 | 97 | builder.Build(() => recurringJobInfos); 98 | } 99 | 100 | /// 101 | /// Builds automatically with the array of . 102 | /// 103 | /// The array of . 104 | public static void AddOrUpdate(params RecurringJobInfo[] recurringJobInfos) 105 | { 106 | if (recurringJobInfos == null) throw new ArgumentNullException(nameof(recurringJobInfos)); 107 | 108 | AddOrUpdate(recurringJobInfos.AsEnumerable()); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/Hangfire.RecurringJobExtensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Hangfire extensions for RecurringJob 5 | Hangfire.RecurringJobExtensions 6 | 1.1.6 7 | icsharp 8 | netstandard1.3;net45 9 | true 10 | true 11 | Hangfire.RecurringJobExtensions 12 | Hangfire.RecurringJobExtensions 13 | Hangfire;RecurringJob 14 | Add support for Hangfire RecurringJob extesions, we can use RecurringJobAttribute or Json configuration file to build RecurringJob automatically. 15 | https://github.com/icsharp/Hangfire.RecurringJobExtensions 16 | https://raw.githubusercontent.com/icsharp/Hangfire.RecurringJobExtensions/master/LICENSE 17 | git 18 | https://github.com/icsharp/Hangfire.RecurringJobExtensions 19 | false 20 | false 21 | false 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/HangfireExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Hangfire.RecurringJobExtensions.Configuration; 4 | 5 | namespace Hangfire.RecurringJobExtensions 6 | { 7 | /// 8 | /// Hangfire extensions. 9 | /// 10 | public static class HangfireExtensions 11 | { 12 | /// 13 | /// Builds automatically within specified interface or class. 14 | /// To the Hangfire client, alternatively way is to use the class to add or update . 15 | /// 16 | /// 17 | /// Specified interface or class 18 | /// 19 | public static IGlobalConfiguration UseRecurringJob(this IGlobalConfiguration configuration, params Type[] types) 20 | { 21 | return UseRecurringJob(configuration, () => types); 22 | } 23 | 24 | /// 25 | /// Builds automatically within specified interface or class. 26 | /// To the Hangfire client, alternatively way is to use the class to add or update . 27 | /// 28 | /// 29 | /// The provider to get specified interfaces or class. 30 | /// 31 | public static IGlobalConfiguration UseRecurringJob(this IGlobalConfiguration configuration, Func> typesProvider) 32 | { 33 | if (typesProvider == null) throw new ArgumentNullException(nameof(typesProvider)); 34 | 35 | CronJob.AddOrUpdate(typesProvider); 36 | 37 | return configuration; 38 | } 39 | /// 40 | /// Builds automatically by using a JSON configuration. 41 | /// To the Hangfire client, alternatively way is to use the class to add or update . 42 | /// 43 | /// . 44 | /// Json file for configuration. 45 | /// Whether the should be reloaded if the file changes. 46 | /// 47 | public static IGlobalConfiguration UseRecurringJob(this IGlobalConfiguration configuration, string jsonFile, bool reloadOnChange = true) 48 | { 49 | if (string.IsNullOrWhiteSpace(jsonFile)) throw new ArgumentNullException(nameof(jsonFile)); 50 | 51 | CronJob.AddOrUpdate(jsonFile, reloadOnChange); 52 | 53 | return configuration; 54 | } 55 | 56 | /// 57 | /// Builds automatically by using multiple JSON configuration files. 58 | /// To the Hangfire client, alternatively way is to use the class to add or update . 59 | /// 60 | /// . 61 | /// The array of json files. 62 | /// Whether the should be reloaded if the file changes. 63 | /// 64 | public static IGlobalConfiguration UseRecurringJob(this IGlobalConfiguration configuration, string[] jsonFiles, bool reloadOnChange = true) 65 | { 66 | if (jsonFiles == null) throw new ArgumentNullException(nameof(jsonFiles)); 67 | 68 | CronJob.AddOrUpdate(jsonFiles, reloadOnChange); 69 | 70 | return configuration; 71 | } 72 | 73 | /// 74 | /// Builds automatically with . 75 | /// To the Hangfire client, alternatively way is to use the class to add or update . 76 | /// 77 | /// . 78 | /// 79 | /// . 80 | public static IGlobalConfiguration UseRecurringJob(this IGlobalConfiguration configuration, IConfigurationProvider provider) 81 | { 82 | if (provider == null) throw new ArgumentNullException(nameof(provider)); 83 | 84 | CronJob.AddOrUpdate(provider); 85 | 86 | return configuration; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/IRecurringJob.cs: -------------------------------------------------------------------------------- 1 | using Hangfire.Server; 2 | 3 | namespace Hangfire.RecurringJobExtensions 4 | { 5 | /// 6 | /// Provides a unified interface to build hangfire , similar to quartz.net. 7 | /// 8 | public interface IRecurringJob 9 | { 10 | /// 11 | /// Execute the . 12 | /// 13 | /// The context to . 14 | void Execute(PerformContext context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/IRecurringJobBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Hangfire.RecurringJobExtensions 5 | { 6 | /// 7 | /// Build automatically. 8 | /// 9 | public interface IRecurringJobBuilder 10 | { 11 | /// 12 | /// Create with the provider for specified interface or class. 13 | /// 14 | /// Specified interface or class 15 | void Build(Func> typesProvider); 16 | /// 17 | /// Create with the provider for specified list . 18 | /// 19 | /// The provider to get list/> 20 | void Build(Func> recurringJobInfoProvider); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/IRecurringJobInfoStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Hangfire.RecurringJobExtensions 5 | { 6 | /// 7 | /// The storage APIs for . 8 | /// 9 | public interface IRecurringJobInfoStorage : IDisposable 10 | { 11 | /// 12 | /// Finds all from storage. 13 | /// 14 | /// The collection of 15 | IEnumerable FindAll(); 16 | 17 | /// 18 | /// Finds by jobId. 19 | /// The job id is associated with 20 | /// 21 | /// The specified 22 | /// 23 | RecurringJobInfo FindByJobId(string jobId); 24 | 25 | /// 26 | /// Finds by recurringJobId. 27 | /// 28 | /// The specified identifier of the RecurringJob. 29 | /// 30 | RecurringJobInfo FindByRecurringJobId(string recurringJobId); 31 | 32 | /// 33 | /// Sets to storage which associated with . 34 | /// 35 | /// The specified identifier of the RecurringJob. 36 | void SetJobData(RecurringJobInfo recurringJobInfo); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/IRecurringJobRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace Hangfire.RecurringJobExtensions 5 | { 6 | /// 7 | /// Register dynamically. 8 | /// 9 | public interface IRecurringJobRegistry 10 | { 11 | /// 12 | /// Register RecurringJob via . 13 | /// 14 | /// the specified method 15 | /// Cron expressions 16 | /// 17 | /// Queue name 18 | void Register(MethodInfo method, string cron, TimeZoneInfo timeZone, string queue); 19 | /// 20 | /// Register RecurringJob via . 21 | /// 22 | /// The identifier of the RecurringJob 23 | /// the specified method 24 | /// Cron expressions 25 | /// 26 | /// Queue name 27 | void Register(string recurringJobId, MethodInfo method, string cron, TimeZoneInfo timeZone, string queue); 28 | /// 29 | /// Register RecurringJob via . 30 | /// 31 | /// info. 32 | void Register(RecurringJobInfo recurringJobInfo); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/PerformContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Hangfire.Common; 4 | using Hangfire.Server; 5 | 6 | namespace Hangfire.RecurringJobExtensions 7 | { 8 | /// 9 | /// Extensions for . 10 | /// 11 | public static class PerformContextExtensions 12 | { 13 | /// 14 | /// Gets job data from storage associated with . 15 | /// 16 | /// The . 17 | /// The dictionary key from the property 18 | /// The value from the property 19 | public static object GetJobData(this PerformContext context, string name) 20 | { 21 | if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 22 | 23 | var jobData = GetJobData(context); 24 | 25 | if (jobData == null) return null; 26 | 27 | return jobData.ContainsKey(name) ? jobData[name] : null; 28 | } 29 | 30 | /// 31 | /// Gets job data from storage associated with . 32 | /// 33 | /// The specified type to json value. 34 | /// The . 35 | /// The dictionary key from the property 36 | /// The value from the property 37 | public static T GetJobData(this PerformContext context, string name) 38 | { 39 | var o = GetJobData(context, name); 40 | 41 | var json = JobHelper.ToJson(o); 42 | 43 | return JobHelper.FromJson(json); 44 | } 45 | 46 | /// 47 | /// Gets job data from storage associated with . 48 | /// 49 | /// The . 50 | /// The job data from storage. 51 | public static IDictionary GetJobData(this PerformContext context) 52 | { 53 | using (var storage = new RecurringJobInfoStorage(context.Connection)) 54 | { 55 | return storage.FindByJobId(context.BackgroundJob.Id)?.JobData; 56 | } 57 | } 58 | 59 | /// 60 | /// Persists job data to storage associated with . 61 | /// 62 | /// The . 63 | /// The dictionary key from the property 64 | /// The persisting value. 65 | public static void SetJobData(this PerformContext context, string name, object value) 66 | { 67 | if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name)); 68 | 69 | SetJobData(context, new Dictionary { [name] = value }); 70 | } 71 | 72 | /// 73 | /// Persists job data to storage associated with . 74 | /// 75 | /// The . 76 | /// The dictionary value to be added or updated. 77 | public static void SetJobData(this PerformContext context, IDictionary jobData) 78 | { 79 | if (jobData == null) throw new ArgumentNullException(nameof(jobData)); 80 | 81 | using (var storage = new RecurringJobInfoStorage(context.Connection)) 82 | { 83 | var recurringJobInfo = storage.FindByJobId(context.BackgroundJob.Id); 84 | 85 | if (recurringJobInfo.JobData == null) 86 | recurringJobInfo.JobData = new Dictionary(); 87 | 88 | foreach (var kv in jobData) 89 | recurringJobInfo.JobData[kv.Key] = kv.Value; 90 | 91 | storage.SetJobData(recurringJobInfo); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("Hangfire.RecurringJobExtensions")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("d75dfe5d-38cf-4150-8264-eb22e579237d")] 20 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/RecurringJobAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Hangfire.States; 3 | 4 | namespace Hangfire.RecurringJobExtensions 5 | { 6 | /// 7 | /// Attribute to add or update automatically 8 | /// by target it to interface/instance/static method. 9 | /// 10 | [AttributeUsage(AttributeTargets.Method)] 11 | public class RecurringJobAttribute : Attribute 12 | { 13 | /// 14 | /// The identifier of the RecurringJob 15 | /// 16 | public string RecurringJobId { get; set; } 17 | /// 18 | /// Cron expressions 19 | /// 20 | public string Cron { get; set; } 21 | /// 22 | /// Queue name 23 | /// 24 | public string Queue { get; set; } 25 | /// 26 | /// Converts to via method , 27 | /// default value is 28 | /// 29 | public string TimeZone { get; set; } 30 | /// 31 | /// Whether to build RecurringJob automatically, default value is true. 32 | /// If false it will be deleted automatically. 33 | /// 34 | public bool Enabled { get; set; } = true; 35 | /// 36 | /// Initializes a new instance of the 37 | /// 38 | /// Cron expressions 39 | public RecurringJobAttribute(string cron) : this(cron, EnqueuedState.DefaultQueue) { } 40 | /// 41 | /// Initializes a new instance of the 42 | /// 43 | /// Cron expressions 44 | /// Queue name 45 | public RecurringJobAttribute(string cron, string queue) : this(cron, "UTC", queue) { } 46 | /// 47 | /// Initializes a new instance of the 48 | /// 49 | /// Cron expressions 50 | /// Converts to via method . 51 | /// Queue name 52 | public RecurringJobAttribute(string cron, string timeZone, string queue) 53 | { 54 | if (string.IsNullOrEmpty(cron)) throw new ArgumentNullException(nameof(cron)); 55 | if (string.IsNullOrEmpty(timeZone)) throw new ArgumentNullException(nameof(timeZone)); 56 | if (string.IsNullOrEmpty(queue)) throw new ArgumentNullException(nameof(queue)); 57 | 58 | Cron = cron; 59 | TimeZone = timeZone; 60 | Queue = queue; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/RecurringJobBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using Hangfire.States; 5 | 6 | namespace Hangfire.RecurringJobExtensions 7 | { 8 | /// 9 | /// Build automatically, interface. 10 | /// 11 | public class RecurringJobBuilder : IRecurringJobBuilder 12 | { 13 | private IRecurringJobRegistry _registry; 14 | 15 | /// 16 | /// Initializes a new instance of the . 17 | /// 18 | public RecurringJobBuilder() : this(new RecurringJobRegistry()) { } 19 | 20 | /// 21 | /// Initializes a new instance of the with . 22 | /// 23 | /// interface. 24 | public RecurringJobBuilder(IRecurringJobRegistry registry) 25 | { 26 | _registry = registry; 27 | } 28 | /// 29 | /// Create with the provider for specified interface or class. 30 | /// 31 | /// Specified interface or class 32 | public void Build(Func> typesProvider) 33 | { 34 | if (typesProvider == null) throw new ArgumentNullException(nameof(typesProvider)); 35 | 36 | foreach (var type in typesProvider()) 37 | { 38 | foreach (var method in type.GetTypeInfo().DeclaredMethods) 39 | { 40 | if (!method.IsDefined(typeof(RecurringJobAttribute), false)) continue; 41 | 42 | var attribute = method.GetCustomAttribute(false); 43 | 44 | if (attribute == null) continue; 45 | 46 | if (string.IsNullOrWhiteSpace(attribute.RecurringJobId)) 47 | { 48 | attribute.RecurringJobId = method.GetRecurringJobId(); 49 | } 50 | 51 | if (!attribute.Enabled) 52 | { 53 | RecurringJob.RemoveIfExists(attribute.RecurringJobId); 54 | continue; 55 | } 56 | _registry.Register( 57 | attribute.RecurringJobId, 58 | method, 59 | attribute.Cron, 60 | string.IsNullOrEmpty(attribute.TimeZone) ? TimeZoneInfo.Utc : TimeZoneInfo.FindSystemTimeZoneById(attribute.TimeZone), 61 | attribute.Queue ?? EnqueuedState.DefaultQueue); 62 | } 63 | } 64 | } 65 | /// 66 | /// Create with the provider for specified list . 67 | /// 68 | /// The provider to get list. 69 | public void Build(Func> recurringJobInfoProvider) 70 | { 71 | if (recurringJobInfoProvider == null) throw new ArgumentNullException(nameof(recurringJobInfoProvider)); 72 | 73 | foreach (RecurringJobInfo recurringJobInfo in recurringJobInfoProvider()) 74 | { 75 | if (string.IsNullOrWhiteSpace(recurringJobInfo.RecurringJobId)) 76 | { 77 | throw new Exception($"The property of {nameof(recurringJobInfo.RecurringJobId)} is null, empty, or consists only of white-space."); 78 | } 79 | if (!recurringJobInfo.Enable) 80 | { 81 | RecurringJob.RemoveIfExists(recurringJobInfo.RecurringJobId); 82 | continue; 83 | } 84 | _registry.Register(recurringJobInfo); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/RecurringJobInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace Hangfire.RecurringJobExtensions 6 | { 7 | /// 8 | /// It is used to build 9 | /// with . 10 | /// 11 | public class RecurringJobInfo 12 | { 13 | /// 14 | /// The identifier of the RecurringJob 15 | /// 16 | public string RecurringJobId { get; set; } 17 | /// 18 | /// Cron expressions 19 | /// 20 | public string Cron { get; set; } 21 | /// 22 | /// TimeZoneInfo 23 | /// 24 | public TimeZoneInfo TimeZone { get; set; } 25 | /// 26 | /// Queue name 27 | /// 28 | public string Queue { get; set; } 29 | /// 30 | /// Method to execute while running. 31 | /// 32 | public MethodInfo Method { get; set; } 33 | /// 34 | /// The data persisted in storage. 35 | /// 36 | public IDictionary JobData { get; set; } 37 | 38 | /// 39 | /// Whether the can be added/updated, 40 | /// default value is true, if false it will be deleted automatically. 41 | /// 42 | public bool Enable { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/RecurringJobInfoStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Hangfire.Common; 4 | using Hangfire.Storage; 5 | 6 | namespace Hangfire.RecurringJobExtensions 7 | { 8 | /// 9 | /// The storage APIs for . 10 | /// 11 | public class RecurringJobInfoStorage : IRecurringJobInfoStorage 12 | { 13 | private static readonly TimeSpan LockTimeout = TimeSpan.FromMinutes(1); 14 | 15 | private readonly IStorageConnection _connection; 16 | 17 | /// 18 | /// Initializes a new 19 | /// 20 | public RecurringJobInfoStorage() : this(JobStorage.Current.GetConnection()) { } 21 | 22 | /// 23 | /// Initializes a new 24 | /// 25 | /// 26 | public RecurringJobInfoStorage(IStorageConnection connection) 27 | { 28 | if (connection == null) throw new ArgumentNullException(nameof(connection)); 29 | 30 | _connection = connection; 31 | } 32 | 33 | /// 34 | /// Finds all from storage. 35 | /// 36 | /// The collection of 37 | public IEnumerable FindAll() 38 | { 39 | var recurringJobIds = _connection.GetAllItemsFromSet("recurring-jobs"); 40 | 41 | foreach (var recurringJobId in recurringJobIds) 42 | { 43 | var recurringJob = _connection.GetAllEntriesFromHash($"recurring-job:{recurringJobId}"); 44 | 45 | if (recurringJob == null) continue; 46 | 47 | yield return InternalFind(recurringJobId, recurringJob); 48 | } 49 | } 50 | 51 | /// 52 | /// Finds by jobId. 53 | /// The job id is associated with 54 | /// 55 | /// The specified 56 | /// 57 | public RecurringJobInfo FindByJobId(string jobId) 58 | { 59 | if (string.IsNullOrEmpty(jobId)) throw new ArgumentNullException(nameof(jobId)); 60 | 61 | var paramValue = _connection.GetJobParameter(jobId, "RecurringJobId"); 62 | 63 | if (string.IsNullOrEmpty(paramValue)) throw new Exception($"There is not RecurringJobId with associated BackgroundJob Id:{jobId}"); 64 | 65 | var recurringJobId = JobHelper.FromJson(paramValue); 66 | 67 | return FindByRecurringJobId(recurringJobId); 68 | } 69 | 70 | /// 71 | /// Finds by recurringJobId. 72 | /// 73 | /// The specified identifier of the RecurringJob. 74 | /// 75 | public RecurringJobInfo FindByRecurringJobId(string recurringJobId) 76 | { 77 | if (string.IsNullOrEmpty(recurringJobId)) throw new ArgumentNullException(nameof(recurringJobId)); 78 | 79 | var recurringJob = _connection.GetAllEntriesFromHash($"recurring-job:{recurringJobId}"); 80 | 81 | if (recurringJob == null) return null; 82 | 83 | return InternalFind(recurringJobId, recurringJob); 84 | } 85 | 86 | private RecurringJobInfo InternalFind(string recurringJobId, Dictionary recurringJob) 87 | { 88 | if (string.IsNullOrEmpty(recurringJobId)) throw new ArgumentNullException(nameof(recurringJobId)); 89 | if (recurringJob == null) throw new ArgumentNullException(nameof(recurringJob)); 90 | 91 | var serializedJob = JobHelper.FromJson(recurringJob["Job"]); 92 | var job = serializedJob.Deserialize(); 93 | 94 | return new RecurringJobInfo 95 | { 96 | RecurringJobId = recurringJobId, 97 | Cron = recurringJob["Cron"], 98 | TimeZone = recurringJob.ContainsKey("TimeZoneId") 99 | ? TimeZoneInfo.FindSystemTimeZoneById(recurringJob["TimeZoneId"]) 100 | : TimeZoneInfo.Utc, 101 | Queue = recurringJob["Queue"], 102 | Method = job.Method, 103 | Enable = recurringJob.ContainsKey(nameof(RecurringJobInfo.Enable)) 104 | ? JobHelper.FromJson(recurringJob[nameof(RecurringJobInfo.Enable)]) 105 | : true, 106 | JobData = recurringJob.ContainsKey(nameof(RecurringJobInfo.JobData)) 107 | ? JobHelper.FromJson>(recurringJob[nameof(RecurringJobInfo.JobData)]) 108 | : null 109 | }; 110 | } 111 | 112 | /// 113 | /// Sets to storage which associated with . 114 | /// 115 | /// The specified identifier of the RecurringJob. 116 | public void SetJobData(RecurringJobInfo recurringJobInfo) 117 | { 118 | if (recurringJobInfo == null) throw new ArgumentNullException(nameof(recurringJobInfo)); 119 | 120 | if (recurringJobInfo.JobData == null || recurringJobInfo.JobData.Count == 0) return; 121 | 122 | using (_connection.AcquireDistributedLock($"recurringjobextensions-jobdata:{recurringJobInfo.RecurringJobId}", LockTimeout)) 123 | { 124 | var changedFields = new Dictionary 125 | { 126 | [nameof(RecurringJobInfo.Enable)] = JobHelper.ToJson(recurringJobInfo.Enable), 127 | [nameof(RecurringJobInfo.JobData)] = JobHelper.ToJson(recurringJobInfo.JobData) 128 | }; 129 | 130 | _connection.SetRangeInHash($"recurring-job:{recurringJobInfo.RecurringJobId}", changedFields); 131 | } 132 | } 133 | 134 | /// 135 | /// Disposes storage connection. 136 | /// 137 | public void Dispose() 138 | { 139 | _connection?.Dispose(); 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/RecurringJobRegistry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | 5 | namespace Hangfire.RecurringJobExtensions 6 | { 7 | /// 8 | /// Register dynamically. 9 | /// interface. 10 | /// 11 | public class RecurringJobRegistry : IRecurringJobRegistry 12 | { 13 | /// 14 | /// Register RecurringJob via . 15 | /// 16 | /// The specified method 17 | /// Cron expressions 18 | /// 19 | /// Queue name 20 | public void Register(MethodInfo method, string cron, TimeZoneInfo timeZone, string queue) 21 | { 22 | if (method == null) throw new ArgumentNullException(nameof(method)); 23 | if (cron == null) throw new ArgumentNullException(nameof(cron)); 24 | if (timeZone == null) throw new ArgumentNullException(nameof(timeZone)); 25 | if (queue == null) throw new ArgumentNullException(nameof(queue)); 26 | 27 | var jobId = method.GetRecurringJobId(); 28 | 29 | Register(jobId, method, cron, timeZone, queue); 30 | } 31 | /// 32 | /// Register RecurringJob via . 33 | /// 34 | /// The identifier of the RecurringJob 35 | /// the specified method 36 | /// Cron expressions 37 | /// 38 | /// Queue name 39 | public void Register(string recurringJobId, MethodInfo method, string cron, TimeZoneInfo timeZone, string queue) 40 | { 41 | if (recurringJobId == null) throw new ArgumentNullException(nameof(recurringJobId)); 42 | if (method == null) throw new ArgumentNullException(nameof(method)); 43 | if (cron == null) throw new ArgumentNullException(nameof(cron)); 44 | if (timeZone == null) throw new ArgumentNullException(nameof(timeZone)); 45 | if (queue == null) throw new ArgumentNullException(nameof(queue)); 46 | 47 | var parameters = method.GetParameters(); 48 | 49 | Expression[] args = new Expression[parameters.Length]; 50 | 51 | for (int i = 0; i < parameters.Length; i++) 52 | { 53 | args[i] = Expression.Default(parameters[i].ParameterType); 54 | } 55 | 56 | var x = Expression.Parameter(method.DeclaringType, "x"); 57 | 58 | var methodCall = method.IsStatic ? Expression.Call(method, args) : Expression.Call(x, method, args); 59 | 60 | var addOrUpdate = Expression.Call( 61 | typeof(RecurringJob), 62 | nameof(RecurringJob.AddOrUpdate), 63 | new Type[] { method.DeclaringType }, 64 | new Expression[] 65 | { 66 | Expression.Constant(recurringJobId), 67 | Expression.Lambda(methodCall, x), 68 | Expression.Constant(cron), 69 | Expression.Constant(timeZone), 70 | Expression.Constant(queue) 71 | }); 72 | 73 | Expression.Lambda(addOrUpdate).Compile().DynamicInvoke(); 74 | } 75 | /// 76 | /// Register RecurringJob via . 77 | /// 78 | /// info. 79 | public void Register(RecurringJobInfo recurringJobInfo) 80 | { 81 | if (recurringJobInfo == null) throw new ArgumentNullException(nameof(recurringJobInfo)); 82 | 83 | Register(recurringJobInfo.RecurringJobId, recurringJobInfo.Method, recurringJobInfo.Cron, recurringJobInfo.TimeZone, recurringJobInfo.Queue); 84 | 85 | using (var storage = new RecurringJobInfoStorage()) 86 | { 87 | storage.SetJobData(recurringJobInfo); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/TimeZoneInfoConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | namespace Hangfire.RecurringJobExtensions 4 | { 5 | /// 6 | /// Converts to and from JSON 7 | /// 8 | public class TimeZoneInfoConverter : JsonConverter 9 | { 10 | /// 11 | /// Determines whether this instance can convert the specified 12 | /// 13 | /// Type of the object. 14 | /// true if this instance can convert the specified ; otherwise, false. 15 | public override bool CanConvert(Type objectType) 16 | { 17 | return objectType == typeof(TimeZoneInfo); 18 | } 19 | /// 20 | /// Reads the JSON representation of the object. 21 | /// 22 | /// The to read from 23 | /// Type of the object 24 | /// The existing value of object being read 25 | /// The calling serializer 26 | /// The object value 27 | public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 28 | { 29 | if (reader.TokenType == JsonToken.Null) return null; 30 | 31 | if (reader.Value == null) return null; 32 | 33 | return TimeZoneInfo.FindSystemTimeZoneById(reader.Value.ToString()); 34 | } 35 | /// 36 | /// Writes the JSON representation of the . 37 | /// 38 | /// The to write to 39 | /// The value 40 | /// The calling serializer 41 | public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 42 | { 43 | var o = value as TimeZoneInfo; 44 | 45 | writer.WriteValue(o.StandardName); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Hangfire.RecurringJobExtensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Hangfire.RecurringJobExtensions 7 | { 8 | internal static class TypeExtensions 9 | { 10 | public static string GetRecurringJobId(this MethodInfo method) 11 | { 12 | return $"{method.DeclaringType.ToGenericTypeString()}.{method.Name}"; 13 | } 14 | /// 15 | /// Fork the extension method from 16 | /// https://github.com/HangfireIO/Hangfire/blob/master/src/Hangfire.Core/Common/TypeExtensions.cs 17 | /// 18 | /// 19 | /// 20 | public static string ToGenericTypeString(this Type type) 21 | { 22 | if (!type.GetTypeInfo().IsGenericType) 23 | { 24 | return type.GetFullNameWithoutNamespace() 25 | .ReplacePlusWithDotInNestedTypeName(); 26 | } 27 | 28 | return type.GetGenericTypeDefinition() 29 | .GetFullNameWithoutNamespace() 30 | .ReplacePlusWithDotInNestedTypeName() 31 | .ReplaceGenericParametersInGenericTypeName(type); 32 | } 33 | private static string GetFullNameWithoutNamespace(this Type type) 34 | { 35 | if (type.IsGenericParameter) 36 | { 37 | return type.Name; 38 | } 39 | 40 | const int dotLength = 1; 41 | // ReSharper disable once PossibleNullReferenceException 42 | return !String.IsNullOrEmpty(type.Namespace) 43 | ? type.FullName.Substring(type.Namespace.Length + dotLength) 44 | : type.FullName; 45 | } 46 | private static string ReplacePlusWithDotInNestedTypeName(this string typeName) 47 | { 48 | return typeName.Replace('+', '.'); 49 | } 50 | private static string ReplaceGenericParametersInGenericTypeName(this string typeName, Type type) 51 | { 52 | var genericArguments = type.GetTypeInfo().GetAllGenericArguments(); 53 | 54 | const string regexForGenericArguments = @"`[1-9]\d*"; 55 | 56 | var rgx = new Regex(regexForGenericArguments); 57 | 58 | typeName = rgx.Replace(typeName, match => 59 | { 60 | var currentGenericArgumentNumbers = int.Parse(match.Value.Substring(1)); 61 | var currentArguments = string.Join(",", genericArguments.Take(currentGenericArgumentNumbers).Select(ToGenericTypeString)); 62 | genericArguments = genericArguments.Skip(currentGenericArgumentNumbers).ToArray(); 63 | return string.Concat("<", currentArguments, ">"); 64 | }); 65 | 66 | return typeName; 67 | } 68 | public static Type[] GetAllGenericArguments(this TypeInfo type) 69 | { 70 | return type.GenericTypeArguments.Length > 0 ? type.GenericTypeArguments : type.GenericTypeParameters; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/Configuration/JsonConfigurationProviderTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using Hangfire.RecurringJobExtensions.Configuration; 5 | using Newtonsoft.Json; 6 | using Xunit; 7 | 8 | namespace Hangfire.RecurringJobExtensions.Tests.Configuration 9 | { 10 | public class JsonConfigurationProviderTest 11 | { 12 | [Fact] 13 | public void Ctor_ThrowsAnException_WhenFileIsNullOrNotExists() 14 | { 15 | Assert.Throws("fileName", () => new JsonConfigurationProvider(null)); 16 | Assert.Throws(() => new JsonConfigurationProvider($"{Guid.NewGuid()}.json")); 17 | } 18 | 19 | [Fact] 20 | public void Load_ThrowsAnException_WhenFileContentIsInvliadJsonData() 21 | { 22 | var provider = new JsonConfigurationProvider("error.json"); 23 | 24 | Assert.Throws(() => provider.Load().ToList()); 25 | } 26 | 27 | [Fact] 28 | public void Load_WithNoJobData() 29 | { 30 | var provider = new JsonConfigurationProvider("job1.json"); 31 | 32 | Assert.Equal(true, provider.ReloadOnChange); 33 | 34 | var data = provider.Load().ToList(); 35 | 36 | Assert.True(data.Count > 0); 37 | } 38 | [Fact] 39 | public void Load_WithJobData() 40 | { 41 | var provider = new JsonConfigurationProvider("job2.json"); 42 | 43 | Assert.Equal(true, provider.ReloadOnChange); 44 | 45 | var data = provider.Load().ToList(); 46 | 47 | Assert.True(data.Count > 0); 48 | 49 | var recurringJobInfo = data.FirstOrDefault(x => x.RecurringJobId == "My Job2"); 50 | 51 | Assert.True(recurringJobInfo.JobData != null && recurringJobInfo.JobData.Count > 0); 52 | } 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/Configuration/RecurringJobJsonOptionsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using Xunit; 5 | using Hangfire.RecurringJobExtensions.Configuration; 6 | using Hangfire.Common; 7 | using Newtonsoft.Json; 8 | 9 | namespace Hangfire.RecurringJobExtensions.Tests.Configuration 10 | { 11 | public class RecurringJobJsonOptionsTest 12 | { 13 | [Fact] 14 | public void SerializeRecurringJobJsonOptionsNormally() 15 | { 16 | var list = new List 17 | { 18 | new RecurringJobJsonOptions { JobName="My Job1", JobType=typeof(MyJob1), Cron="*/1 * * * *", Queue="jobs" }, 19 | new RecurringJobJsonOptions { JobName="My Job12", JobType=typeof(MyJob2), Cron="*/2 * * * *", Queue="jobs", Enable=false, TimeZone=TimeZoneInfo.Utc } 20 | }; 21 | 22 | var json = JobHelper.ToJson(list); 23 | Assert.NotNull(json); 24 | 25 | var o = JobHelper.FromJson>(json); 26 | 27 | Assert.Equal(2, o.Count); 28 | } 29 | [Fact] 30 | public void SerializeRecurringJobJsonOptionsContainsExtendedDataProperty() 31 | { 32 | var list = new List 33 | { 34 | new RecurringJobJsonOptions 35 | { 36 | JobName = "My Job1", 37 | JobType = typeof(MyJob1), 38 | Cron = "*/1 * * * *", 39 | Queue = "jobs" , 40 | JobData = new Dictionary 41 | { 42 | ["IntVal"] = 1, 43 | ["StringVal"] = "abcdef", 44 | ["DateVal"] = DateTime.Now, 45 | ["SimpleObject"] = new { Name = "Foo",Age = 100 } 46 | } 47 | } 48 | }; 49 | 50 | var json = JobHelper.ToJson(list); 51 | Assert.NotNull(json); 52 | 53 | var o = JobHelper.FromJson>(json); 54 | 55 | Assert.Equal(1, o.Count); 56 | } 57 | [Theory] 58 | [InlineData("job1.json")] 59 | [InlineData("job2.json")] 60 | public void DeserializeRecurringJobJsonOptionsFromJsonFile(string jsonFile) 61 | { 62 | var dir = Directory.GetCurrentDirectory(); 63 | var jsonPath = Path.Combine(dir, jsonFile); 64 | Assert.True(File.Exists(jsonPath)); 65 | 66 | var jsonData = File.ReadAllText(jsonPath); 67 | 68 | var o = JobHelper.FromJson>(jsonData); 69 | 70 | Assert.Equal(2, o.Count); 71 | } 72 | 73 | /// 74 | /// https://github.com/icsharp/Hangfire.RecurringJobExtensions/issues/5 75 | /// 76 | [Fact] 77 | public void DeserializeRecurringJobJsonOptionsWithIssues5() 78 | { 79 | var data = File.ReadAllText("job1.json"); 80 | var o = JsonConvert.DeserializeObject>(data); 81 | Assert.Equal(2, o.Count); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/Hangfire.RecurringJobExtensions.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp1.1 5 | Hangfire.RecurringJobExtensions.Tests 6 | Hangfire.RecurringJobExtensions.Tests 7 | true 8 | $(PackageTargetFallback);dnxcore50 9 | 1.1.1 10 | false 11 | false 12 | false 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | Always 29 | 30 | 31 | Always 32 | 33 | 34 | Always 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/MyJob1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Hangfire.Server; 4 | 5 | namespace Hangfire.RecurringJobExtensions.Tests 6 | { 7 | public class MyJob1 : IRecurringJob 8 | { 9 | public void Execute(PerformContext context) 10 | { 11 | Debug.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} MyJob1 Running ..."); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/MyJob2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Hangfire.Server; 4 | 5 | namespace Hangfire.RecurringJobExtensions.Tests 6 | { 7 | public class MyJob2 : IRecurringJob 8 | { 9 | public void Execute(PerformContext context) 10 | { 11 | Debug.WriteLine($"{DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss")} MyJob2 Running ..."); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyConfiguration("")] 9 | [assembly: AssemblyCompany("")] 10 | [assembly: AssemblyProduct("Hangfire.RecurringJobExtensions.Tests")] 11 | [assembly: AssemblyTrademark("")] 12 | 13 | // Setting ComVisible to false makes the types in this assembly not visible 14 | // to COM components. If you need to access a type in this assembly from 15 | // COM, set the ComVisible attribute to true on that type. 16 | [assembly: ComVisible(false)] 17 | 18 | // The following GUID is for the ID of the typelib if this project is exposed to COM 19 | [assembly: Guid("b1aa8c91-161a-4a92-9b0a-b47742cddb38")] 20 | -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/RecurringJobAttributeTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | namespace Hangfire.RecurringJobExtensions.Tests 4 | { 5 | public class RecurringJobAttributeTest 6 | { 7 | [Fact] 8 | public void Ctor_ThrowsAnException_WhenCronIsNullOrEmpty() 9 | { 10 | Assert.Throws("cron", () => new RecurringJobAttribute(null)); 11 | Assert.Throws("cron", () => new RecurringJobAttribute("")); 12 | } 13 | [Fact] 14 | public void Ctor_ThrowsAnException_WhenQueueIsNullOrEmpty() 15 | { 16 | Assert.Throws("queue", () => new RecurringJobAttribute("* * * * *", null)); 17 | Assert.Throws("queue", () => new RecurringJobAttribute("* * * * *", "")); 18 | } 19 | [Fact] 20 | public void Ctor_ThrowsAnException_WhenTimeZoneIsNullOrEmpty() 21 | { 22 | Assert.Throws("timeZone", () => new RecurringJobAttribute("* * * * *", null, "default")); 23 | Assert.Throws("timeZone", () => new RecurringJobAttribute("* * * * *", "", "default")); 24 | } 25 | [Fact] 26 | public void Ctor_Normally() 27 | { 28 | var attr = new RecurringJobAttribute("* * * * *", "UTC", "default"); 29 | 30 | Assert.Equal("* * * * *", attr.Cron); 31 | Assert.Equal("UTC", attr.TimeZone); 32 | Assert.Equal("default", attr.Queue); 33 | Assert.Equal(TimeZoneInfo.Utc, TimeZoneInfo.FindSystemTimeZoneById(attr.TimeZone)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/error.json: -------------------------------------------------------------------------------- 1 | {"error_message":"N/A" } -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/job1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "job-name": "My Job1", 4 | "job-type": "Hangfire.RecurringJobExtensions.Tests.MyJob1, Hangfire.RecurringJobExtensions.Tests", 5 | "cron-expression": "*/1 * * * *", 6 | "timezone": "China Standard Time", 7 | "queue": "jobs" 8 | }, 9 | { 10 | "job-name": "My Job2", 11 | "job-type": "Hangfire.RecurringJobExtensions.Tests.MyJob2, Hangfire.RecurringJobExtensions.Tests", 12 | "cron-expression": "*/5 * * * *" 13 | } 14 | ] -------------------------------------------------------------------------------- /test/Hangfire.RecurringJobExtensions.Tests/job2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "job-name": "My Job1", 4 | "job-type": "Hangfire.RecurringJobExtensions.Tests.MyJob1, Hangfire.RecurringJobExtensions.Tests", 5 | "cron-expression": "*/1 * * * *", 6 | "timezone": "China Standard Time", 7 | "queue": "jobs" 8 | }, 9 | { 10 | "job-name": "My Job2", 11 | "job-type": "Hangfire.RecurringJobExtensions.Tests.MyJob2, Hangfire.RecurringJobExtensions.Tests", 12 | "cron-expression": "*/5 * * * *", 13 | "job-data": { 14 | "IntVal": 1, 15 | "StringVal": "abcdef", 16 | "BooleanVal": true, 17 | "SimpleObject": { 18 | "Name": "Foo", 19 | "Age": 100 20 | } 21 | } 22 | } 23 | ] --------------------------------------------------------------------------------