ASP.NET Core Web API özel durum işleme


280

Uzun yıllar boyunca düzenli ASP.NET Web API kullandıktan sonra yeni REST API projem için ASP.NET Core kullanıyorum. ASP.NET Core Web API'sındaki özel durumları işlemek için iyi bir yol görmüyorum. İstisna işleme filtresi / özniteliğini uygulamaya çalıştım:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

İşte benim Başlangıç ​​filtresi kaydım:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

Benim yaşadığım sorun benim istisna meydana geldiğinde AuthorizationFilterele alınmıyor olmasıdır ErrorHandlingFilter. Ben eski ASP.NET Web API ile çalıştı gibi orada yakalanmasını bekliyordum.

Öyleyse, eylem filtrelerinin tüm uygulama istisnalarını ve istisnalarını nasıl yakalayabilirim?


3
Ara UseExceptionHandlerkatman yazılımını denediniz mi?
Pawel

BuradaUseExceptionHandler katman yazılım kullanma hakkında bir örnek var
Ilya Chernomordik

Yanıtlar:


538

İstisna İşleme Ara Yazılımı

Farklı istisna işleme yaklaşımları ile yapılan birçok deneyden sonra ara katman yazılımı kullandım. ASP.NET Core Web API uygulamam için en iyi sonucu verdi. Uygulama istisnalarının yanı sıra eylem filtrelerinden istisnaları işler ve istisna işleme ve HTTP yanıtı üzerinde tam denetime sahibim. İşte benim istisna işleme middleware:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}

Bunu Kayıt MVC önce de Startupsınıfta:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();

Yığın izleme, özel durum türü adı, hata kodları veya istediğiniz herhangi bir şeyi ekleyebilirsiniz. Çok esnek. İstisna yanıtı örneği:

{ "error": "Authentication token is not valid." }

ASP.NET MVC'nin serileştirme ayarlarını tüm uç noktalarda daha iyi serileştirme tutarlılığı için kullanmak üzere yanıt nesnesini serileştirdiğinizde kullanılacak yöntemi enjekte IOptions<MvcJsonOptions>etmeyi düşünün .InvokeJsonConvert.SerializeObject(errorObj, opts.Value.SerializerSettings)

Yaklaşım 2

UseExceptionHandlerBasit senaryo için "ok" olarak adlandırılan açık olmayan başka bir API var :

