在ASP.NET Web API中返回错误的最佳实践

我担心我们将错误返回给客户端的方式。

当出现错误时,是否通过抛出HttpResponseException立即返回错误:

public void Post(Customer customer) { if (string.IsNullOrEmpty(customer.Name)) { throw new HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest) } if (customer.Accounts.Count == 0) { throw new HttpResponseException("Customer does not have any account", HttpStatusCode.BadRequest) } } 

或者我们累积所有错误,然后发送回客户端:

 public void Post(Customer customer) { List<string> errors = new List<string>(); if (string.IsNullOrEmpty(customer.Name)) { errors.Add("Customer Name cannot be empty"); } if (customer.Accounts.Count == 0) { errors.Add("Customer does not have any account"); } var responseMessage = new HttpResponseMessage<List<string>>(errors, HttpStatusCode.BadRequest); throw new HttpResponseException(responseMessage); } 

这只是一个示例代码,无论是validation错误还是服务器错误都无关紧要,我只想知道每种方法的最佳做法,优缺点。

对我来说,我通常会发回一个HttpResponseException并根据抛出的exception设置相应的状态码,如果exception是致命的,将决定是否立即发回HttpResponseException

在一天结束的时候,它的一个API发回的是响应,而不是视图,所以我认为可以发送带有exception和状态码的消息给消费者。 我目前不需要积累错误并将其发回,因为大多数例外通常是由于不正确的参数或调用等原因造成的。

在我的应用程序的一个例子是,有时客户端会要求数据,但没有任何数据可用,所以我抛出一个自定义的noDataAvailableException,让它冒泡到web api应用程序,然后在我的自定义筛选器捕获它发回相关消息以及正确的状态码。

我并不十分确定最佳做法是什么,但是目前这种做法对我来说是如此,所以即使在做什么。

更新

由于我回答了这个问题,在这个主题上写了一些博客文章:

http://weblogs.asp.net/fredriknormen/archive/2012/06/11/asp-net-web-api-exception-handling.aspx

(这个在夜间版本中有一些新function) http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi.aspx

更新2

更新到我们的error handling过程中,我们有两种情况:

  1. 对于一般的错误,如未find,或无效的parameter passing给一个行动,我们返回一个HttpResponseException立即停止处理。 另外,对于我们的操作中的模型错误,我们将模型状态字典交给Request.CreateErrorResponse扩展,并将其包装在一个HttpResponseException中。 添加模型状态字典会生成响应正文中发送的模型错误列表。

  2. 对于发生在高层错误,服务器错误,我们让exception冒泡的Web API应用程序,在这里我们有一个全球性的exceptionfilter,看看exception,logging与elmah和trys的意义,设置正确的http状态码和一个相关友好的错误信息作为一个HttpResponseExceptionexception。 对于我们并不期望客户端将会收到默认500内部服务器错误的exception,但是由于安全原因而产生一个通用消息。

更新3

最近,在拿起Web API 2之后,为了发回一般错误,我们现在使用IHttpActionResult接口,特别是System.Web.Http.Results命名空间中的内置类,比如NotFound,BadRequest,如果它们不合适扩展它们,例如一个没有find响应消息的结果:

 public class NotFoundWithMessageResult : IHttpActionResult { private string message; public NotFoundWithMessageResult(string message) { this.message = message; } public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { var response = new HttpResponseMessage(HttpStatusCode.NotFound); response.Content = new StringContent(message); return Task.FromResult(response); } } 

ASP.NET Web API 2真的简化了它。 例如,下面的代码:

 public HttpResponseMessage GetProduct(int id) { Product item = repository.Get(id); if (item == null) { var message = string.Format("Product with id = {0} not found", id); HttpError err = new HttpError(message); return Request.CreateResponse(HttpStatusCode.NotFound, err); } else { return Request.CreateResponse(HttpStatusCode.OK, item); } } 

找不到该项目时,将以下内容返回给浏览器:

 HTTP/1.1 404 Not Found Content-Type: application/json; charset=utf-8 Date: Thu, 09 Aug 2012 23:27:18 GMT Content-Length: 51 { "Message": "Product with id = 12 not found" } 

build议:不要抛出HTTP错误500,除非有一个灾难性的错误(例如,WCF错误exception)。 select一个表示数据状态的适当的HTTP状态码。 (请参阅下面的apigee链接。)

链接:

  • ASP.NET Web API (asp.net)和.NET中的exception处理
  • REST风格的APIdevise:错误呢? (apigee.com)

看起来你在validation方面比在错误/exception方面遇到了更多的麻烦,所以我稍微说一下。

validation

控制器操作通常应该使用input模型,直接在模型上声明validation。

 public class Customer { [Require] public string Name { get; set; } } 

