Hangfire + SignalR 之做好回你

參考了前輩所分享有關hangefire的相關文章與官方文件,
除了提供排程性的工作如:
射後不理-Fire-and-forget ,
延遲-Delayed ,
定時-Recurring  處裡外.
還提供了
延續-Continuations

如果說射後不理是一次性作業.
那麼Continuations 則是射中目標後下一步該做甚麼!

 

如果較長且需冗長的作業我們又需要先得到快速的回應,
確實使用
射後不理-Fire-and-forget ,
延遲-Delayed ,
定時-Recurring
會達到API迅速回應的需求.
但如果希望每一個冗長的需求,
做完會有一個確實做完的進度回應,
又不希望Client等待,
這時候官方建議可以使用Continuations搭配SignalR


參考連結:Tracking the progress

以下情境與實作皆剛好肚子不爭氣的時候想到,
至於
射後不理-Fire-and-forget ,
延遲-Delayed ,
定時-Recurring
這三種方法的使用與Hangfire Dashboard的設定方式,
小弟在此就不多做描述.
會附上前輩們的文章連結請各位參考
再此如有描述錯誤的地方也請前輩多多指導...^^a

情境
這時候我想到,
小時候媽媽在煮飯.突然發現忘了買醬油,或雞蛋水果...等等之類的.
我想應該就喊著.大明幫我買個醬油,之後就忙著先處裡廚房別的事務.
等到大明把醬油買回來,接續其他的事務!

 

專案結構如下圖


Hanfire_Signalr
\Hubs\SampleHUB.cs

    public class SampleHUB : Hub
    {
        /// <summary>
        /// 提供HangFire_API作業完成後呼叫
        /// </summary>
        /// <param name="taskId"></param>
        /// <param name="who"></param>
        /// <param name="message"></param>
        public void Send(string taskId, string who, string message)
        {
            //提供receiveMessage,當作業完成讓Client端接收訊息
            Clients.All.receiveMessage(taskId, who, message);
        }
    }

HangFire_API
\EvenCls\BuysomethingCls.cs

    public class BuysomethingCls
    {
        /// <summary>
        /// 由WEB傳來的參數轉換為中文
        /// </summary>
        private static readonly Dictionary<string, string> transString = new Dictionary<string, string>() {
            {"buyEgg","雞蛋"},
            {"buydaoyo","醬油"},
            {"buyfruit","水果"}
        };
        /// <summary>
        /// 
        /// </summary>
        /// <param name="name"></param>
        /// <param name="message"></param>
        public void GobuySomething(string name, string message)
        {
            //Do Somthing 買醬油,買雞蛋,買水果....

        }
        /// <summary>
        /// 當GobuySomething事件處理完成後呼叫,將訊息推往至SignalR
        /// </summary>
        /// <param name="parentId"></param>
        /// <param name="name"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public async Task GoHome(string parentId, string name, string message)
        {
            string feedbackmessage = string.Format("阿母)))~{0},買回來了", transString.Where(w => w.Key == message).Select(s => s.Value).FirstOrDefault());
            using (HubConnection connection = new HubConnection("http://localhost:50925/habfireSR"))
            {
                IHubProxy myHub = connection.CreateHubProxy("SampleHUB");
                // 與SignalR連線
                await connection.Start().ContinueWith(task =>
                {
                    //連線成功時
                    if (!task.IsFaulted)
                    {
                        //呼叫SampleHUB中提供Send的方法,將TaskId,誰去買,跟訊息傳送過去
                        myHub.Invoke("Send", parentId, name, feedbackmessage);
                    }
                });

                connection.Stop();
            }
        }
    }

\Controllers\BuyOrderController.cs

    public IHttpActionResult hangefire( string name, string message)
        {
            BackgroundJobClient _jobs = new BackgroundJobClient();
            //新增一個作業
            var parentId = _jobs.Enqueue<BuysomethingCls>(x => x.GobuySomething(name, message));
            //作業完成後執行
            _jobs.ContinueWith<BuysomethingCls>(parentId, x => x.GoHome(parentId, name, message));
            //回傳TaskId
            return Ok(parentId);
        }




 public IHttpActionResult taskcontinuation(string name, string message)
        {

            BuysomethingCls buyCls = new BuysomethingCls();
            //新增一個作業
            var firstTask = Task.Run(() => {
                buyCls.GobuySomething(name, message);
            });
            //作業完成後執行
            Task continuationTask = firstTask.ContinueWith(async (antecedent) =>
            {
                int parentId = antecedent.Id;
                await buyCls.GoHome(parentId.ToString(), name, message);
            });
            //回傳TaskId
            return Ok(firstTask.Id);
        }

