整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          38.JavaScript:try...catch異常處理

          JavaScript編程中,錯誤處理是不可或缺的一部分。良好的錯誤處理可以讓我們的應用更加健壯和用戶友好。try...catch語句是JavaScript中處理運行時錯誤的一種基本方式。本文將通過幾個實例來展示如何在HTML5中使用try...catch來捕獲和處理錯誤。

          什么是 try...catch

          try...catch語句包含兩個部分:try塊和catch塊。

          • try塊:包圍著可能會拋出錯誤的代碼。
          • catch塊:當try塊中的代碼拋出錯誤時執行的代碼塊。

          如果try塊中的代碼運行正常,則跳過catch塊。如果try塊中的代碼拋出錯誤,則立即停止執行try塊中的剩余代碼,并跳轉到catch塊。

          基本語法

          try {
              // 嘗試執行的代碼
          } catch (error) {
              // 發生錯誤時執行的代碼
          }
          

          示例1:捕獲語法錯誤

          <!DOCTYPE html>
          <html lang="zh-CN">
          <head>
              <meta charset="UTF-8">
              <title>try...catch 示例1</title>
          </head>
          <body>
              <script>
                  try {
                      eval('alert("Hello world)'); // 缺少引號導致的語法錯誤
                  } catch (error) {
                      console.error('捕獲到錯誤:', error.message);
                  }
              </script>
          </body>
          </html>
          

          在這個例子中,我們嘗試使用eval函數執行一段代碼,但由于字符串沒有閉合,導致了語法錯誤。try...catch捕獲到這個錯誤,并在控制臺輸出了錯誤信息。

          示例2:處理JSON解析錯誤

          <!DOCTYPE html>
          <html lang="zh-CN">
          <head>
              <meta charset="UTF-8">
              <title>try...catch 示例2</title>
          </head>
          <body>
              <script>
                  try {
                      var json = '{name:"John Doe"'; // JSON格式不正確
                      var user = JSON.parse(json);
                      console.log(user.name);
                  } catch (error) {
                      console.error('JSON解析錯誤:', error.message);
                  }
              </script>
          </body>
          </html>
          

          在這個例子中,我們嘗試解析一個不正確的JSON字符串。JSON.parse在嘗試解析時會拋出錯誤,try...catch捕獲到這個錯誤,并在控制臺輸出了錯誤信息。

          示例3:處理DOM操作錯誤

          <!DOCTYPE html>
          <html lang="zh-CN">
          <head>
              <meta charset="UTF-8">
              <title>try...catch 示例3</title>
          </head>
          <body>
              <script>
                  try {
                      var elem = document.getElementById('myElement');
                      elem.innerHtml = 'Hello World'; // 正確的屬性是innerHTML
                  } catch (error) {
                      console.error('DOM操作錯誤:', error.message);
                  }
              </script>
          </body>
          </html>
          

          在這個例子中,我們嘗試設置一個不存在的DOM元素的innerHtml屬性,這會導致一個TypeError,因為elem是null。try...catch捕獲到這個錯誤,并在控制臺輸出了錯誤信息。

          示例4:使用 finally 語句

          finally塊是try...catch結構的一個可選部分,無論是否發生錯誤,finally塊中的代碼總是會被執行。

          <!DOCTYPE html>
          <html lang="zh-CN">
          <head>
              <meta charset="UTF-8">
              <title>try...catch 示例4</title>
          </head>
          <body>
              <script>
                  try {
                      // 一些可能會拋出錯誤的代碼
                  } catch (error) {
                      // 處理錯誤
                  } finally {
                      // 清理或完成工作的代碼
                      console.log('無論是否發生錯誤,這段代碼都會執行');
                  }
              </script>
          </body>
          </html>
          

          在這個例子中,無論try塊中的代碼是否拋出錯誤,finally塊中的console.log都會被執行。

          總結

          try...catch是處理JavaScript中錯誤的有效方式,它可以幫助我們捕獲運行時錯誤,并根據需要進行處理。通過合理使用try...catch,我們的應用程序可以更加健壯和可靠。記住,錯誤處理不僅僅是捕獲錯誤,更重要的是如何根據不同的錯誤類型給用戶提供有用的反饋和恢復程序的運行。

          章涵蓋

          • 模型綁定和數據驗證概述
          • 內置和自定義驗證屬性
          • 模型狀態驗證方法
          • 錯誤和異常處理技術

          為簡單起見,到目前為止,我們假設來自客戶端的數據始終正確且足以滿足 Web API 的終結點。不幸的是,情況并非總是如此:無論我們喜歡與否,我們經常必須處理錯誤的HTTP請求,這可能是由多種因素(包括惡意攻擊)引起的,但總是因為我們的應用程序面臨意外或未經處理的行為而發生。

          在本章中,我們將討論在客戶端-服務器交互期間處理意外情況的一系列技術。這些技術依賴于兩個主要概念:

          • 數據驗證 - 一組方法、檢查、例程和規則,用于確保進入我們系統的數據有意義、準確和安全,因此允許進行處理
          • 錯誤處理 — 預測、檢測、分類和管理程序執行流中可能發生的應用程序錯誤的過程

          在接下來的部分中,我們將了解如何在代碼中將它們付諸實踐。

          6.1 數據驗證

          我們從第1章中知道,Web API的主要目的是使不同的各方能夠通過交換信息進行交互。在后面的章節中,我們看到了如何實現幾個可用于創建、讀取、更新和刪除數據的 HTTP 端點。這些終結點中的大多數(如果不是全部)都需要來自調用客戶端的某種輸入。例如,考慮 GET /BoardGames 端點所需的參數,我們在第 5 章中對此進行了極大的改進:

          • pageIndex - 一個可選的整數值,用于設置要返回的棋盤游戲的起始頁
          • pageSize - 用于設置每個頁面大小的可選整數值
          • sortColumn - 一個可選的字符串值,用于設置列以對返回的棋盤游戲進行排序
          • 排序順序 - 用于設置排序順序的可選字符串值
          • filterQuery - 一個可選的字符串值,如果存在,將僅用于返回名稱包含它的棋盤游戲

          所有這些參數都是可選的。我們選擇允許它們不存在(換句話說,在沒有它們的情況下接受傳入請求),因為我們可以輕松提供合適的默認值,以防調用方未顯式提供它們。因此,以下所有 HTTP 請求都將以相同的方式處理,因此將提供相同的結果(直到默認值更改):

          • https://localhost:40443/BoardGames
          • https://localhost:40443/BoardGames?pageIndex=0&pageSize=10
          • https://localhost:40443/BoardGames?pageIndex=0&pageSize=10&sortColumn=Name&sortOrder=ASC

          同時,我們要求其中一些參數的值與給定的 .NET 類型兼容,而不是原始字符串。pageIndex 和 pageSize 就是這種情況,它們的值應為整數類型。如果我們嘗試傳遞其中一個不兼容的值,例如 HTTP 請求 https://localhost:40443/BoardGames?pageIndex=test,我們的應用程序將響應 HTTP 400 - 錯誤請求錯誤,甚至不開始執行 BoardGamesController 的 Get 操作方法:

          {
            "type":"https://tools.ietf.org/html/rfc7231#section-6.5.1",
            "title":"One or more validation errors occurred.",
            "status":400,
            "traceId":"00-a074ebace7131af6561251496331fc65-ef1c633577161417-00",
            "errors":{
              "pageIndex":["The value 'string' is not valid."]
            }
          }

          我們可以很容易地看到,通過允許和/或拒絕此類請求,我們已經通過主動檢查兩個重要的驗收標準對這些參數執行了某種數據驗證活動:

          • 為每個參數提供一個未定義的值是可以的,因為我們有服務器定義的回退(操作方法的默認值)。
          • 為 pageIndex 和 pageSize 提供非整數值是不行的,因為我們希望它們是整數類型。

          我們顯然是在談論隱式活動,因為空檢查和回退到默認值的任務是由底層框架執行的,而無需我們編寫任何內容。具體來說,我們正在利用 ASP.NET Core的模型綁定系統,這是自動處理所有這些的機制。

          6.1.1 模型綁定

          來自 HTTP 請求的所有輸入數據(請求標頭、路由數據、查詢字符串、表單字段等)都通過原始字符串傳輸并接收。ASP.NET Core 框架檢索這些值,并自動將它們從字符串轉換為 .NET 類型,從而使開發人員免于繁瑣、容易出錯的手動活動。具體而言,模型綁定系統從 HTTP 請求查詢字符串和/或正文中檢索輸入數據,并將其轉換為強類型方法參數。此過程在每個 HTTP 請求時自動執行,但可以根據開發人員的要求配置一組基于屬性的約定。

          讓我們看看模型綁定在后臺的作用。考慮HTTP GET請求 https://localhost:40443/BoardGames?pageIndex=2&pageSize=50,它被路由到我們的BoardGamesController的Get操作方法:

          public async Task<RestDTO<BoardGame[]>> Get(
            int pageIndex = 0,
            int pageSize = 10,
            string? sortColumn = "Name",
            string? sortOrder = "ASC",
            string? filterQuery = null)

          模型綁定系統執行以下任務:

          • 標識頁面索引和頁面大小 GET 參數的存在
          • 檢索其原始字符串值(“2”和“50”),將其轉換為整數類型(2 和 50),并將轉換后的值分配給相應操作方法的屬性
          • 標識缺少 sortColumn、sortOrder 和 filterQuery GET 參數,并將 null 值分配給相應操作方法的屬性,以便改用相應的默認值

          簡而言之,模型綁定系統的主要用途是將給定的(原始字符串)源轉換為一個或多個預期的(.NET 類型)目標。在我們的示例中,URL 發出的原始 GET 參數是模型綁定的源,操作方法的類型化參數是目標。目標可以是簡單類型(整數、布爾值等)或復雜類型(如數據傳輸對象 [DTO]),我們將在后面看到。

          6.1.2 數據驗證屬性

          除了執行標準類型轉換之外,還可以將模型綁定配置為使用 System.ComponentModel.DataAnnotation 命名空間中包含的一組內置數據批注屬性來執行多個數據驗證任務。以下是這些屬性中最值得注意的列表:

          • [信用卡] - 確保給定輸入是信用卡號
          • [電子郵件地址] - 確保給定的字符串輸入具有電子郵件地址格式
          • [MaxLength(n)] - 確保給定字符串或數組輸入的長度小于或等于指定值
          • [最小長度 (n)] - 確保給定字符串或數組輸入的長度等于或大于指定值
          • [范圍(nMin, nMax)] - 確保給定輸入介于指定值的最小值和最大值之間
          • [正則表達式(正則表達式)] - 確保給定的輸入與給定的正則表達式匹配
          • [必需] - 確保給定輸入具有非空值
          • [字符串長度] - 確保給定的字符串輸入不超過指定的長度限制
          • [Url] - 確保給定的字符串輸入具有 URL 格式

          學習如何使用這些驗證屬性的最佳方法是在我們的MyBGList Web API中實現它們。假設我們希望(或被要求)將 GET /BoardGames 端點的頁面大小限制為最大值 100。以下是我們如何通過使用 [Range] 屬性來做到這一點:

                  public async Task<RestDTO<BoardGame[]>> Get(
                      int pageIndex = 0,
                      [Range(1, 100)] int pageSize = 10,    ?
                      string? sortColumn = "Name",
                      string? sortOrder = "ASC",
                      string? filterQuery = null)

          ? 范圍驗證器(1 至 100)

          注意此更改請求是可信的。無限制地接受任何頁面大小意味著允許可能昂貴的數據檢索請求,這可能會導致 HTTP 響應延遲、速度減慢和性能下降,從而使我們的 Web 應用程序面臨拒絕服務 (DoS) 攻擊。

          此更改將導致 URL https://localhost:40443/BoardGames?pageSize=200 返回 HTTP 400 - 錯誤請求狀態錯誤,而不是前 200 個棋盤游戲。正如我們很容易理解的那樣,每當我們想在輸入數據周圍放置一些邊界而不手動實施相應的檢查時,數據注釋屬性都很有用。如果我們不想使用 [Range] 屬性,我們可以使用以下代碼獲得相同的結果:

          if (pageSize < 0 || pageSize > 100) {
            // .. do something
          }

          該“某些內容”可以通過各種方式實現,例如引發異常,返回HTTP錯誤狀態或執行任何其他合適的錯誤處理操作。但是,手動方法可能難以維護,并且通常容易出現人為錯誤。出于這個原因,使用 ASP.NET Core 的最佳實踐是采用框架提供的集中式接口,只要我們可以使用它來實現我們需要做的事情。

          定義這種方法被稱為面向方面的編程AOP),這是一種范式,旨在通過向現有代碼添加行為而不修改代碼本身來提高源代碼的模塊化。ASP.NET 提供的數據注釋屬性就是一個很好的例子,因為它們允許開發人員添加功能而不會使代碼混亂。

          6.1.3 一個非平凡的驗證示例

          我們用來限制頁面大小的 [Range(1, 100)] 驗證器很容易實現。讓我們嘗試一個更困難的更改請求。假設我們希望(或被要求)驗證 sortOrder 參數,該參數當前接受任何字符串,以僅接受可被視為對其特定目的有效的值,即“ASC”或“DESC”。同樣,此更改請求非常合理。接受參數(如 sortOrder)的任意字符串值(以編程方式用于使用動態 LINQ 編寫 LINQ 表達式)可能會使我們的 Web 應用程序面臨危險的漏洞,例如 SQL 注入或 LINQ 注入。出于這個原因,為我們的應用程序提供這些“動態”字符串的驗證器是我們應該注意的安全要求。

          提示有關此主題的其他信息,請查看 http://mng.bz/Q8w4 中的 StackOverflow 線程

          同樣,我們可以通過采用編程方法輕松實現此更改請求,在操作方法本身中進行以下“手動檢查”:

          if (sortOrder != "ASC" && sortOrder != "DESC") {
            // .. do something
          }

          但是我們至少有兩種其他方法可以使用 ASP.NET Core 提供的內置驗證器接口實現相同的結果:使用 [RegularExpression] 屬性或實現自定義驗證屬性。在接下來的部分中,我們將使用這兩種技術。

          使用正則表達式屬性

          類是最有用和最可自定義的數據批注屬性之一,因為它使用了正則表達式的強大功能和靈活性。[RegularExpression] 屬性依賴于 .NET 正則表達式引擎,該引擎由 System.Text.RegularExpressions 命名空間及其 Regex 類表示。此引擎接受使用 Perl 5 兼容語法編寫的正則表達式模式,并根據所使用的方法將它們用于輸入字符串以確定匹配項、檢索匹配項或替換匹配文本。具體來說,該屬性在內部調用 IsMatch() 方法來確定模式是否在輸入字符串中找到匹配項,這正是我們場景中所需要的。

          正則表達式

          正則表達式(也稱為 RegExRegExp)是用于匹配字符串中的字符組合的標準化模式。該技術起源于 1951 年,但直到 1980 年代后期才開始流行,這要歸功于 Perl 語言(自 1986 年以來一直以正則表達式庫為特色)的全球采用。此外,在1990年代,Perl兼容正則表達式(PCRE)庫被許多現代工具(如PHP和Apache HTTP Server)采用,成為事實上的標準。

          在本書中,我們很少使用正則表達式,只是在基本程度上使用。要了解有關該主題的更多信息,請查看以下網站,該網站提供了一些有見地的教程、示例和快速入門指南:https://www.regular-expressions.info。

          下面是一個合適的正則表達式模式,我們可以用來檢查是否存在 ASC 或 DESC 字符串:

          ASC|DESC

          此模式可以通過以下方式在 BoardGamesController 的 Get 操作方法中的 [RegularExpression] 屬性中使用:

          public async Task<RestDTO<BoardGame[]>> Get(
            int pageIndex = 0,
            [Range(1, 100)] int pageSize = 10,
            string? sortColumn = "Name",
            [RegularExpression("ASC|DESC")] string? sortOrder = "ASC",
            string? filterQuery = null)

          之后,包含不同于“ASC”和“DESC”的sortOrder參數值的所有傳入請求都將被視為無效,從而導致HTTP 400 - 錯誤請求響應。

          使用自定義驗證屬性

          如果我們不想使用 [RegularExpression] 屬性來滿足我們的更改請求,我們可以使用自定義驗證屬性實現相同的結果。所有現有的驗證屬性都擴展了 ValidationAttribute 基類,該基類提供了一個方便(且可重寫)的 IsValid() 方法,該方法執行實際的驗證任務并返回包含結果的 ValidationResult 對象。要實現我們自己的驗證屬性,我們需要執行以下步驟:

          1. 添加新的類文件,該文件將包含自定義驗證器的源代碼。
          2. 擴展驗證屬性基類。
          3. 用我們自己的實現重寫 IsValid 方法。
          4. 配置并返回包含結果的驗證結果對象。

          添加 SortOrderValidator 類文件

          在Visual Studio的解決方案資源管理器中,在MyBGList項目的根目錄中創建一個新的/Attributes/文件夾。然后右鍵單擊該文件夾,并添加新的 SortOrderValidatorAttribute.cs 類文件,以在新的 MyBGList.Attributes 命名空間中生成一個空樣板。現在,我們已準備好實現自定義驗證器。

          實現 SortOrderValidator

          下面的清單提供了一個最小實現,該實現根據“ASC”和“DESC”值檢查輸入字符串,僅當其中一個值完全匹配時,才返回成功的結果。

          清單 6.1 排序順序驗證器屬性

          using System.ComponentModel.DataAnnotations;
           
          namespace MyBGList.Attributes
          {
              public class SortOrderValidatorAttribute : ValidationAttribute
              {
                  public string[] AllowedValues { get; set; } = 
                      new[] { "ASC", "DESC" };
                  public SortOrderValidatorAttribute()
                      : base("Value must be one of the following: {0}.") { }
                
                  protected override ValidationResult? IsValid(
                      object? value, 
                      ValidationContext validationContext)
                  {
                      var strValue = value as string;
                      if (!string.IsNullOrEmpty(strValue)
                          && AllowedValues.Contains(strValue))
                          return ValidationResult.Success;
           
                      return new ValidationResult(
                          FormatErrorMessage(string.Join(",", AllowedValues))
                      );
                  }
              }
          }

          代碼易于閱讀。根據允許的字符串值數組(AllowedValues 字符串數組)檢查輸入值,以確定它是否有效。請注意,如果驗證失敗,生成的 ValidationResult 對象將使用方便的錯誤消息進行實例化,該消息將為調用方提供有關失敗檢查的一些有用的上下文信息。此消息的默認文本在構造函數中定義,但我們可以使用 ValidationAttribute 基類提供的公共 ErrorMessage 屬性按以下方式更改它:

          [SortOrderValidator(ErrorMessage = "Custom error message")]

          此外,我們將 AllowedValues 字符串數組屬性設置為 public,這使我們有機會通過以下方式自定義這些值:

          [SortOrderValidator(AllowedValues = new[] { "ASC", "DESC", "OtherString" })]

          提示自定義允許的排序值在某些邊緣情況下可能很有用,例如將 SQL Server 替換為支持不同排序語法的數據庫管理系統 (DBMS)。這就是我們為該屬性定義 set 訪問器的原因。

          現在我們可以回到 BoardGamesController 的 Get 方法,并將我們之前添加的 [RegularExpression] 屬性替換為新的 [SortOrderValidator] 自定義屬性:

          using MyBGList.Attributes;
           
          // ...
           
          public async Task<RestDTO<BoardGame[]>> Get(
            int pageIndex = 0,
            [Range(1, 100)] int pageSize = 10,
            string? sortColumn = "Name",
            [SortOrderValidator] string? sortOrder = "ASC",
            string? filterQuery = null)

          實現 SortColumnValidator

          在繼續之前,讓我們實現另一個自定義驗證器來修復正在進行的 BoardGamesControlle 的 Get 操作方法中的其他安全問題:sortColumn 參數。同樣,我們必須處理用于動態構建 LINQ 表達式樹的任意用戶提供的字符串參數,這可能會使我們的 Web 應用程序受到一些 LINQ 注入攻擊。為了防止這些類型的威脅,我們至少可以做的是相應地驗證該字符串。

          但是,這一次,“允許”值由 [BoardGame] 數據庫表的屬性確定,該表在我們的代碼庫中由 BoardGame 實體表示。我們可以采取以下兩種方法之一:

          • 使用固定字符串對所有 BoardGame 實體的屬性名稱進行硬編碼,并像處理“ASC”和“DESC”值一樣繼續。
          • 找到一種方法來根據給定的輸入字符串動態檢查實體的屬性名稱。

          第一種方法很容易通過使用 [RegularExpression] 屬性或類似于我們創建的 SortOrderValidator 的自定義屬性來實現。但是,從長遠來看,此解決方案可能很難維護,特別是如果我們計劃向 BoardGame 實體添加更多屬性。此外,除非我們每次都將整套“有效”固定字符串作為參數傳遞,否則它不夠靈活,無法與域、力學等實體一起使用。

          動態方法可能是更好的選擇,特別是考慮到我們可以讓它接受 EntityType 屬性,我們可以使用它來傳遞要檢查的實體類型。然后,使用 LINQ 循環訪問所有 EntityType 的屬性以檢查其中一個屬性是否與輸入字符串匹配,這將很容易。下面的清單顯示了我們如何在一個新的 SortColumnValidatorAttribute.cs 文件中實現這種方法。

          清單 6.2 排序列驗證器屬性

          using System.ComponentModel.DataAnnotations;
           
          namespace MyBGList.Attributes
          {
              public class SortColumnValidatorAttribute : ValidationAttribute
              {
                  public Type EntityType { get; set; }
                  
                  public SortColumnValidatorAttribute(Type entityType) 
                      : base("Value must match an existing column.")
                  {
                      EntityType = entityType;
                  }
           
                  protected override ValidationResult? IsValid(
                      object? value, 
                      ValidationContext validationContext)
                  {
                      if (EntityType != null)
                      {
                          var strValue = value as string;
                          if (!string.IsNullOrEmpty(strValue)
                              && EntityType.GetProperties()
                                  .Any(p => p.Name == strValue))
                              return ValidationResult.Success;
                      }
           
                      return new ValidationResult(ErrorMessage);
                  }
              }
          }

          如我們所見,IsValid() 方法源代碼的核心部分依賴于 GetProperties() 方法,該方法返回與類型屬性對應的 PropertyInfo 對象數組。

          警告正如我們已經實現的那樣,IsValid() 方法將考慮任何對排序目的有效的屬性,只要它存在:盡管這種方法在我們的特定場景中可能有效,但在處理具有私有屬性的實體、包含個人或敏感數據的公共屬性等時,它并不是最安全的選擇。為了更好地了解此潛在問題,請考慮使用具有包含密碼哈希的密碼屬性的用戶實體。我們不希望允許客戶端使用該屬性對用戶列表進行排序,對嗎?這些問題可以通過調整前面的實現以顯式排除某些屬性來解決,或者(更好地)通過強制實施在與客戶端交互時始終使用 DTO 而不是實體類的良好做法來解決,除非我們 100% 確定實體的數據不會構成任何威脅。

          我們在此處使用的以編程方式讀取/檢查代碼元數據的技術稱為反射。大多數編程框架通過一組專用庫或模塊支持它。在 .NET 中,此方法可通過 System.Reflection 命名空間提供的類和方法使用。

          提示有關反射技術的其他信息,請查看以下指南:http://mng.bz/X57E

          現在我們有了新的自定義驗證屬性,我們可以通過以下方式在 BoardGamesController 的 Get 方法中使用它:

          public async Task<RestDTO<BoardGame[]>> Get(
            int pageIndex = 0,
            [Range(1, 100)] int pageSize = 10,
            [SortColumnValidator(typeof(BoardGameDTO))] string? sortColumn = "Name",
            [SortOrderValidator] string? sortOrder = "ASC",
            string? filterQuery = null)

          請注意,我們對 SortColumnValidator 的 EntityType 參數使用了 BoardGameDTO 而不是 BoardGame 實體,因此遵循了第 5 章中介紹的單一責任原則。每當我們與客戶交換數據時,使用 DTO 而不是實體類型是一種很好的做法,它將大大提高 Web 應用程序的安全狀況。出于這個原因,我建議始終遵循這種做法,即使它需要額外的工作。

          6.1.4 數據驗證和開放API

          由于 Swashbuckle 中間件的內省活動,模型綁定系統遵循的標準會自動記錄在自動生成的 swagger.json 文件中,該文件表示我們 Web API 端點的 OpenAPI 規范文件(第 3 章)。我們可以通過執行 URL https://localhost:40443/swagger/v1/swagger.json 然后查看文件的 JSON 內容來檢查此類行為。下面是包含 GET /BoardGames 終結點的前兩個參數的摘錄:

          {
            "name": "PageIndex",   ?
            "in": "query",
            "schema": {
              "type": "integer",   ?
              "format": "int32",   ?
              "default": 0         ?
            }
          },
          {
            "name": "PageSize",    ?
            "in": "query",
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer",   ?
              "format": "int32",   ?
              "default": 10        ?
            }
          }

          ? 參數名稱

          ? 參數類型

          ? 參數格式

          ? 參數默認值

          ? 參數名稱

          ? 參數類型

          ? 參數格式

          ? 參數默認值

          如我們所見,關于我們的參數的所有值得注意的內容都記錄在那里。理想情況下,使用我們 Web API 的客戶端將使用此信息來創建兼容的用戶界面,該界面可用于以最佳方式與我們的數據進行交互。一個很好的例子是 SwaggerUI,它使用 swagger.json 文件來創建可用于測試 API 端點的輸入表單。執行 URL https://localhost:40443/swagger/index.xhtml,使用右句柄展開 GET/BoardGames 終結點面板,然后檢查“參數”選項卡上的參數列表(圖 6.1)。


          圖 6.1 GET /桌游端點參數信息

          每個參數的類型、格式和默認值信息都有很好的文檔記錄。如果我們點擊 試用 按鈕,我們可以訪問同一輸入表單的編輯模式,我們可以在其中用實際值填充文本框。如果我們嘗試插入一些明顯無效的數據,例如字符串而不是整數,然后單擊“執行”按鈕,則 UI 不會執行調用;相反,它顯示了我們需要糾正的錯誤(圖 6.2)。


          圖 6.2 包含無效數據的 GET /BoardGames 輸入表單

          尚未向 GET /BoardGame 端點發出任何請求。輸入錯誤是由 SwaggerUI 在從 swagger.json 文件中檢索的參數信息中構建的客戶端驗證技術檢測到的。所有這些都是自動發生的,而無需我們(幾乎)編寫任何代碼;我們正在充分利用框架的內置功能。

          我們應該依賴客戶端驗證嗎?

          請務必了解,SwaggerUI 的客戶端驗證功能僅用于改善用戶體驗和防止無用的服務器往返。這些功能沒有安全目的,因為任何具有最少HTML和/或JavaScript知識的用戶都可以輕松繞過它們。

          所有客戶端驗證控件、規則和檢查也是如此。它們對于增強應用程序的表示層和阻止無效請求而不觸發服務器端對應項非常有用,從而提高客戶端應用程序的整體性能,但它們無法確保或保護數據的完整性。因此,我們不能也不應該依賴客戶端驗證。在本書中,由于我們正在處理一個Web API,它代表了我們可以想象的任何客戶端-服務器模型的服務器端伴侶,因此我們必須始終驗證所有輸入數據,無論客戶端做什么。

          內置驗證屬性

          大多數內置驗證屬性都由 Swashbuckle 原生支持,它會自動檢測它們并將其記錄在 swagger.json 文件中。如果我們現在查看我們的 swagger.json 文件,我們將看到 [Range] 屬性是按以下方式記錄的:

          {
            "name": "pageSize",
            "in": "query",
            "schema": {
              "maximum": 100,      ?
              "minimum": 1,        ?
              "type": "integer",
              "format": "int32",
              "default": 10
            }
          }

          ? 范圍屬性最小值

          ? 范圍屬性最大值

          以下是記錄 [RegularExpression] 屬性的方式:

          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "pattern": "ASC|DESC",   ?
              "type": "string",
              "default": "ASC"
            }
          }

          ? 正則表達式屬性的正則表達式模式

          客戶端還可以使用此有價值的信息來實現其他客戶端驗證規則和功能。

          自定義驗證屬性

          不幸的是,Swashbuckle本身并不支持自定義驗證器,這并不奇怪,因為Swashbuckle不可能知道它們是如何工作的。但是該庫公開了一個方便的過濾器管道,該管道與 swagger.json 文件生成過程掛鉤。此功能允許我們創建自己的過濾器,將它們添加到管道中,并使用它們來自定義文件的內容。

          注意Swashbuckle的過濾器管道在第11章中進行了廣泛的介紹。在本節中,我通過介紹 IParameterFilter 接口僅提供此功能的一小部分預覽,因為我們需要它來滿足我們當前的需求。有關界面的其他信息,請查看第 11 章和/或以下 URL:http://mng.bz/ydNe

          簡而言之,如果我們想將自定義驗證屬性的信息添加到 swagger.json 文件中,我們需要執行以下操作:

          1. 創建一個新的篩選器類,為每個自定義驗證屬性實現 IParameterFilter 接口。Swashbuckle 將在為控制器的操作方法(和最小 API 方法)使用的所有參數創建 JSON 塊之前調用并執行此過濾器。
          2. 實現 IParameterFilter 接口的 Apply 方法,以便它檢測使用我們的自定義驗證屬性修飾的所有參數,并將每個參數的相關信息添加到 swagger.json 文件中。

          讓我們把這個計劃付諸實踐,從 [SortOrderValidator] 屬性開始。

          添加排序順序篩選器

          在 Visual Studio 的“解決方案資源管理器”面板中,創建一個新的 /Swagger/ 文件夾,右鍵單擊它,然后添加新的 SortOrderFilter.cs類文件。新類必須實現 IParameterFilter 接口及其 Apply 方法,以便向 swagger.json 文件添加合適的 JSON 密鑰,如內置驗證屬性。下面的清單顯示了我們如何做到這一點。

          清單 6.3 排序順序過濾器

          using Microsoft.OpenApi.Any;
          using Microsoft.OpenApi.Models;
          using Swashbuckle.AspNetCore.SwaggerGen;
          using MyBGList.Attributes;
           
          namespace MyBGList.Swagger
          {
              public class SortOrderFilter : IParameterFilter
              {
                  public void Apply(
                      OpenApiParameter parameter, 
                      ParameterFilterContext context)
                  {
                      var attributes = context.ParameterInfo?
                          .GetCustomAttributes(true)
                          .OfType<SortOrderValidatorAttribute>();   ?
           
                      if (attributes != null)
                      {           
                          foreach (var attribute in attributes)     ?
                          {
                              parameter.Schema.Extensions.Add(
                                  "pattern", 
                                  new OpenApiString(string.Join("|",
                                      attribute.AllowedValues.Select(v => $"^{v}$")))
                                  );
                          }
                      }
                  }
              }
          }

          ? 檢查參數是否具有屬性

          ? 如果該屬性存在,則采取相應的行動

          請注意,我們使用“模式”JSON 鍵和正則表達式模式作為值 — 與 [RegularExpression] 內置驗證屬性使用的行為相同。我們這樣做是為了促進客戶端驗證檢查的實施,假設客戶端在收到信息時已經能夠提供正則表達式支持(這恰好與我們的驗證要求“兼容”)。我們可以使用不同的鍵和/或值類型,將實現細節留給客戶端。接下來,讓我們為第二個自定義驗證屬性創建另一個篩選器。

          添加排序列篩選器

          在 /Swagger/ 文件夾中添加新的 SortColumnFilter.cs 類文件。此類類似于 SortOrderFilter 類,但有一些細微的區別:這一次,我們必須檢索 EntityType 屬性的名稱,而不是 AllowedValues 字符串數組,這需要一些額外的工作。下面的清單提供了源代碼。

          清單 6.4 排序列過濾器

          using Microsoft.OpenApi.Any;
          using Microsoft.OpenApi.Models;
          using Swashbuckle.AspNetCore.SwaggerGen;
          using MyBGList.Attributes;
           
          namespace MyBGList.Swagger
          {
              public class SortColumnFilter : IParameterFilter
              {
                  public void Apply(
                      OpenApiParameter parameter, 
                      ParameterFilterContext context)
                  {
                      var attributes = context.ParameterInfo?
                          .GetCustomAttributes(true)
                          .OfType<SortColumnValidatorAttribute>();     ?
                      if (attributes != null)            
                      {
                          foreach (var attribute in attributes)        ?
                          {
                              var pattern = attribute.EntityType
                                  .GetProperties()
                                  .Select(p => p.Name);
                              parameter.Schema.Extensions.Add(
                                  "pattern",
                                  new OpenApiString(string.Join("|",
                                      pattern.Select(v => $"^{v}$")))
                                  );
                          }
                      }
                  }
              }
          }

          ? 檢查參數是否具有屬性

          ? 如果該屬性存在,則采取相應的行動

          同樣,我們使用“模式”鍵和正則表達式模式作為值,因為即使是這個驗證器也與基于正則表達式的客戶端驗證檢查兼容。現在我們需要將這些過濾器掛接到程序.cs文件中的 Swashbuckle 中間件,以便在生成 Swagger 文件時將它們考慮在內。

          綁定 IParameterFilters

          打開 Program.cs 文件,并在頂部添加與我們新實現的過濾器相對應的命名空間:

          using MyBGList.Swagger;

          向下滾動到我們將 Swashbuckle 的 Swagger 生成器中間件添加到管道的行,并按以下方式對其進行更改:

          builder.Services.AddSwaggerGen(options => {
              options.ParameterFilter<SortColumnFilter>();    ?
              options.ParameterFilter<SortOrderFilter>();     ?
          });

          ? 將排序列篩選器添加到篩選器管道

          ? 將 SortOrderFilter 添加到篩選器管道

          現在,我們可以通過在調試模式下運行項目并查看自動生成的 swagger.json 文件來測試我們所做的工作,使用與之前相同的 URL (https://localhost:40443/swagger/v1/swagger.json)。如果我們做對了所有事情,我們應該看到 sortOrder 和 sortColumn 參數,其中存在 “pattern” 鍵并根據驗證器的規則填充:

          {
            "name": "sortColumn",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "Name",
              "pattern": "^Id$|^Name$|^Year$"      ?
            }
          },
          {
            "name": "sortOrder",
            "in": "query",
            "schema": {
              "type": "string",
              "default": "ASC",
              "pattern": "^ASC$|^DESC$"            ?
            }
          }

          ? 排序列驗證器的正則表達式模式

          ? SortOrderValidator的正則表達式模式

          重要的是要了解,實現自定義驗證器可能是一項挑戰,而且在時間和源代碼行方面是一項昂貴的任務。在大多數情況下,我們不需要這樣做,因為內置驗證屬性可以滿足我們的所有需求。但是,每當我們處理復雜或可能麻煩的客戶端定義輸入時,能夠創建和記錄它們可以有所作為。

          6.1.5 綁定復雜類型

          到目前為止,我們一直使用簡單的類型參數來處理我們的操作方法:整數、字符串、布爾值等。此方法是了解模型綁定和驗證屬性如何工作的好方法,并且在處理一小組參數時通常是首選方法。但是,使用復雜類型參數(如 DTO)可以極大地受益于幾種方案,特別是考慮到 ASP.NET Core 模型綁定系統也可以處理它們。

          當模型綁定的目標為復雜類型時,每個類型屬性都被視為綁定和驗證的單獨參數。復雜類型的每個屬性都充當一個簡單的類型參數,在代碼可擴展性和靈活性方面具有很大的好處。我們可以將所有參數框入單個 DTO 類中,而不是可能很長的方法參數列表。了解這些優勢的最好方法是將它們付諸實踐,將我們當前的簡單類型參數替換為單個、全面的復雜類型。

          創建請求DTO類

          在 Visual Studio 的“解決方案資源管理器”面板中,右鍵單擊 /DTO/ 文件夾,然后添加新的 RequestDTO.cs類文件。此類將包含我們在 BoardGamesController 的 Get 操作方法中接收的所有客戶端定義的輸入參數;我們所要做的就是為每個屬性創建一個屬性,如下面的列表所示。

          清單 6.5 請求 DTO.cs文件

          using MyBGList.Attributes;
          using System.ComponentModel;
          using System.ComponentModel.DataAnnotations;
           
          namespace MyBGList.DTO
          {
              public class RequestDTO
              {
                  [DefaultValue(0)]                                  ?
                  public int PageIndex { get; set; } = 0;
           
                  [DefaultValue(10)]                                 ?
                  [Range(1, 100)]                                    ?
                  public int PageSize { get; set; } = 10;
           
                  [DefaultValue("Name")]                             ?
                  [SortColumnValidator(typeof(BoardGameDTO))]        ?
                  public string? SortColumn { get; set; } = "Name";
           
                  [DefaultValue("ASC")]                              ?
                  [SortOrderValidator]                               ?
                  public string? SortOrder { get; set; } = "ASC";
           
                  [DefaultValue(null)]                               ?
                  public string? FilterQuery { get; set; } = null;
              }
          }

          ? 默認值屬性

          ? 內置驗證屬性

          ? 自定義驗證屬性

          請注意,我們已經使用 [DefaultValue] 屬性修飾了每個屬性。此屬性使 Swagger 生成器中間件能夠在 swagger.json 文件中創建“默認”鍵,因為它將無法看到我們使用方便的 C# 內聯語法設置的初始值。幸運的是,此屬性受支持,并提供了一個很好的解決方法。現在我們有了 RequestDTO 類,我們可以使用它來通過以下方式替換 BoardGamesController 的 Get 方法的簡單類型參數:

          [HttpGet(Name = "GetBoardGames")]
          [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
          public async Task<RestDTO<BoardGame[]>> Get(
              [FromQuery] RequestDTO input)               ?
          {
              var query = _context.BoardGames.AsQueryable();
              if (!string.IsNullOrEmpty(input.FilterQuery))
                  query = query.Where(b => b.Name.Contains(input.FilterQuery));
              query = query
                      .OrderBy($"{input.SortColumn} {input.SortOrder}")
                      .Skip(input.PageIndex * input.PageSize)
                      .Take(input.PageSize);
           
              return new RestDTO<BoardGame[]>()
              {
                  Data = await query.ToArrayAsync(),
                  PageIndex = input.PageIndex,
                  PageSize = input.PageSize,
                  RecordCount = await _context.BoardGames.CountAsync(),
                  Links = new List<LinkDTO> {
                      new LinkDTO(
                          Url.Action(
                              null,
                              "BoardGames",
                              new { input.PageIndex, input.PageSize },
                              Request.Scheme)!,
                          "self",
                          "GET"),
                  }
              };
          }

          ? 新的復雜類型參數

          在此代碼中,我們使用 [FromQuery] 屬性告訴路由中間件我們希望從查詢字符串中獲取輸入值,從而保留以前的行為。但是我們可以使用任何其他可用屬性:

          • [FromQuery] - 從查詢字符串中獲取值
          • [FromRoute] - 從路徑數據中獲取值
          • [發件人表單] - 從已發布的表單域中獲取值
          • [FromBody] - 從請求正文獲取值
          • [FromHeader] - 從 HTTP 標頭獲取值
          • [FromServices] - 從已注冊服務的實例中獲取值
          • [FromUri] - 從外部 URI 獲取值

          能夠使用基于屬性的方法在參數綁定技術之間切換是框架的另一個方便功能。稍后我們將使用其中一些屬性。

          我們必須顯式使用 [FromQuery] 屬性,因為復雜類型參數的默認方法是從請求正文中獲取值。我們還必須將源代碼中所有參數的引用替換為新類的屬性。現在,“新”實現看起來比前一個更時尚和DRY(不要重復自己原則)。此外,我們有一個靈活的通用 DTO 類,可用于在 BoardGamesController 以及我們將來要添加的其他控制器中實現類似的基于 GET 的操作方法:DomainsController、MechanicsControllers 等。右?

          嗯,沒有。如果我們更好地查看當前的 RequestDTO 類,我們會發現它根本不是通用的。問題出在 [SortColumnValidator] 屬性中,該屬性需要一個類型參數。通過查看源代碼,我們可以看到,此參數被硬編碼為 BoardGameDTO 類型:

          [SortColumnValidator(typeof(BoardGameDTO))]

          我們如何解決這個問題?乍一想到,我們可能會想動態傳遞該參數,也許使用泛型 <T> 類型。此方法需要將 RequestDTO 的類聲明更改為

          public class RequestDTO<T>

          這將允許我們通過以下方式在操作方法中使用它:

          [FromQuery] RequestDTO<BoardGameDTO> input

          然后我們將以這種方式更改驗證器:

          [SortColumnValidator(typeof(T))]

          不幸的是,這種方法行不通。在 C# 中,修飾類的屬性在編譯時計算,但泛型 <T> 類在運行時之前不會收到其最終類型信息。原因很簡單:由于某些屬性可能會影響編譯過程,因此編譯器必須能夠在編譯時完整地定義它們。因此,屬性不能使用泛型類型參數。

          C 語言中的泛型屬性類型限制#

          根據 Eric Lippert(前Microsoft工程師和 C# 語言設計團隊成員)的說法,添加此限制是為了降低語言和編譯器代碼的復雜性,以應對不會增加太多價值的用例。他的解釋(釋義)可以在Jon Skeet給出的StackOverflow答案中找到:http://mng.bz/Mlw8。

          有關本主題的其他信息,請查看 http://mng.bz/Jl0K 上的 C# 泛型Microsoft指南。

          此行為將來可能會更改,因為 .NET 社區經常要求 C# 語言設計團隊重新評估它。

          如果屬性不能使用泛型類型,我們如何解決這個問題?答案不難猜:如果山不去穆罕默德,穆罕默德必須去山上。換句話說,我們需要將屬性方法替換為 ASP.NET 框架支持的另一種驗證技術。幸運的是,這樣的技術恰好存在,它的名字是IValidatableObject。

          實現 IValidatableObject

          IValidatableObject 接口提供了一種驗證類的替代方法。它的工作方式類似于類級屬性,這意味著我們可以使用它來驗證任何 DTO 類型,而不管其屬性如何,以及它包含的所有屬性級驗證屬性。與驗證屬性相比,IValidatableObject 接口有兩個主要優點:

          • 它不需要在編譯時定義,因此它可以使用泛型類型(這使我們能夠克服我們的問題)。
          • 它旨在驗證整個類,因此我們可以使用它來同時檢查多個屬性,并執行交叉驗證和任何其他需要整體方法的任務。

          讓我們使用 IValidatableObject 接口在當前的 RequestDTO 類中實現排序列驗證檢查。以下是我們需要做的:

          1. 更改 RequestDTO 的類聲明,以便它可以接受泛型 <T> 類型。
          2. 將 IValidatableObject 接口添加到 RequestDTO 類型。
          3. 實現 IValidatableObject 接口的 Validate 方法,以便它將提取泛型 <T> 類型的屬性,并使用其名稱來驗證 SortColumn 屬性。

          下面的清單顯示了我們如何實現這些步驟。

          清單 6.6 請求DTO.cs文件(版本2)

          using MyBGList.Attributes;
          using System.ComponentModel;
          using System.ComponentModel.DataAnnotations;
           
          namespace MyBGList.DTO
          {
              public class RequestDTO<T> : IValidatableObject          ?
              {
                  [DefaultValue(0)]
                  public int PageIndex { get; set; } = 0;
           
                  [DefaultValue(10)]
                  [Range(1, 100)]
                  public int PageSize { get; set; } = 10;
           
                  [DefaultValue("Name")]
                  public string? SortColumn { get; set; } = "Name";
           
                  [SortOrderValidator]
                  [DefaultValue("ASC")]
                  public string? SortOrder { get; set; } = "ASC";
           
                  [DefaultValue(null)]
                  public string? FilterQuery { get; set; } = null;
           
                  public IEnumerable<ValidationResult> Validate(       ?
                      ValidationContext validationContext)
                  {
                      var validator = new SortColumnValidatorAttribute(typeof(T));
                      var result = validator
                          .GetValidationResult(SortColumn, validationContext);
                      return (result != null) 
                          ? new [] { result } 
                          : new ValidationResult[0];
                  }
              }
          }

          ? 泛型類型和 IValidatableObject 接口

          ? 驗證方法實現

          在此代碼中,我們看到 Validate 方法實現呈現了一個情節轉折:我們在后臺使用 SortColumnValidator!主要區別在于,這一次,我們將其用作“標準”類實例,而不是數據注釋屬性,這允許我們將泛型類型作為參數傳遞。

          這幾乎感覺像作弊,對吧?但事實并非如此;我們正在回收我們已經做過的事情。我們可以做到這一點,這要歸功于 ValidationAttribute 基類公開的 GetValidationResult 方法被定義為公共的,這允許我們創建驗證器的實例并調用它來驗證 SortColumn 屬性。

          現在我們有一個要在代碼中使用的泛型 DTO 類,請打開 BoardGamesControllers.cs 文件,向下滾動到 Get 方法,并按以下方式更新其簽名:

          public async Task<RestDTO<BoardGame[]>> Get(
              [FromQuery] RequestDTO<BoardGameDTO> input)

          該方法的其余代碼不需要任何更改。我們已將 BoardGameDTO 指定為泛型類型參數,以便 RequestDTO 的 Validate 方法將根據 SortColumn 輸入數據檢查其屬性,確保客戶端設置的用于對數據進行排序的列對該特定請求有效。

          添加域控制器和機械控制器

          現在是創建DomainsController和MechanicsController的好時機,復制我們迄今為止在BoardGamesController中實現的所有功能。這樣做將允許我們對通用的 RequestDTO 類和我們的 IValidatableObject 靈活實現進行適當的測試。我們還需要添加幾個新的DTO,DomainDTO類和MechanicDTO,它們將與BoardGameDTO類類似。

          由于篇幅原因,我在這里不列出這四個文件的源代碼。該代碼位于本書 GitHub 存儲庫的 /Chapter_06/ 文件夾中的 /Controllers/ 和 /DTO/ 子文件夾中。我強烈建議您嘗試在不查看 GitHub 文件的情況下實現它們,因為這是練習您到目前為止所學的所有內容的好機會。

          測試新控制器

          當新控制器準備就緒時,我們可以使用以下 URL 端點(對于 GET 方法)徹底檢查它們,

          • https://localhost:40443/Domains/
          • https://localhost:40443/Mechanics/

          以及 SwaggerUI(用于 POST 和 DELETE 方法)。

          注意每當我們刪除域或機制時,我們在第 4 章中為這些實體設置的級聯規則也會刪除其對相應多對多查找表的所有引用。所有棋盤游戲都將失去與該特定領域或機制的關系(如果有的話)。要恢復,我們需要刪除所有棋盤游戲,然后使用 SeedController 的 Put 方法重新加載它們。

          更新 IParameterFilters

          在我們進一步討論之前,我們需要做最后一件事。現在我們已經用 DTO 替換了簡單的類型參數,SortOrderFilter 和 SortColumnFilter 將無法再找到我們的自定義驗證器。原因很簡單:他們當前的實現是使用上下文的 GetCustomAttributes 方法查找它們。ParameterInfo 對象,它返回應用于篩選器處理的參數的屬性數組。現在,此 ParameterInfo 包含 DTO 本身的引用,這意味著前面的方法將返回應用于整個 DTO 類的屬性,而不是其屬性。

          為了解決這個問題,我們需要擴展屬性查找行為,以便它還檢查分配給給定參數屬性的屬性(如果有)。以下是我們如何更新 SortOrderFilter 的源代碼來執行此操作:

          var attributes = context.ParameterInfo
              .GetCustomAttributes(true)
              .Union(                                                  ?
                  context.ParameterInfo.ParameterType.GetProperties()
                  .Where(p => p.Name == parameter.Name)
                  .SelectMany(p => p.GetCustomAttributes(true))
              )
              .OfType<SortOrderValidatorAttribute>();

          ? 檢索參數的屬性自定義屬性

          請注意,我們使用聯合 LINQ 擴展方法來生成單個數組,其中包含分配給 ParameterInfo 對象本身的自定義屬性,以及分配給該 ParameterInfo 對象的屬性的屬性以及篩選器當前正在處理的參數的名稱(如果有)。由于這種新的實現,我們的過濾器將能夠找到分配給任何復雜類型參數的屬性以及簡單類型參數的自定義屬性,從而確保完全向后兼容。

          SortOrderFilter 已修復,但 SortColumnFilter 呢?不幸的是,修復并不是那么簡單。[SortColumnValidator] 屬性不應用于任何屬性,因此 SortColumnFilter 無法找到它。我們可能會認為,通過將屬性添加到 IValidatableObject 的 Validate 方法,然后調整篩選器的查找行為以包含屬性以外的方法,我們可以解決此問題。但我們已經知道此解決方法將失敗;該屬性仍需要無法在編譯時設置的泛型類型參數。由于空間原因,我們現在不會解決這個問題;我們將把這個任務推遲到第11章,屆時我們將學習其他涉及Swagger和Swashbuckle的API文檔技術。

          提示在繼續操作之前,請確保將前面的修補程序也應用于排序列篩選器。要添加的源代碼是相同的,因為兩個篩選器使用相同的查找策略。這個補丁似乎毫無用處,因為 SortColumnFilter 不起作用(并且在一段時間內不起作用),但讓我們的類保持最新是一種很好的做法,即使我們沒有積極使用或指望它們。

          我們的數據驗證之旅已經結束,至少目前是這樣。在下一節中,我們將學習如何處理驗證錯誤和程序異常。

          6.2 錯誤處理

          現在我們已經實現了幾個服務器端數據驗證檢查,除了通常在公然無效的 HTTP 請求的情況下提供的場景之外,我們還為 Web API 創建了許多其他失敗場景。根據確定模型綁定失敗的驗證規則并受其限制,每條缺失、格式錯誤、不正確或其他無效的輸入數據都將被我們的 Web API 拒絕,并顯示 HTTP 400 - 錯誤請求錯誤響應。在本章開頭,當我們嘗試將字符串值傳遞給 pageIndex 參數而不是數字 1 時,我們遇到了這種行為。但是,HTTP 400狀態并不是來自服務器的唯一響應。我們還得到了一個有趣的響應正文,值得再看一看:

          {
            "type":"https://tools.ietf.org/html/rfc7231#section-6.5.1",
            "title":"One or more validation errors occurred.",
            "status":400,
            "traceId":"00-a074ebace7131af6561251496331fc65-ef1c633577161417-00",
            "errors":{
              "pageIndex":["The value 'string' is not valid."]
            }
          }

          正如我們所看到的,我們的 Web API 不僅告訴客戶端出了問題;它還提供有關錯誤的上下文信息,包括帶有拒絕值的參數,使用 HTTP API 響應格式標準在 https://tools.ietf.org/html/rfc7807 中定義,該標準在第 2 章中簡要介紹。所有這些工作都由引擎蓋下的框架自動執行;我們不需要做任何事情。這項工作是 [ApiController] 屬性的內置功能,用于裝飾我們的控制器。

          6.2.1 模型狀態對象

          若要了解 [ApiController] 屬性為我們做了什么,我們需要退后一步,查看整個模型綁定和驗證系統生命周期。圖 6.3 說明了框架在典型 HTTP 請求中執行的各種步驟的流程。


          圖 6.3 具有 [ApiController] 屬性的模型綁定和驗證生命周期

          我們的興趣點在 HTTP 請求到達后立即開始,路由中間件調用模型綁定系統,該系統按順序執行兩個相關任務:

          • 將輸入值綁定到操作方法的簡單類型和/或復雜類型參數。如果綁定過程失敗,則會立即返回 HTTP 錯誤 400 響應;否則,請求將進入下一階段。
          • 使用內置驗證屬性、自定義驗證屬性和/或 IValidatableObject 驗證模型。所有驗證檢查的結果都記錄在 ModelState 對象中,該對象最終變為有效(未發生驗證錯誤)或無效(發生一個或多個驗證錯誤)。如果 ModelState 對象最終有效,則請求由操作方法處理;否則,將返回 HTTP 錯誤 400 響應。

          重要的教訓是,綁定錯誤和驗證錯誤都由框架處理(使用 HTTP 400 錯誤響應),甚至無需調用操作方法。換句話說,[ApiController] 屬性提供了一個完全自動化的錯誤處理管理系統。如果我們沒有特定的要求,這種方法可能很棒,但是如果我們想自定義某些東西怎么辦?在以下部分中,我們將了解如何執行此操作。

          6.2.2 自定義錯誤消息

          我們可能要做的最重要的事情是定義一些自定義錯誤消息而不是默認錯誤消息。讓我們從模型綁定錯誤開始。

          自定義模型綁定錯誤

          要更改默認的模型綁定錯誤消息,我們需要修改 ModelBindingMessageProvider 的設置,可以從 ControllersMiddleware 的配置選項訪問該設置。打開程序.cs文件,找到構建器。Services.AddControllers 方法,并按以下方式替換當前的無參數實現(粗體換行):

          builder.Services.AddControllers(options => {
              options.ModelBindingMessageProvider.SetValueIsInvalidAccessor(
                  (x) => $"The value '{x}' is invalid.");
              options.ModelBindingMessageProvider.SetValueMustBeANumberAccessor(
                  (x) => $"The field {x} must be a number.");
              options.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor(
                  (x, y) => $"The value '{x}' is not valid for {y}.");
              options.ModelBindingMessageProvider.SetMissingKeyOrValueAccessor(
                  () => $"A value is required.");
          });

          為簡單起見,此示例僅更改許多可用消息中的三個。

          自定義模型驗證錯誤

          更改模型驗證錯誤消息很容易,因為 ValidationAttribute 基類附帶了一個方便的 ErrorMessage 屬性,可用于此目的。我們在實現自己的自定義驗證器時使用了它。相同的技術可用于所有內置驗證器:

          [Required(ErrorMessage = "This value is required.")]
          [Range(1, 100, ErrorMessage = "The value must be between 1 and 100.")]

          但是,通過這樣做,我們將自定義錯誤消息,而不是 ModelState 驗證過程本身,該過程仍由框架自動執行。

          6.2.3 手動模型驗證

          假設我們希望(或被要求)將當前的 HTTP 400 - 錯誤請求替換為不同的狀態代碼,以防某些特定的驗證失敗,例如不正確的 pageSize 的 HTTP 501 - 未實現狀態代碼整數值(小于 1 或大于 100)。除非我們找到一種方法在操作方法中手動檢查 ModelState(并相應地采取行動),否則無法處理此更改請求。但我們知道我們不能這樣做,因為由于 [ApiController] 功能,ModelState 驗證和錯誤處理過程由框架自動處理。如果 ModelState 無效,操作方法甚至不會發揮作用;將改為返回默認(和不需要的)HTTP 400 錯誤。

          可能想到的第一個解決方案是擺脫 [ApiController] 屬性,這將刪除自動行為并允許我們手動檢查 ModelState,即使它無效。這種方法行得通嗎?會的。圖 6.4 顯示了模型綁定和驗證生命周期圖如何在沒有 [ApiController] 屬性的情況下工作。


          圖 6.4 沒有 [ApiController] 屬性的模型綁定和驗證生命周期

          正如我們所看到的,現在無論 ModelState 狀態如何,都將執行操作方法,從而允許我們檢查它,查看出了什么問題,并采取相應的行動,這正是我們想要的。但是我們不應該承諾如此苛刻的解決方法,因為 [ApiController] 屬性為我們的控制器提供了我們可能想要保留的其他幾個功能。相反,我們應該禁用自動模型狀態驗證功能,這可以通過調整 [ApiController] 屬性本身的默認配置設置來實現。

          配置 API 控制器的行為

          打開 Program.cs 文件,找到我們實例化應用程序局部變量的行:

          var app = builder.Build();

          將以下代碼行放在其正上方:

          builder.Services.Configure<ApiBehaviorOptions>(options =>  
              options.SuppressModelStateInvalidFilter = true);
           
          var app = builder.Build();

          此設置禁止在模型狀態無效時自動返回 BadRequestObjectResult 的篩選器。現在,我們可以在不刪除 [ApiController] 屬性的情況下實現所有控制器的預期,并且我們已準備好通過有條件地返回 HTTP 501 狀態代碼來實現更改請求。

          實現自定義 HTTP 狀態代碼

          為簡單起見,假設更改請求僅影響域控制器。打開 /Controllers/DomainsController.cs 文件,然后向下滾動到 Get 操作方法。以下是我們需要做的:

          1. 檢查模型狀態(有效或無效)。
          2. 如果模型狀態有效,請保留現有行為。
          3. 如果模型狀態無效,請檢查錯誤是否與 pageSize 參數相關。如果是這種情況,請返回 HTTP 501 狀態代碼;否則,請堅持使用 HTTP 400。

          以下是我們如何實現它:

          [HttpGet(Name = "GetDomains")]
          [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
          public async Task<ActionResult<RestDTO<Domain[]>>> Get(                ?
              [FromQuery] RequestDTO<DomainDTO> input)
          {
              if (!ModelState.IsValid)                                           ?
              {
                  var details = new ValidationProblemDetails(ModelState);
                  details.Extensions["traceId"] = 
                      System.Diagnostics.Activity.Current?.Id 
                        ?? HttpContext.TraceIdentifier;
                  if (ModelState.Keys.Any(k => k == "PageSize"))
                  {
                      details.Type = 
                          "https://tools.ietf.org/html/rfc7231#section-6.6.2";
                      details.Status = StatusCodes.Status501NotImplemented;
                      return new ObjectResult(details) {
                          StatusCode = StatusCodes.Status501NotImplemented
                      };
                  }
                  else
                  {
                      details.Type = 
                          "https://tools.ietf.org/html/rfc7231#section-6.5.1";
                      details.Status = StatusCodes.Status400BadRequest;
                      return new BadRequestObjectResult(details);
                  }
              }
           
              // ... code omitted ...                                            ?
          }

          ? 新的返回值(操作結果<T>)

          ? 模型狀態無效時要執行的步驟

          ? 由于空格原因省略了代碼(不變)

          我們可以通過使用返回 true 或 false 的 IsValid 屬性輕松檢查 ModelState 狀態。如果我們確定 ModelState 無效,我們會檢查錯誤集合中是否存在“PageSize”鍵,并創建一個 UnprocessableEntity 或 BadRequest 結果以返回到客戶端。該實現需要幾行代碼,因為我們希望構建一個記錄錯誤詳細信息的豐富請求正文,包括對記錄錯誤狀態代碼的 RFC 的引用、traceId 等。

          這種方法迫使我們將操作方法的返回類型從 Task<RestDTO<Domain[]>>更改為 Task<ActionResult<RestDTO<Domain[]>>>,因為現在我們需要處理兩種不同類型的響應:如果 ModelState 驗證失敗,則為 ObjectResult,如果成功,則為 JSON 對象。ActionResult 是一個不錯的選擇,因為由于其泛型類型的支持,它可以處理這兩種類型。

          現在,我們可以測試 DomainsController 的 Get 操作方法的新行為。此 URL 應返回 HTTP 501 狀態代碼:https://localhost:40443/Domains?pageSize=101。這個應該用HTTP 400狀態代碼響應:https://localhost:40443/Domains?sortOrder=InvalidValue。

          由于我們還需要檢查 HTTP 狀態代碼,而不僅僅是響應正文,因此請務必在執行網址之前打開瀏覽器的“網絡”標簽頁(可在所有基于 Chrome 的瀏覽器中通過開發者工具訪問),這是一種實時查看每個 HTTP 響應狀態代碼的快速有效方法。

          意外的回歸錯誤

          到目前為止,一切都很好 - 除了我們在所有控制器中無意中造成的非平凡回歸錯誤!要了解我在說什么,請嘗試針對 BoardGamesController 的 Get 方法執行上一節中的兩個“無效”URL:

          • https://localhost:40443/BoardGames?pageSize=101
          • https://localhost:40443/BoardGames?sortOrder=invalidValue

          第一個 URL 返回 101 個棋盤游戲,第二個 URL 由于動態 LINQ 中的語法錯誤而引發未經處理的異常。我們的驗證器怎么了?

          答案應該是顯而易見的:它們仍然有效,但由于我們禁用了 [ApiController] 的自動 ModelState 驗證功能(和 HTTP 400 響應),因此即使某些輸入參數無效,也會執行所有操作方法,除了 DomainsController 的 Get 操作方法外,無需手動驗證來填補空白!我們的 BoardGamesController 和 MechanicsController,以及除 Get 之外的所有 DomainsController 操作方法,不再受到不安全輸入的保護。不過,不要驚慌;我們可以解決問題。

          同樣,我們可能會想從 DomainsController 中刪除 [ApiController] 屬性,并解決我們的回歸錯誤,而不會進一步麻煩。不幸的是,這種方法不起作用;它將防止錯誤影響其他控制器,但不能解決域控制器的其他操作方法的問題。此外,我們將失去[ApiController]的其他有用功能,這就是為什么我們一開始沒有擺脫它的原因。

          想想我們做了什么:為整個 Web 應用程序禁用了 [ApiController] 的一個功能,因為我們不希望它為單個控制器的操作方法觸發。這就是錯誤。這個想法很好;我們需要縮小范圍。

          實現 IActionModelConvention 篩選器

          我們可以通過使用方便的 ASP.NET Core 過濾器管道來獲取我們想要的東西,它允許我們自定義 HTTP 請求/響應生命周期的行為。我們將創建一個篩選器屬性,用于檢查給定操作方法中是否存在 ModelStateInvalidFilter 并將其刪除。此設置將具有與我們放置在 Program.cs 文件中的配置設置相同的效果,但僅針對我們將選擇使用該 filter 屬性修飾的操作方法。換句話說,我們將能夠有條件地禁用 ModelState 自動驗證功能(選擇退出、默認加入),而不必為所有人關閉它(默認退出)。

          讓我們把這個理論付諸實踐。在 /Attributes/ 文件夾中創建一個新的 ManualValidationFilterAttribute.cs 類文件,并用下面的清單中的源代碼填充它。

          清單 6.7 手動驗證過濾器屬性

          using Microsoft.AspNetCore.Mvc.ApplicationModels;
          using Microsoft.AspNetCore.Mvc.Infrastructure;
           
          namespace MyBGList.Attributes
          {
              public class ManualValidationFilterAttribute 
                  : Attribute, IActionModelConvention
              {
                  public void Apply(ActionModel action)
                  {
                      for (var i = 0; i < action.Filters.Count; i++)
                      {
                          if (action.Filters[i] is ModelStateInvalidFilter
                              || action.Filters[i].GetType().Name == 
                                  "ModelStateInvalidFilterFactory")
                          {
                              action.Filters.RemoveAt(i);
                              break;
                          }
                      }
                  }
              }
          }

          遺憾的是,ModelStateInvalidFilterFactory 類型被標記為內部,這使我們無法使用強類型方法檢查篩選器是否存在。我們必須將 Name 屬性與類的文字名稱進行比較。這種方法并不理想,如果名稱在框架的未來版本中發生更改,則可能會停止工作,但就目前而言,它將解決問題。現在我們有了過濾器,我們需要像任何其他屬性一樣將其應用于 DomainsController 的 Get 操作方法:

          [HttpGet(Name = "GetDomains")]
          [ResponseCache(Location = ResponseCacheLocation.Any, Duration = 60)]
          [ManualValidationFilter]                                              ?
          public async Task<ActionResult<RestDTO<Domain[]>>> Get(
              [FromQuery] RequestDTO<DomainDTO> input)

          ? 新的手動驗證過濾器屬性

          現在我們可以刪除(或注釋掉)導致程序.cs文件中回歸錯誤的應用程序范圍的設置:

          // Code replaced by the [ManualValidationFilter] attribute
          // builder.Services.Configure<ApiBehaviorOptions>(options =>
          //    options.SuppressModelStateInvalidFilter = true);

          我們為所有控制器和方法重新啟用了自動 ModelState 驗證功能,僅將已實現合適回退的單個操作方法保留為手動狀態。我們已經找到了一種方法來滿足我們的更改請求,同時修復我們意想不到的錯誤 - 并且不放棄任何東西。此外,我們在這里所做的一切都幫助我們獲得了經驗,提高了我們對 ASP.NET Core 請求/響應管道以及底層模型綁定和驗證機制的認識。我們的手動模型狀態驗證概述已經結束,至少目前是這樣。

          6.2.4 異常處理

          ModelState 對象并不是我們可能想要處理的應用程序錯誤的唯一來源。我們在使用 Web API 時遇到的大多數應用程序錯誤不是由于客戶端定義的輸入數據造成的,而是由于源代碼的意外行為造成的:空引用異常、DBMS 連接失敗、數據檢索錯誤、堆棧溢出等。所有這些問題都可能會引發異常,這些異常(正如我們從第2章開始知道的那樣)將由DeveloperExceptionPageMiddleware(如果相應的應用程序設置為true)和ExceptionHandlingMiddleware(如果設置為false)捕獲并處理。

          在第 2 章中,當我們實現 UseDeveloperExceptionPage 應用程序設置時,我們在通用 appsettings.json 文件中將其設置為 false,在 appsettings 中將其設置為 true。開發.json 文件。我們使用此方法來確保僅在開發環境中執行應用時才使用 DeveloperExceptionPageMiddleware。此行為在程序.cs文件的代碼部分中清晰可見,我們將 ExceptionHandling 中間件添加到管道中:

          if (app.Configuration.GetValue<bool>("UseDeveloperExceptionPage"))
              app.UseDeveloperExceptionPage();
          else
              app.UseExceptionHandler("/error");

          讓我們暫時禁用此開發覆蓋,以便我們可以專注于 Web API 在處理實際客戶端(換句話說,在生產中)時如何處理異常。打開 appSettings.Development.json 文件,并將 UseDeveloperExceptionPage 設置的值從 true 更改為 false:

          "UseDeveloperExceptionPage": false

          現在,即使在開發中,我們的應用程序也將采用生產錯誤處理行為,允許我們在更新它時檢查我們正在做什么。在第 2 章中,我們將 ExceptionHandlingMiddleware 的錯誤處理路徑設置為 “/error” 端點,我們在程序.cs文件中使用以下最小 API MapGet 方法實現了該端點:

          app.MapGet("/error",
              [EnableCors("AnyOrigin")]
              [ResponseCache(NoStore = true)] () =>
              Results.Problem());

          我們當前的實現由一行代碼組成,該代碼返回一個 ProblemDetails 對象,從而生成符合 RFC 7807 的 JSON 響應。我們在第 2 章中通過實現和執行 /error/test 端點(引發異常)測試了此行為。讓我們再次執行它以再次查看它:

          {
            "type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
            "title":"An error occurred while processing your request.",
            "status":500
          }

          這種簡單而有效的響應清楚地表明,我們已經為生產環境設置了一個不錯的異常處理策略。每次出現問題時,或者當我們想要手動在代碼中引發異常時,我們可以確定調用客戶端將收到 HTTP 500 錯誤狀態代碼以及標準(且符合 RFC 7807)響應正文。

          同時,我們可以看到整體結果沒有信息量。我們僅通過返回 HTTP 500 狀態代碼和以人類可讀形式解釋錯誤的最小響應正文來告訴客戶端出了問題。

          我們面臨的場景與 [ApiController] 的 ModelState 驗證所經歷的情況相同,這是一種自動行為,對于大多數方案來說可能很方便,但如果我們需要進一步自定義它,可能會受到限制。我們可能需要返回不同的狀態代碼,具體取決于引發的異常。或者,我們可能希望在某處記錄錯誤和/或向某人發送電子郵件通知(取決于異常類型和/或上下文)。

          幸運的是,ExceptionHandlingMiddleware可以配置為執行所有這些操作,甚至更多,只需相對較少的代碼行。在下面的部分中,我們將更好地研究 ExceptionHandlingMiddleware(畢竟,我們只是在第 2 章中觸及了它的表面),并了解如何充分利用它。

          使用異常處理中間件

          自定義當前行為的第一件事是為 ProblemDetails 對象提供有關異常的一些其他詳細信息,例如其 Message 屬性值。為此,我們需要檢索兩個對象:

          • 當前 HttpContext,可以作為參數添加到所有最小 API 方法中
          • 一個 IExceptionHandlerPathFeature 接口實例,它允許我們在方便的處理程序中訪問原始異常

          以下是我們如何做到這一點(相關代碼以粗體顯示):

          app.MapGet("/error",
              [EnableCors("AnyOrigin")]
              [ResponseCache(NoStore = true)] (HttpContext context) =>           ?
              {
                  var exceptionHandler =
                      context.Features.Get<IExceptionHandlerPathFeature>();      ?
                  
                  // TODO: logging, sending notifications, and more              ?
                  
                  var details = new ProblemDetails();
                  details.Detail = exceptionHandler?.Error.Message;              ?
                  details.Extensions["traceId"] =
                      System.Diagnostics.Activity.Current?.Id 
                        ?? context.TraceIdentifier;
                  details.Type =
                      "https://tools.ietf.org/html/rfc7231#section-6.6.1";
                  details.Status = StatusCodes.Status500InternalServerError;
                  return Results.Problem(details);
              });

          ? 添加 HttpContext

          ? 檢索異常處理程序

          ? 執行其他與錯誤相關的管理任務

          ? 設置異常消息

          執行此升級后,我們可以啟動應用并執行 /error/test 終結點以獲取更詳細的響應正文:

          {
            "type":"https://tools.ietf.org/html/rfc7231#section-6.6.1",
            "title":"An error occurred while processing your request.",
            "status":500,
            "detail":"test",                                                     ?
            "traceId":"00-7cfd2605a885fbaed6a2abf0bc59944e-28bf94ef8a8c80a7-00"  ?
          }
          ? New JSON data

          請注意,我們正在手動實例化 ProblemDetails 對象實例,根據需要對其進行配置,然后將其傳遞給 Results.Problem 方法重載,該方法重載將其作為參數接受。但是,我們可以做的不僅僅是配置 ProblemDetails 對象的屬性。我們還可以執行以下操作:

          • 根據異常的類型返回不同的 HTTP 狀態代碼,就像我們在 DomainsController 的 Get 方法中使用 ModelState 手動驗證所做的那樣。
          • 在某處記錄異常,例如在我們的 DBMS 中
          • 向管理員、審核員和/或其他方發送電子郵件通知

          其中一些可能性將在后面的章節中介紹。

          警告請務必了解,異常處理中間件將使用原始 HTTP 方法重新執行請求。處理程序終結點(在我們的方案中,處理 /error/ 路徑的 MapGet 方法)不應限制為一組有限的 HTTP 方法,因為它僅適用于它們。如果我們想根據原始 HTTP 方法以不同的方式處理異常,我們可以將不同的 HTTP 動詞屬性應用于具有相同名稱的多個操作。例如,我們可以使用 [HttpGet] 只處理 GET 異常,使用 [HttpPost] 只處理 POST 異常。

          異常處理操作

          我們可以使用 UseExceptionHandler 方法的重載,而不是將異常處理過程委托給自定義終結點,該方法接受 Action<IApplicationBuilder> 對象實例作為參數。這種方法允許我們在不指定專用端點的情況下獲得相同級別的自定義。以下是我們如何使用該重載來使用我們當前在最小 API 的 MapGet 方法中的實現:

          app.UseExceptionHandler(action => {
              action.Run(async context =>
              {
                  var exceptionHandler =
                      context.Features.Get<IExceptionHandlerPathFeature>();
           
                  var details = new ProblemDetails();
                  details.Detail = exceptionHandler?.Error.Message;
                  details.Extensions["traceId"] =
                      System.Diagnostics.Activity.Current?.Id 
                        ?? context.TraceIdentifier;
                  details.Type =
                      "https://tools.ietf.org/html/rfc7231#section-6.6.1";
                  details.Status = StatusCodes.Status500InternalServerError;
                  await context.Response.WriteAsync(
                      System.Text.Json.JsonSerializer.Serialize(details));   ?
              });
          });

          ? JSON 序列化問題詳細信息對象

          正如我們所看到的,源代碼幾乎與MapGet方法的實現相同。唯一真正的區別是,在這里,我們需要將響應正文直接寫入 HTTP 響應緩沖區;我們必須注意將 ProblemDetails 對象實例序列化為實際的 JSON 格式字符串。現在我們已經對框架提供的各種錯誤處理方法進行了一些實踐,我們已經準備好將這些知識應用于第 7 章:應用程序日志記錄的主題。

          6.3 練習

          是時候用我們的產品所有者給出的通常的假設任務分配列表來挑戰自己了。練習的解決方案可在 GitHub 的 /Chapter_06/Exercises/ 文件夾中找到。若要測試它們,請將 MyBGList 項目中的相關文件替換為該文件夾中的文件,然后運行應用。

          6.3.1 內置驗證器

          將內置驗證程序添加到 DomainDTO 對象的 Name 屬性,以便僅當它不為 null、不為空且僅包含大寫和小寫字母(不含數字、空格或任何其他字符)時,它才會被視為有效。有效值的示例包括“策略”、“系列”和“抽象”。無效值的示例包括“策略游戲”、“兒童”、“101指南”、“”和 null。

          如果值無效,驗證器應發出錯誤消息“值必須僅包含字母(不能包含空格、數字或其他 字符)”。DomainsController 的 Post 方法接受 DomainDTO 復雜類型作為參數,可用于測試包含驗證結果的 HTTP 響應。

          6.3.2 自定義驗證器

          創建一個 [LettersOnly] 驗證器屬性,并實現它以滿足第 6.3.1 節中給出的相同規范,包括錯誤消息。實際值檢查應使用正則表達式或字符串操作技術執行,具體取決于自定義 UseRegex 參數是設置為 true 還是 false(默認值)。當自定義驗證程序屬性準備就緒時,將其應用于 MechanicDTO 對象的 Name 屬性,并使用機械控制器的 Post 方法使用可用的兩個 UseRegex 參數值對其進行測試。

          6.3.3 可識別對象

          實現 DomainDTO 對象的 IValidatableObject 接口,并使用其 Valid 方法認為僅當 Id 值等于 3 或 Name 值等于“Wargames”時才有效。如果模型無效,驗證程序應發出錯誤消息“Id 和/或名稱值必須與允許的域匹配”。DomainsController 的 Post 方法接受 DomainDTO 復雜類型作為參數,可用于測試包含驗證結果的 HTTP 響應。

          6.3.4 模型狀態驗證

          將 [ManualValidatonFilter] 屬性應用于 DomainsController 的 Post 方法,以禁用由 [ApiController] 執行的自動 ModelState 驗證。然后實現手動模型狀態驗證,以便在模型狀態無效時有條件地返回以下 HTTP 狀態代碼:

          • HTTP 403 - 禁止訪問 - 如果 ModelState 無效,因為 Id 值不等于 3 且名稱值不等于“Wargames”
          • HTTP 400 - 錯誤請求 - 如果模型狀態因任何其他原因無效

          如果模型狀態有效,則必須正常處理 HTTP 請求。

          6.3.5 異常處理

          修改當前 /error 終結點行為以有條件地返回以下 HTTP 狀態代碼,具體取決于引發的異常類型:

          • HTTP 501 - 未實現 - 對于未實現的異常類型
          • HTTP 504 - 網關超時 - 對于超時異常類型
          • HTTP 500 - 內部服務器錯誤 - 對于任何其他異常類型

          若要測試新的錯誤處理實現,請使用最小 API 創建兩個新的 MapGet 方法并實現它們,以便它們引發相應類型的異常:

          • /error/test/501 的 HTTP 501 - 未實現狀態代碼
          • /error/test/504 用于 HTTP 504 - 網關超時狀態代碼

          總結

          • 數據驗證和錯誤處理使我們能夠在客戶端和服務器之間的交互過程中處理大多數意外情況,從而降低數據泄漏、速度變慢以及其他安全和性能問題的風險。
          • ASP.NET Core 模型綁定系統負責處理來自 HTTP 請求的所有輸入數據,包括將它們轉換為 .NET 類型(綁定)并根據我們的數據驗證規則檢查它們(驗證)。
          • 我們可以通過使用內置或自定義數據注釋屬性將數據驗證規則分配給輸入參數和復雜類型屬性。此外,我們可以使用 IValidatableObject 接口在復雜類型中創建交叉驗證檢查。
          • ModelState 對象包含針對輸入參數執行的數據驗證檢查的組合結果。ASP.NET Core 允許我們以兩種方式使用它:
            • 自動處理它(感謝 [ApiController] 的自動驗證功能)。
            • 手動檢查其值,這使我們能夠自定義整個驗證過程和生成的 HTTP 響應。
          • 應用程序級錯誤和異常可以使用 ExceptionHandling 中間件進行處理。此中間件可以配置為根據我們的需求自定義錯誤處理體驗,例如
            • 根據異常的類型返回不同的 HTTP 狀態代碼和/或人類可讀的信息。
            • 在某處記錄異常(DBMS、文本文件、事件注冊表等)。
            • 向相關方(如系統管理員)發送電子郵件通知。

          SON解析失敗可能有多種原因,包括JSON格式不正確、JSON數據缺失、JSON數據類型不匹配、代碼錯誤、語法錯誤、格式錯誤、編碼錯誤等。解決方法包括檢查JSON數據是否符合JSON格式、檢查JSON數據中是否包含特殊字符或非法字符、確認JSON數據是否完整、確認解析JSON數據的代碼是否正確、嘗試使用其他JSON解析庫或工具等。如果使用某個庫或框架進行JSON解析出現問題,可以查看相關文檔或社區支持論壇,尋求幫助或解決方案。

          JSON(JavaScript Object Notation)是一種輕量級的數據交換格式,通常用于前后端數據傳輸。如果您在解析 JSON 數據時遇到了問題,可能有以下幾種情況:

          1、JSON 格式不正確:JSON 格式要求使用雙引號表示字符串,屬性名也必須使用雙引號包括,同時屬性名和屬性值之間使用冒號隔開。如果這些要求沒有滿足,解析器可能會拋出解析錯誤。

          2、JSON 數據缺失:如果 JSON 數據中某些屬性缺失,解析器可能無法正確解析該數據。此時可以通過檢查數據格式,或者在代碼中加入異常處理來避免出錯。

          3、JSON 數據類型不匹配:JSON 中有多種數據類型,包括字符串、數字、布爾值、數組和對象等。如果 JSON 數據類型與代碼中期望的不匹配,解析器也可能無法正確解析數據。

          4、代碼錯誤:有時候 JSON 解析失敗可能是因為代碼中存在語法錯誤或邏輯錯誤。此時可以檢查代碼并進行調試。

          5、語法錯誤:JSON數據必須遵循特定的語法規則。如果JSON數據中有語法錯誤,解析器將無法正確解析數據。請確保JSON數據的語法正確,并符合JSON規范。

          6、格式錯誤:JSON數據必須以JSON對象或JSON數組的形式進行編寫。如果JSON數據不是一個有效的JSON對象或數組,解析器將無法正確解析數據。請檢查JSON數據的格式是否正確。

          7、編碼錯誤:JSON數據必須使用正確的字符編碼進行編寫。如果JSON數據中使用了不支持的字符編碼,解析器將無法正確解析數據。請確保JSON數據使用了正確的字符編碼。

          8、檢查 JSON 數據是否符合 JSON 格式。在 JSON 中,所有屬性名稱必須用雙引號括起來,字符串也必須用雙引號括起來,不能使用單引號。同時,JSON 數據必須是有效的 JSON 對象或 JSON 數組格式。

          9、檢查 JSON 數據中是否包含特殊字符或非法字符。例如,如果 JSON 數據中包含換行符或回車符等特殊字符,可能會導致解析失敗。可以嘗試使用 JSON.stringify() 方法將 JSON 數據轉換為字符串,并使用正則表達式去除特殊字符。

          10、確認 JSON 數據是否完整。如果 JSON 數據缺少屬性或值,或者格式不正確,也會導致解析失敗。可以使用在線 JSON 校驗工具檢查 JSON 數據是否符合標準格式。

          11、確認解析 JSON 數據的代碼是否正確。可能存在代碼錯誤或邏輯錯誤,導致解析失敗。可以使用調試器或日志記錄工具查找代碼問題并進行修復。

          12、嘗試使用其他 JSON 解析庫或工具。如果您正在使用的是某個 JSON 解析庫或工具,可以嘗試使用其他庫或工具進行解析,看是否可以解決問題。

          13、檢查網絡連接。如果您的 JSON 數據來源于網絡,可能是網絡連接問題導致解析失敗。可以檢查網絡連接是否正常,或者嘗試從其他網絡位置獲取數據。

          14、檢查數據編碼。如果您的 JSON 數據使用了非 UTF-8 編碼,可能會導致解析失敗。可以嘗試將數據轉換為 UTF-8 編碼,再進行解析。

          15、檢查解析器設置。如果您正在使用某個 JSON 解析庫或工具,可能是解析器設置有誤導致解析失敗。可以查看相關文檔或社區支持論壇,了解正確的解析器設置方法。

          如果是在使用某個庫或框架進行 JSON 解析時出現問題,可以查看相關文檔或社區支持論壇,尋求幫助或解決方案。


          主站蜘蛛池模板: 国产成人无码精品一区在线观看| 精品视频一区二区| 精品人体无码一区二区三区| 国产乱人伦精品一区二区在线观看 | 午夜视频一区二区三区| 久久精品无码一区二区WWW| 久久久精品人妻一区亚美研究所| 无码少妇精品一区二区免费动态| 好吊视频一区二区三区| 亚洲AV无码一区二区乱子伦| 亚洲av午夜福利精品一区人妖| 日韩免费视频一区二区| 人体内射精一区二区三区| 中文字幕日韩精品一区二区三区 | 人妻精品无码一区二区三区| 精品一区二区三区在线观看l| 怡红院美国分院一区二区 | 日本一区二区三区四区视频| 久久精品国产第一区二区三区| 国产99久久精品一区二区| 日韩中文字幕精品免费一区| 国产伦理一区二区三区| 搡老熟女老女人一区二区| 亚洲AV无一区二区三区久久| 无码人妻精品一区二区蜜桃 | 亚洲国产美国国产综合一区二区| 丰满少妇内射一区| 国产精品久久久久一区二区| 91在线一区二区| 无码国产精成人午夜视频一区二区| 亚洲一区二区三区在线网站| 久久精品免费一区二区喷潮| 国产成人精品一区二区三区免费 | 国产午夜三级一区二区三| 亚洲一区二区三区在线| 中文字幕精品一区二区精品| 国产乱码精品一区二区三区麻豆| 中文字幕精品一区| 中文字幕精品亚洲无线码一区应用| 中文字幕一区二区三| 国产精品无码一区二区三区在|