然后,您可以使用一个ActionFilter自动将偏差消息发送回客户端。

 public class ValidationActionFilter : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { var modelState = actionContext.ModelState; if (!modelState.IsValid) { actionContext.Response = actionContext.Request .CreateErrorResponse(HttpStatusCode.BadRequest, modelState); } } } 

有关此检查的更多信息http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

error handling

最好是将消息返回给表示发生exception的客户端(使用正确的状态码)。

如果你想指定一个消息Request.CreateErrorResponse(HttpStatusCode, message)你必须使用Request.CreateErrorResponse(HttpStatusCode, message) 。 但是,这将代码绑定到Request对象,您不应该这样做。

我通常创build我自己的“安全”exceptiontypes,我希望客户端知道如何处理和包装所有其他人通用500错误。

使用动作filter处理exception将如下所示:

 public class ApiExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext context) { var exception = context.Exception as ApiException; if (exception != null) { context.Response = context.Request.CreateErrorResponse(exception.StatusCode, exception.Message); } } } 

那么你可以在全球注册。

 GlobalConfiguration.Configuration.Filters.Add(new ApiExceptionFilterAttribute()); 

这是我的自定义exceptiontypes。

 using System; using System.Net; namespace WebApi { public class ApiException : Exception { private readonly HttpStatusCode statusCode; public ApiException (HttpStatusCode statusCode, string message, Exception ex) : base(message, ex) { this.statusCode = statusCode; } public ApiException (HttpStatusCode statusCode, string message) : base(message) { this.statusCode = statusCode; } public ApiException (HttpStatusCode statusCode) { this.statusCode = statusCode; } public HttpStatusCode StatusCode { get { return this.statusCode; } } } } 

我的API可以抛出的示例exception。

 public class NotAuthenticatedException : ApiException { public NotAuthenticatedException() : base(HttpStatusCode.Forbidden) { } } 

你可以抛出一个HttpResponseException

 HttpResponseMessage response = this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message"); throw new HttpResponseException(response); 

对于Web API 2,我的方法始终返回IHttpActionResult,所以我使用…

 public IHttpActionResult Save(MyEntity entity) { .... return ResponseMessage( Request.CreateResponse( HttpStatusCode.BadRequest, validationErrors)); } 

我会避免抛出exception的代码,因为抛出exception是代价高昂的。

您可以在Web Api中使用自定义ActionFilter来validation模型

 public class DRFValidationFilters : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { if (!actionContext.ModelState.IsValid) { actionContext.Response = actionContext.Request .CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState); //BadRequest(actionContext.ModelState); } } public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) { return Task.Factory.StartNew(() => { if (!actionContext.ModelState.IsValid) { actionContext.Response = actionContext.Request .CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState); } }); } public class AspirantModel { public int AspirantId { get; set; } public string FirstName { get; set; } public string MiddleName { get; set; } public string LastName { get; set; } public string AspirantType { get; set; } [RegularExpression(@"^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$", ErrorMessage = "Not a valid Phone number")] public string MobileNumber { get; set; } public int StateId { get; set; } public int CityId { get; set; } public int CenterId { get; set; } } [HttpPost] [Route("AspirantCreate")] [DRFValidationFilters] public IHttpActionResult Create(AspirantModel aspirant) { if (aspirant != null) { } else { return Conflict(); } return Ok(); 

}

在webApiConfig.cs中注册CustomAttribute类config.Filters.Add(new DRFValidationFilters());

build立在Manish Jain的答案(这是为了简化事物的Web API 2):

1)使用validation结构来响应尽可能多的validation错误。 这些结构也可以用来响应来自表单的请求。

 public class FieldError { public String FieldName { get; set; } public String FieldMessage { get; set; } } // a result will be able to inform API client about some general error/information and details information (related to invalid parameter values etc.) public class ValidationResult<T> { public bool IsError { get; set; } /// <summary> /// validation message. It is used as a success message if IsError is false, otherwise it is an error message /// </summary> public string Message { get; set; } = string.Empty; public List<FieldError> FieldErrors { get; set; } = new List<FieldError>(); public T Payload { get; set; } public void AddFieldError(string fieldName, string fieldMessage) { if (string.IsNullOrWhiteSpace(fieldName)) throw new ArgumentException("Empty field name"); if (string.IsNullOrWhiteSpace(fieldMessage)) throw new ArgumentException("Empty field message"); // appending error to existing one, if field already contains a message var existingFieldError = FieldErrors.FirstOrDefault(e => e.FieldName.Equals(fieldName)); if (existingFieldError == null) FieldErrors.Add(new FieldError {FieldName = fieldName, FieldMessage = fieldMessage}); else existingFieldError.FieldMessage = $"{existingFieldError.FieldMessage}. {fieldMessage}"; IsError = true; } public void AddEmptyFieldError(string fieldName, string contextInfo = null) { AddFieldError(fieldName, $"No value provided for field. Context info: {contextInfo}"); } } public class ValidationResult : ValidationResult<object> { } 

