web-dev-qa-db-ja.com

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

これは単なるサンプルコードです。検証エラーでもサーバーエラーでも問題ありません。ベストプラクティス、各アプローチの長所と短所を知りたいだけです。

332
cuongle

私にとっては通常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

エラー処理プロセスを更新しました。2つのケースがあります。

  1. 見つからないなどの一般的なエラー、または無効なパラメータがアクションに渡されると、HttpResponseExceptionが返されて処理がすぐに停止します。さらに、私たちのアクションにおけるモデルエラーのために、モデル状態辞書をRequest.CreateErrorResponse拡張子に渡し、それをHttpResponseExceptionにラップします。モデル状態辞書を追加すると、応答本文で送信されたモデルエラーのリストが表示されます。

  2. 上位層で発生するエラー、サーバーエラーの場合は、例外をWeb APIアプリにバブルさせます。ここでは、例外を調べ、それをelmahとtrysでログに記録して正しい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);
    }
}
261
gdp

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 Fault Exceptionなど)がない限り、HTTPエラー500をスローしないでください。データの状態を表す適切なHTTPステータスコードを選択してください。 (下記のapigeeリンクを参照してください。)

リンク集

163
Manish Jain

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)
    {
    }
}
71
Daniel Little

あなたはHttpResponseExceptionを投げることができます

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

Web API 2では、私のメソッドは一貫してIHttpActionResultを返すので、次のようにします。

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

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

ASP.NET Web API 2を使用している場合、最も簡単な方法はApiController Short-Methodを使用することです。これによりBadRequestResultが発生します。

return BadRequest("message");
10

モデルを検証するために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 config.Filters.Add(new DRFValidationFilters())にCustomAttributeクラスを登録します。

4
LokeshChikkala

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) サービス層 は、操作が成功したかどうかにかかわらず、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 Controller サービス機能の結果に基づいてレスポンスを構築します

1つの選択肢は、事実上すべてのパラメータをオプションとして設定し、より意味のある応答を返すカスタム検証を実行することです。また、例外がサービスの境界を超えないように注意します。

    [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

組み込みの "InternalServerError"メソッド(ApiControllerで利用可能)を使用してください。

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

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