C# - System.Net.Http.HttpClient擴充

  • 707
  • 0
  • C#
  • 2019-04-09

雖然微軟已經公布了新一代的HttpClient (Windows.Web)並解決了不少既有的問題,但由於專案的整合度與舊架構的相容性等問題,上一個世代的HttpClient (System.Net.Http)仍然還是會用到,本文將介紹System.Net.Http.HttpClient的功能擴充,分別是處理【插入API呼叫(時)與結束時事件處理】與【解決Cookie不共用的問題】。

前言

請注意這是System.Net.Http.HttpClient

開始實作

1.【插入API呼叫(時)與結束時事件處理】,由於程式執行時遇到API讀取狀態時使用者總是會要求要弄出一個"讀取中"的小視窗。身為一個對上下班時間嚴謹的工程師,提出的解法絕對不是在每一段程式碼裡面包個甚麼 await Task.WhenAll之類,因為這樣後續調整程式會搞死自己。因此直接從根本源頭下手,"呼叫時API一併將事件處理放入不就好了?",由此想法出發後一切豁然開朗了起來,就冒出了下面這段東西。

*主要重點為【確保檢查非同步處理只有一個】【實際處理事件應由外部指派(委派/設定)】

    public delegate void VoidDelegate();
    public partial class HttpClientPlus : HttpClient
    {
        public HttpClientPlus(VoidDelegate loadingProcessing, VoidDelegate loadingEnd) : base()
        {
            OnLoadingProcessing = loadingProcessing;
            OnLoadingEnd = loadingEnd;
        }
        public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
        {
            Task<HttpResponseMessage> responseMsg = base.SendAsync(request);
            LoadingProcess(responseMsg);
            return responseMsg;
        }
        public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
        {
            Task<HttpResponseMessage> responseMsg = base.SendAsync(request, completionOption, cancellationToken);
            LoadingProcess(responseMsg);
            return responseMsg;
        }
    }
    partial class HttpClientPlus
    {
        object _Locker { get; }
            = new object();
        List<Task<HttpResponseMessage>> _LoadingTasks { get; set; }
            = new List<Task<HttpResponseMessage>>();
        Task _DetectLoadingLoop { get; set; }
            = null;

        event VoidDelegate OnLoadingProcessing;
        event VoidDelegate OnLoadingEnd;
        void LoadingProcess(Task<HttpResponseMessage> responseMsg)
        {
            _LoadingTasks.Add(responseMsg);
            lock (_Locker)
            {
                if (_DetectLoadingLoop == null)
                {
                    _DetectLoadingLoop = CreateDetectLoadingLoop();
                    _DetectLoadingLoop.ContinueWith(task =>
                    {
                        _DetectLoadingLoop = null;
                    });
                }
            }
        }
        Task CreateDetectLoadingLoop()
        {
            const int msDetectIterval = 300;
            return Task.Run(() =>
            {
                while (_LoadingTasks.Any())
                {
                    OnLoadingProcessing?.Invoke();
                    Thread.Sleep(msDetectIterval);
                    _LoadingTasks.RemoveAll(task =>
                           task.IsCanceled
                        || task.IsFaulted
                        || task.Status == TaskStatus.RanToCompletion);
                }
                OnLoadingEnd?.Invoke();
            });
        }
    }

簡單測試

2.【解決Cookie不共用的問題】,每一個 HttpClient 在實體化時,如果沒有特別指派CookieContainer,那個各自的Cookie就會獨立的。因此當多個Api所使用的登入驗證為同一個時(如 Forms Authentication ),如果不小心HttpClient沒有保持單一的話就會毫無懸念的炸掉(因Cookie不共用,自然發出request時不會夾帶授權資訊)。其實這部分就只要稍微改寫一下原本HtppClient的宣告。就可以使其不管怎樣都是共用同一個Cookie。

    public partial class HttpClientPlus : HttpClient
    {
        static HttpClientHandler _Handler { get; }
            = new HttpClientHandler { CookieContainer = new CookieContainer() };
        public HttpClientPlus(VoidDelegate loadingProcessing, VoidDelegate loadingEnd) : base(_Handler)
        {
            OnLoadingProcessing = loadingProcessing;
            OnLoadingEnd = loadingEnd;
        }
    }

 

備註

*請注意這是System.Net.Http.HttpClient

*你說跳出"讀取中的視窗"好像忘了寫?原因是那個也挺複雜的,要抽出成獨立一篇文章介紹。(由於彈跳出的視窗建立執行續不同,也必須不同,因此操作起來有很多需注意的事項)

*根據MSDN(https://docs.microsoft.com/en-us/uwp/api/windows.web.http),Windows.Web.Http.HttpClient只支援UWP app (Nuget上也沒有)

*基本上是為了NSwag寫得沒錯。

 

延伸閱讀

https://channel9.msdn.com/Events/Build/2013/4-092

 

備註的備註

*當然後續會把那個讀取事件抽出來成為元件,畢竟實際應用程式需要的資料來源可能不只有單單Api,還可能要與執行的電腦進行其他資料介接(連線)。

    public interface ILoadingProcess
    {
        void AddTask(Task task);
    }
    public partial class LoadingProcess : ILoadingProcess
    {
        public LoadingProcess(VoidDelegate loadingProcessing, VoidDelegate loadingEnd)
        {
            OnLoadingProcessing = loadingProcessing;
            OnLoadingEnd = loadingEnd;
        }
        public void AddTask(Task taskAdd)
        {
            _LoadingTasks.Add(taskAdd);
            lock (_Locker)
            {
                if (_DetectLoadingLoop == null)
                {
                    _DetectLoadingLoop = CreateDetectLoadingLoop();
                    _DetectLoadingLoop.ContinueWith(task =>
                    {
                        _DetectLoadingLoop = null;
                    });
                }
            }
        }
    }
    partial class LoadingProcess
    {
        object _Locker { get; }
            = new object();
        List<Task> _LoadingTasks { get; set; }
            = new List<Task>();
        Task _DetectLoadingLoop { get; set; }
            = null;
        event VoidDelegate OnLoadingProcessing;
        event VoidDelegate OnLoadingEnd;
        Task CreateDetectLoadingLoop()
        {
            const int msDetectIterval = 300;
            return Task.Run(() =>
            {
                while (_LoadingTasks.Any())
                {
                    OnLoadingProcessing?.Invoke();
                    Task.Delay(msDetectIterval).Wait();
                    _LoadingTasks.RemoveAll(task =>
                           task.IsCanceled
                        || task.IsFaulted
                        || task.Status == TaskStatus.RanToCompletion);
                }
                OnLoadingEnd?.Invoke();
            });
        }
    }
    public partial class HttpClientPlus : HttpClient
    {
        protected ILoadingProcess LoadingProcess { get; }
        public HttpClientPlus(ILoadingProcess loadingProcess) : base()
        {
            LoadingProcess = loadingProcess;
        }
        public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
        {
            Task<HttpResponseMessage> responseMsg = base.SendAsync(request);
            LoadingProcess.AddTask(responseMsg);
            return responseMsg;
        }
        public new Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
        {
            Task<HttpResponseMessage> responseMsg = base.SendAsync(request, completionOption, cancellationToken);
            LoadingProcess.AddTask(responseMsg);
            return responseMsg;
        }
    }
    partial class HttpClientPlus
    {
        static HttpClientHandler _Handler { get; }
            = new HttpClientHandler { CookieContainer = new CookieContainer() };
    }