web-dev-qa-db-ger.com

Bewährte Methode zum Zurückgeben von Fehlern in der ASP.NET-Web-API

Ich habe Bedenken, wie wir Fehler an den Kunden zurücksenden.

Geben wir einen Fehler sofort zurück, indem wir HttpResponseException auslösen, wenn wir einen Fehler erhalten:

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

Oder wir akkumulieren alle Fehler und senden sie an den Kunden zurück:

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

Dies ist nur ein Beispielcode, es spielt keine Rolle, ob es sich um Validierungsfehler oder Serverfehler handelt. Ich möchte nur die besten Vorgehensweisen kennen, die Vor- und Nachteile der einzelnen Ansätze.

347
cuongle

Für mich sende ich normalerweise ein HttpResponseException zurück und setze den Statuscode entsprechend der geworfenen Ausnahme und ob die Ausnahme schwerwiegend ist oder nicht, wird bestimmen, ob ich das HttpResponseException sofort zurücksende.

Letztendlich ist es eine API, die Antworten und keine Aufrufe zurücksendet. Ich denke, es ist in Ordnung, eine Nachricht mit dem Ausnahme- und Statuscode an den Verbraucher zurückzusenden. Ich musste zurzeit keine Fehler ansammeln und zurücksenden, da die meisten Ausnahmen normalerweise auf falsche Parameter oder Aufrufe usw. zurückzuführen sind.

Ein Beispiel in meiner App ist, dass der Client manchmal nach Daten fragt, aber es sind keine Daten verfügbar. Daher wirfe ich eine benutzerdefinierte noDataAvailableException aus und lasse sie in die Web-API-App sprudeln, wo sie dann in meinem benutzerdefinierten Filter erfasst wird, der sie zurücksendet Nachricht zusammen mit dem richtigen Statuscode.

Ich bin nicht hundertprozentig sicher, was die beste Vorgehensweise dafür ist, aber das funktioniert derzeit für mich, und das ist es, was ich tue.

Update :

Seitdem ich diese Frage beantwortet habe, wurden ein paar Blogposts zum Thema geschrieben:

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

(Dieser hat einige neue Funktionen in den nächtlichen Builds) http://blogs.msdn.com/b/youssefm/archive/2012/06/28/error-handling-in-asp-net-webapi. aspx

Update 2

Update zu unserem Fehlerbehandlungsprozess, wir haben zwei Fälle:

  1. Für allgemeine Fehler wie nicht gefunden oder ungültige Parameter, die an eine Aktion übergeben werden, geben wir eine HttpResponseException zurück, um die Verarbeitung sofort zu beenden. Außerdem übergeben wir für Modellfehler in unseren Aktionen das Modellstatuswörterbuch an die Erweiterung Request.CreateErrorResponse und verpacken es in eine HttpResponseException. Das Hinzufügen des Modellstatuswörterbuchs führt zu einer Liste der im Antworttext gesendeten Modellfehler.

  2. Für Fehler, die in höheren Schichten auftreten, Serverfehler, lassen wir die Ausnahmebedingung in die Web-API-App sprudeln. Hier haben wir einen globalen Ausnahmefilter, der die Ausnahmebedingung prüft, sie mit elmah protokolliert und versucht, das richtige http festzulegen Statuscode und eine entsprechende freundliche Fehlermeldung als Hauptteil erneut in einer HttpResponseException. Ausnahmefällen, von denen wir nicht erwarten, dass der Client den Standardfehler von 500 internen Servern erhält, wird jedoch aus Sicherheitsgründen eine generische Nachricht angezeigt.

Update 3

Nachdem wir die Web-API 2 aufgegriffen haben, verwenden wir kürzlich zum Zurücksenden allgemeiner Fehler die Schnittstelle IHttpActionResult , insbesondere die eingebauten Klassen für den Namespace System.Web.Http.Results, z. B. NotFound, BadRequest when Sie passen, wenn wir sie nicht erweitern, zum Beispiel ein nicht gefundenes Ergebnis mit einer Antwortnachricht:

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

