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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/10732644/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-09 15:02:46  来源:igfitidea点击:

Best practice to return errors in ASP.NET Web API

c#restasp.net-web-api

提问by cuongle

I have concerns on the way that we returns errors to client.

我对我们向客户返回错误的方式感到担忧。

Do we return error immediately by throwing HttpResponseExceptionwhen we get an error:

当我们收到错误时,我们是否通过抛出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) 
    }
}

Or we accumulate all errors then send back to client:

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

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);
}

This is just a sample code, it does not matter either validation errors or server error, I just would like to know the best practice, the pros and cons of each approach.

这只是一个示例代码,验证错误或服务器错误都没有关系,我只想知道最佳实践,每种方法的优缺点。

采纳答案by gdp

For me I usually send back an HttpResponseExceptionand set the status code accordingly depending on the exception thrown and if the exception is fatal or not will determine whether I send back the HttpResponseExceptionimmediately.

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

At the end of the day it's an API sending back responses and not views, so I think it's fine to send back a message with the exception and status code to the consumer. I currently haven't needed to accumulate errors and send them back as most exceptions are usually due to incorrect parameters or calls etc.

归根结底,它是一个 API 发送响应而不是视图,所以我认为向消费者发送带有异常和状态代码的消息是可以的。我目前不需要累积错误并将它们发回,因为大多数异常通常是由于不正确的参数或调用等引起的。

An example in my app is that sometimes the client will ask for data, but there isn't any data available so I throw a custom NoDataAvailableExceptionand let it bubble to the Web API app, where then in my custom filter which captures it sending back a relevant message along with the correct status code.

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

I am not 100% sure on what's the best practice for this, but this is working for me currently so that's what I'm doing.

我不是 100% 确定什么是最佳实践,但这目前对我有用,所以这就是我正在做的。

Update:

更新

Since I answered this question a few blog posts have been written on the topic:

自从我回答了这个问题以来,已经写了一些关于该主题的博客文章:

https://weblogs.asp.net/fredriknormen/asp-net-web-api-exception-handling

https://weblogs.asp.net/fredriknormen/asp-net-web-api-exception-handling

(this one has some new features in the nightly builds) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

(这个在每晚构建中有一些新功能) https://docs.microsoft.com/archive/blogs/youssefm/error-handling-in-asp-net-webapi

Update 2

更新 2

Update to our error handling process, we have two cases:

更新我们的错误处理流程,我们有两种情况:

  1. For general errors like not found, or invalid parameters being passed to an action we return a HttpResponseExceptionto stop processing immediately. Additionally for model errors in our actions we will hand the model state dictionary to the Request.CreateErrorResponseextension and wrap it in a HttpResponseException. Adding the model state dictionary results in a list of the model errors sent in the response body.

  2. For errors that occur in higher layers, server errors, we let the exception bubble to the Web API app, here we have a global exception filter which looks at the exception, logs it with ELMAH and tries to make sense of it setting the correct HTTP status code and a relevant friendly error message as the body again in a HttpResponseException. For exceptions that we aren't expecting the client will receive the default 500 internal server error, but a generic message due to security reasons.

  1. 对于诸如未找到之类的一般错误,或传递给操作的无效参数,我们返回 aHttpResponseException以立即停止处理。此外,对于我们操作中的模型错误,我们会将模型状态字典交给Request.CreateErrorResponse扩展并将其包装在HttpResponseException. 添加模型状态字典会生成在响应正文中发送的模型错误列表。

  2. 对于发生在更高层的错误,服务器错误,我们让异常冒泡到 Web API 应用程序,这里我们有一个全局异常过滤器,它查看异常,用 ELMAH 记录它并尝试理解它设置正确的 HTTP状态代码和相关的友好错误消息再次作为HttpResponseException. 对于我们不期望客户端会收到默认的 500 内部服务器错误的例外情况,但出于安全原因,会收到一条通用消息。

Update 3

更新 3

Recently, after picking up Web API 2, for sending back general errors we now use the IHttpActionResultinterface, specifically the built in classes for in the System.Web.Http.Resultsnamespace such as NotFound, BadRequest when they fit, if they don't we extend them, for example a NotFound result with a response message:

最近,在拿起 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);
    }
}

回答by Manish Jain

