目录
介绍
为什么?
怎么做?
VMD.RESTApiResponseWrapper Nuget软件包
安装及使用
ASP.NET Core集成
ASP.NET Web API集成
样本响应输出
定义自定义异常
定义自己的响应对象
包源代码
实现
包装器类
ValidationError类
ApiError类
ApiException类
ApiResponse类
ResponseMessage枚举
ASP.NET Core实现
标准ASP.NET Web API实现
异常过滤器
委托处理程序
总结
参考文献
介绍如今,构建RESTFul API非常流行,我们今天构建的大多数项目都严重依赖API /服务与数据进行通信。如您所知,创建Web API是一件容易的事,但是设计一个好的API并不像您想象的那么容易,尤其是当您正在处理许多暴露某些public API端点的项目或微服务时。
本文将讨论如何为ASP.NET Core和Web API应用程序实现自定义包装,以管理异常,从而为使用者提供有意义且一致的响应。
为什么?在继续深入之前,让我们先谈一谈“为什么”。为什么我们需要实现一个自定义包装,为什么这是一件好事。
ASP.NET Core和标准的ASP.NET Web API允许我们快速创建API。但是,对于开箱即用的成功请求和错误,它们不会提供一致的响应。为了更清楚,如果你对API采用RESTful方式,那么你将被使用HTTP动词,如GET、POST、PUT和DELETE。根据您的方法/操作的设计方式,每个操作都可能返回不同的类型。你的POST、PUT和DELETE终端可能会返回一个数据,或者根本没有。您的GET终端可能返回一个 string、 List、IEnumerable,自定义class或一个 object。另一方面,如果您的API引发错误,它将返回一个object或更糟糕的HTML string说明错误原因。所有这些响应之间的差异使得使用API变得很困难,因为使用者需要了解每种情况下返回的数据的类型和结构。客户代码和服务代码都变得难以管理。
在为“实际”应用程序项目构建API时,许多开发人员并不关心使用者。他们最关心的是他们可以将数据返回给消费者,仅此而已。API不仅是通过HTTP来回传递JSON,还包括如何向使用API的开发人员呈现有意义的响应。
永远记住
引用:
“对于使用API的开发人员来说,良好的API设计是UX。”
作为重视消费者的开发人员,我们希望为他们提供有意义且一致的API响应。这就是为什么在本文中,我们将实现一个可在具有以下功能的应用程序之间重用的自定义包装器:
- 处理意外错误
- 处理ModelState验证错误
- 可配置的自定义API异常
- 结果和错误的一致响应对象
- 详细的结果回复
- 详细的错误响应
- 可配置的HTTP状态码
- 支持Swagger
使用ASP.NET Core或标准Web API时,重要的是处理异常并为API处理的所有请求返回一致的响应,无论成功或失败。这使得使用API变得容易得多,而无需在客户端上使用复杂的代码。通过为ASP.NET Core和Web API响应使用自定义包装,可以确保所有响应具有一致的结构,并且将处理所有异常。
我们将研究如何实现一个自定义包装,该包装可以处理上面列出的ASP.NET Core和标准Web API的所有功能。
VMD.RESTApiResponseWrapper Nuget软件包如果要跳过实际的代码实现,可以将两个单独的Nuget包直接集成到项目中:
- VMD.RESTApiResponseWrapper.Core (对于ASP.NET Core Apps)
- VMD.RESTApiResponseWrapper.Net (对于标准Web API应用)
这些库中的每一个都是单独创建的。VMD.RESTApiResponseWrapper.Core使用ASP.NET 2.0的Core和Visual Studio 2017构建。它使用middleware实现包装器并管理例外。从另一个方面说,VMD.RESTApiResponseWrapper.Net是使用Visual Studio 2015的完整的.NET Framework V4.6构建的。它使用DelegatingHandler实现包装器,并使用一个ExceptionFilterAttribute处理异常。
安装及使用首先,您需要先安装Newtonsoft.json软件包,然后再安装VMD.RESTApiResponseWrapper软件包。
ASP.NET Core集成对于ASP.NET Core应用,可以通过NPM或使用以下命令来安装软件包:
PM> Install-Package VMD.RESTApiResponseWrapper.Core -Version 1.0.3
安装之后,可以按照以下步骤开始将包装器集成到ASP.NET Core项目中:
1、在Startup.cs中声明以下名称空间:
using VMD.RESTApiResponseWrapper.Core.Extensions;
2、在Startup.cs的Configure()方法中注册以下中间件:
app.UseAPIResponseWrapperMiddleware();
注意:确保在“MVC中间件”之前注册它。
3、做完了
这里的文章演示了如何将这个库集成到您的ASP.NET Core REST API项目中: ASP.NET Core 2.1:将VMD.RESTApiResponseWrapper.Core集成到您的REST API应用程序中
ASP.NET Web API集成对于标准的ASP.NET Web API应用程序,您可以执行以下操作:
PM> Install-Package VMD.RESTApiResponseWrapper.Net -Version 1.0.3
安装之后,可以按照以下步骤开始将包装器集成到ASP.NET Web API项目中:
1、在WebApiConfig.cs中声明以下名称空间:
using VMD.RESTApiResponseWrapper.Net;
using VMD.RESTApiResponseWrapper.Net.Filters;
2、在WebApiConfig.cs中注册以下内容:
config.Filters.Add(new ApiExceptionFilter());
config.MessageHandlers.Add(new WrappingHandler());
3、做完了
引用:
注意:截止撰写本文时,两个软件包的最新版本均为v1.0.3。
样本响应输出以下是响应输出的示例:
成功的响应格式和数据:
{
"Version": "1.0.0.0",
"StatusCode": 200,
"Message": "Request successful.",
"Result": [
"value1",
"value2"
]
}
没有数据的成功响应格式:
{
"Version": "1.0.0.0",
"StatusCode": 201,
"Message": "Student with ID 6 has been created."
}
验证错误的响应格式:
{
"Version": "1.0.0.0",
"StatusCode": 400,
"Message": "Request responded with exceptions.",
"ResponseException": {
"IsError": true,
"ExceptionMessage": "Validation Field Error.",
"Details": null,
"ReferenceErrorCode": null,
"ReferenceDocumentLink": null,
"ValidationErrors": [
{
"Field": "LastName",
"Message": "'Last Name' should not be empty."
},
{
"Field": "FirstName",
"Message": "'First Name' should not be empty."
}
]
}
}
错误的响应格式:
{
"Version": "1.0.0.0",
"StatusCode": 404,
"Message": "Unable to process the request.",
"ResponseException": {
"IsError": true,
"ExceptionMessage": "The specified URI does not exist. Please verify and try again.",
"Details": null,
"ReferenceErrorCode": null,
"ReferenceDocumentLink": null,
"ValidationErrors": null
}
}
定义自定义异常
这个库不仅仅是中间件或包装器。它还提供了一种可用于定义自己的异常的方法。例如,如果您想抛出自己的异常消息,则可以简单地执行以下操作:
throw new ApiException("Your Message",401, ModelStateExtension.AllErrors(ModelState));
该ApiException有可以设置以下参数:
ApiException(string message,
int statusCode = 500,
IEnumerable errors = null,
string errorCode = "",
string refLink = "")
定义自己的响应对象
除了引发您自己的自定义异常外,您还可以通过使用API控制器中的ApiResponse对象来返回自己的自定义定义JSON响应。例如:
return new APIResponse(201,"Created");
该APIResponse有可以设置以下参数:
APIResponse(int statusCode,
string message = "",
object result = null,
ApiError apiError = null,
string apiVersion = "1.0.0.0")
包源代码
这些包装器的代码是开源的,可以在github上找到:
- https://github.com/proudmonkey/RESTApiResponseWrapper.Core
- https://github.com/proudmonkey/RESTApiResponseWrapper.Net
随时免费检出。
实现让我们看看如何为ASP.NET Core和标准Web API实现自定义包装器。让我们从两个项目中使用的通用类开始。
包装器类ASP.NET Core和标准Web API项目都使用以下类:
- ValidationError
- ApiError
- ApiException
- ApiResponse
- ResponseMessageEnum
上面的每个类都将用于实现用于管理异常和响应一致性的自定义包装器。请记住,本文演示的代码只是库的基本基础。您显然可以自由地修改和添加自己的属性,甚至可以根据业务需要自定义实现。
这是每个类的实际代码:
ValidationError类public class ValidationError
{
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public string Field { get; }
public string Message { get; }
public ValidationError(string field, string message)
{
Field = field != string.Empty ? field : null;
Message = message;
}
}
ValidationError类包含一些用于存储字段及其相应消息的属性。注意[JsonProperty(NullValueHandling=NullValueHandling.Ignore)]特性上的Field属性。这是为了确保在有null值的情况下不会对该字段进行序列化。
public class ApiError
{
public bool IsError { get; set; }
public string ExceptionMessage { get; set; }
public string Details { get; set; }
public string ReferenceErrorCode { get; set; }
public string ReferenceDocumentLink { get; set; }
public IEnumerable ValidationErrors { get; set; }
public ApiError(string message)
{
this.ExceptionMessage = message;
this.IsError = true;
}
public ApiError(ModelStateDictionary modelState)
{
this.IsError = true;
if (modelState != null && modelState.Any(m => m.Value.Errors.Count > 0))
{
this.ExceptionMessage = "Please correct the specified validation errors and try again.";
this.ValidationErrors = modelState.Keys
.SelectMany(key => modelState[key].Errors.Select
(x => new ValidationError(key, x.ErrorMessage)))
.ToList();
}
}
}
ApiError类是一种自定义序列化类型,用于通过JSON将错误信息返回给使用者。此类包含一些重要的特性,以提供有意义的信息给消费者,如ExceptionMessage,Details,ReferenceErrorCode,ReferenceDocumentLink和ValidationErrors。该类还具有一个重载构造函数,以传入ModelStateDictionary以将验证错误列表返回给使用者。
public class ApiException : System.Exception
{
public int StatusCode { get; set; }
public IEnumerable Errors { get; set; }
public string ReferenceErrorCode { get; set; }
public string ReferenceDocumentLink { get; set; }
public ApiException(string message,
int statusCode = 500,
IEnumerable errors = null,
string errorCode = "",
string refLink = "") :
base(message)
{
this.StatusCode = statusCode;
this.Errors = errors;
this.ReferenceErrorCode = errorCode;
this.ReferenceDocumentLink = refLink;
}
public ApiException(System.Exception ex, int statusCode = 500) : base(ex.Message)
{
StatusCode = statusCode;
}
}
ApiException类是用于抛出显示的和应用产生的错误的自定义异常。这些通常用于验证错误或可能具有已知负面响应(例如,失败的登录尝试)的常见操作。目的是返回明确定义的错误消息,以供使用者安全使用。
[DataContract]
public class APIResponse
{
[DataMember]
public string Version { get; set; }
[DataMember]
public int StatusCode { get; set; }
[DataMember]
public string Message { get; set; }
[DataMember(EmitDefaultValue = false)]
public ApiError ResponseException { get; set; }
[DataMember(EmitDefaultValue = false)]
public object Result { get; set; }
public APIResponse(int statusCode, string message = "", object result = null,
ApiError apiError = null, string apiVersion = "1.0.0.0")
{
this.StatusCode = statusCode;
this.Message = message;
this.Result = result;
this.ResponseException = apiError;
this.Version = apiVersion;
}
}
APIResponse类是被用于为所有的API响应提供一致的数据结构的自定义包装响应对象。这包含了一些基本特性,例如Version、StatusCode、Message、ResponseException和Result。我们使用DataContract属性来定义要返回的属性,例如,ResponseException和Result属性不会返回,如果值为null。
public enum ResponseMessageEnum
{
[Description("Request successful.")]
Success,
[Description("Request responded with exceptions.")]
Exception,
[Description("Request denied.")]
UnAuthorized,
[Description("Request responded with validation error(s).")]
ValidationError,
[Description("Unable to process the request.")]
Failure
}
ResponseMessageEnum提供了枚举,用于响应描述,例如Success,Exception,UnAuthorize和ValidationError。
现在我们已经准备好了包装器类,是时候对它们进行实际的实现了。
对于ASP.NET Core实现,我们将使用中间件来实现上面列出的自定义包装器功能。中间件是构成处理应用程序请求和响应的管道的组件。每个被调用的中间件都可以选择对请求进行一些处理,然后再在线调用下一个中间件。在执行从调用返回到下一个中间件之后,就有机会对响应进行处理。有关更多详细信息,请参见ASP.NET Core中间件。
我们需要在中间件类中做一些工作,因为我们想吐出自己的预定义Response对象,并且希望捕获或过滤掉显式和未处理的API异常。
这是自定义ASP.NET Core中间件的代码。
public class APIResponseMiddleware
{
private readonly RequestDelegate _next;
public APIResponseMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (IsSwagger(context))
await this._next(context);
else
{
var originalBodyStream = context.Response.Body;
using (var responseBody = new MemoryStream())
{
context.Response.Body = responseBody;
try
{
await _next.Invoke(context);
if (context.Response.StatusCode == (int)HttpStatusCode.OK)
{
var body = await FormatResponse(context.Response);
await HandleSuccessRequestAsync(context, body, context.Response.StatusCode);
}
else
{
await HandleNotSuccessRequestAsync(context, context.Response.StatusCode);
}
}
catch (System.Exception ex)
{
await HandleExceptionAsync(context, ex);
}
finally
{
responseBody.Seek(0, SeekOrigin.Begin);
await responseBody.CopyToAsync(originalBodyStream);
}
}
}
}
private static Task HandleExceptionAsync(HttpContext context, System.Exception exception)
{
ApiError apiError = null;
APIResponse apiResponse = null;
int code = 0;
if (exception is ApiException)
{
var ex = exception as ApiException;
apiError = new ApiError(ex.Message);
apiError.ValidationErrors = ex.Errors;
apiError.ReferenceErrorCode = ex.ReferenceErrorCode;
apiError.ReferenceDocumentLink = ex.ReferenceDocumentLink;
code = ex.StatusCode;
context.Response.StatusCode = code;
}
else if (exception is UnauthorizedAccessException)
{
apiError = new ApiError("Unauthorized Access");
code = (int)HttpStatusCode.Unauthorized;
context.Response.StatusCode = code;
}
else
{
#if !DEBUG
var msg = "An unhandled error occurred.";
string stack = null;
#else
var msg = exception.GetBaseException().Message;
string stack = exception.StackTrace;
#endif
apiError = new ApiError(msg);
apiError.Details = stack;
code = (int)HttpStatusCode.InternalServerError;
context.Response.StatusCode = code;
}
context.Response.ContentType = "application/json";
apiResponse = new APIResponse
(code, ResponseMessageEnum.Exception.GetDescription(), null, apiError);
var json = JsonConvert.SerializeObject(apiResponse);
return context.Response.WriteAsync(json);
}
private static Task HandleNotSuccessRequestAsync(HttpContext context, int code)
{
context.Response.ContentType = "application/json";
ApiError apiError = null;
APIResponse apiResponse = null;
if (code == (int)HttpStatusCode.NotFound)
apiError = new ApiError
("The specified URI does not exist. Please verify and try again.");
else if (code == (int)HttpStatusCode.NoContent)
apiError = new ApiError("The specified URI does not contain any content.");
else
apiError = new ApiError("Your request cannot be processed. Please contact a support.");
apiResponse = new APIResponse
(code, ResponseMessageEnum.Failure.GetDescription(), null, apiError);
context.Response.StatusCode = code;
var json = JsonConvert.SerializeObject(apiResponse);
return context.Response.WriteAsync(json);
}
private static Task HandleSuccessRequestAsync(HttpContext context, object body, int code)
{
context.Response.ContentType = "application/json";
string jsonString, bodyText = string.Empty;
APIResponse apiResponse = null;
if (!body.ToString().IsValidJson())
bodyText = JsonConvert.SerializeObject(body);
else
bodyText = body.ToString();
dynamic bodyContent = JsonConvert.DeserializeObject(bodyText);
Type type;
type = bodyContent?.GetType();
if (type.Equals(typeof(Newtonsoft.Json.Linq.JObject)))
{
apiResponse = JsonConvert.DeserializeObject(bodyText);
if (apiResponse.StatusCode != code)
jsonString = JsonConvert.SerializeObject(apiResponse);
else if (apiResponse.Result != null)
jsonString = JsonConvert.SerializeObject(apiResponse);
else
{
apiResponse = new APIResponse
(code, ResponseMessageEnum.Success.GetDescription(), bodyContent, null);
jsonString = JsonConvert.SerializeObject(apiResponse);
}
}
else
{
apiResponse = new APIResponse
(code, ResponseMessageEnum.Success.GetDescription(), bodyContent, null);
jsonString = JsonConvert.SerializeObject(apiResponse);
}
return context.Response.WriteAsync(jsonString);
}
private async Task FormatResponse(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
var plainBodyText = await new StreamReader(response.Body).ReadToEndAsync();
response.Body.Seek(0, SeekOrigin.Begin);
return plainBodyText;
}
private bool IsSwagger(HttpContext context)
{
return context.Request.Path.StartsWithSegments("/swagger");
}
}
我们的定制中间件的主要方法是Invoke()。此方法接受HttpContext作为参数。上下文从管道对象中保持当前的Request和Response。这使我们可以截取上下文并进行一些自定义处理,在这种情况下:(a)处理异常(b)返回标准的自定义响应对象。
APIResponseMiddleware还包含三个主要private方法:
- HandleExceptionAsync()
- HandleNotSuccessRequestAsync()
- HandleSuccessRequestAsync()。
HandleExceptionAsync()方法处理已引发的异常,然后从中构造一个自定义响应对象,并将其作为最终响应对象返回。HandleNotSuccessRequestAsync()方法根据状态码处理特定的响应。在这个例子中,我们筛选出NotFound和NoContent StatusCodes并再构建一个自定义的响应。最后,HandleSuccessRequestAsync()方法处理成功的响应并构造一个自定义响应对象,该对象将返回给使用者。
请注意,以上所有方法都使用APIResponse类作为最终响应对象。
既然我们已经实现了自定义中间件,那么我们可以创建一个static类来简化将中间件添加到应用程序管道中的过程:
public static class ApiResponseMiddlewareExtension
{
public static IApplicationBuilder UseAPIResponseWrapperMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware();
}
}
使用自定义中间件的最后一步是在Startup类的Configure()方法内调用我们在上面创建的扩展方法:
app.UseAPIResponseMiddleware();
标准ASP.NET Web API实现
由于中间件是为ASP.NET Core应用程序设计的,因此在标准Web API项目中,我们将使用ExceptionFilterAttribute来处理和管理异常,并使用DelegatingHandler来实现自定义响应包装器。
异常过滤器让我们从异常过滤器实现开始。这是过滤器实现的代码:
public class ApiExceptionFilter : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
ApiError apiError = null;
APIResponse apiResponse = null;
int code = 0;
if (context.Exception is ApiException)
{
var ex = context.Exception as ApiException;
apiError = new ApiError(ex.Message);
apiError.ValidationErrors = ex.Errors;
apiError.ReferenceErrorCode = ex.ReferenceErrorCode;
apiError.ReferenceDocumentLink = ex.ReferenceDocumentLink;
code = ex.StatusCode;
}
else if (context.Exception is UnauthorizedAccessException)
{
apiError = new ApiError("Unauthorized Access");
code = (int)HttpStatusCode.Unauthorized;
}
else
{
#if !DEBUG
var msg = "An unhandled error occurred.";
string stack = null;
#else
var msg = context.Exception.GetBaseException().Message;
string stack = context.Exception.StackTrace;
#endif
apiError = new ApiError(msg);
apiError.Details = stack;
code = (int)HttpStatusCode.InternalServerError;
}
apiResponse = new APIResponse
(code, ResponseMessageEnum.Exception.GetDescription(), null, apiError);
HttpStatusCode c = (HttpStatusCode)code;
context.Response = context.Request.CreateResponse(c, apiResponse);
}
}
就像ASP.NET Core的HandleExceptionAsync()方法一样,自定义异常筛选器方法处理从应用程序引发的异常。筛选器的实现与ASP.NET Core HandleExceptionAsync()方法的实现几乎相同。
让我们详细说明一下过滤器的实际作用。异常过滤器区分几种不同的异常类型。首先,它查看自定义ApiException类型,这是特殊应用程序生成的Exception,可用于向消费者显示有意义的响应。
接下来是UnAuthorized的异常,通过返回强制401异常来特殊处理,该异常可以在客户端上用于强制认证。
最后,还有Unhandled异常——这些是应用程序未明确知道的意外失败。这可能是硬件故障,null引用异常,意外的分析错误。基本上所有未处理的内容。这些错误会在生产中生成通用错误消息,因此不会返回任何敏感数据。
委托处理程序DelegatingHandlers对于横切关注点非常有用。它们连接到请求-响应管道的最早和最晚阶段,因此非常适合在将响应发送回客户端之前对其进行操作。这是委托处理程序的代码。
public class WrappingHandler : DelegatingHandler
{
protected override async Task
SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (IsSwagger(request))
{
return await base.SendAsync(request, cancellationToken);
}
else
{
var response = await base.SendAsync(request, cancellationToken);
return BuildApiResponse(request, response);
}
}
private static HttpResponseMessage
BuildApiResponse(HttpRequestMessage request, HttpResponseMessage response)
{
dynamic content = null;
object data = null;
string errorMessage = null;
ApiError apiError = null;
var code = (int)response.StatusCode;
if (response.TryGetContentValue(out content) && !response.IsSuccessStatusCode)
{
HttpError error = content as HttpError;
//handle exception
if (error != null)
{
content = null;
if (response.StatusCode == HttpStatusCode.NotFound)
apiError = new ApiError("The specified URI does not exist.
Please verify and try again.");
else if (response.StatusCode == HttpStatusCode.NoContent)
apiError = new ApiError("The specified URI does not contain any content.");
else
{
errorMessage = error.Message;
#if DEBUG
errorMessage = string.Concat
(errorMessage, error.ExceptionMessage, error.StackTrace);
#endif
apiError = new ApiError(errorMessage);
}
data = new APIResponse((int)code, ResponseMessageEnum.Failure.GetDescription(),
null, apiError);
}
else
data = content;
}
else
{
if (response.TryGetContentValue(out content))
{
Type type;
type = content?.GetType();
if (type.Name.Equals("APIResponse"))
{
response.StatusCode = Enum.Parse(typeof(HttpStatusCode),
content.StatusCode.ToString());
data = content;
}
else if (type.Name.Equals("SwaggerDocument"))
data = content;
else
data = new APIResponse(code, ResponseMessageEnum.Success.GetDescription(), content);
}
else
{
if (response.IsSuccessStatusCode)
data = new APIResponse((int)response.StatusCode,
ResponseMessageEnum.Success.GetDescription());
}
}
var newResponse = request.CreateResponse(response.StatusCode, data);
foreach (var header in response.Headers)
{
newResponse.Headers.Add(header.Key, header.Value);
}
return newResponse;
}
private bool IsSwagger(HttpRequestMessage request)
{
return request.RequestUri.PathAndQuery.StartsWith("/swagger");
}
}
上面的代码以不同的方式实现,但完成的功能与ASP.NET Core中间件中实现的功能相同。对于这种情况,我们使用委托处理程序来拦截当前上下文并为使用者构造一个自定义响应对象。我们使用Request.CreateResponse()方法使用适当的格式化程序创建了新的响应,然后在返回最终响应对象之前复制旧的未包装响应的任何头。
要使用过滤器和包装器,只需在WebApiConfig.cs文件中注册它们:
config.Filters.Add(new ApiExceptionFilter());
config.MessageHandlers.Add(new WrappingHandler());
总结
在本文中,我们学习了如何为ASP.NET Core和标准Web API项目创建用于管理API异常和一致响应的简单自定义包装。我们还学习了如何轻松地将VMS.RESTApiResponseWrapper库集成到ASP.NET Core和标准Web API项目中,而无需执行本文中演示的所有代码实现。
随时下载源代码或查看github存储库。谢谢!:)
参考文献- ASP.NET Core中间件
- ASP.NET Core 2.1:将VMD.RESTApiResponseWrapper.Core集成到REST API应用程序
- ASP.NET Web API中的HTTP消息处理程序
- ASP.NET Web API中的异常处理
- ASP.NET Core中的日志请求和响应
- ASP.NET Core API的错误处理和ExceptionFilter依赖项注入
- 包装ASP.NET Web API响应以保持一致性并提供其他信息