在上述中hangefire與taskcontinuation的方法中,
功能上是相同的,
只是hangefire此方法流程上會與taskcontinuation有些差異如官網提供的下圖

當我們每發一次請求,就等於是新增一個作業存入在Job Storage(MongoDB,MSSQL,Memory),透過Hangfire Sever在做背景處理!
所以我們可以透過Hangfire所提供的Hangfire Dashboard查看作業是否有順利完成,甚至是否有錯誤需要重新執行等等..如下圖

http://your Domain/hangfire


Hangfire_web_client
\Views\Home\Index.cshtml

    <script>
        /*暫存由HangFire_API回傳的Task ID*/
        Globallocalstory = [];
        /*呼叫HangFire_API 位置*/
        GlobalAPILocation = { BuyOrder: function (whosend, doshomthing) { return 'http://localhost:56321/api/BuyOrder/hangfire' + "/" + whosend + "/" + doshomthing } };
        //GlobalAPILocation = { BuyOrder: function (whosend, doshomthing) { return 'http://localhost:56321/api/BuyOrder/taskcontinuation' + "/" + whosend + "/" + doshomthing } };

        var clientGobusomthing = {
            /*按鈕觸發事件*/
            ButtonClickEvent: function (obj) {

                $(obj).attr('disabled', 'true');

                var selectele = $(obj).parent().find('select');
                var whobuyele = $(obj).parent().find('b1');
                var eleTextid = $(obj).next().attr('id');

                var whobuytext = $(whobuyele).text();
                var selecteleVal = $(selectele).val();
                $.get(GlobalAPILocation.BuyOrder(whobuytext, selecteleVal), clientGobusomthing.ajaxCallBack.bind({ whobuy: whobuytext, selectvalue: selecteleVal, textId: eleTextid }));

            },
            /*取回API回傳Call Back事件*/
            ajaxCallBack: function (data) {
                var addobj = { taskid: data, whobuy: this.whobuy, selecteleVal: this.selectvalue, textid: this.textId };
                Globallocalstory.push(addobj);
                clientGobusomthing.UpdateTextUI(this.textId, '出發中~~');

            },
            /*註冊接收SignalR回傳訊息*/
            ReceverSignalrMsg: function (taskId, who, message) {

                var result = $.grep(Globallocalstory, function (obj) { return obj.taskid == taskId });

                if (result.length > 0) {
                    clientGobusomthing.UpdateTextUI(result[0].textid, message);
                    var findIndes = Globallocalstory.findIndex(function (obj) { return obj.taskid == result[0].taskid; });
                    Globallocalstory.splice(findIndes, 1);

                    $('#' + result[0].textid).parents().find('button').attr('disabled', false);

                }

            },
            /*更新UI文字*/
            UpdateTextUI: function (tagId, msg) {
                $('#' + tagId).text(msg);
            }
        };

        $(function () {

            $.connection.hub.url = "http://localhost:50925/habfireSR"
            var hfhub = $.connection.sampleHUB;
            hfhub.client.receiveMessage = clientGobusomthing.ReceverSignalrMsg;
            $.connection.hub.start({ jsonp: true });
        });
    </script>

Client端的流程就較為單純,
呼叫API將回傳的Task ID存入暫存Globallocalstory中,
然後等待SignalR將訊息回傳告知作業已完成,
並刪除Globallocalstory中暫存的資料!

畫面如下


呼叫HangFire_API取得發出請求的Task Id暫存在Globallocalstory中


等待Signalr通知,比對接回來的Task Id是否有在Globallocalstory暫存中,存在的話刪除Globallocalstory內的ID並顯示X明買回來的

最後聽到老媽的碎念....夜深了該睡了...冏..


在此附上原始檔:請點我

參考連結:

Tracking the progress

Tutorial: Getting Started with SignalR 2

C# 學習筆記:多執行緒 (6) - TPL

[ASP.NET]使用 Hangfire 來處理非同步的工作

使用HANGFIRE處理ASP.NET MVC/WEB API長時間與排程工作