app.UseExceptionHandler(a => a.Run(async context =>
{
    var feature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = feature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Bu, istisna yönetimini kurmanın çok açık ama kolay bir yolu değildir. Bununla birlikte, gerekli bağımlılıkları enjekte etme yeteneği ile daha fazla kontrole sahip olduğum için hala ara katman yazılımı yaklaşımını tercih ediyorum.


4
Bugün çalışmak için özel bir ara katman yazılımı almaya çalışırken masaya karşı kafamı dayak ve temelde aynı şekilde çalışır (bir istek için iş / işlem birimini yönetmek için kullanıyorum). Karşılaştığım sorun, 'next' öğesinde yükseltilmiş istisnaların ara katman yazılımına yakalanmaması. Tahmin edebileceğiniz gibi, bu sorunlu. Neyi yanlış / eksik yapıyorum? Herhangi bir işaretçi veya öneriniz var mı?
17:17

5
@ brappleye3 - Sorunun ne olduğunu anladım. Ben sadece Middleup.cs sınıfında yanlış yerde middleware kaydediyordum. Daha app.UseMiddleware<ErrorHandlingMiddleware>();önce taşındım app.UseStaticFiles();. İstisna şimdi doğru bir şekilde yakalanmış gibi görünüyor. Bu inanıyorum bana app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();ara katman doğru sipariş almak için bazı dahili sihirli ara katman yazılım kesmek yapmak.
Jamadan

4
Özel ara katman yazılımlarının çok yararlı olabileceğini ancak NotFound, Yetkisiz ve BadRequest durumları için istisnalar kullanarak soru soracağını kabul ediyorum. Neden durum kodunu (NotFound () vb. Kullanarak) ayarlayıp özel ara katman yazılımınızda veya UseStatusCodePagesWithReExecute aracılığıyla işlemiyorsunuz? Daha fazla bilgi için devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api adresine bakınız
Paul Hiles

4
Kötü çünkü her zaman JSON'a serileştiriyor, içerik pazarlığını tamamen görmezden geliyor.
Konrad

5
@Konrad geçerli noktası. Bu yüzden, bu örneğin, sonuç değil, başlayabileceğiniz yer olduğunu söyledim. API'lerin% 99'u için JSON fazlasıyla yeterlidir. Bu cevabın yeterince iyi olmadığını düşünüyorsanız, katkıda bulunmaktan çekinmeyin.
Andrei

60

En son Asp.Net Core(en azından 2.2, muhtemelen daha önce), kabul edilen yanıttaki uygulamaya kıyasla biraz daha kolaylaştıran yerleşik bir ara katman yazılımına sahiptir:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

Hemen hemen aynı şeyi yapmalı, sadece biraz daha az kod yazmalı.

Önemli: Sipariş önemli olduğundan önce UseMvc(veya UseRouting.Net Core 3'te) eklemeyi unutmayın .


DI'yi işleyiciye bir argüman olarak destekliyor mu, yoksa işleyicinin içinde bir servis bulucu deseni kullanmak zorunda mıdır?
lp

32

En iyi seçeneğiniz aradığınız günlük kaydını elde etmek için ara katman yazılımı kullanmaktır. İstisna günlüğünüzü bir ara katman yazılımına koymak ve kullanıcıya görüntülenen hata sayfalarınızı farklı bir ara katman yazılımında işlemek istiyorsunuz. Bu, mantığın ayrılmasına izin verir ve Microsoft'un 2 ara katman bileşeni ile ortaya koyduğu tasarımı izler. İşte Microsoft'un belgelerine iyi bir bağlantı: ASP.Net Core'da Hata İşleme

Spesifik örneğiniz için, StatusCodePage ara katman yazılımındaki uzantılardan birini kullanmak veya kendi URL'nizi bu şekilde döndürmek isteyebilirsiniz .

İstisnaları günlüğe kaydetmek için burada bir örnek bulabilirsiniz: ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

Bu özel uygulamayı beğenmezseniz, ELM Middleware'i de kullanabilirsiniz ve işte bazı örnekler: Elm Exception Middleware

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

Bu, ihtiyaçlarınız için işe yaramıyorsa, kendi Middleware bileşeninizi her zaman kendi ExceptionHandlerMiddleware ve ElmMiddleware uygulamalarına bakarak kendi konseptlerinizi kavrayabilirsiniz.

StatusCodePages ara katman yazılımının altına, ancak diğer tüm ara katman yazılımı bileşenlerinizin istisna işleme ara katmanı eklemek önemlidir. Bu şekilde İstisna ara yazılımınız istisnayı yakalar, günlüğe kaydeder, ardından isteğin kullanıcıya kolay hata sayfasını görüntüleyecek StatusCodePage ara katmanına ilerlemesine izin verir.


Rica ederim. Ayrıca, isteğinizi daha iyi karşılayabilecek uç durumlarda varsayılan UseStatusPages'ı geçersiz kılmak için bir örneğe bağlantı sağladım.
Ashley Lee

1
Elm'in günlükleri ıslatmadığını ve serileştirmeyi sağlamak için Serilog veya NLog kullanılması önerilir. Bkz. ELM günlükleri kaybolur. Bir dosyaya veya DB'ye devam edebilir miyiz?
Michael Freidgeim

2
Bağlantı kesildi.
Mathias Lykkegaard Lorenzen

@AshleyLee, UseStatusCodePagesWeb API hizmeti uygulamalarında kullanılan soruyu sordum . Hiç görünüm veya HTML yok, sadece JSON yanıtları ...
Paul Michalik

23

İyi kabul edilen cevap bana çok yardımcı oldu ama çalışma zamanında hata durum kodunu yönetmek için benim Middleware HttpStatusCode geçmek istedim.

Bu bağlantıya göre , aynı şeyi yapmak için bir fikrim var. Bu yüzden Andrei Answer ile birleştim. Son kodum şu şekildedir:
1. Temel sınıf

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}

2. Özel İstisna Sınıfı Türü

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3. Özel İstisna Ara Katman

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4. Genişletme Yöntemi

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }

5. startup.cs dosyasında Yöntemi Yapılandırma

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

Şimdi Hesap denetleyicisindeki giriş yöntemim:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }

İ Daha sonra kullanıcı HttpStatusCodeException hangi i geçti HttpStatusCode.NotFound statü ve özel bir mesaj yükselterek bulamamaları durumunda Gördüğünüz Üstü
katman olarak

catch (HttpStatusCodeException ex)

kontrolünü geçecek olan

private Task HandleExceptionAsync (HttpContext bağlamı, HttpStatusCodeException istisna) yöntemi

.


Ama daha önce çalışma zamanı hatası alırsam ne olur? Bunun için istisna atmak ve catch (Exception exceptionObj) bloğunda yakalanacak olan denemek catch blok kullandık ve

Task HandleExceptionAsync (HttpContext bağlamı, İstisna istisnası)

yöntem.

Tekdüzelik için tek bir ErrorDetails sınıfı kullandım.


Uzatma yöntemi nereye konmalı? Maalesef içinde startup.csde void Configure(IapplicationBuilder app)bir hata alıyorum IApplicationBuilder does not contain a definition for ConfigureCustomExceptionMiddleware. Ve referansı ekledim, nerede CustomExceptionMiddleware.cs.
Spedo De La Rossa

apislerinizi yavaşlattıklarından istisnalar kullanmak istemezsiniz. istisnalar çok pahalıdır.
lnaie

@Inaie, Bu konuda söyleyemem ... ama başa çıkmak için hiç bir istisnanız yok gibi görünüyor .. Büyük iş
Arjun

19

Özel durum türü başına özel durum işleme davranışını yapılandırmak için NuGet paketlerinden Middleware kullanabilirsiniz:

Kod örneği:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}

