2017年10月16日 星期一

C# HttpClient WebAPI : 1. 研究專題介紹

這是一篇關於如何在用戶端 (Client) 使用 HttpClient 類別的說明系列文章,在這裡我們將會說明如何使用 HttpClient 所建立的物件,進行遠端 Web API 的 Get / Post / Put / Delete 等動作的呼叫與接收回傳結果,當然,我們也會說明如何使用 QueryString / Form-Data / Multi-Part / JSON 等不同格式,將用戶端的資料,傳遞到遠端 Web API 的使用方式;除了一般純文字的 Web API 內容呼叫與回傳,我們在這裡也會進行如何處理 Binary 二進位的資料上傳與取得,我們將會說明如何上傳圖片檔案到遠端 Web API 伺服器上與如何使用 HttpClient 取得遠端 Web Server 上的圖片檔案,接著儲存到本機上。
當然,我們也需要了解如何從用戶端傳送 Http Header 與 Cookie 到遠端 Web API上,接著,如何在用戶端中,取得 Web API回報的 Cookie 與 Header 資料;尤其,我們也會展示如何顯示 HttpClient 下載大量資料的時候的進度回報事件設計方式與如何取消較長時間的 HttpClient 呼叫,最後,我們將會設定一個逾時時間,當執行 HttpClient 的 Web API 執行期間,花費時間超過我們所設定的時間,將會終止這樣的 Web API 呼叫之程式寫法。
這篇系列文章的所有使用到的專案原始碼,您可以從 https://github.com/vulcanlee/CSharpNotes/tree/master/WebAPI 取得

了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式