2) 服务层将返回ValidationResult ,无论操作是否成功。 例如:

  public ValidationResult DoSomeAction(RequestFilters filters) { var ret = new ValidationResult(); if (filters.SomeProp1 == null) ret.AddEmptyFieldError(nameof(filters.SomeProp1)); if (filters.SomeOtherProp2 == null) ret.AddFieldError(nameof(filters.SomeOtherProp2 ), $"Failed to parse {filters.SomeOtherProp2} into integer list"); if (filters.MinProp == null) ret.AddEmptyFieldError(nameof(filters.MinProp)); if (filters.MaxProp == null) ret.AddEmptyFieldError(nameof(filters.MaxProp)); // validation affecting multiple input parameters if (filters.MinProp > filters.MaxProp) { ret.AddFieldError(nameof(filters.MinProp, "Min prop cannot be greater than max prop")); ret.AddFieldError(nameof(filters.MaxProp, "Check")); } // also specify a global error message, if we have at least one error if (ret.IsError) { ret.Message = "Failed to perform DoSomeAction"; return ret; } ret.Message = "Successfully performed DoSomeAction"; return ret; } 

3) API控制器将基于服务function结果构build响应

一种select是几乎所有的参数都是可选的,并执行自定义validation,返回更有意义的响应。 此外,我正在注意不要让任何例外超出服务界限。

  [Route("DoSomeAction")] [HttpPost] public HttpResponseMessage DoSomeAction(int? someProp1 = null, string someOtherProp2 = null, int? minProp = null, int? maxProp = null) { try { var filters = new RequestFilters { SomeProp1 = someProp1 , SomeOtherProp2 = someOtherProp2.TrySplitIntegerList() , MinProp = minProp, MaxProp = maxProp }; var result = theService.DoSomeAction(filters); return !result.IsError ? Request.CreateResponse(HttpStatusCode.OK, result) : Request.CreateResponse(HttpStatusCode.BadRequest, result); } catch (Exception exc) { Logger.Log(LogLevel.Error, exc, "Failed to DoSomeAction"); return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, new HttpError("Failed to DoSomeAction - internal error")); } } 

只是为了更新ASP.NET WebAPI的当前状态。 这个接口现在被称为IActionResult ,实现并没有太大的改变:

 [JsonObject(IsReference = true)] public class DuplicateEntityException : IActionResult { public DuplicateEntityException(object duplicateEntity, object entityId) { this.EntityType = duplicateEntity.GetType().Name; this.EntityId = entityId; } /// <summary> /// Id of the duplicate (new) entity /// </summary> public object EntityId { get; set; } /// <summary> /// Type of the duplicate (new) entity /// </summary> public string EntityType { get; set; } public Task ExecuteResultAsync(ActionContext context) { var message = new StringContent($"{this.EntityType ?? "Entity"} with id {this.EntityId ?? "(no id)"} already exist in the database"); var response = new HttpResponseMessage(HttpStatusCode.Ambiguous) { Content = message }; return Task.FromResult(response); } #endregion } 

使用内置的“InternalServerError”方法(在ApiController中可用):

 return InternalServerError(); //or... return InternalServerError(new YourException("your message")); 

对于那些modelstate.isvalid为false的错误,我通常发送代码抛出的错误。 对于正在使用我的服务的开发人员来说,这很容易理解。 我通常使用下面的代码发送结果。

  if(!ModelState.IsValid) { List<string> errorlist=new List<string>(); foreach (var value in ModelState.Values) { foreach(var error in value.Errors) errorlist.Add( error.Exception.ToString()); //errorlist.Add(value.Errors); } HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.BadRequest,errorlist);} 

这将以下面的格式将错误发送到客户端,这基本上是一个错误列表:

  [ "Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: abc. Path 'Country',** line 6, position 16.\r\n at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)", "Newtonsoft.Json.JsonReaderException: **Could not convert string to integer: ab. Path 'State'**, line 7, position 13.\r\n at Newtonsoft.Json.JsonReader.ReadAsInt32Internal()\r\n at Newtonsoft.Json.JsonTextReader.ReadAsInt32()\r\n at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.ReadForType(JsonReader reader, JsonContract contract, Boolean hasConverter, Boolean inArray)\r\n at Newtonsoft.Json.Serialization.JsonSerializerInternalReader.PopulateObject(Object newObject, JsonReader reader, JsonObjectContract contract, JsonProperty member, String id)" ]