65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET Web API:客户端解包 HTTP 错误结果和模型状态字典

starIconstarIconstarIconstarIconstarIcon

5.00/5 (17投票s)

2014年10月2日

CPOL

14分钟阅读

viewsIcon

112120

当从 .NET 客户端使用 ASP.NET Web Api 时,最令人困惑的事情之一就是处理 Api 返回的错误情况。具体来说,就是解包从特定 API 操作方法返回的各种类型的错误,并将错误内容翻译成

墙上的404-320当从 .NET 客户端使用 ASP.NET Web Api 时,最令人困惑的事情之一就是处理 Api 返回的错误情况。具体来说,就是解包从特定 API 操作方法返回的各种类型的错误,并将错误内容翻译成有意义的信息供客户端使用。

我们如何处理可能返回给 Api 客户端应用程序的各种类型的错误,很大程度上取决于特定的应用程序需求,以及我们正在构建的客户端类型。

图片Damien Roué 拍摄   |  部分权利保留

在本篇文章中,我们将探讨在客户端处理错误结果时可能遇到的几种常见问题,并希望找到一些可以应用于特定情况的见解。

理解 ApiController 中的 HTTP 响应创建

大多数 Web Api 操作方法将返回以下之一:

  • Void:如果操作方法返回 void,ASP.NET Web Api 创建的 HTTP 响应将具有 204 状态码,表示“无内容”。
  • HttpResponseMessage:如果操作方法返回 HttpResponseMessage,则该值将直接转换为 HTTP 响应消息。我们可以使用 Request.CreateResponse() 方法来创建 HttpResponseMessage 的实例,并且可以选择将域模型作为方法参数传递,该模型将作为生成的 HTTP 响应消息的一部分被序列化。
  • IHttpActionResult:在 ASP.NET Web API 2.0 中引入,IHttpActionResult 接口提供了一个方便的抽象,用于创建 HttpResponseMessage 的机制。此外,System.Web.Http.Results 中定义了许多 IHttpActionResult 的预定义实现,并且 ApiController 类提供了返回各种形式的 IHttpActionResult 的辅助方法,可以直接在控制器中使用。
  • 其他类型:任何其他返回类型都需要使用适当的媒体格式化程序进行序列化。

有关上述更多详细信息,请参阅 Mike Wasson 的 Web API 2 中的操作结果

从 Web Api 2.0 开始,大多数 Web Api 操作方法的推荐返回类型是 IHttpActionResult,除非此类型根本不适用。

在 Visual Studio 中创建一个新的 ASP.NET Web Api 项目

为了保持通用和基础,让我们从使用默认 Visual Studio 模板启动一个标准的 ASP.NET Web Api 项目开始。如果您不熟悉 Web Api,请 花点时间回顾基础知识,并熟悉项目结构和各个组件的位置。

创建项目后,请确保更新 Nuget 包。

创建一个基本的控制台客户端应用程序

接下来,让我们构建一个非常基础的客户端应用程序。打开另一个 Visual Studio 实例,然后创建一个新的控制台应用程序。然后,使用 Nuget 包管理器将 ASP.NET Web Api 客户端库安装到解决方案中。

我们将使用简单的 Register() 方法作为起点,以了解我们可能需要如何解包一些错误,以便在客户端创建更实用的错误处理模型。

AccountController 中的 Register 方法

如果我们回到 Web Api 项目并检查 Register() 方法,我们会看到以下内容:

AccountController 中的 Register() 方法
[AllowAnonymous]
[Route("Register")]
public async Task<IHttpActionResult> Register(RegisterBindingModel model)
{
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
 
    var user = new ApplicationUser() 
    { 
        UserName = model.Email, 
        Email = model.Email 
    };
 
    IdentityResult result = await UserManager.CreateAsync(user, model.Password);
 
    if (!result.Succeeded)
    {
        return GetErrorResult(result);
    }
    return Ok();
}

在上面,我们可以看到 IHttpActionResult 有多种选项可以返回。

首先,如果模型状态无效,将调用 ApiController 类中定义的 BadRequest() 辅助方法,并将当前的 ModelStateDictionary 传递给它。这代表了简单的验证,并且没有调用额外的进程或数据库请求。

如果模型状态有效,则调用 UserManagerCreateAsync() 方法,返回 IdentityResult。如果 Succeeded 属性不为 true,则调用 GetErrorResult(),并将调用 CreateAsync() 的结果传递给它。

GetErrorResult() 是一个方便的辅助方法,它为给定的错误条件返回适当的 IHttpActionResult