HttpClient 系列文章索引


      1. +
      2. C# HttpClient WebAPI : 13. 上傳本機圖片檔案到遠端伺服器上
      3. C# HttpClient WebAPI : 17. 有趣的 HttpClient 管道,自訂 HttpMessage Handler
      4. C# HttpClient WebAPI : 18. 套用 ProgressMessageHandler ,訂閱請求與回應的資料傳輸事件

        後端 Web API 的原始碼

        底下將會是我們要使用到的 Web API 專案原始碼,在這裡,我們採用的是 ASP.NET Core Web API 類型專案來開發,並且將這個 Web API 專案佈署到 Azure 平台上。在每個 HttpClient 研究文章中,將也會針對所使用的後端 Web API 方法,進行進一步的說明。
        底下是絕大部分的 Web API 控制器 ValuesController 的程式碼:
        namespace VulcanWebAPI.Controllers
        {
            [Route("api/[controller]")]
            public class ValuesController : Controller
            {
                // http://vulcanwebapi.azurewebsites.net/api/values
                [HttpGet]
                public APIResult Get()
                {
                    APIResult foo = new APIResult()
                    {
                        Success = true,
                        Message = "成功得所有資料集合",
                        Payload = new List<APIData>()
                        {
                            new APIData()
                            {
                                Id =777,
                                Name = "Vulcan01"
                            },
                            new APIData()
                            {
                                Id =234,
                                Name ="Vulcan02"
                            }
                        }
                    };
                    return foo;
                }
        
                [HttpGet("QueryStringGet")]
                public APIResult QueryStringGet([FromQuery] APIData value)
                {
                    APIResult foo;
                    if (value.Id == 777)
                    {
                        foo = new APIResult()
                        {
                            Success = true,
                            Message = "透過 Get 方法,接收到 Id=777",
                            Payload = new APIData()
                            {
                                Id = 777,
                                Name = "Vulcan by QueryStringGet"
                            }
                        };
                    }
                    else
                    {
                        foo = new APIResult()
                        {
                            Success = false,
                            Message = "無法發現到指定的 ID",
                            Payload = null
                        };
                    }
                    return foo;
                }
        
                [HttpGet("GetException")]
                public APIResult GetException()
                {
                    APIResult foo = new APIResult();
                    throw new Exception("喔喔,我發生錯誤了");
                    return foo;
                }
        
                [HttpGet("GetExceptionFilter")]
                [CustomExceptionFilter]
                public APIResult GetExceptionFilter()
                {
                    APIResult foo = new APIResult();
                    throw new Exception("喔喔,我發生錯誤了");
                    return foo;
                }
        
                // http://vulcanwebapi.azurewebsites.net/api/values/777
                [HttpGet("{id}")]
                public APIResult Get(int id)
                {
                    APIResult foo;
                    if (id == 777)
                    {
                        foo = new APIResult()
                        {
                            Success = true,
                            Message = "透過 Get 方法,接收到 Id=777",
                            Payload = new APIData()
                            {
                                Id = 777,
                                Name = "Vulcan01"
                            }
                        };
                    }
                    else
                    {
                        foo = new APIResult()
                        {
                            Success = false,
                            Message = "無法發現到指定的 ID",
                            Payload = null
                        };
                    }
                    return foo;
                }
        
                // http://vulcanwebapi.azurewebsites.net/api/Values
                // 使用 JSON 格式
                [HttpPost]
                public APIResult Post([FromBody]APIData value)
                {
                    APIResult foo;
        
                    if (value.Id == 777)
                    {
                        foo = new APIResult()
                        {
                            Success = true,
                            Message = "透過 post 方法,接收到 Id=777 資料",
                            Payload = value
                        };
                    }
                    else
                    {
                        foo = new APIResult()
                        {
                            Success = false,
                            Message = "無法發現到指定的 ID",
                            Payload = null
                        };
                    }
                    return foo;
                }
        
                // http://vulcanwebapi.azurewebsites.net/api/Values/FormUrlencodedPost
                // 使用 FormUrlEncodedContent
                [HttpPost("FormUrlencodedPost")]
                public APIResult FormUrlencodedPost([FromForm]APIData value)
                {
                    APIResult foo;
        
                    if (value.Id == 777)
                    {
                        foo = new APIResult()
                        {
                            Success = true,
                            Message = "透過 post 方法,接收到 Id=777 資料",
                            Payload = value
                        };
                    }
                    else
                    {
                        foo = new APIResult()
                        {
                            Success = false,
                            Message = "無法發現到指定的 ID",
                            Payload = null
                        };
                    }
                    return foo;
                }
        
                [HttpPut]
                public APIResult Put(int id, [FromBody]APIData value)
                {
                    APIResult foo;
        
                    if (value.Id == 777)
                    {
                        foo = new APIResult()
                        {
                            Success = true,
                            Message = "透過 Put 方法,接收到 Id=777 資料",
                            Payload = value
                        };
                    }
                    else
                    {
                        foo = new APIResult()
                        {
                            Success = false,
                            Message = "無法發現到指定的 ID",
                            Payload = null
                        };
                    }
                    return foo;
                }
        
                [HttpDelete("{id}")]
                public APIResult Delete(int id)
                {
                    APIResult foo;
        
                    if (id == 777)
                    {
                        foo = new APIResult()
                        {
                            Success = true,
                            Message = "Id=777 資料 已經刪除了",
                            Payload = null
                        };
                    }
                    else
                    {
                        foo = new APIResult()
                        {
                            Success = false,
                            Message = "無法發現到指定的 ID",
                            Payload = null
                        };
                    }
                    return foo;
                }
        
                // http://vulcanwebapi.azurewebsites.net/api/values/HeaderPost
                [HttpPost("HeaderPost")]
                public APIResult HeaderGet([FromBody]LoginInformation loginInformation)
                {
                    APIResult foo;
                    StringValues VerifyCode = "";
        
                    this.HttpContext.Request.Headers.TryGetValue("VerifyCode", out VerifyCode);
                    if (StringValues.IsNullOrEmpty(VerifyCode))
                    {
                        foo = new APIResult()
                        {
                            Success = false,
                            Message = "驗證碼沒有發現",
                            Payload = null
                        };
                    }
                    else
                    {
                        if (VerifyCode != "123")
                        {
                            foo = new APIResult()
                            {
                                Success = false,
                                Message = "驗證碼不正確",
                                Payload = null
                            };
                        }
                        else
                        {
                            if (loginInformation.Account == "Vulcan" &&
                                loginInformation.Password == "123")
                            {
                                foo = new APIResult()
                                {
                                    Success = true,
                                    Message = "這個帳號與密碼正確無誤",
                                    Payload = null
                                };
                            }
                            else
                            {
                                foo = new APIResult()
                                {
                                    Success = false,
                                    Message = "這個帳號與密碼不正確",
                                    Payload = null
                                };
                            }
                        }
                    }
                    return foo;
                }
        
                [HttpPost("Login")]
                public async Task<APIResult> Login([FromBody]LoginInformation loginInformation)
                {
                    APIResult foo;
        
                    if (loginInformation.Account == "Vulcan" &&
                        loginInformation.Password == "123")
                    {
                        var claims = new List<Claim>() {
                        new Claim(ClaimTypes.Name, "Herry"),
                        new Claim(ClaimTypes.Role, "Users")
                    };
                        var claimsIdentity = new ClaimsIdentity(claims, "myTest");
                        var principal = new ClaimsPrincipal(claimsIdentity);
                        try
                        {
                            await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
                                principal,
                                new AuthenticationProperties
                                {
                                    ExpiresUtc = DateTime.UtcNow.AddMinutes(20),
                                    IsPersistent = true,
                                    AllowRefresh = true
                                });
                            foo = new APIResult()
                            {
                                Success = true,
                                Message = "這個帳號與密碼正確無誤",
                                Payload = null
                            };
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                            foo = new APIResult()
                            {
                                Success = false,
                                Message = "這個帳號與密碼不正確",
                                Payload = null
                            };
                        }
                    }
                    else
                    {
                        foo = new APIResult()
                        {
                            Success = false,
                            Message = "這個帳號與密碼不正確",
                            Payload = null
                        };
                    }
        
                    return foo;
                }
        
                [Authorize]
                [HttpGet("LoginCheck")]
                public APIResult LoginCheck()
                {
                    APIResult foo = new APIResult()
                    {
                        Success = true,
                        Message = "成功得所有資料集合",
                        Payload = new List<APIData>()
                        {
                            new APIData()
                            {
                                Id =777,
                                Name = "Vulcan01"
                            },
                            new APIData()
                            {
                                Id =234,
                                Name ="Vulcan02"
                            }
                        }
                    };
                    return foo;
                }
        
                [HttpGet("LongTimeGet")]
                public async Task<APIResult> LongTimeGet()
                {
                    APIResult foo;
        
                    await Task.Delay(5000);
                    foo = new APIResult()
                    {
                        Success = true,
                        Message = "透過 Get 方法",
                        Payload = new APIData()
                        {
                            Id = 777,
                            Name = "Vulcan01"
                        }
                    };
                    return foo;
                }
        
            }
        }
        
        底下將是圖片上傳的 UploadController 控制器原始碼:
        namespace VulcanWebAPI.Controllers
        {
            [Route("api/[controller]")]
            public class UploadController : Controller
            {
                APIResult fooAPIResult = new APIResult();
                private IHostingEnvironment _HostingEnvironment;
        
                public UploadController(IHostingEnvironment hostingEnvironment)
                {
                    _HostingEnvironment = hostingEnvironment;
                }
        
                [HttpPost]
                public async Task<APIResult> Post(List<IFormFile> files)
                {
                    // https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads
        
                    string webDatasRoot = Path.Combine(_HostingEnvironment.WebRootPath, "Datas");
        
                    long size = files.Sum(f => f.Length);
        
                    // full path to file in temp location
        
                    if (files.Count > 0)
                    {
                        foreach (var formFile in files)
                        {
                            if (formFile.Length > 0)
                            {
                                var filePath = Path.Combine(webDatasRoot, formFile.FileName);
                                using (var stream = new FileStream(filePath, FileMode.Create))
                                {
                                    await formFile.CopyToAsync(stream);
                                }
        
                                fooAPIResult.Success = true;
                                fooAPIResult.Message = "檔案上傳成功";
                                fooAPIResult.Payload = new APIData
                                {
                                    Id = 3000,
                                    Name = "Your Name",
                                    Filename = formFile.FileName
                                };
                            }
                        }
                    }
                    else
                    {
                        fooAPIResult.Success = false;
                        fooAPIResult.Message = "沒有任何檔案上傳";
                        fooAPIResult.Payload = null;
                    }
        
                    return fooAPIResult;
                }
        
        
                [HttpPost("FileAndData")]
                public async Task<APIResult> FileAndData(List<IFormFile> files, LoginInformation loginInformation)
                {
                    // https://docs.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads
        
                    string webDatasRoot = Path.Combine(_HostingEnvironment.WebRootPath, "Datas");
        
                    long size = files.Sum(f => f.Length);
        
                    // full path to file in temp location
                    if (files.Count > 0)
                    {
                        foreach (var formFile in files)
                        {
                            if (formFile.Length > 0)
                            {
                                using (var memoryStream = new MemoryStream())
                                {
                                    await formFile.CopyToAsync(memoryStream);
                                    memoryStream.Seek(0, SeekOrigin.Begin);
                                    var streamReader = new StreamReader(memoryStream);
                                    var fooContent = streamReader.ReadToEnd();
        
                                    fooAPIResult.Success = true;
                                    fooAPIResult.Message = "檔案上傳成功";
                                    fooAPIResult.Payload = new LoginInformation
                                    {
                                        Account = $">> {loginInformation.Account}",
                                        Password = $">> {loginInformation.Account}",
                                        VerifyCode = fooContent
                                    };
                                }
                            }
                        }
                    }
                    else
                    {
                        fooAPIResult.Success = false;
                        fooAPIResult.Message = "沒有任何檔案上傳";
                        fooAPIResult.Payload = null;
                    }
        
                    return fooAPIResult;
                }
            }
        }

        了解更多關於 [HttpClient Class] 的使用方式
        了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式

        關於 Xamarin 在台灣的學習技術資源

        Xamarin 實驗室 粉絲團
        歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
        Xamarin.Forms @ Taiwan
        歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
        Xamarin 實驗室 部落格
        Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
        Xamarin.Forms 系列課程
        Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程



        2017年10月15日 星期日

        C# 非同步程式設計的 async void 與 async Task 的差異

        若你正在觀看此篇文章,那麼你將會對於 為什麼需要使用非同步程式設計,真的可以提升整體應用程式的執行效能嗎? 問題更有興趣的。

        今天,想來談談絕大部分的 C# 開發者都會產生這樣的不正確的 C# 程式碼寫法與用法,那就是當我們採用 工作式非同步模式 (TAP, Task-based Asynchronous Pattern) 進行非同步的程式開發的時候,並且想要寫個非同步運作的分法,在很多時候,您會想說,底下的方法,就是一個非同步運作的程式碼方法。

        了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式
        了解更多關於 [Thread Class] 的使用方式
        了解更多關於 [Task Class] 的使用方式


        在這裡,我們在方法 M1 前面使用了 async 修飾詞,不過,這個方法並沒有任何物件值需要回傳,因此,在底下的程式碼中,您看到了這個方法使用了 async void
        在工作式非同步模式的方法中,回傳值的類型僅有 Task / Task<T>,前者代表沒有任何實際物件要回傳,而後者表示要回傳型別為 T 的物件;不過,那我們為什麼又可以使用 void 這個關鍵字呢?通常,我們會在綁定事件的方法上,需要使用 async void 這樣的標示,這是因為這些事件的函式簽章就是僅支援 void 這樣的回傳值,另外,我們在這個綁定的事件方法中,又需要撰寫工作式非同步模式的功能,因此,又需要在這個事件方法前面,加入 async 這個修飾詞。
        所以,除了是在綁定事件的方法內,其他任何情況下,對於您開發的工作式非同步模式方法,就不需要使用async void這樣的回傳值標示。
        上面的說明,很重要,很重要,很重要
        public async void M1()
        {
            var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
            Console.WriteLine($"M1 開始時間 : {foo}");
            await Task.Delay(3000);
            foo = DateTime.UtcNow.ToString("mm:ss.ffff");
            Console.WriteLine($"M1 結束時間 : {foo}");
        }
        
        現在,讓我們來看看這個方法實際使用的情況,首先,我們定義一個類別 A,它的定義如下程式碼,其中, M1 是我們使用 async void 的方式建立的方法,而 M2,則是正常的工作式非同步模式方法的程式碼寫法。
        class A
        {
            public async void M1()
            {
                var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M1 開始時間 : {foo}");
                await Task.Delay(3000);
                foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M1 結束時間 : {foo}");
            }
        
            public async Task M2()
            {
                var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M2 開始時間 : {foo}");
                await Task.Delay(3000);
                foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M2 結束時間 : {foo}");
            }
        }
        
        M1這個方法,會先顯示這個方法的開始執行時間,接著,便會暫停3秒鐘,接著便會顯示結束執行這個方法的時間。
        在測試程式碼中,我們先建立一個類別A的物件 objA,接著呼叫 objA.M1() 方法,最後,等候使用按下任一按鍵。
        class Program
        {
            static async Task Main(string[] args)
            {
                A objA = new A();
        
                objA.M1();
                Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
                Console.ReadKey();
            }
        }
        
        上面的程式碼執行結果如底下所示,你可以看到,當呼叫 M1 方法之後,接著, M1 方法似乎就以另外一個執行緒的方式進行執行,所以, Main 這個方法內的程式碼,沒有等到 M1 執行完畢,就直接執行 objA.M1() 之後的所有敘述,而當三秒鐘之後,就會顯示 M1 方法已經結束的時間。
        這是因為我們使用了 async void 的方式來宣告這個方法,因此,我們沒有任何方法,可以等候 M1 方法執行完成之後,才要繼續執行 objA.M1() 之後的所有敘述。
        M1 開始時間 : 09:23.1819
        Press any key to Exist...
        
        M1 結束時間 : 09:26.2171
        
        現在,讓我們修改測試程式碼,我們在呼叫 M1 方法前後,與在 M1 方法內,當要呼叫 await Task.Delay(3000); 的前後,都顯示出當時執行緒的 ID (這裡顯示的是受管理的執行緒ID) Thread.CurrentThread.ManagedThreadId
        class A
        {
            public async void M1()
            {
                var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M1 開始時間 : {foo}");
                Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
                await Task.Delay(3000);
                Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
                foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M1 結束時間 : {foo}");
            }
        
            public async Task M2()
            {
                var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M2 開始時間 : {foo}");
                await Task.Delay(3000);
                foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M2 結束時間 : {foo}");
            }
        }
        class Program
        {
            static async Task Main(string[] args)
            {
                A objA = new A();
        
                Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
                objA.M1();
                Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
                Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
                Console.ReadKey();
            }
        }
        
        此時,執行結果將會如下所示,您會看到,當 M1 的方法內,其 await Task.Delay(3000); 敘述執行完畢之後,此時的執行緒 ID 變成了 4。
        Main 1
        M1 開始時間 : 20:55.2604
        M1 1
        Main 1
        Press any key to Exist...
        
        M1 4
        M1 結束時間 : 20:58.2897
        
        最後,讓我們加入 M2 的方法呼叫,對於要呼叫 M2 的非同步方法,我們使用了 await 關鍵字 await objA.M2();
        class A
        {
            public async void M1()
            {
                var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M1 開始時間 : {foo}");
                Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
                await Task.Delay(3000);
                Console.WriteLine($"M1 {Thread.CurrentThread.ManagedThreadId}");
                foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M1 結束時間 : {foo}");
            }
        
            public async Task M2()
            {
                var foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M2 開始時間 : {foo}");
                await Task.Delay(1000);
                foo = DateTime.UtcNow.ToString("mm:ss.ffff");
                Console.WriteLine($"M2 結束時間 : {foo}");
            }
        }
        class Program
        {
            static async Task Main(string[] args)
            {
                A objA = new A();
        
                Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
                objA.M1();
                await objA.M2();
                Console.WriteLine($"Main {Thread.CurrentThread.ManagedThreadId}");
                Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
                Console.ReadKey();
            }
        }
        
        底下將為這個測試程式碼的執行輸出結果,我們可以看到, objA.M1() 方法一執行到 await Task.Delay(3000);敘述之後,就立即返回到 Main 方法內,接著執行 await objA.M2();,不過,此時,整個程式將會等候到 objA.M2() 方法執行完後,才會繼續進行下去,而在這個時候, M1 的方法,也持續在等候 Task.Delay 的甦醒時間
        ain 1
        M1 開始時間 : 24:53.7485
        M1 1
        M2 開始時間 : 24:53.7645
        M2 結束時間 : 24:54.7670
        Main 4
        Press any key to Exist...
        
        M1 5
        M1 結束時間 : 24:56.7678



        對於已經具備擁有 .NET / C# 開發技能的開發者,可以使用 Xamarin.Forms Toolkit 開發工具,便可以立即開發出可以在 Android / iOS 平台上執行的 App;對於要學習如何使用 Xamarin.Forms & XAML 技能,現在已經推出兩本電子書來幫助大家學這這個開發技術。
        這兩本電子書內包含了豐富的逐步開發教學內容與相關觀念、各種練習範例,歡迎各位購買。
        Xamarin.Forms 電子書
        想要購買 Xamarin.Forms 快速上手 電子書,請點選 這裡

        想要購買 XAML in Xamarin.Forms 基礎篇 電子書,請點選 這裡