0%

[DotnetCore]排程神器-Hangfire:進階篇1

前提情要

上一篇講完Hangfire的簡單應用,這篇要來講解改造Dashboard的經驗,筆者公司專案,因為大系統,筆者把各個功能區塊分隔為各自專案,API站台Scheduler等專案,對於Hangfire所屬的Scheduler站台的DashboardAuthentication會變得困難,筆者的想法是乾脆就不要打開Dashboard或者是Dashboard本機瀏覽即可,然後於API站台對應的前台(Angular站台)中建立一個客製化的Hangfire DashboardAPI站台收到需求後轉發Request至Scheduler站台並實作Hangfire Dashboard相關API。

設計在前台有幾個好處:

  • 權限由原有權限去控
  • 開放手動觸發,可以記錄是哪位使用者執行
  • 更直觀的介面
  • CronExpression使用第三方套件轉成一般文字化的方式呈現

內容

先來實作Hangfire DashboardScheduler API

Scheduler專案改造

安裝CronExpression翻譯套件

1
dotnet add package CronExpressionDescriptor

定義RecurringJob的ResponseModel

基本上RecurringJob對應的Model滿複雜的,筆者這邊另外訂了一個,只宣告前端頁面需顯示的屬性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class JobInfoResponseModel
{
// JobId
public string Id { get; set; }
// Job最後執行狀態(Succeeded、Failed、Processing及空白)
public string LastJobState { get; set; }
// Job最後執行時間(UTC時間)
public DateTime? LastExecution { get; set; }
// Job最後執行時間(轉成Local-Taiwan時間)
public DateTime? LastExecutionLocalTime
{
get
{
return LastExecution?.ToLocalTime();
}
}
// Job下次執行時間(UTC時間)
public DateTime? NextExecution { get; set; }
// Job下次執行時間(轉成Local-Taiwan時間)
public DateTime? NextExecutionLocalTime
{
get
{
return NextExecution?.ToLocalTime();
}
}
// Job對應的Cron時間
public string Cron { get; set; }
// Job對應的Cron時間翻譯內容
public string CronDescription
{
get
{
var cronParser = new CronExpressionDescriptor.ExpressionParser(Cron, new Options()
{
Locale = " zh-Hant"
});
try
{
// 以防萬一翻譯錯誤,則不翻譯
cronParser.Parse();
return CronExpressionDescriptor.ExpressionDescriptor.GetDescription(Cron);
}
catch (FormatException ex)
{

return Cron;
}
}
}
}

撰寫取得Hangfire的RecurringJobsAction

接下來於Scheduler站台中寫一個Action,讓API站台呼叫,取得Hangfire Recurring Jobs資訊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class SchedulerInfoController : SchedulerBaseController
{
private readonly IConfiguration _config;
private string _connStr;
private readonly SqlConnectionFactory _factory;
public SchedulerInfoController(IConfiguration config, SqlConnectionFactory factory)
{
_config = config;
_connStr = Encoding.UTF8.GetString(Convert.FromBase64String(config.GetConnectionString("Hangfire")));
_factory = factory;
}

/// <summary>
/// 列出RecurringJobs
/// </summary>
/// <returns></returns>
[HttpPost("jobs")]
public IActionResult GetSchedulerJobs()
{
var result = new List<JobInfoResponseModel>();
var jobStorage = new SqlServerStorage(_connStr);
foreach (var item in StorageConnectionExtensions.GetRecurringJobs(jobStorage.GetConnection()))
{
result.Add(new JobInfoResponseModel()
{
Id = item.Id,
LastJobState = item.LastJobState,
LastExecution = item.LastExecution,
NextExecution = item.NextExecution,
Cron = item.Cron
});
}
return Ok(result);
}

/// <summary>
/// 立刻執行Job
/// </summary>
/// <param name="jobId"></param>
/// <returns></returns>
[HttpPost("execute")]
public IActionResult ExecuteJob(JobInfoRequestModel jobInfo)
{
// TODO: 設計資料表紀錄某登入者於某個時間點執行某個Job
var jobStorage = new SqlServerStorage(_connStr);
foreach (var item in StorageConnectionExtensions.GetRecurringJobs(jobStorage.GetConnection()))
{
if(item.Id == jobInfo.Id)
{
RecurringJob.Trigger(item.Id);
}
}
return Ok("");
}
}

API站台專案

筆者因主要排程使用Hangfire完成,且獨立為一個專案,API站台以這個需求,筆者需要一個定時Job去取得Hangfire的RecurringJobs資訊,並透過SingalR更新至前端,因此API站台中定時排程需求就使用短小精幹的Coravel來完成

撰寫SchedulerPageJob

因透過Coravel執行,實作IInvocable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SchedulerPageJob : IInvocable
{
private readonly HttpClient _client;
private readonly IHubContext<NoticeHub> _hubContext;
public SchedulerPageJob(HttpClient client, IHubContext<NoticeHub> hubContext)
{
_client = client;
_hubContext = hubContext;
}

public Task Invoke()
{
// Step1: Call Scheduler API
var result = _client.GetResponse<string, MyTokenResult, List<JobInfoResponseModel>>(
"", HttpAction.Post, "api/schedulerinfo/jobs", HttpStatusCode.OK
, out var rtnErrorCode, out var response, tokenNeeded: false
, mediaType: "application/json");
if (rtnErrorCode == HttpStatusCode.OK)
{
// Step2: Broadcast Result
_hubContext.Clients.All.SendAsync("SchedulerInfosResult"
, JsonConvert.SerializeObject(result));
}
return Task.CompletedTask;
}
}

其中可以看到_client為筆者自己寫的HttpClientExtension,可以忽略它,筆者再為它寫一篇吧,該段就是透過HttpClientScheduler站台中的API取得資訊,_hubContext則筆者在API站台中建立的SingalR Hub,透過Clients.All.SendAsync的方式Broadcast出去。

Startup.cs中設定定時器

最後於Startup中透過Coravel的設定,讓取得RecurringJobs資訊定時執行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void ConfigureServices(IServiceCollection services)
{
// 以上省略
services.AddScheduler();
// 以下省略
}

public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// 以上省略
app.ApplicationServices.UseScheduler(scheduler =>
{
// 筆者這邊設定每15秒執行一次
scheduler.Schedule<SchedulerPageJob>().EveryFifteenSeconds();
}).OnError((ex) =>
{
_logger.Error($"[Coravel:Job] {ex}");
});
// 以下省略
}

前端專案

筆者這邊是使用angular開發其前端頁面,因為主要這個呈現方式看個人,筆者就不在另外貼angular相關程式碼,筆者公司有買版型,因此算美觀了,主要概念是依照LastJobStateCardHeader的顏色有所不一樣

  • Succeded:藍色底
  • Failed:紅色底
  • Processing:黃色底
  • 空白則白色底

再加上若有Failed時使用相關Toast套件於右上角浮出,藉由達到提醒效果。

結論

透過改造Dashboard,筆者有機會看Hangfire的原始碼,因為Document實在是太難看,也沒辦法用F12追看程式碼相關性,索性就直接下載原始碼來觀看一下,才知道說那些物件有甚麼屬性,關聯是甚麼,才最終以完美呈現算美觀的客製化Dashboard,加上CronExpression的翻譯套件,整個完美。