2019年5月29日 星期三

C# 編譯器對於 async Task Main 方法做了什麼事情,使得可以在 Main 方法內使用非同步方法的 await 關鍵字

C# 編譯器對於 async Task Main 方法做了什麼事情,使得可以在 Main 方法內使用非同步方法的 await 關鍵字

當使用 .NET Framework 或者 .NET Core 建立一個新的 主控台應用程式的時候,若想要在該主控台應用程式的進入點 Entry Point,也就是這個專案一起動的時候,第一個要執行的方法,那就是 static void Main(string[] args) 這個方法;可是,若想要在這個方法內使用 await 關鍵字來等候一個非同步工作的話,編譯器將會提示您有錯誤產生,例如,底下的範例程式碼將會有編譯錯誤。
CS4009%26rd%3Dtrue) 不得同步傳回進入點的 void 或 int
CS5001%26rd%3Dtrue)) 程式未包含適合進入點的靜態 'Main' 方法
其中, CS4009 這個錯誤,透過 Visual Studio 上面的連結並打開使錯誤說明文件,將會看到這樣的說明: 您無法使用async關鍵字,在應用程式進入點 (通常Main方法)。
會有這樣的問題,那是因為可以在 Main 方法上使用 async 這個修飾詞,需要使用 C# 7.1 以上的版本,如同 CS4009 錯誤說明文件上提到的:開頭為C# 7.1 Main方法可以有async修飾詞,因此,想要解決此一錯誤,那就是要將這個專案設定為使用 C# 7.1 以上的版本。
而在 CS5001 的錯誤說明網頁中,將會看到這樣的內容:程式 'program' 未包含適合進入點的靜態 'Main' 方法。當沒有靜態時,會發生此錯誤Main產生可執行檔的程式碼中找不到具有正確的簽章的方法。
根據 CS5001 錯誤說明文件中提到的:當沒有靜態時,會發生此錯誤Main產生可執行檔的程式碼中找不到具有正確的簽章的方法。那是因為有了 CS4009 的錯誤,在 Main 方法中使用了 async 是不合法的,導致編譯器無法找到正確、合法的 Main 簽章方法。
可是,若不能夠在 Main 方法內使用 await 關鍵字 (因為,不想要使用封鎖執行緒 Block Thread 的做法),這要如何做到當要等候一個非同步工作的時候,可以立即返回到原先呼叫端那裏去這樣的需求呢?
C Sharp / C#
class Program
{
    static async void Main(string[] args)
    {
        await Task.Delay(1000);
        Console.WriteLine("Hello World!");
    }
}
要解決此一問題,請在方案總管內,使用滑鼠右擊該主控台應用程式專案的節點,點選 [屬性] 項目;當該專案的屬性視窗出現之後,切換到 [建置] 標籤頁次,在該標籤頁次最下方,會有一個 [進階] 按鈕,請點選此一按鈕;如下面螢幕截圖所示:
現在,[進階建置設定] 對話窗將會出現,請點選該對話窗的 [語言版本] 這個欄位旁邊的下拉選單控制項,這個下拉選單控制項原先預設的選項為 [c# 支援的最新次要版本],請在這個下拉選單中選擇 C# 7.1 以上的版本,在這裡將會選取 C# 7.3。完成後,在 [進階建置設定] 對話窗右下方點選 [確定] 按鈕,關閉此對話窗
操作方式摘要:[打開專案屬性視窗] > [建置] 標籤頁次 > [進階] 按鈕 > [語言版本] 7.3 > [確定] 按鈕
當經過這樣修正之後,再度重新進行專案建置, CS4009 的錯誤訊息消失了,而 CS5001 的錯誤訊息依然存在,這是因為還需要修正 Main 方法的宣告,把 void 修改成為 Task,也就是這樣 static async Task Main(string[] args) 這樣的目的是要讓 Main 這個方法成為一個非同步的方法,所以,這個 Main 方法將會回傳一個 Task 物件,因此,當執行到 await Task.Delay(1000); 敘述之後,就可以立即返回到原先呼叫 Main 方法的呼叫端。
現在,再度重新建置該專案,就會發現到所有的錯誤都消失了。
會需要這樣做,這是因為 C# 要求這樣做,需要一個 async Task Main 的方法,可是,這也產生另外一個問題,當在 Main 方法內執行敘述的時候,遇到了 await 關鍵字,將會立即返回到呼叫端,而當初呼叫該 Main 方法的呼叫端,也就是開始執行該程式的一開始處,如此,那不就造成了該程式就會結束執行了呀。可是,當實際執行該專案,卻發現到該專案真的會停留 1秒鐘後,才會顯示出一段字串內容,並沒有遇到 await 關鍵字之後,就立即結束執行了。
想要知道為什麼會發生這樣的問題,那就需要使用 .NET 組件 Assembly 反组譯工具,查看編譯器到底做了甚麼事情,以及真正要執行的程式碼變成如何?所以,在這裡將會透過 ILSpy 這套 .NET 組件反组譯工具來進行深度了解,想要取得 ILSpy 這套工具,可以從 ILSpy 這裡免費取得。
當下載完成 ILSpy 這套工具後,這裡下載的是 .zip 檔案格式,下載完成之後,可以解開該壓縮檔案到任何一個電腦上的目錄,接著在解壓縮目錄內找到 ILSpy.exe 這個檔案,請執行這個檔案。底下螢幕截圖為執行 ILSpy 工具的畫面。
如此,可以點選 ILSpy 功能表 [File] > [Open...] ,接著,切換到剛剛建立專案的 bin\Debug 目錄下,找到該專案的組件檔案名稱,開啟這個組件檔案;現在, ILSpy 將會顯示出這個組件中有哪些類別、方法與相關程式碼,如下圖所示。
在這裡建立的專案名稱為 ConsoleApp2,展開這個節點,將會看到 Program 命名空間下會有 Base Type、 Derived Typed 、 <Main>d__0 、 Program() 、 <Main>(string[]) : void 、 Main(string[]) : Task 這些傑點出現;現在又發現到一個怪異點,那就是這個範例程式僅有一個 Program 命名空間與 Main 方法,那麼,為什麼從 ILSpy 上看到這麼多其他的項目出現。這些原先程式碼沒有的項目,都是編譯器自動幫忙產生的,這是因為要能夠處理、讓這個程式可以使用非同步的 await 方法呼叫。
若看不到如上圖的畫面內容,請點選 ILSpy 功能表 [View] > [Show all types and members] 選項,就可以看到這些內容。
好的,接下來進行剖析,了解到底出了甚麼樣的變化,首先,在 C# 原始碼寫的 static async Task Main(string[] args) 方法,將會出現在上圖 標記1 的節點,其實,這個方法僅僅是一個非同步的方法,並不是這個程式的進入點,那麼,這隻程式的進入點在哪裡呢?請查看 標記2 的節點,也就是 <Main>(string[]) : void ,這就是這個程式的進入點,不過,編譯器將原先程式進入點的 Main 方法,改成使用 <Main> 方法;請注意,該方法的回傳值也是 void 喔。
而在 標記3 的節點,這個類別是編譯器產生出來的,這是因為有使用到 Main(string[]) : Task 方法,編譯器將會產生相對應的非同步處理狀態機所需要用到的相關程式碼。
到現在為止,原本單純 C# 程式碼中的Main 方法 ,經過建置之後,產生出這麼多的程式碼,接下來將逐步了解這些程式碼做了甚麼事情。
首先,來看看編譯過後的專案進入點,也就是 <Main>(string[]) : void ;請在 ILSpy 中點選這個節點,將會看到如下圖畫面,其該程式進入點的程式碼經過反组譯後,將會如同底下程式碼列表。
當這個程式一開始啟動之後,將會進入到這個進入點方法( <Main>)來執行,這個方法內僅有一行敘述,一開始將會呼叫 Main(args) 方法,而這個方法就是使用 C# 所設計的非同步方法,因為是非同步方法,將會回傳一個 Task 物件,所以,可以呼叫 Task.GetAwaiter 方法,該方法將會取得用來等候這個 Task 的 awaiter。而且,這個方法僅供編譯器使用,而不是應用程式程式碼中使用。
當取得了 TaskAwaiter 物件之後(因為 GetAwaiter() 方法會回傳這個型別的物件), 就可以再度呼叫 TaskAwaiter.GetResult 。這個方法將會結束對非同步工作完成的等候。此 API 支援此產品基礎結構,但無法直接用於程式碼之中。也就是說,這個方法將會由編譯器來使用,不建議在自己的 C# 程式碼中來使用。
講白話一點,這一行敘述,就是要呼叫 Main 非同步方法,並且使用封鎖執行緒的方式,等候非同步方法結束完成,因此,當在 C# 程式碼中的 static async Task Main(string[] args) 方法內有使用到 await 關鍵字之後,將會返回到原先呼叫端,也就是這個 <Main> 方法,而這個方法將會等候到該非同步方法完全結束之後,才會回傳到原先呼叫端,也就是結束執行,所以,遇到有 await 關鍵字之後,並不會造成該程式結束執行。
C Sharp / C#
// ConsoleApp2.Program
private static void <Main>(string[] args)
{
    Main(args).GetAwaiter().GetResult();
}
那麼,為什麼編譯器要在產生一個 <Main>(string[]) : void 方法呢?因為原先在 C# 程式碼中,已經將原先的 void Main 方法改寫成為一個非同務的方法,static async Task Main(string[] args),使得原先 C# 內的程式進入點函式不見了,所以,C# 編譯器要產生一個新的 Main 函式,作為這個程式的進入點要呼叫的函式,但是,若這個編譯器產生的進入點函式名稱命名為 Main(string[]) : void,將會與C#寫的 Main(string[]) : Task 方法會視為相同,也就是同一個方法,這樣會產生錯誤;會有這樣的錯誤,這是因為在 C# 內,判斷一個類別內的方法,其 方法名稱 / 參數型別與數量 (這是函式特徵的定義) 都要相同,就會視為兩個相同的方法,而不會因為其回傳值的型別不同,而是為不同的函數。所以,需要重新命名一個新的函式名稱,作為此程序的進入點要呼叫的函數。
最後,若不想要變更 [進階建置設定] 對話窗內的 [語言版本] 下拉選單控制項值,此時,可以參考使用底下的程式碼寫法,也會達成相同的執行結果的,也就是可以在 Console App 內輕鬆使用 await 來呼叫非同步方法。
C Sharp / C#
class Program
{
    static void Main(string[] args)
    {
        MainAsync(args).GetAwaiter().GetResult();
    }
    static async Task MainAsync(string[] args)
    {
        await Task.Delay(5000);
        Console.WriteLine("Hello World!");
    }
}



2019年5月21日 星期二

將圖片檔案上傳到遠端伺服器

將圖片檔案上傳到遠端伺服器

由於最近正在進行 Xamarin.Forms 的拍照與檔案上傳的練習,因此,特別設計一個 RESTful 上傳圖片的 API,不過,該 API 是不需要經過身分驗證,而是可以直接將圖片檔案上傳到遠端伺服器,所以,撰寫出一篇文章,可以使用 Console 類型的專案,將一個特定的圖片檔案,使用 HttpClient 配合 Multipart 編碼方式,將圖片檔案上傳到遠端伺服器上,若成功上傳之後,將會取得該圖片的 URL 網址,最後,可以透過該 URL 網址來取得該圖片。不過,該上傳 URL 僅用於練習之用,因此,上傳上來的圖片將會隨時有可能的不經通告,就會直接刪除。
在這篇文章所提到的範例程式碼,可以從這裡 GitHub 取得
首先,先要設計呼叫這個上傳圖片 Web API 會用到的相關類別,因為這個練習用的 ASP.NET Core 設計的 Web API 專案,是遵循 REST 規範而設計出來的,因此,當呼叫這台伺服器上的 RESTful Web API 的時候,都會回傳 APIResult 格式的 JSON 物件,透過該物件可以了解到此次呼叫 Web API 是否成功或者失敗,失敗的原因為何?而該失敗原因的說明內容,將會定義在遠端伺服器上的錯誤訊息對應表中。
若此次呼叫 Web API 成功,則可以從 APIResult.Payload 這個屬性中取得遠端伺服器回傳的 JSON 物件內容,現在,便可以根據所呼叫的 Web API 當初設計的規格,使用相對應的類別,反序列化該 JSON 物件成為 .NET 物件即可;最後,透過 UploadImageResponseDTO.ImageUrl 屬性,便可以得到該圖片在遠端伺服器上的 URL 囉。
C Sharp / C#
/// <summary>
/// 呼叫 API 回傳的制式格式
/// </summary>
public class APIResult
{
    /// <summary>
    /// 此次呼叫 API 是否成功
    /// </summary>
    public bool Status { get; set; } = true;
    public int HTTPStatus { get; set; } = 200;
    public int ErrorCode { get; set; }
    /// <summary>
    /// 呼叫 API 失敗的錯誤訊息
    /// </summary>
    public string Message { get; set; } = "";
    /// <summary>
    /// 呼叫此API所得到的其他內容
    /// </summary>
    public object Payload { get; set; }
}
public class UploadImageResponseDTO
{
    public string ImageUrl { get; set; }
}
當要進行圖片上傳的時候,當然需要先取得該圖片檔案,在這裡需要取得該圖片檔案的絕對路徑名稱(含檔案名稱) imageFileName 、該圖片檔案的名稱(沒有路徑) fileName 。
現在,可以建立一個 HttpClient 物件,這個練習專案使用的上傳圖片 API URL 為 http://lobworkshop.azurewebsites.net/api/UploadImage 。
為了要能夠上傳圖片檔案,不是一般的文字內容,而是二進位 Binary 的內容,此時,需要使用 MultipartFormDataContent 類別,將二進位的圖片檔案進行編碼,這樣就可以上傳到遠端伺服器上,而遠端伺服器上也可以透過相同的編碼方式,還原到原先的圖片檔案,對於這樣的編碼規範,可以參考 https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html 。當要上傳圖片的時候,將會使用 StreamContent 這個類別,取得該上傳檔案的 Stream 物件,便可以得到該圖片的檔案內容。
當完成圖片檔案的編碼物件產生之後,就可以使用 await client.PostAsync(url, content); 將該圖片上傳到遠端伺服器上。
當呼叫完成上傳圖片 API 之後,將會得到一個 JSON 文字物件,這個時候僅需使用 JsonConvert.DeserializeObject 將這個 JSON 物件反序列化成為 .NET 物件,這裡使用的方法將會是強型別的泛型方法。最後,上傳圖片的網址可以透過 uploadImageResponseDTO.ImageUrl 屬性來取得。
C Sharp / C#
class Program
{
    static async Task Main(string[] args)
    {
        Console.WriteLine("使用 HttpClient 上傳檔案範例");
        string path = Directory.GetCurrentDirectory();
        string fileName = "XamarinForms.jpg";
        string imageFileName = Path.Combine(path, fileName);

        HttpClient client = new HttpClient();
        string url = $"http://lobworkshop.azurewebsites.net/api/UploadImage";
        #region 將圖片檔案,上傳到網路伺服器上(使用 Multipart 的規範)
        // 規格說明請參考 https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
        using (var content = new MultipartFormDataContent())
        {
            // 開啟這個圖片檔案,並且讀取其內容
            using (var fs = File.Open(imageFileName, FileMode.Open))
            {
                var streamContent = new StreamContent(fs);
                streamContent.Headers.Add("Content-Type", "application/octet-stream");
                streamContent.Headers.Add("Content-Disposition", "form-data; name=\"file\"; filename=\"" + fileName + "\"");
                content.Add(streamContent, "file", fileName);

                // 上傳到遠端伺服器上
                HttpResponseMessage response = await client.PostAsync(url, content);

                if (response != null)
                {
                    if (response.IsSuccessStatusCode == true)
                    {
                        // 取得呼叫完成 API 後的回報內容
                        String strResult = await response.Content.ReadAsStringAsync();
                        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
                        if (apiResult?.Status == true)
                        {
                            UploadImageResponseDTO uploadImageResponseDTO = JsonConvert.DeserializeObject<UploadImageResponseDTO>(apiResult.Payload.ToString());
                            Console.WriteLine("上傳圖片的網址");
                            Console.WriteLine(uploadImageResponseDTO.ImageUrl);
                        }
                    }
                }
            }
        }
        #endregion

        Console.WriteLine("Press any key for continuing...");
        Console.ReadKey();
    }
}
C Sharp / C#
csharp



2019年5月20日 星期一

取得後端 Web API 的 JWT Token ,並且呼叫相關 API 的做法

取得後端 Web API 的 JWT Token ,並且呼叫相關 API 的做法

在上一篇文章 
使用 C# HttpClient 追蹤與顯示 HTTP Request / Response 封包內容
 文章中,有說明如何呼叫遠端 Web API,進行使用者帳號身分驗證的工作,一旦身分驗證成功之後,將會得到遠端 API 伺服器產生的 JWT 權杖 Token,不過,該權杖有效期限僅有 15 分鐘有效時間。
在這篇文章中,將會使用登入驗證完成之後取得的權杖,呼叫 https://lobworkshop.azurewebsites.net/api/Invoices 這個發票 API,說明如何進行發票的 CRUD ,也就是 Create 新增、Retrive 查詢、Update 更新、Delete 刪除的作業。
在這篇文章所提到的範例程式碼,可以從這裡 GitHub 取得
為了要能夠完成這樣的練習,首先需要建立發票 API 會用到的 DTO 類別,這些類別如下所示:
C Sharp / C#
#region DTO 型別宣告
public class LoginRequestDTO
{
    public string Account { get; set; }
    public string Password { get; set; }
}
public class LoginResponseDTO
{
    public int Id { get; set; }
    public string Account { get; set; }
    public string Name { get; set; }
    public string Token { get; set; }
    public int TokenExpireMinutes { get; set; }
    public string RefreshToken { get; set; }
    public int RefreshTokenExpireDays { get; set; }
    public string Image { get; set; }
}
public class InvoiceRequestDTO
{
    public int Id { get; set; }
    public string InvoiceNo { get; set; }
    public UserDTO user { get; set; }
    public DateTime Date { get; set; }
    public string Memo { get; set; }
}
public class InvoiceResponseDTO
{
    public int Id { get; set; }
    public string InvoiceNo { get; set; }
    public UserDTO user { get; set; }
    public DateTime Date { get; set; }
    public string Memo { get; set; }
}
public class UserDTO
{
    public int Id { get; set; }
}
/// <summary>
/// 呼叫 API 回傳的制式格式
/// </summary>
public class APIResult
{
    /// <summary>
    /// 此次呼叫 API 是否成功
    /// </summary>
    public bool Status { get; set; } = false;
    /// <summary>
    /// 呼叫 API 失敗的錯誤訊息
    /// </summary>
    public string Message { get; set; } = "";
    /// <summary>
    /// 呼叫此API所得到的其他內容
    /// </summary>
    public object Payload { get; set; }
}
#endregion
首先,來看看如何呼叫新增發票 API,在這裡要先建立要新增發票的物件 InvoiceRequestDTO invoiceRequestDTO ,接著,使用 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token); 敘述,把登入驗證成功後取得的 JWT Token,放入此次新增發票的請求的 HTTP 標頭 Header 內,如此,對於後端 Web Server 收到這個新增發票請求的時候,將會先檢查 JWT 權杖是否存,若存在,將會檢查該權杖是否正確、還在有效期限等等狀況,若沒有問題發生,將會觸發新增發票的 程式碼。
在這裡,將會使用 await client.PostAsync(url, new StringContent(httpJsonPayload, System.Text.Encoding.UTF8, "application/json")); 表示式,呼叫遠端的新增發票 API,第二個參數將會是此次要新增發票的內容。
C Sharp / C#
private static async Task<InvoiceResponseDTO> CreateInvoiceAsync(LoginResponseDTO loginResponseDTO)
{
    InvoiceResponseDTO invoiceResponseDTO = new InvoiceResponseDTO();
    string url = "https://lobworkshop.azurewebsites.net/api/Invoices";
    InvoiceRequestDTO invoiceRequestDTO = new InvoiceRequestDTO()
    {
        InvoiceNo = "123",
        Memo = "查理王",
        Date = new DateTime(2019, 05, 20),
        user = new UserDTO()
        {
            Id = loginResponseDTO.Id
        }
    };
    var httpJsonPayload = JsonConvert.SerializeObject(invoiceRequestDTO);
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token);
    HttpResponseMessage response = await client.PostAsync(url,
        new StringContent(httpJsonPayload, System.Text.Encoding.UTF8, "application/json"));

    if (response.IsSuccessStatusCode)
    {
        String strResult = await response.Content.ReadAsStringAsync();
        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        if (apiResult.Status == true)
        {
            string itemJsonContent = apiResult.Payload.ToString();
            Console.WriteLine($"成功新增一筆發票 : {itemJsonContent}");
            invoiceResponseDTO = JsonConvert.DeserializeObject<InvoiceResponseDTO>(itemJsonContent, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        }
    }
    return invoiceResponseDTO;
}
對於想要修改遠端 Web API 資料庫內的發票紀錄,當然需要先取得該發票紀錄內容,在此,將會透過剛剛呼叫玩新增發票 API 後所得到的該發票紀錄,緊接著來呼叫修改 API。
在底下的呼叫修改發票方法中,第二個參數將會這次要修改發票的物件,而根據 REST 規範,當我們要進行修改某個紀錄的時候,需要提供一個 URI,該 URI 可以指向到該筆發票紀錄,因此,當要進行修改發票的時候,所要使用的 URL ,將會設定為 string url = $"https://lobworkshop.azurewebsites.net/api/Invoices/{UpdateItem.Id}"; ,也就是要把該發票紀錄的 ID 放到 URL 路徑。
接下來將會進行任意修改該發票紀錄,當然也需要將 JWT Token,使用這個敘述 client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token); ,設定到此次 HTTP REST API 呼叫過程中,如此,就可以接著使用 await client.PutAsync(url, new StringContent(httpJsonPayload, System.Text.Encoding.UTF8, "application/json")); 敘述,呼叫遠端的發票更新 API。
C Sharp / C#
private static async Task<InvoiceResponseDTO> UpdateInvoiceAsync(LoginResponseDTO loginResponseDTO, InvoiceResponseDTO UpdateItem)
{
    InvoiceResponseDTO invoiceResponseDTO = new InvoiceResponseDTO();
    string url = $"https://lobworkshop.azurewebsites.net/api/Invoices/{UpdateItem.Id}";
    InvoiceRequestDTO invoiceRequestDTO = new InvoiceRequestDTO()
    {
        Id = UpdateItem.Id,
        InvoiceNo = UpdateItem.InvoiceNo,
        Memo = "修正" +UpdateItem.Memo,
        Date = UpdateItem.Date.AddDays(5),
        user = UpdateItem.user
    };
    var httpJsonPayload = JsonConvert.SerializeObject(invoiceRequestDTO);
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token);
    HttpResponseMessage response = await client.PutAsync(url,
        new StringContent(httpJsonPayload, System.Text.Encoding.UTF8, "application/json"));

    String strResult = await response.Content.ReadAsStringAsync();
    if (response.IsSuccessStatusCode)
    {
        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        if (apiResult.Status == true)
        {
            string itemJsonContent = apiResult.Payload.ToString();
            Console.WriteLine($"成功修改一筆發票 : {itemJsonContent}");
            invoiceResponseDTO = JsonConvert.DeserializeObject<InvoiceResponseDTO>(itemJsonContent, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        }
    }
    return invoiceResponseDTO;
}
當想要查詢遠端 Web API 上的發票紀錄,可以使用 HTTP Get 方法,當然,也是要將 JWT Token 放到 HTTP 標頭 Header 上;在這個發票查詢的 API,將會回傳 List ,也就是 InvoiceResponseDTO 的集合物件。
C Sharp / C#
private static async Task<List<InvoiceResponseDTO>> QueryInvoiceAsync(LoginResponseDTO loginResponseDTO)
{
    List<InvoiceResponseDTO> invoiceResponseDTOs = new List<InvoiceResponseDTO>();
    string url = "https://lobworkshop.azurewebsites.net/api/Invoices";
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token);
    HttpResponseMessage response = await client.GetAsync(url);

    if (response.IsSuccessStatusCode)
    {
        String strResult = await response.Content.ReadAsStringAsync();
        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        String strResult = await response.Content.ReadAsStringAsync();
        if (apiResult.Status == true)
        {
            Console.WriteLine($"成功查詢發票 : {itemJsonContent}");
            invoiceResponseDTOs = JsonConvert.DeserializeObject<List<InvoiceResponseDTO>>(itemJsonContent, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        }
    }
    return invoiceResponseDTOs;
}
當要刪除某個發票紀錄的時候,根據 REST 規範,一樣需要使用 URI 標示出該發票紀錄所在的地方,在這裡將會使用這樣的敘述 string url = $"https://lobworkshop.azurewebsites.net/api/Invoices/{Id}"; ,當然,同樣也需要把 JWT Token 放到此次的 HTTP 呼叫標頭上。
最後,使用 await client.DeleteAsync(url); 敘述,呼叫遠端刪除的 API。
C Sharp / C#
private static async Task<InvoiceResponseDTO> DeleteInvoiceAsync(LoginResponseDTO loginResponseDTO, int Id)
{
    InvoiceResponseDTO invoiceResponseDTO = new InvoiceResponseDTO();
    string url = $"https://lobworkshop.azurewebsites.net/api/Invoices/{Id}";
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", loginResponseDTO.Token);
    HttpResponseMessage response = await client.DeleteAsync(url);

    String strResult = await response.Content.ReadAsStringAsync();
    if (response.IsSuccessStatusCode)
    {
        APIResult apiResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        if (apiResult.Status == true)
        {
            string itemJsonContent = apiResult.Payload.ToString();
            Console.WriteLine($"成功刪除一筆發票 : {itemJsonContent}");
            invoiceResponseDTO = JsonConvert.DeserializeObject<InvoiceResponseDTO>(itemJsonContent, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
        }
    }
    return invoiceResponseDTO;
}
現在,可以在 Main 方法內,使用底下的方法來進行發票的 CRUD 呼叫
C Sharp / C#
static async Task Main(string[] args)
{
    // 登入系統,取得 JTW Token
    await LoginAsync();

    // 從檔案中取得 JWT 權杖 Token
    string fileContent = await StorageUtility.ReadFromDataFileAsync("", "MyDataFolder", "MyFilename.txt");
    LoginResponseDTO loginResponseDTO = JsonConvert.DeserializeObject<LoginResponseDTO>(fileContent);

    #region CRUD => Retrive 取得該使用者的發票資料
    List<InvoiceResponseDTO> invoiceResponseDTOs = await QueryInvoiceAsync(loginResponseDTO);
    PrintAllInvoice(invoiceResponseDTOs);
    #endregion

    #region CRUD => Create 新增發票資料
    InvoiceResponseDTO invoiceResponseDTO = await CreateInvoiceAsync(loginResponseDTO);
    #endregion

    #region CRUD => Retrive 取得該使用者的發票資料
    invoiceResponseDTOs = await QueryInvoiceAsync(loginResponseDTO);
    PrintAllInvoice(invoiceResponseDTOs);
    #endregion

    #region CRUD => Update 修改發票資料
    invoiceResponseDTO = await UpdateInvoiceAsync(loginResponseDTO, invoiceResponseDTO);
    #endregion

    #region CRUD => Retrive 取得該使用者的發票資料
    invoiceResponseDTOs = await QueryInvoiceAsync(loginResponseDTO);
    PrintAllInvoice(invoiceResponseDTOs);
    #endregion

    #region CRUD => Delete 刪除發票資料
    foreach (var item in invoiceResponseDTOs)
    {
        await DeleteInvoiceAsync(loginResponseDTO, item.Id);
    }
    #endregion

    #region CRUD => Retrive 取得該使用者的發票資料
    invoiceResponseDTOs = await QueryInvoiceAsync(loginResponseDTO);
    PrintAllInvoice(invoiceResponseDTOs);
    #endregion

    Console.WriteLine("Press any key for continuing...");
    Console.ReadKey();
}
由於後端 Web API 所發出的 JWT Token 僅僅會有 15 分鐘的有效期限,因此,當您成功執行過該專案後,等候 15 分鐘之後,可以把 Main 方法內的 await LoginAsync(); 註解起來,再度執行看看,不過,先在 QueryInvoiceAsync 方法內的 if (apiResult.Status == true) 敘述上設定一個中斷點,當執行到這個中斷點,先查詢看看 apiResult.Status 屬性值,應該是 false,而在該敘述的上一行,那就是 String strResult = await response.Content.ReadAsStringAsync(); 這個敘述,請檢查看看 strResult 變數值,將會看到底下的內容,後端 Web API 將會因為這次呼叫查詢發票 API,因為所提供的 JWT 權杖 Token 已經逾期,因此,將會得到 401 的錯誤訊息。
{"Status":false,"HTTPStatus":401,"ErrorCode":1,"Message":"錯誤代碼 1, 存取權杖可用期限已經逾期超過","Payload":null}