diff --git a/Hangfire.drawio b/Hangfire.drawio new file mode 100644 index 0000000..3beeab8 --- /dev/null +++ b/Hangfire.drawio @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/HangfireExample.WebService/Controllers/JobController.cs b/HangfireExample.WebService/Controllers/JobController.cs new file mode 100644 index 0000000..af86580 --- /dev/null +++ b/HangfireExample.WebService/Controllers/JobController.cs @@ -0,0 +1,76 @@ +using Hangfire; +using HangfireExample.WebService.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel; + +namespace HangfireExample.WebService.Controllers +{ + [Route("Job")] + [ApiController] + public class JobController : ControllerBase, IJobService + { + public JobController() { } + + [HttpPost("Continuation", Name = "Continuation"), Produces("application/json")] + public string Continuation(string message, string jobId) + { + return BackgroundJob.ContinueJobWith(jobId, () => ContinuationJob(message, jobId)); + } + + [NonAction] + [DisplayName("Continuation, Job Id = {1}")] + public static void ContinuationJob(string message, string jobId) + { + Console.WriteLine("{0}: {1}", DateTimeOffset.Now, message); + } + + [HttpPost("Delay", Name = "Delay"), Produces("application/json")] + public string Delay(string message, int seconds, int minutes = 0, int hours = 0, int days = 0) + { + TimeSpan delay = + seconds > 0 ? TimeSpan.FromSeconds(seconds) : + minutes > 0 ? TimeSpan.FromMinutes(minutes) : + hours > 0 ? TimeSpan.FromHours(hours) : + days > 0 ? TimeSpan.FromDays(days) : throw new Exception("未指定延遲時間"); + return BackgroundJob.Schedule(() => DelayJob(message, seconds, minutes, hours, days), delay); + } + + [NonAction] + [DisplayName("Delay, Day = {4}, Hour = {3}, Minute = {2}, Second = {1}")] + public static void DelayJob(string message, int seconds, int minutes = 0, int hours = 0, int days = 0) + { + Console.WriteLine("{0}: {1}", DateTimeOffset.Now, message); + } + + [HttpPost("Fire", Name = "Fire"), Produces("application/json")] + public string Fire(string message) + { + return BackgroundJob.Enqueue(() => FireJob(message)); + } + + [NonAction] + [DisplayName("Fire")] + public static void FireJob(string message) + { + Console.WriteLine("{0}: {1}", DateTimeOffset.Now, message); + } + + [HttpPost("Recurring", Name = "Recurring"), Produces("application/json")] + public string Recurring(string message, string jobId, string expression) + { + RecurringJob.RemoveIfExists(jobId); + + RecurringJob.AddOrUpdate(jobId, () => _RecurringJob(message, jobId, expression), expression); + + return jobId; + } + + [NonAction] + [DisplayName("Recurring, JobId = {1}, Cron expression = {2}")] + public static void _RecurringJob(string message, string jobId, string expression) + { + Console.WriteLine("{0}: {1}", DateTimeOffset.Now, message); + } + } +} diff --git a/HangfireExample.WebService/Extensions/HangfireExtension.cs b/HangfireExample.WebService/Extensions/HangfireExtension.cs index 013efbe..0f26f85 100644 --- a/HangfireExample.WebService/Extensions/HangfireExtension.cs +++ b/HangfireExample.WebService/Extensions/HangfireExtension.cs @@ -1,5 +1,7 @@ using Hangfire; +using Hangfire.Dashboard; using Hangfire.Storage.SQLite; +using HangfireExample.WebService.Filters; namespace HangfireExample.WebService.Extensions { @@ -18,13 +20,37 @@ namespace HangfireExample.WebService.Extensions // 將 Hangfire 加入服務,使用 SQLite 作為儲存區 services.AddHangfire(configuration => configuration .UseSQLiteStorage("HangfireExample.db")); + // 將 Hangfire Server 加入服務 + services.AddHangfireServer(options => + { + // 指定 Hangfire Server 的名稱 + options.ServerName = "HangfireExample"; + // 指定 Hangfire Server 執行佇列的 Tags + options.Queues = ["default", "wb",]; + }); return services; } public static WebApplication UseHangfireDashboard(this WebApplication app, IConfiguration configuration) { - // HangfireDashboard 預設路由為 /Hangfire - app.UseHangfireDashboard(configuration.GetValue("DashboardRoot")); + // Hangfire Dashboard 預設路由為 /Hangfire + app.UseHangfireDashboard(configuration.GetValue("DashboardRoot"), new DashboardOptions + { + Authorization = [new DashboardAuthorizationFilter()], + // 指定 Dashboard 指定讀取,不能操作,如重新執行、再次加入排程與刪除任務等 + IsReadOnlyFunc = (DashboardContext context) => true, + // 指定 Back to site 的連結 + AppPath = "/swagger", + }); + // 設定第二資料來源的 Dashboard + //app.UseHangfireDashboard(configuration.GetValue("DashboardRoot"), new DashboardOptions + //{ + // Authorization = [new DashboardAuthorizationFilter()], + // // 指定 Dashboard 指定讀取,不能操作,如重新執行、再次加入排程與刪除任務等 + // IsReadOnlyFunc = (DashboardContext context) => true, + // // 指定 Back to site 的連結 + // AppPath = "/swagger", + //}, new SQLiteStorage("HangfireExample2.db")); return app; } } diff --git a/HangfireExample.WebService/Filters/DashboardAuthorizationFilter.cs b/HangfireExample.WebService/Filters/DashboardAuthorizationFilter.cs new file mode 100644 index 0000000..38c7e10 --- /dev/null +++ b/HangfireExample.WebService/Filters/DashboardAuthorizationFilter.cs @@ -0,0 +1,17 @@ +using Hangfire.Annotations; +using Hangfire.Dashboard; + +namespace HangfireExample.WebService.Filters +{ + /// + /// Hangfire Dashboard 的認證過濾中介。 + /// + public class DashboardAuthorizationFilter : IDashboardAuthorizationFilter + { + public bool Authorize([NotNull] DashboardContext context) + { + // 認證時,直接回傳成功(無驗證) + return true; + } + } +} diff --git a/HangfireExample.WebService/HangfireExample.WebService.csproj b/HangfireExample.WebService/HangfireExample.WebService.csproj index e5635c9..801b9ed 100644 --- a/HangfireExample.WebService/HangfireExample.WebService.csproj +++ b/HangfireExample.WebService/HangfireExample.WebService.csproj @@ -12,8 +12,4 @@ - - - - diff --git a/HangfireExample.WebService/HangfireExample.db b/HangfireExample.WebService/HangfireExample.db index 237a782..957072f 100644 Binary files a/HangfireExample.WebService/HangfireExample.db and b/HangfireExample.WebService/HangfireExample.db differ diff --git a/HangfireExample.WebService/Properties/launchSettings.json b/HangfireExample.WebService/Properties/launchSettings.json index 5c3f504..eff80e2 100644 --- a/HangfireExample.WebService/Properties/launchSettings.json +++ b/HangfireExample.WebService/Properties/launchSettings.json @@ -1,4 +1,24 @@ -{ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5185" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "hangfire", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + }, "$schema": "http://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, @@ -7,25 +27,5 @@ "applicationUrl": "http://localhost:1922", "sslPort": 0 } - }, - "profiles": { - "http": { - "commandName": "Project", - "dotnetRunMessages": true, - "launchBrowser": true, - "launchUrl": "swagger", - "applicationUrl": "http://localhost:5185", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "swagger", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } } -} +} \ No newline at end of file diff --git a/HangfireExample.WebService/Services/IJobService.cs b/HangfireExample.WebService/Services/IJobService.cs new file mode 100644 index 0000000..150fdc8 --- /dev/null +++ b/HangfireExample.WebService/Services/IJobService.cs @@ -0,0 +1,13 @@ +namespace HangfireExample.WebService.Services +{ + /// + /// Job 服務。 + /// + public interface IJobService + { + public string Fire(string message); + public string Delay(string message, int seconds, int minutes = 0, int hours = 0, int days = 0); + public string Recurring(string message, string jobId, string expression); + public string Continuation(string message, string jobId); + } +} diff --git a/HangfireExample.WebService/appsettings.json b/HangfireExample.WebService/appsettings.json index e1258a6..6203d99 100644 --- a/HangfireExample.WebService/appsettings.json +++ b/HangfireExample.WebService/appsettings.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Hangfire": "HangfireExample.db" + }, "Logging": { "LogLevel": { "Default": "Information", diff --git a/HangfireExample/Program.cs b/HangfireExample/Program.cs index 05417a7..8593808 100644 --- a/HangfireExample/Program.cs +++ b/HangfireExample/Program.cs @@ -1,4 +1,5 @@ using Hangfire; +using Hangfire.SqlServer; using Hangfire.Storage.SQLite; // 第三方擴充: Hangfire.Storage.SQLite 0.4.1 diff --git a/HangfireResources/BackToSite.jpg b/HangfireResources/BackToSite.jpg new file mode 100644 index 0000000..0de8efa Binary files /dev/null and b/HangfireResources/BackToSite.jpg differ diff --git a/README.md b/README.md index 3f891f1..3f8c98c 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,131 @@ GlobalConfiguration CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), QueuePollInterval = TimeSpan.Zero, - UseRecommendedIsolationLevel = true + UseRecommendedIsolationLevel = true, + DisableGlobalLocks = true, // Migration to Schema 7 is required }); + +// Hangfire SQLite +GlobalConfiguration + .Configuration + .UseSQLiteStorage("HangfireExample.db")); +``` + +## Server + +啟動 Hangfire 伺服器 + +``` +services.AddHangfireServer(); +``` + +## Using Dashboard UI + +開啟 Dashboard 功能 + +``` +app.UseHangfireDashboard(); +``` + +* Configuring Authorization + +設定自定義驗證 + +``` +app.UseHangfireDashboard("hangfire", new DashboardOptions +{ + Authorization = [new DashboardAuthorizationFilter()], +}); +``` + +* Read-only view + +設定 UI 為 ReadOnly + +``` +app.UseHangfireDashboard("hangfire", new DashboardOptions +{ + IsReadOnlyFunc = (DashboardContext context) => true, +}); +``` + +* Change URL Mapping + +修改 Dashboard UI 的預設根路由 + +``` +app.UseHangfireDashboard("hangfire"); +``` + +* Change Back to site Link + +修改返回按鈕的路由 + +``` +app.UseHangfireDashboard("hangfire", new DashboardOptions +{ + AppPath = "/swagger", // 導向到 Swagger UI +}); +``` + +![BackToSite](HangfireResources/BackToSite.jpg) + +* Multiple Dashboards + +設定不同資料來源的 Dashboard + +``` +app.UseHangfireDashboard(configuration.GetValue("DashboardRoot"), new DashboardOptions +{ + Authorization = [new DashboardAuthorizationFilter()], + // 指定 Dashboard 指定讀取,不能操作,如重新執行、再次加入排程與刪除任務等 + IsReadOnlyFunc = (DashboardContext context) => true, + // 指定 Back to site 的連結 + AppPath = "/swagger", +}, new SQLiteStorage("HangfireExample2.db")); +``` + +## Background Methods + +排程可以直接執行特定方法,Hangfire 會將方法(可使用非同步)、類型與參數序列化後保存到資料庫,Hangfire 依照排程類型,分為以下四種 + +* Fire-and-Forget jobs: 將任務丟入執行佇列,直接執行 + +``` +BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-Forget")); +``` + +* Delayed jobs: 延遲指定時間後,將任務丟入執行佇列 + +``` +BackgroundJob.Schedule(() => Console.WriteLine("Delayed"), TimeSpan.FromDays(1)); +``` + +* Recurring jobs: 依照指定間隔,將任務丟入執行佇列 + +``` +RecurringJob.AddOrUpdate("job_id", () => Console.Write("Recurring By CRON"), Cron.Daily); + +RecurringJob.AddOrUpdate("job_id", () => Console.Write("Recurring By CRON expressions"), "0 12 * */2"); +``` + +移除已存在的排程任務 + +``` +RecurringJob.RemoveIfExists("job_id"); +``` + +手動觸發已存在的排程任務 + +``` +RecurringJob.Trigger("job_id"); +``` + + +* Continuations: 指定的任務完成後,才丟入執行佇列 + +``` +var jobId = BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-Forget")); + +BackgroundJob.ContinueJobWith(jobid, () => Console.WriteLine("Continuations")); ``` \ No newline at end of file