ASP.NET Web API 2'de Genel Hata İşleme

Tarafından David Matson, Rick Anderson

Bu konu, ASP.NET 4.x için ASP.NET Web API 2'de genel hata işlemeye genel bir bakış sağlar. Bugün Web API'sinde hataları genel olarak günlüğe kaydetmenin veya işlemenin kolay bir yolu yoktur. bazı işlenmeyen özel durumlar özel durum filtreleri aracılığıyla işlenebilir, ancak özel durum filtrelerinin işleyebileceği birkaç durum vardır. Örnek:

  1. Denetleyici oluşturucularından gelen özel durumlar.
  2. İleti işleyicilerinden oluşturulan özel durumlar.
  3. Yönlendirme sırasında oluşturulan özel durumlar.
  4. Yanıt içeriği serileştirmesi sırasında oluşturulan özel durumlar.

Bu özel durumları günlüğe kaydetmek ve işlemek için (mümkün olduğunda) basit ve tutarlı bir yol sağlamak istiyoruz.

Özel durumları işlemeye yönelik iki önemli durum vardır. Burada hata yanıtı gönderebiliyoruz ve tek yapabileceğimiz özel durumu günlüğe kaydetmektir. İkinci duruma örnek olarak akış yanıtı içeriğinin ortasında bir özel durum oluşturulur; bu durumda durum kodu, üst bilgiler ve kısmi içerik zaten kablodan geçmiş olduğundan yeni bir yanıt iletisi göndermek için çok geç olduğundan bağlantıyı durdurmamız yeterlidir. Özel durum yeni bir yanıt iletisi oluşturmak için işlenemese de, özel durumun günlüğe kaydedilmesini desteklemeye devam ediyoruz. Bir hatayı algılayabildiğimiz durumlarda, aşağıdaki gibi uygun bir hata yanıtı döndürebiliriz:

public IHttpActionResult GetProduct(int id)
{
    var product = products.FirstOrDefault((p) => p.Id == id);
    if (product == null)
    {
        return NotFound();
    }
    return Ok(product);
}

Mevcut Seçenekler

Özel durum filtrelerine ek olarak, ileti işleyicileri bugün 500 düzeyindeki yanıtların tümünü gözlemlemek için kullanılabilir, ancak özgün hatayla ilgili bağlamları olmadığından bu yanıtlar üzerinde işlem yapmak zordur. İleti işleyicileri, işleyebileceği durumlarla ilgili özel durum filtreleri ile aynı sınırlamalardan bazılarına da sahiptir. Web API'sinin hata koşullarını yakalayan izleme altyapısı olsa da, izleme altyapısı tanılama amaçlıdır ve üretim ortamlarında çalışmak için tasarlanmamış veya uygun değildir. Genel özel durum işleme ve günlüğe kaydetme, üretim sırasında çalışabilen ve mevcut izleme çözümlerine (örneğin ELMAH) bağlanabilen hizmetler olmalıdır.

Çözüme Genel Bakış

İşlenmeyen özel durumları günlüğe kaydetmek ve işlemek için IExceptionLogger ve IExceptionHandler olmak üzere iki yeni kullanıcı tarafından değiştirilebilir hizmet sunuyoruz. Hizmetler birbirine çok benzer ve iki ana fark vardır:

  1. Birden çok özel durum günlüğe kaydetmeyi destekliyoruz, ancak yalnızca tek bir özel durum işleyicisi.
  2. Bağlantıyı durdurmak üzere olsak bile özel durum günlükçüleri her zaman çağrılır. Özel durum işleyicileri yalnızca hangi yanıt iletisinin gönderileceğini seçebildiğimizde çağrılır.

Her iki hizmet de özellikle HttpRequestMessage, HttpRequestContext, atılan özel durum ve özel durum kaynağı (aşağıdaki ayrıntılar) olmak üzere özel durumun algılandığı noktadan ilgili bilgileri içeren bir özel durum bağlamı erişimi sağlar.