AccountController 中的 GetErrorResult 方法
private IHttpActionResult GetErrorResult(IdentityResult result)
{
    if (result == null)
    {
        return InternalServerError();
    }
    if (!result.Succeeded)
    {
        if (result.Errors != null)
        {
            foreach (string error in result.Errors)
            {
                ModelState.AddModelError("", error);
            }
        }
        if (ModelState.IsValid)
        {
            // No ModelState errors are available to send, 
            // so just return an empty BadRequest.
            return BadRequest();
        }
        return BadRequest(ModelState);
    }
    return null;
}

从上面可以看出,我们可能会收到多个不同的响应,每个响应的内容略有不同,这应该有助于客户端确定出了什么问题。

发出一个错误的请求 - 验证错误

所以,让我们看看当从我们的控制台客户端应用程序向 Register() 方法发出简单的 POST 请求时,可能会出现的一些问题。

将以下代码添加到控制台应用程序中。请注意,我们故意发出一个有问题的请求。我们将传递一个有效的密码和一个匹配的确认密码,但我们会传递一个无效的电子邮件地址。我们知道 Web Api 将不喜欢这一点,并应该因此返回一个模型状态错误。

 
控制台应用程序的错误请求代码
static void Main(string[] args)
{
    // This is not a valid email address, so the POST should fail:
    string email = "john";
    string password = "Password@123";
    string confirmPassword = "Password@123";
 
    HttpResponseMessage result = 
        Register(email, password, confirmPassword);
 
    if(result.IsSuccessStatusCode)
    {
        Console.WriteLine(
            "The new user {0} has been successfully added.", email);
    }
    else
    {
        Console.WriteLine(result.ReasonPhrase);
    }
    Console.Read();
}
 
 
public static HttpResponseMessage Register(
    string email, string password, string confirmPassword)
{
    //Attempt to register:
    using (var client = new HttpClient())
    {
        var response =
            client.PostAsJsonAsync("https://:51137/api/Account/Register",
 
            // Pass in an anonymous object that maps to the expected 
            // RegisterUserBindingModel defined as the method parameter 
            // for the Register method on the API:
            new
            {
                Email = email,
                Password = password,
                ConfirmPassword = confirmPassword
            }).Result;
        return response;
    }
}

如果我们运行我们的 Web Api 应用程序,等待它启动,然后运行我们的控制台应用程序,我们会看到以下输出:

来自错误请求的控制台输出
Bad Request

嗯,这没什么帮助。

如果我们反序列化响应内容为字符串,我们会发现有更多信息可供获取。将 Main() 方法更新如下:

反序列化响应内容
static void Main(string[] args)
{
    // This is not a valid email address, so the POST should fail:
    string email = "john";
    string password = "Password@123";
    string confirmPassword = "Password@123";
 
    HttpResponseMessage result = 
        Register(email, password, confirmPassword);
 
    if(result.IsSuccessStatusCode)
    {
        Console.WriteLine(
            "The new user {0} has been successfully added.", email);
    }
    else
    {
        string content = result.Content.ReadAsStringAsync().Result;
        Console.WriteLine(content);
    }
    Console.Read();
}

现在,如果我们再次运行控制台应用程序,我们会看到以下输出:

带有反序列化响应内容的控制台应用程序的输出
{"Message":"The request is invalid.","ModelState":{"":["Email 'john' is invalid."]}}

现在,我们上面看到的是 JSON。显然,JSON 对象包含一个 Message 属性和一个 ModelState 属性。但是 ModelState 属性本身是另一个 JSON 对象,其中包含一个未命名的属性,一个数组,其中包含验证模型时发生的错误。

由于 JSON 对象本质上只是一组键值对,我们通常期望能够将 JSON 对象解包到 Dictionary<string, object> 中。然而,服务器端 ModelState 字典中枚举的无名属性使得这具有挑战性。

使用 Newtonsoft.Json 库解包这样的对象是可行的,但有点麻烦。同样重要的是,我们的 API 返回的错误可能包含也可能不包含与之关联的 ModelState 字典。

另一个错误的请求 - 更多验证错误

假设我们发现提交请求到 Register() 方法时需要提供一个有效的电子邮件地址。相反,假设我们不注意,而是输入了两个略有不同的密码,并且还忘记了密码有最小长度要求。

再次将 Main() 方法中的代码修改如下:

带有密码不匹配的错误请求
{
    "Message":"The request is invalid.",
    "ModelState": {
        "model.Password": [
            "The Password must be at least 6 characters long."],
        "model.ConfirmPassword": [
            "The password and confirmation password do not match."]
    }
}

在这种情况下,似乎 ModelState 字典中的项由有效的键值对表示,并且每个键的值是一个数组。

服务器错误和异常