ASP.NET Web API 2 really simplified it. For example, the following code:

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);
    }
}

returns the following content to the browser when the item is not found:

未找到该项目时,向浏览器返回以下内容:

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"
}

Suggestion: Don't throw HTTP Error 500 unless there is a catastrophic error (for example, WCF Fault Exception). Pick an appropriate HTTP status code that represents the state of your data. (See the apigee link below.)

建议:除非出现灾难性错误(例如,WCF 故障异常),否则不要抛出 HTTP 错误 500。选择一个合适的 HTTP 状态代码来表示您的数据状态。(请参阅下面的 apigee 链接。)

Links:

链接:

回答by tartakynov

You can throw a HttpResponseException

你可以抛出一个 HttpResponseException

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

回答by Daniel Little

It looks like you're having more trouble with Validation than errors/exceptions so I'll say a bit about both.

看起来您在验证方面遇到的麻烦比错误/异常要多,所以我会说一下两者。

Validation

验证

Controller actions should generally take Input Models where the validation is declared directly on the model.

控制器操作通常应采用输入模型,其中验证直接在模型上声明。

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

Then you can use an ActionFilterthat automatically sends validation messages back to the client.

然后您可以使用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);
        }
    }
} 

For more information about this check out http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

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

Error handling

错误处理

It's best to return a message back to the client that represents the exception that happened (with relevant status code).

最好向客户端返回一条消息,表示发生的异常(带有相关的状态代码)。

Out of the box you have to use Request.CreateErrorResponse(HttpStatusCode, message)if you want to specify a message. However, this ties the code to the Requestobject, which you shouldn't need to do.

Request.CreateErrorResponse(HttpStatusCode, message)如果要指定消息,则必须立即使用。但是,这会将代码与Request对象联系起来,您不需要这样做。

I usually create my own type of "safe" exception that I expect the client would know how to handle and wrap all others with a generic 500 error.

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

Using an action filter to handle the exceptions would look like this:

使用操作过滤器处理异常如下所示:

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);
        }
    }
}

Then you can register it globally.

然后就可以全局注册了。

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

This is my custom exception type.

这是我的自定义异常类型。

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; }
        }
    }
}

An example exception that my API can throw.

我的 API 可以抛出的示例异常。

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

回答by Ashish Sahu

For those errors where modelstate.isvalid is false, I generally send the error as it is thrown by the code. Its easy to understand for the developer who is consuming my service. I generally send the result using below code.

对于那些 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);}

This sends the error to the client in below format which is basically a list of errors:

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

    [  
    "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)"
    ]

回答by Mick

For Web API 2 my methods consistently return IHttpActionResult so I use...

对于 Web API 2,我的方法始终返回 IHttpActionResult,因此我使用...

public IHttpActionResult Save(MyEntity entity)
{
  ....

    return ResponseMessage(
        Request.CreateResponse(
            HttpStatusCode.BadRequest, 
            validationErrors));
}

回答by LokeshChikkala

you can use custom ActionFilter in Web Api to validate model

您可以在 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();

}

}

Register CustomAttribute class in webApiConfig.cs config.Filters.Add(new DRFValidationFilters());

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

回答by Thomas Hagstr?m

Just to update on the current state of ASP.NET WebAPI. The interface is now called IActionResultand implementation hasn't changed much:

只是为了更新 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
}

回答by Alexei

Building up upon Manish Jain's answer (which is meant for Web API 2 which simplifies things):

建立在Manish Jain的答案上(这适用于简化了事情的 Web API 2):

1) Use validation structuresto response as many validation errors as possible. These structures can also be used to response to requests coming from forms.

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) Service layerwill return ValidationResults, regardless of operation being successful or not. E.g:

2)无论操作成功与否,服务层都会返回ValidationResults。例如:

    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 Controllerwill construct the response based on service function result

3) API Controller将根据服务功能结果构造响应

One option is to put virtually all parameters as optional and perform custom validation which return a more meaningful response. Also, I am taking care not to allow any exception to go beyond the service boundary.

一种选择是将几乎所有参数都作为可选参数并执行自定义验证,从而返回更有意义的响应。此外,我会注意不允许任何异常超出服务边界。

    [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"));
        }
    }

回答by Rusty

Use the built in "InternalServerError" method (available in ApiController):

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

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