Tasarım İlkeleri

  1. Hataya neden olan değişiklik yok Bu işlev ikincil bir sürüme eklendiğinden, çözümü etkileyen önemli bir kısıtlama, sözleşmeleri yazmak veya davranışa yönelik hataya neden olan hiçbir değişiklik olmamasıdır. Bu kısıtlama, özel durumları 500 yanıta dönüştüren mevcut catch blokları açısından yapmak istediğimiz bazı temizlemeleri elerdi. Bu ek temizleme, sonraki bir ana sürümde dikkate alebileceğimiz bir özelliktir.
  2. Web API'si yapılarıyla tutarlılığı koruma Web API'sinin filtre işlem hattı, mantığı eyleme özgü, denetleyiciye özgü veya genel bir kapsamda uygulama esnekliğiyle çapraz kesme sorunlarını gidermenin harika bir yoludur. Özel durum filtreleri de dahil olmak üzere filtreler, genel kapsama kaydedildiğinde bile her zaman eylem ve denetleyici bağlamlarına sahiptir. Bu sözleşme filtreler için mantıklıdır, ancak genel olarak kapsamlı olanlar bile özel durum filtrelerinin, hiçbir eylem veya denetleyici bağlamı bulunmayan ileti işleyicilerinden gelen özel durumlar gibi bazı özel durum işleme durumlarına uygun olmadığı anlamına gelir. Özel durum işleme için filtreler tarafından sunulan esnek kapsam belirlemeyi kullanmak istiyorsak yine de özel durum filtrelerine ihtiyacımız vardır. Ancak bir denetleyici bağlamı dışında özel durumu işlememiz gerekirse, tam genel hata işleme için ayrı bir yapıya da ihtiyacımız vardır (denetleyici bağlamı ve eylem bağlamı kısıtlamaları olmayan bir şey).

Ne Zaman Kullanılır?

  • Özel durum günlüğe kaydedilenler, Web API'sinin yakaladığı işlenmeyen tüm özel durumları görmek için çözüm olarak kullanılır.
  • Özel durum işleyicileri, Web API'sinin yakaladığı işlenmeyen özel durumlara yönelik tüm olası yanıtları özelleştirmeye yönelik çözümdür.
  • Özel durum filtreleri, belirli bir eylem veya denetleyiciyle ilgili alt küme işlenmeyen özel durumları işlemek için en kolay çözümdir.

Hizmet Ayrıntıları

Özel durum günlükçü ve işleyici hizmeti arabirimleri, ilgili bağlamları alan basit zaman uyumsuz yöntemlerdir:

public interface IExceptionLogger
{
   Task LogAsync(ExceptionLoggerContext context, 
                 CancellationToken cancellationToken);
}

public interface IExceptionHandler
{
   Task HandleAsync(ExceptionHandlerContext context, 
                    CancellationToken cancellationToken);
}

Ayrıca bu arabirimlerin her ikisi için de temel sınıflar sağlıyoruz. Çekirdek (eşitleme veya zaman uyumsuz) yöntemleri geçersiz kılma, önerilen zamanlarda günlüğe kaydetmek veya işlemek için gereken tek işlemdir. Günlüğe kaydetme için ExceptionLogger temel sınıf, çekirdek günlüğe kaydetme yönteminin her özel durum için yalnızca bir kez çağrılmasını sağlar (daha sonra çağrı yığınını daha fazla yaysa ve yeniden yakalansa bile). ExceptionHandler Temel sınıf, temel işleme yöntemini yalnızca çağrı yığınının en üstündeki özel durumlar için çağırır ve eski iç içe geçmiş catch bloklarını yoksayır. (Bu temel sınıfların basitleştirilmiş sürümleri aşağıdaki ekte yer almaktadır.) IExceptionHandler Hem hem de IExceptionLogger aracılığıyla ExceptionContextözel durum hakkında bilgi alın.

public class ExceptionContext
{
   public Exception Exception { get; set; }

   public HttpRequestMessage Request { get; set; }

   public HttpRequestContext RequestContext { get; set; }

   public HttpControllerContext ControllerContext { get; set; }

   public HttpActionContext ActionContext { get; set; }

   public HttpResponseMessage Response { get; set; }

   public string CatchBlock { get; set; }

   public bool IsTopLevelCatchBlock { get; set; }
}