我们已经看到了一些关于在我们的 POST 请求中使用无效模型时可能发生的情况。但是,如果我们的 API 不可用怎么办?

让我们假设我们终于设法让我们的电子邮件和密码都正确了,但现在服务器离线了。

停止 Web Api 应用程序,然后重新运行控制台应用程序。当然,在合理的服务器超时后,我们的客户端应用程序将抛出一个 AggregateException

什么是 AggregateException?嗯,这是当在异步方法的执行期间发生异常时我们得到的东西。如果我们假装不知道我们的请求为什么失败,我们需要深入挖掘 AggregateExceptionInnerExceptions 属性来找到一些有用的信息。

在我们的基础控制台应用程序的上下文中,我们将实现一些顶层异常处理,以便我们的控制台能够向我们报告此类异常的结果。

再次更新控制台应用程序的 Main() 方法,如下所示:

将异常处理添加到控制台应用程序的 Main() 方法
static void Main(string[] args)
{
    // This is not a valid email address, so the POST should fail:
    string email = "john@example.com";
    string password = "Password@123";
    string confirmPassword = "Password@123";
 
    // Add a Try/Catch in case something goes wrong and the server throws:
    try
    {
        HttpResponseMessage result =
            Register(email, password, confirmPassword);
 
        if (result.IsSuccessStatusCode)
        {
            Console.WriteLine(
                "The new user {0} has been successfully added.", email);
        }
        else
        {
            string content = result.Content.ReadAsStringAsync().Result;
            Console.WriteLine(content);
        }
    }
    catch (AggregateException ex)
    {
        Console.WriteLine("One or more exceptions has occurred:");
        foreach (var exception in ex.InnerExceptions)
        {
            Console.WriteLine("  " + exception.Message);
        }
    }
    Console.Read();
}

如果我们现在运行控制台应用程序,而我们的 Web Api 应用程序是离线的,我们会得到以下结果:

带有异常处理和服务器超时的控制台输出
One or more exceptions has occurred:
  An error occurred while sending the request.

这里,我们被告知“发送请求时出错”,这至少告诉了我们一些信息,并避免了应用程序因未处理的 AggregateException 而崩溃。

解包和处理 Web Api 中的错误和异常

我们已经看到了在我们的客户端应用程序注册用户时可能出现的几种不同类型的错误和异常。

虽然从响应内容中输出 JSON 有点帮助,但我怀疑这并不是我们期望在控制台中看到的输出。我们需要一种方法来解包各种类型的响应内容,并以清晰、简洁且对用户有用的格式显示有用的控制台消息。

在我为未来的文章准备一个更深入、更具交互性的控制台项目时,我实现了一个自定义异常和一个特殊方法来处理这些情况。

ApiException - 用于 Api 错误的自定义异常

是的,是的,我知道。上面的一些情况在技术上并不符合“异常”的崇高定义。然而,对于简单的控制台应用程序来说,一个简单的、基于异常的系统是合理的。此外,将我们所有的 Api 错误都封装在一个单一的抽象层后面,可以轻松地演示如何解包它们。

根据您应用程序的具体需求,结果可能会有所不同。显然,基于 GUI 的应用程序可以扩展或扩展这种方法,减少对 Try/Catch 的依赖,并抛出异常,而更多地依赖于可用的 GUI 元素的细节。

向控制台项目添加一个名为 ApiException 的类,并添加以下代码:

ApiException - 一个自定义异常
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
 
namespace ApiWithErrorsTest
{
    public class ApiException : Exception
    {
        public HttpResponseMessage Response { get; set; }
        public ApiException(HttpResponseMessage response)
        {
            this.Response = response;
        }
 
        public HttpStatusCode StatusCode
        {
            get
            {
                return this.Response.StatusCode;
            }
        }
 
 
        public IEnumerable<string> Errors
        {
            get
            {
                return this.Data.Values.Cast<string>().ToList();
            }
        }
    }

}

解包错误响应和模型状态字典

接下来,让我们向我们的 Program 类添加一个接受 HttpResponseMessage 作为方法参数并返回 ApiException 实例的方法。将以下代码添加到控制台应用程序的 Program 类中:

