我担心我们将错误返回给客户的方式。
当我们得到错误时,我们是否通过抛出 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);
}
这只是一个示例代码,无论是验证错误还是服务器错误都无关紧要,我只想知道最佳实践,每种方法的优缺点。
ModelState
。
HttpResponseException
类的任何构造函数重载,它采用您的帖子中提到的两个参数 - HttpResponseException("Customer Name cannot be empty", HttpStatusCode.BadRequest)
即 HttpResponseException(string, HttpStatusCode)
对我来说,我通常发回 HttpResponseException
并根据抛出的异常相应地设置状态代码,如果异常是致命的,将决定我是否立即发回 HttpResponseException
。
归根结底,它是一个 API 发回响应而不是视图,所以我认为向消费者发回一条带有异常和状态代码的消息是可以的。我目前不需要累积错误并将它们发回,因为大多数异常通常是由于不正确的参数或调用等造成的。
我的应用程序中的一个示例是,有时客户端会要求提供数据,但没有任何可用数据,因此我抛出自定义 NoDataAvailableException
并让它冒泡到 Web API 应用程序,然后在我的自定义过滤器中捕获它发回相关消息以及正确的状态代码。
我不确定 100% 的最佳做法是什么,但这目前对我有用,所以这就是我正在做的事情。
更新:
自从我回答了这个问题以来,已经写了一些关于该主题的博客文章:
https://weblogs.asp.net/fredriknormen/asp-net-web-api-exception-handling
(这个在夜间版本中有一些新功能)https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi
更新 2
更新我们的错误处理过程,我们有两种情况:
对于一般错误,例如未找到,或传递给操作的参数无效,我们返回 HttpResponseException 以立即停止处理。此外,对于我们操作中的模型错误,我们会将模型状态字典交给 Request.CreateErrorResponse 扩展并将其包装在 HttpResponseException 中。添加模型状态字典会生成响应正文中发送的模型错误列表。对于发生在更高层的错误,服务器错误,我们让异常冒泡到 Web API 应用程序,这里我们有一个全局异常过滤器,它查看异常,用 ELMAH 记录它并尝试理解它设置正确的 HTTP状态代码和相关的友好错误消息作为正文再次出现在 HttpResponseException 中。对于我们不期望客户端会收到默认的 500 内部服务器错误的异常,但出于安全原因,会收到一条通用消息。
更新 3
最近,在使用 Web API 2 后,为了发回一般错误,我们现在使用 IHttpActionResult 接口,特别是 System.Web.Http.Results
命名空间中的内置类,例如 NotFound、BadRequest,如果它们不适合我们扩展它们,例如带有响应消息的 NotFound 结果:
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"
}
建议:除非出现灾难性错误(例如,WCF 故障异常),否则不要抛出 HTTP 错误 500。选择代表数据状态的适当 HTTP 状态代码。 (请参阅下面的 apigee 链接。)
链接:
ASP.NET Web API (asp.net) 中的异常处理和
RESTful API 设计:错误怎么办? (apigee.com)
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
有什么具体用途吗?CreateResponse
和 CreateErrorResponse
有什么区别?
using System.Net.Http;
才能显示 CreateResponse()
扩展方法。
看起来您在验证方面遇到的问题比错误/异常更多,所以我会说一下两者。
验证
控制器操作通常应采用直接在模型上声明验证的输入模型。
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
错误处理
最好向客户端返回一条消息,表示发生的异常(带有相关的状态代码)。
如果要指定消息,则必须使用 Request.CreateErrorResponse(HttpStatusCode, message)
开箱即用。但是,这会将代码绑定到 Request
对象,您不需要这样做。
我通常会创建自己的“安全”异常类型,我希望客户端知道如何处理和包装所有其他带有通用 500 错误的异常。
使用动作过滤器处理异常如下所示:
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());
这是我的自定义异常类型。
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 可以抛出的示例异常。
public class NotAuthenticatedException : ApiException
{
public NotAuthenticatedException()
: base(HttpStatusCode.Forbidden)
{
}
}
var exception = context.Exception as WebException;
,则应该是 ApiException
你可以抛出一个 HttpResponseException
HttpResponseMessage response =
this.Request.CreateErrorResponse(HttpStatusCode.BadRequest, "your message");
throw new HttpResponseException(response);
如果您使用的是 ASP.NET Web API 2,最简单的方法是使用 ApiController Short-Method。这将导致 BadRequestResult。
return BadRequest("message");
return BadRequest(ModelState);
对于 Web API 2,我的方法始终返回 IHttpActionResult 所以我使用...
public IHttpActionResult Save(MyEntity entity)
{
....
if (...errors....)
return ResponseMessage(
Request.CreateResponse(
HttpStatusCode.BadRequest,
validationErrors));
// otherwise success
return Ok(returnData);
}
System.Net.Http
的引用
您可以在 Web Api 中使用自定义 ActionFilter 来验证模型:
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());
以 Manish Jain
的回答为基础(这适用于简化事情的 Web API 2):
1) 使用验证结构来响应尽可能多的验证错误。这些结构也可用于响应来自表单的请求。
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 Controller 将根据服务函数结果构造响应
一种选择是将几乎所有参数作为可选参数并执行自定义验证,从而返回更有意义的响应。另外,我注意不要让任何异常超出服务边界。
[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"));
}
}
使用内置的“InternalServerError”方法(在 ApiController 中可用):
return InternalServerError();
//or...
return InternalServerError(new YourException("your message"));
只是为了更新 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
}
对于那些 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)"
]
尝试这个
[HttpPost]
public async Task<ActionResult<User>> PostUser(int UserTypeId, User user)
{
if (somethingFails)
{
// Return the error message like this.
return new BadRequestObjectResult(new
{
message = "Something is not working here"
});
}
return ok();
}
不定期副业成功案例分享