Çerçeve bir özel durum günlükçüsüsü veya özel durum işleyicisi çağırdığında, her zaman bir Exception ve Requestsağlar. Birim testi dışında, her zaman bir RequestContextde sağlar. Nadiren bir ControllerContext ve ActionContext sağlar (yalnızca özel durum filtreleri için catch bloğundan çağrılırken). Çok nadiren bir Responsesağlar (yalnızca yanıt yazmaya çalışırken belirli IIS durumlarda). Bu özelliklerden bazılarının özel durum sınıfının üyelerine erişmeden önce denetlenecek null tüketiciye ait olabileceğini null unutmayın.CatchBlock , hangi catch bloğunun özel durumu gördüğünü belirten bir dizedir. Catch bloğu dizeleri aşağıdaki gibidir:

  • HttpServer (SendAsync yöntemi)

  • HttpControllerDispatcher (SendAsync yöntemi)

  • HttpBatchHandler (SendAsync yöntemi)

  • IExceptionFilter (ApiController'ın ExecuteAsync'te özel durum filtresi işlem hattını işlemesi)

  • OWIN konağı:

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (çıkış arabelleğe alma için)
    • HttpMessageHandlerAdapter.CopyResponseContentAsync (akış çıktısı için)
  • Web konağı:

    • HttpControllerHandler.WriteBufferedResponseContentAsync (arabelleğe alma çıktısı için)
    • HttpControllerHandler.WriteStreamedResponseContentAsync (akış çıkışı için)
    • HttpControllerHandler.WriteErrorResponseContentAsync (arabelleğe alınan çıkış modunda hata kurtarma hataları için)

Catch blok dizelerinin listesi statik salt okunur özellikler aracılığıyla da kullanılabilir. (Çekirdek catch bloğu dizesi statik ExceptionCatchBlocks üzerindedir; geri kalan her biri OWIN ve web konağı için bir statik sınıfta görünür).IsTopLevelCatchBlock , yalnızca çağrı yığınının en üstünde özel durumları işlemeye yönelik önerilen deseni takip etme konusunda yararlıdır. Özel durumları iç içe bir yakalama bloğunun gerçekleştiği her yerde 500 yanıta dönüştürmek yerine, özel durum işleyicisi konak tarafından görülmek üzere olana kadar özel durumların yayılmasına izin verebilir.

ek olarak ExceptionContext, bir günlükçü tam ExceptionLoggerContextaracılığıyla bir bilgi daha alır:

public class ExceptionLoggerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public bool CanBeHandled { get; set; }
}

İkinci özelliği olan CanBeHandled, günlükçülerin işlenemeyen bir özel durumu tanımlamasına izin verir. Bağlantı durdurulmak üzereyken yeni yanıt iletisi gönderilemediğinde, günlüğe kaydedilenler çağrılır, ancak işleyici çağrılmayacak ve günlüğe kaydedilenler bu özellikten bu senaryoyu tanımlayabilir.

ek olarak ExceptionContext, bir işleyici özel durumu işlemek için tam ExceptionHandlerContext olarak ayarlayabileceğiniz bir özellik daha alır:

public class ExceptionHandlerContext
{
   public ExceptionContext ExceptionContext { get; set; }
   public IHttpActionResult Result { get; set; }
}

Özel durum işleyicisi, özelliğini bir eylem sonucuna (örneğin ExceptionResult, InternalServerErrorResult, StatusCodeResult veya özel bir sonuç) ayarlayarak Result bir özel durumu işlediğini gösterir. Result Özellik null ise, özel durum işlenmemiştir ve özgün özel durum yeniden oluşturulur.

Çağrı yığınının üst kısmındaki özel durumlar için yanıtın API çağıranlarına uygun olduğundan emin olmak için ek bir adım attık. Özel durum konağa yayılırsa, çağıran ölümün sarı ekranını veya genellikle HTML olan ve genellikle uygun bir API hata yanıtı olmayan başka bir konağın sağladığı yanıtı görür. Böyle durumlarda Sonuç null olmayan bir değerle başlar ve yalnızca özel durum işleyicisi bunu açıkça ( null işlenmemiş) olarak ayarlarsa özel durum konağa yayılır. Result Bu gibi durumlarda ayarı null iki senaryo için yararlı olabilir:

  1. Web API'si öncesinde/dışında kayıtlı ara yazılımı özel özel durum işlemeye sahip OWIN tarafından barındırılan Web API'si.
  2. Sarı ölüm ekranının aslında işlenmeyen bir özel durum için yararlı bir yanıt olduğu bir tarayıcı aracılığıyla yerel hata ayıklama.

