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


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

1416   11  

11 ответов:

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

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

пример в моем приложении заключается в том, что иногда клиент запрашивает данные, но нет никаких доступных данных, поэтому я бросаю пользовательское исключение noDataAvailableException и позволяю ему пузыриться в приложение web 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

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

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

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

обновление 3

недавно, после сбора веб-API 2, для отправки общих ошибок мы теперь используем IHttpActionResult интерфейс, в частности встроенные классы для в системе.Сеть.Http.Пространство имен результатов, такое как 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"
}

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

ссылки:

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

проверка

действия контроллера обычно должны принимать входные модели, где проверка объявляется непосредственно на модели.

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

затем вы можете использовать ActionFilter это автоматически отправляет valiation сообщения обратно клиенту.

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

вы можете бросить 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));
}

вы можете использовать пользовательский ActionFilter в веб-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.цезий конфиг.Фильтры.Add (new DRFValidationFilters ());

дом по Manish Jainответ (который предназначен для веб-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) слой сервиса вернутся 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 будет построить ответ на основе результата служебной функции

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

    [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 API-интерфейс 2, Самый простой способ заключается в использовании ApiController короткий способ. Это приведет к BadRequestResult.

return BadRequest("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)"
    ]

Comments

    Ничего не найдено.