整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

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 解析时出现问题,可以查看相关文档或社区支持论坛,寻求帮助或解决方案。