Hem özel durum günlüğe kaydedenler hem de özel durum işleyicileri için, günlükçü veya işleyicinin kendisi bir özel durum oluşturursa kurtarmak için hiçbir şey yapmayız. (Özel durumun yayılmasına izin vermek dışında, daha iyi bir yaklaşımınız varsa bu sayfanın en altında geri bildirim bırakın.) Özel durum günlüğe kaydedenler ve işleyiciler için sözleşme, özel durumların çağıranlarına yayılmasına izin vermemeleri gerektiğidir; aksi takdirde, özel durum genellikle ana bilgisayara yayılır ve html hatasıyla sonuçlanır (ASP gibi). NET'in sarı ekranı) istemciye geri gönderiliyor (bu genellikle JSON veya XML bekleyen API çağıranları için tercih edilen seçenek değildir).

Örnekler

İzleme Özel Durum Günlükçü

Aşağıdaki özel durum günlükçü yapılandırılan İzleme kaynaklarına (Visual Studio'da Hata Ayıklama çıkış penceresi dahil) özel durum verileri gönderir.

class TraceExceptionLogger : ExceptionLogger
{
    public override void LogCore(ExceptionLoggerContext context)
    {
        Trace.TraceError(context.ExceptionContext.Exception.ToString());
    }
}

Özel Hata İletisi Özel Durum İşleyicisi

Aşağıdaki özel durum işleyicisi, desteğe başvurmak için bir e-posta adresi de dahil olmak üzere istemcilere özel bir hata yanıtı oluşturur.

class OopsExceptionHandler : ExceptionHandler
{
    public override void HandleCore(ExceptionHandlerContext context)
    {
        context.Result = new TextPlainErrorResult
        {
            Request = context.ExceptionContext.Request,
            Content = "Oops! Sorry! Something went wrong." +
                      "Please contact support@contoso.com so we can try to fix it."
        };
    }

    private class TextPlainErrorResult : IHttpActionResult
    {
        public HttpRequestMessage Request { get; set; }

        public string Content { get; set; }

        public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
        {
            HttpResponseMessage response = 
                             new HttpResponseMessage(HttpStatusCode.InternalServerError);
            response.Content = new StringContent(Content);
            response.RequestMessage = Request;
            return Task.FromResult(response);
        }
    }
}

Özel Durum Filtrelerini Kaydetme

Projenizi oluşturmak için "ASP.NET MVC 4 Web Uygulaması" proje şablonunu kullanırsanız, Web API yapılandırma kodunuzu sınıfının içine WebApiConfigApp_Start klasörüne yerleştirin:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new ProductStore.NotImplExceptionFilterAttribute());

        // Other configuration code...
    }
}

Ek: Temel Sınıf Ayrıntıları

public class ExceptionLogger : IExceptionLogger
{
    public virtual Task LogAsync(ExceptionLoggerContext context, 
                                 CancellationToken cancellationToken)
    {
        if (!ShouldLog(context))
        {
            return Task.FromResult(0);
        }

        return LogAsyncCore(context, cancellationToken);
    }

    public virtual Task LogAsyncCore(ExceptionLoggerContext context, 
                                     CancellationToken cancellationToken)
    {
        LogCore(context);
        return Task.FromResult(0);
    }

    public virtual void LogCore(ExceptionLoggerContext context)
    {
    }

    public virtual bool ShouldLog(ExceptionLoggerContext context)
    {
        IDictionary exceptionData = context.ExceptionContext.Exception.Data;

        if (!exceptionData.Contains("MS_LoggedBy"))
        {
            exceptionData.Add("MS_LoggedBy", new List<object>());
        }

        ICollection<object> loggedBy = ((ICollection<object>)exceptionData[LoggedByKey]);

        if (!loggedBy.Contains(this))
        {
            loggedBy.Add(this);
            return true;
        }
        else
        {
            return false;
        }
    }
}

public class ExceptionHandler : IExceptionHandler
{
    public virtual Task HandleAsync(ExceptionHandlerContext context, 
                                    CancellationToken cancellationToken)
    {
        if (!ShouldHandle(context))
        {
            return Task.FromResult(0);
        }

        return HandleAsyncCore(context, cancellationToken);
    }

    public virtual Task HandleAsyncCore(ExceptionHandlerContext context, 
                                       CancellationToken cancellationToken)
    {
        HandleCore(context);
        return Task.FromResult(0);
    }

    public virtual void HandleCore(ExceptionHandlerContext context)
    {
    }

    public virtual bool ShouldHandle(ExceptionHandlerContext context)
    {
        return context.ExceptionContext.IsOutermostCatchBlock;
    }
}