16

İlk olarak, çözümümü örneğine dayandığım için Andrei'ye teşekkürler.

Benimkini daha eksiksiz bir örnek olarak dahil ediyorum ve okuyucuları biraz zaman kazandırabilir.

Andrei'nin yaklaşımının sınırlandırılması, günlüğe kaydetme, potansiyel olarak yararlı istek değişkenlerini ve içerik anlaşmasını yakalamamasıdır (müşterinin istediği ne olursa olsun her zaman JSON'u döndürür - XML ​​/ düz metin vb.).

Benim yaklaşımım MVC içine pişmiş işlevselliği kullanmamızı sağlayan bir ObjectResult kullanmaktır.

Bu kod aynı zamanda yanıtın önbelleğe alınmasını da önler.

Hata yanıtı, XML serileştiricisi tarafından serileştirilebilecek şekilde dekore edilmiştir.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}

9

İlk olarak, ASP.NET Core 2'yi Startupweb sunucusundaki hatalar ve işlenmeyen istisnalar için bir hata sayfasına yeniden çalışacak şekilde yapılandırın .

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

Ardından, HTTP durum kodlarıyla hata atmanıza izin verecek bir istisna türü tanımlayın.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

Son olarak, hata sayfası için kumandanızda, hatanın nedenine ve yanıtın doğrudan bir son kullanıcı tarafından görülüp görülmeyeceğine göre yanıtı özelleştirin. Bu kod, tüm API URL'lerinin ile başladığını varsayar /api/.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core hata ayıklamanız için hata ayrıntılarını kaydeder, böylece bir durum kodu (potansiyel olarak güvenilir olmayan) bir istekte bulunana vermek istediğiniz her şey olabilir. Daha fazla bilgi göstermek istiyorsanız, bunu HttpExceptionsağlamak için geliştirebilirsiniz . API hatalar için, değiştirerek ileti gövdesinde JSON kodlanmış hata bilgisi koyabilirsiniz return StatusCode...ile return Json....


0

ara katman yazılımı veya IExceptionHandlerPathFeature iyidir. başka bir yol varmağazada

bir istisna filtresi oluşturun ve kaydedin

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}
services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.