将 CreateApiException 方法添加到 Program 类
public static ApiException CreateApiException(HttpResponseMessage response)
{
    var httpErrorObject = response.Content.ReadAsStringAsync().Result;
 
    // Create an anonymous object to use as the template for deserialization:
    var anonymousErrorObject = 
        new { message = "", ModelState = new Dictionary<string, string[]>() };
 
    // Deserialize:
    var deserializedErrorObject = 
        JsonConvert.DeserializeAnonymousType(httpErrorObject, anonymousErrorObject);
 
    // Now wrap into an exception which best fullfills the needs of your application:
    var ex = new ApiException(response);
 
    // Sometimes, there may be Model Errors:
    if (deserializedErrorObject.ModelState != null)
    {
        var errors = 
            deserializedErrorObject.ModelState
                                    .Select(kvp => string.Join(". ", kvp.Value));
        for (int i = 0; i < errors.Count(); i++)
        {
            // Wrap the errors up into the base Exception.Data Dictionary:
            ex.Data.Add(i, errors.ElementAt(i));
        }
    }
    // Othertimes, there may not be Model Errors:
    else
    {
        var error = 
            JsonConvert.DeserializeObject<Dictionary<string, string>>(httpErrorObject);
        foreach (var kvp in error)
        {
            // Wrap the errors up into the base Exception.Data Dictionary:
            ex.Data.Add(kvp.Key, kvp.Value);
        }
    }
    return ex;
}

上面,我们对解包包含模型状态字典的 HttpResponseMessage 所涉及的内容有了一些了解。

当响应内容包含一个名为 ModeState 的属性时,我们使用 LINQ 的魔力来解包 ModelState 字典。我们将字符串键与每个存在项的值数组的内容结合起来,然后使用整数索引作为键将每个项添加到异常的 Data 字典中。

如果响应内容中不存在 ModelState 属性,我们只需解包存在的其他错误,并将它们添加到异常的 Data 字典中。

示例应用程序中的错误和异常处理

我们已经在应用程序的顶层添加了一些最小的异常处理。即,我们已经捕获并处理了由异步调用我们的 api 可能抛出的 AggregateExceptions,这些异常在调用堆栈的更深层没有被处理。

现在我们已经添加了一个自定义异常和一个用于解包某些类型错误响应的方法,让我们添加一些额外的异常处理,看看我们能否在更深层做得更好一点。

Register() 方法更新如下:

在 Register() 方法中添加错误处理
public static HttpResponseMessage Register(
    string email, string password, string confirmPassword)
{
    //Attempt to register:
    using (var client = new HttpClient())
    {
        var response =
            client.PostAsJsonAsync("https://:51137/api/Account/Register",
  
            // Pass in an anonymous object that maps to the expected 
            // RegisterUserBindingModel defined as the method parameter 
            // for the Register method on the API:
            new
            {
                Email = email,
                Password = password,
                ConfirmPassword = confirmPassword
            }).Result;
 
        if(!response.IsSuccessStatusCode)
        {
            // Unwrap the response and throw as an Api Exception:
            var ex = CreateApiException(response);
            throw ex;
        }
        return response;
    }
}

您可以看到,我们正在检查与响应相关的 HttpStatusCode,如果它不是成功的,我们将调用我们的 CreateApiException() 方法,获取新的 ApiException,然后抛出。

实际上,对于这个简单的控制台示例,我们很可能可以用一个普通的 System.Exception 代替自定义异常实现。然而,对于任何不属于最简单情况的场景,ApiException 将包含有用的附加信息。

此外,它是一个自定义异常的事实使我们能够专门捕获 ApiException 并对其进行处理,因为我们可能希望我们的应用程序对 Api 响应中的错误条件做出与对其他异常不同的行为。

现在,我们唯一需要做的(至少对于我们非常简单的示例客户端来说)是在我们的 Main() 方法中处理 ApiException

在 Main() 方法中捕获 ApiException

现在我们希望能够在 Main() 中捕获任何抛出的 ApiExceptions。我们的控制台应用程序,作为架构和复杂设计要求的典范,几乎只需要一个错误处理点就可以正确地解包异常并将它们作为控制台文本输出!

将以下代码添加到 Main()

在 Main() 方法中处理 ApiException
static void Main(string[] args)
{
    // This is not a valid email address, so the POST should fail:
    string email = "john@example.com";
    string password = "Password@123";
    string confirmPassword = "Password@123";
 
    // Add a Try/Cathc in case something goes wrong and the server throws:
    try
    {
        HttpResponseMessage result =
            Register(email, password, confirmPassword);
        if (result.IsSuccessStatusCode)
        {
            Console.WriteLine(
                "The new user {0} has been successfully added.", email);
        }
        else
        {
            string content = result.Content.ReadAsStringAsync().Result;
            Console.WriteLine(content);
        }
    }
    catch (AggregateException ex)
    {
        Console.WriteLine("One or more exceptions has occurred:");
        foreach (var exception in ex.InnerExceptions)
        {
            Console.WriteLine("  " + exception.Message);
        }
    }
    catch(ApiException apiEx)
    {
        var sb = new StringBuilder();
        sb.AppendLine("  An Error Occurred:");
        sb.AppendLine(string.Format("    Status Code: {0}", apiEx.StatusCode.ToString()));
        sb.AppendLine("    Errors:");
        foreach (var error in apiEx.Errors)
        {
            sb.AppendLine("      " + error);
        }
        // Write the error info to the console:
        Console.WriteLine(sb.ToString());
    }
    Console.Read();
}