ASP.NET Web API 2 hat es wirklich vereinfacht. Zum Beispiel der folgende Code:

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

gibt den folgenden Inhalt an den Browser zurück, wenn das Element nicht gefunden wird:

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

Vorschlag: Geben Sie keinen HTTP-Fehler 500 aus, es sei denn, es liegt ein schwerwiegender Fehler vor (z. B. WCF-Fehlerausnahme). Wählen Sie einen geeigneten HTTP-Statuscode aus, der den Status Ihrer Daten darstellt. (Siehe den Apigee-Link unten.)

Links:

171
Manish Jain

Es sieht so aus, als hätten Sie mehr Probleme mit der Validierung als mit Fehlern/Ausnahmen.

Validierung

Controller-Aktionen sollten im Allgemeinen Eingabemodelle verwenden, bei denen die Validierung direkt im Modell deklariert wird.

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

Dann können Sie ein ActionFilter verwenden, das automatisch Bewertungsnachrichten an den Client zurücksendet.

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

Weitere Informationen hierzu finden Sie unter http://ben.onfabrik.com/posts/automatic-modelstate-validation-in-aspnet-mvc

Fehlerbehandlung

Am besten senden Sie eine Nachricht an den Client zurück, die die aufgetretene Ausnahme darstellt (mit dem entsprechenden Statuscode).

Im Auslieferungszustand müssen Sie Request.CreateErrorResponse(HttpStatusCode, message) verwenden, wenn Sie eine Nachricht angeben möchten. Dadurch wird der Code jedoch an das Objekt Request gebunden, was nicht erforderlich sein sollte.

Normalerweise erstelle ich meine eigene Art von "sicherer" Ausnahme, von der ich erwarte, dass der Client weiß, wie er mit allen anderen mit einem generischen 500-Fehler umgeht.

Die Verwendung eines Aktionsfilters zur Behandlung der Ausnahmen sieht folgendermaßen aus:

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

Dann können Sie es global registrieren.

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

Dies ist mein benutzerdefinierter Ausnahmetyp.

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

Eine beispielhafte Ausnahme, die meine API auslösen kann.

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

Sie können eine HttpResponseException auslösen

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

Für die Web-API 2 geben meine Methoden konsistent IHttpActionResult zurück, sodass ich ...

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

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

Wenn Sie ASP.NET Web API 2 verwenden, können Sie am einfachsten die ApiController-Kurzmethode verwenden. Dies führt zu einem BadRequestResult.

return BadRequest("message");
15

sie können benutzerdefinierten ActionFilter in Web Api verwenden, um das Modell zu validieren

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

}

Registrieren Sie die CustomAttribute-Klasse in webApiConfig.cs config.Filters.Add (new DRFValidationFilters ());

4
LokeshChikkala

Aufbauend auf der Antwort von Manish Jain (die für die Web-API 2 gedacht ist, die die Dinge vereinfacht):

1) Verwenden Sie Validierungsstrukturen, um so viele Validierungsfehler wie möglich zu beheben. Diese Strukturen können auch zur Beantwortung von Anfragen aus Formularen verwendet werden.

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 Layer gibt ValidationResults zurück, unabhängig davon, ob der Vorgang erfolgreich war oder nicht. Z.B:

    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 erstellt die Antwort basierend auf dem Ergebnis der Servicefunktion

Eine Möglichkeit besteht darin, praktisch alle Parameter als optional zu definieren und eine benutzerdefinierte Validierung durchzuführen, die eine aussagekräftigere Antwort liefert. Außerdem achte ich darauf, dass keine Ausnahme die Servicegrenze überschreitet.

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

Verwenden Sie die eingebaute "InternalServerError" -Methode (verfügbar in ApiController):

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

Nur um den aktuellen Status von ASP.NET WebAPI zu aktualisieren. Die Schnittstelle heißt jetzt IActionResult und die Implementierung hat sich nicht wesentlich geändert:

[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
}
0