Лучшая практика для возврата ошибок в 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);
}

Это всего лишь пример кода, это не имеет значения ни ошибок проверки, ни ошибок сервера, я просто хотел бы узнать лучшие практики, плюсы и минусы каждого подхода.

Ответ 1

Для меня я обычно отправляю обратно HttpResponseException и соответствующим образом устанавливаю код состояния в зависимости от генерируемого исключения и если исключение является фатальным или не будет определять, немедленно ли я отправлю HttpResponseException.

В конце дня его API отсылает ответы, а не просмотры, поэтому я думаю, что это прекрасно, чтобы отправить сообщение с кодом исключения и статуса для потребителя. В настоящее время мне не нужно накапливать ошибки и отправлять их обратно, поскольку большинство исключений обычно вызвано неправильными параметрами или вызовами и т.д.

Примером моего приложения является то, что иногда клиент запрашивает данные, но нет доступных данных, поэтому я бросаю пользовательское исключение noDataAvailableException и позволяю ему пузыриться в веб-приложении api, где затем в моем настраиваемом фильтре, который фиксирует его отправку назад соответствующее сообщение вместе с правильным кодом состояния.

Я не уверен на 100%, что лучше всего подходит для этого, но сейчас это работает для меня так, что я делаю.

Обновление

Поскольку я ответил на этот вопрос, в блоге написано несколько сообщений в блоге:

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

(у этого есть некоторые новые функции в ночных сборках) http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi.aspx

Обновление 2

Обновите наш процесс обработки ошибок, у нас есть два случая:

  • Для общих ошибок, подобных не найденным, или недопустимых параметров, передаваемых в действие, мы возвращаем исключение HttpResponseException, чтобы немедленно прекратить обработку. Кроме того, для ошибок модели в наших действиях мы передадим словарь состояния модели в расширение Request.CreateErrorResponse и перенесем его в исключение HttpResponseException. Добавление слова состояния модели приводит к списку ошибок модели, отправленных в тело ответа.

  • Для ошибок, возникающих на более высоких уровнях, ошибок сервера, мы позволяем пузырьку исключения в приложении веб-API, здесь мы имеем глобальный фильтр исключений, который рассматривает исключение, регистрирует его с помощью elmah и пытается понять из него установка правильного кода статуса http и соответствующее дружественное сообщение об ошибке как тело снова в исключении HttpResponseException. Для исключений, которые мы не ожидаем, клиент получит внутреннюю ошибку сервера 500 по умолчанию, но общее сообщение по причинам безопасности.

Обновление 3

Недавно, получив Web API 2, для отправки общих ошибок мы теперь используем интерфейс IHttpActionResult, в частности встроенные классы для системы. Web.Http.Results пространство имен, такое как NotFound, BadRequest, когда они подходят, если они не продлят их, например, неподтвержденный результат с ответным сообщением:

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

Ответ 2

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

Предложение: не высылайте HTTP-ошибку 500, если нет катастрофической ошибки (например, исключение ошибки WCF). Выберите соответствующий код состояния HTTP, который представляет состояние ваших данных. (См. Ссылку apigee ниже.)

Ссылки:

Ответ 3

Похоже, у вас больше проблем с проверкой, чем с ошибками/исключениями, поэтому я немного расскажу о них.

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

Обработка ошибок

Лучше всего вернуть сообщение клиенту, представляющему исключение, которое произошло (с правильным кодом состояния).

Из коробки вы должны использовать 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)
    {
    }
}

Ответ 4

Вы можете создать исключение HttpResponseException

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

Ответ 5

Для Web API 2 мои методы последовательно возвращают IHttpActionResult, поэтому я использую...

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

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

Ответ 6

вы можете использовать пользовательский ActionFilter в Web Api для проверки модели

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

}

Зарегистрировать класс CustomAttribute в webApiConfig.cs config.Filters.Add(новый DRFValidationFilters());

Ответ 7

Настройте ответ 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 s, независимо от успешной операции или нет. Например:

    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 построит ответ на основе результата функции службы

Один из вариантов - поставить практически все параметры как необязательные и выполнить выборочную проверку, которые возвращают более значимый ответ. Кроме того, я стараюсь не допускать каких-либо исключений, выходящих за пределы обслуживания.

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

Ответ 8

Использовать встроенный метод "InternalServerError" (доступный в ApiController):

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

Ответ 9

Просто обновить текущее состояние 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
}

Ответ 10

Для тех ошибок, в которых modelstate.isvalid является ложным, я обычно отправляю ошибку, поскольку она вызывается кодом. Его легко понять для разработчика, который использует мой сервис. Я обычно отправляю результат, используя приведенный ниже код.

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