前言
AutoWrapper是一个简单可自定义全局异常处理程序和ASP.NET Core API响应的包装。他使用ASP.NET Core middleware拦截传入的HTTP请求,并将最后的结果使用统一的格式来自动包装起来.目的主要是让我们更多的关注业务特定的代码要求,并让包装器自动处理HTTP响应。这可以在构建API时加快开发时间,同时为HTTP响应试试我们统一的标准。
安装
AutoWrapper.Core从NuGet或通过CLI下载并安装
PM> Install-Package AutoWrapper.Core
在Startup.cs Configure方法中注册以下内容,但是切记要放在UseRouting前
app.UseApiResponseAndExceptionWrapper();
启动属性映射
默认情况下AutoWrapper将在成功请求成功时输出以下格式:
1{ 2 "message": "Request successful.", 3 "isError": false, 4 "result": [ 5 { 6 "id": 7002, 7 "firstName": "Vianne", 8 "lastName": "Durano", 9 "dateOfBirth": "2018-11-01T00:00:00" 10 } 11 ] 12}
如果说不喜欢默认属性命名方式,那么我们可以通过AutoWrapperPropertyMap属性进行映射为我们需要指定的任何名称。例如我么可以将result属性的名称更改为data。如下所示
1public class MapResponseObject 2{ 3 [AutoWrapperPropertyMap(Prop.Result)] 4 public object Data { get; set; } 5}
然后将MapResponseObject类传递给AutpWrapper middleware
app.UseApiResponseAndExceptionWrapper<MapResponseObject>();
通过映射重新请求后,现在影响格式如下所示
1{ 2 "message": "Request successful.", 3 "isError": false, 4 "data": { 5 "id": 7002, 6 "firstName": "Vianne", 7 "lastName": "Durano", 8 "dateOfBirth": "2018-11-01T00:00:00" 9 } 10}
可以从中看出result属性已经更换为data属性了
默认情况下AutoWrapper发生异常时将吐出以下响应格式
1{ 2 "isError": true, 3 "responseException": { 4 "exceptionMessage": "Unhandled Exception occurred. Unable to process the request." 5 } 6} 7
而且如果在AutoWrapperOptions中设置了IsDebug,则将产生带有堆栈跟踪信息的类似信息
1{ 2 "isError": true, 3 "responseException": { 4 "exceptionMessage": " Input string was not in a correct format.", 5 "details": " at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)\r\n at System.Number.ParseInt32(ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)\r\n …" 6 } 7}
如果想将某些APIError属性名称更改为其他名称,只需要在以下代码中添加以下映射MapResponseObject
1public class MapResponseObject 2{ 3 [AutoWrapperPropertyMap(Prop.ResponseException)] 4 public object Error { get; set; } 5 6 [AutoWrapperPropertyMap(Prop.ResponseException_ExceptionMessage)] 7 public string Message { get; set; } 8 9 [AutoWrapperPropertyMap(Prop.ResponseException_Details)] 10 public string StackTrace { get; set; } 11}
通过如下代码来模拟错误
int num = Convert.ToInt32("10s");
现在映射后的输出如下所示
1{ 2 "isError": true, 3 "error": { 4 "message": " Input string was not in a correct format.", 5 "stackTrace": " at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)\r\n at System.Number.ParseInt32(ReadOnlySpan`1 value, NumberStyles styles, NumberFormatInfo info)\r\n …" 6 } 7}
请注意APIError现在根据MapResponseObject类中定义的属性更改了模型的默认属性。
我们可以自由的选择映射任何属性,下面是映射属性相对应的列表
1[AutoWrapperPropertyMap(Prop.Version)] 2[AutoWrapperPropertyMap(Prop.StatusCode)] 3[AutoWrapperPropertyMap(Prop.Message)] 4[AutoWrapperPropertyMap(Prop.IsError)] 5[AutoWrapperPropertyMap(Prop.Result)] 6[AutoWrapperPropertyMap(Prop.ResponseException)] 7[AutoWrapperPropertyMap(Prop.ResponseException_ExceptionMessage)] 8[AutoWrapperPropertyMap(Prop.ResponseException_Details)] 9[AutoWrapperPropertyMap(Prop.ResponseException_ReferenceErrorCode)] 10[AutoWrapperPropertyMap(Prop.ResponseException_ReferenceDocumentLink)] 11[AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors)] 12[AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors_Field)] 13[AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors_Message)]
自定义错误架构
AutoWrapper还提供了一个APIException可用于定义自己的异常的对象,如果想抛出自己的异常消息,则可以简单地执行以下操作
throw new ApiException("Error blah", 400, "511", "http://blah.com/error/511");
默认输出格式如下所示
1{ 2 "isError": true, 3 "responseException": { 4 "exceptionMessage": "Error blah", 5 "referenceErrorCode": "511", 6 "referenceDocumentLink": "http://blah.com/error/511" 7 } 8}
当然我们可以自定义错误格式
1public class MapResponseObject 2{ 3 [AutoWrapperPropertyMap(Prop.ResponseException)] 4 public object Error { get; set; } 5} 6 7public class Error 8{ 9 public string Message { get; set; } 10 11 public string Code { get; set; } 12 public InnerError InnerError { get; set; } 13 14 public Error(string message, string code, InnerError inner) 15 { 16 this.Message = message; 17 this.Code = code; 18 this.InnerError = inner; 19 } 20 21} 22 23public class InnerError 24{ 25 public string RequestId { get; set; } 26 public string Date { get; set; } 27 28 public InnerError(string reqId, string reqDate) 29 { 30 this.RequestId = reqId; 31 this.Date = reqDate; 32 } 33}
然后我们可以通过如下代码进行引发我们错误
1throw new ApiException( 2 new Error("An error blah.", "InvalidRange", 3 new InnerError("12345678", DateTime.Now.ToShortDateString()) 4));
输出格式如下所示
1{ 2 "isError": true, 3 "error": { 4 "message": "An error blah.", 5 "code": "InvalidRange", 6 "innerError": { 7 "requestId": "12345678", 8 "date": "10/16/2019" 9 } 10 } 11}
使用自定义API响应格式
如果映射满足不了我们的需求。并且我们需要向API响应模型中添加其他属性,那么我们现在可以自定义自己的格式类,通过设置UseCustomSchema为true来实现,代码如下所示
app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { UseCustomSchema = true });
现在假设我们想在主API中响应中包含一个属性SentDate和Pagination对象,我们可能希望将API响应模型定义为以下格式
1public class MyCustomApiResponse 2{ 3 public int Code { get; set; } 4 public string Message { get; set; } 5 public object Payload { get; set; } 6 public DateTime SentDate { get; set; } 7 public Pagination Pagination { get; set; } 8 9 public MyCustomApiResponse(DateTime sentDate, object payload = null, string message = "", int statusCode = 200, Pagination pagination = null) 10 { 11 this.Code = statusCode; 12 this.Message = message == string.Empty ? "Success" : message; 13 this.Payload = payload; 14 this.SentDate = sentDate; 15 this.Pagination = pagination; 16 } 17 18 public MyCustomApiResponse(DateTime sentDate, object payload = null, Pagination pagination = null) 19 { 20 this.Code = 200; 21 this.Message = "Success"; 22 this.Payload = payload; 23 this.SentDate = sentDate; 24 this.Pagination = pagination; 25 } 26 27 public MyCustomApiResponse(object payload) 28 { 29 this.Code = 200; 30 this.Payload = payload; 31 } 32 33} 34 35public class Pagination 36{ 37 public int TotalItemsCount { get; set; } 38 public int PageSize { get; set; } 39 public int CurrentPage { get; set; } 40 public int TotalPages { get; set; } 41}
通过如下代码片段进行测试结果
1public async Task<MyCustomApiResponse> Get() 2{ 3 var data = await _personManager.GetAllAsync(); 4 5 return new MyCustomApiResponse(DateTime.UtcNow, data, 6 new Pagination 7 { 8 CurrentPage = 1, 9 PageSize = 10, 10 TotalItemsCount = 200, 11 TotalPages = 20 12 }); 13 14}
运行后会得到如下影响格式
1{ 2 "code": 200, 3 "message": "Success", 4 "payload": [ 5 { 6 "id": 1, 7 "firstName": "Vianne", 8 "lastName": "Durano", 9 "dateOfBirth": "2018-11-01T00:00:00" 10 }, 11 { 12 "id": 2, 13 "firstName": "Vynn", 14 "lastName": "Durano", 15 "dateOfBirth": "2018-11-01T00:00:00" 16 }, 17 { 18 "id": 3, 19 "firstName": "Mitch", 20 "lastName": "Durano", 21 "dateOfBirth": "2018-11-01T00:00:00" 22 } 23 ], 24 "sentDate": "2019-10-17T02:26:32.5242353Z", 25 "pagination": { 26 "totalItemsCount": 200, 27 "pageSize": 10, 28 "currentPage": 1, 29 "totalPages": 20 30 } 31}
但是从这里要注意一旦我们对API响应进行自定义,那么就代表我们完全控制了要格式化数据的方式,同时丢失了默认API响应的某些选项配置。但是我们仍然可以利用ApiException()方法引发用户定义的错误消息
如下所示
1[Route("{id:long}")] 2[HttpPut] 3public async Task<MyCustomApiResponse> Put(long id, [FromBody] PersonDTO dto) 4{ 5 if (ModelState.IsValid) 6 { 7 try 8 { 9 var person = _mapper.Map<Person>(dto); 10 person.ID = id; 11 12 if (await _personManager.UpdateAsync(person)) 13 return new MyCustomApiResponse(DateTime.UtcNow, true, "Update successful."); 14 else 15 throw new ApiException($"Record with id: {id} does not exist.", 400); 16 } 17 catch (Exception ex) 18 { 19 _logger.Log(LogLevel.Error, ex, "Error when trying to update with ID:{@ID}", id); 20 throw; 21 } 22 } 23 else 24 throw new ApiException(ModelState.AllErrors()); 25}
现在当进行模型验证时,可以获得默认响应格式
1{ 2 "isError": true, 3 "responseException": { 4 "exceptionMessage": "Request responded with validation error(s). Please correct the specified validation errors and try again.", 5 "validationErrors": [ 6 { 7 "field": "FirstName", 8 "message": "'First Name' must not be empty." 9 } 10 ] 11 } 12} 13