我们在上面所做的就是解包 ApiException 并将 Data 字典的内容转换为控制台输出(带有一些非常粗糙的缩进)。

现在让我们看看这一切是如何工作的。

通过错误和异常处理运行更多错误场景

回过头来,让我们看看如果我们尝试使用无效的电子邮件地址注册用户会发生什么。

Main() 中的注册值改回以下:

// This is not a valid email address, so the POST should fail:
string email = "john";
string password = "Password@123";
string confirmPassword = "Password@123";

再次运行 Web Api 应用程序。一旦它正确启动,使用修改后的注册值运行控制台应用程序。控制台输出应如下所示:

使用无效电子邮件地址注册用户
An Error Occurred:
Status Code: BadRequest
Errors:
  Email 'john' is invalid.

类似地,如果我们使用有效的电子邮件地址,但密码值都太短,并且也不匹配,我们会得到以下输出:

使用无效密码注册用户
An Error Occurred:
Status Code: BadRequest
Errors:
  The Password must be at least 6 characters long.
  The password and confirmation password do not match.

最后,让我们看看如果我们尝试多次注册同一个用户会发生什么。

将注册值更改为以下:

使用有效的注册值
string email = "john@example.com";
string password = "Password@123";
string confirmPassword = "Password@123";

现在,连续运行两次控制台应用程序。第一次,控制台输出应为:

成功注册用户的控制台输出
The new user john@example.com has been successfully added.

但是,下一次,Web Api 返回了一个错误结果:

重复注册用户的控制台输出
An Error Occurred:
Status Code: BadRequest
Errors:
  Name john@example.com is already taken.. Email 'simon@example.com' is already taken.

哦不,你没有使用异常来处理 Api 错误!!

哦,是的,我做到了……至少在这种情况下。这是一个简单的、基于控制台的应用程序,其中几乎所有结果都需要以文本输出的形式呈现。而且,我猜我就是这么叛逆。有时。

重要的是要认识到**如何从构成响应内容的 JSON 中提取我们需要的信息**,而这在这个例子中并不像看起来那么直接。正如往常一样,如何处理不同的错误需要根据您的应用程序的最佳方式来解决。

在很多情况下,将 Api 错误视为异常对我来说是值得的。这样做很可能会让一些架构纯粹主义者感到不适(许多传入响应内容中的错误并不真正符合“异常”的教科书定义)。也就是说,对于不太复杂的基于 .NET 的 Api 客户端应用程序,将错误从响应内容中解包并抛出异常,由适当的处理程序捕获,可以节省大量重复代码,并提供一种已知的处理问题机制。

在其他情况下,或者为了您自己的目的,您可以选择重构上面的代码来提取您需要的内容,但否则会不使用异常来处理错误。Register()(以及您用于调用 Api 的任何其他方法)在简单的控制台应用程序中,可能会返回字符串,准备好进行输出。在这种情况下,您可以绕过异常问题。

Needless to say(不用说),在很多情况下,您很可能不是从桌面 .NET 应用程序调用 Web Api 应用程序,而是从 Web 客户端调用,很可能使用 Ajax 或类似的技术。

这篇关于处理错误的漫长而疯狂的帖子 - 搞什么鬼?

好吧,我正在构建一个更复杂、更具交互性的基于控制台的应用程序,以便在接下来的帖子中演示一些概念。在这个过程中,一个更令人恼火的方面是想出一个合理的方法来处理可能出现的各种问题,而此时您唯一能做的就是通过命令行界面向用户报告输出。

这是解决方案的一部分(好吧,在我正在构建的应用程序中,事情会更复杂一些,组织得更好一些,并且有更多内容。但在这里我们看到了一些基础知识)。

但是……我们不能在服务器上做些不同的事情吗?

嗯……是的!

很可能,您只是会根据您的 Web Api 的性质和预期的客户端用例来调整您向客户端推送的内容以及推送方式。在这篇文章中,我采用了基本、默认的设置(而且实际上,我们只看了一个方法)。但是,根据您的 Api 的使用方式,您可能会在服务器端以不同的方式处理错误和异常,这可能会影响您在客户端如何处理事情。

其他资源和感兴趣的项目

© . All rights reserved.