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şleyemediği bir dizi durum vardır. Örneğin:

  1. Denetleyici oluşturucularından fırlatılan istisnalar.
  2. Mesaj işleyicilerinden fırlatılan istisnalar.
  3. Yönlendirme sırasında oluşan özel durumlar.
  4. Yanıt içeriği serileştirmesi sırasında oluşan ö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şlemek için iki önemli durum vardır: hata yanıtı gönderebilmemiz 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çtiğinden 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 destekliyoruz. 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ığı için 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) takılabilen hizmetler olmalıdır.

Çözüme Genel Bakış

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

  1. Birden çok özel durum kaydedicisini destekliyoruz, ancak yalnızca tek bir özel durum işleyicisini.
  2. Bağlantıyı kesmek üzere olsak bile, özel durum kaydedicileri her zaman çağrılır. Özel durum işleyicileri, yalnızca hangi yanıt iletisinin gönderileceğine karar verebildiğimiz durumlarda ç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 sürüme eklendiğinden, çözümü etkileyen önemli bir kısıtlama, sözleşmeleri yazmak veya davranışa yönelik herhangi bir hataya neden olan değişiklik olmamasıdır. Bu kısıtlama, özel durumları 500 yanıta dönüştüren mevcut catch bloklarıyla ilgili yapmak istediğimiz bazı temizlemeleri eledi. Bu ek temizleme, sonraki bir önemli sürüm için göz önünde bulunduracağımız 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ı ele almak için harika bir yoldur. Ö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 anlamlıdır, ancak genel olarak kapsamı belirlenmiş olanlar bile, 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 gerekiyorsa, 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 kaydedicileri, Web API tarafından yakalanan işlenmemiş tüm özel durumları görmenin çözümüdür.
  • Özel durum işleyicileri, Web API'sinin yakaladığı işlenmeyen özel durumlara 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 hizmet arabirimleri, ilgili bağlamları kabul eden basit asenkron 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 (senkron veya asenkron) yöntemleri geçersiz kılmak, önerilen zamanlarda günlüğe kaydetmek veya işlemek için yeterlidir. Günlük kaydı için temel sınıf, ExceptionLogger ana günlükleme yönteminin her özel durum için yalnızca bir kez çağrılmasını sağlar (daha sonra çağrı yığını boyunca daha fazla yayılırsa ve yeniden yakalanırsa bile). ExceptionHandler Temel sınıf, yalnızca çağrı yığınının en üstündeki özel durumlar için çekirdek işleme yöntemini çağırır ve eski iç içe geçmiş catch bloklarını yoksayır. Her iki IExceptionLogger ve IExceptionHandler, ExceptionContext aracılığıyla özel durum hakkında bilgi alır.

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ü veya özel durum işleyicisi çağırdığında, her zaman bir Exception ve Request sağlar. Birim testi dışında, her zaman bir RequestContext 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 Response sağlanır (yalnızca belirli IIS durumlarında, yanıt yazmaya çalışırken). Bu özelliklerden bazıları null olabileceği için, özel durum sınıfının üyelerine erişmeden önce null kontrol etmenin tüketicinin sorumluluğunda olduğunu unutmayın. CatchBlock , hangi catch bloğunun özel durumu gördüğünü gösteren 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 sunucusu

    • HttpMessageHandlerAdapter.BufferResponseContentAsync (arabelleğe alma çıktısı 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 sürecindeki başarısızlıklar için)

Catch bloğu dizelerinin listesi, ayrıca statik salt okunur özellikler aracılığıyla da erişilebilir durumda. (Çekirdek catch bloğu dizesi statik ExceptionCatchBlocks üzerindedir; geri kalanlar her biri OWIN ve web sunucusu için ayrı 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 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 kayıt tutucu tam ExceptionLoggerContext aracı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ı bitmek üzereyken ve yeni yanıt mesajı gönderilemiyorsa, kaydediciler çağrılır ancak işleyici çağrılmaz, ve kaydediciler bu özelliği kullanarak durumu belirleyebilir.

Ek olarak ExceptionContext, bir işleyici, özel durumu ele almak için tam ExceptionHandlerContext üzerinde ayarlayabileceği bir özellik daha kazanı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 Result, InternalServerErrorResult, StatusCodeResult veya özel bir sonuç) ayarlayarak bir özel durumu işlediğini gösterir. Result özelliği null ise, özel durum ele alınmamıştır ve özgün özel durum yeniden fırlatılır.

Çağrı yığınının en üstündeki özel durumlar için yanıtın API çağıranlara 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 ana bilgisayar tarafından sağlanan yanıtı görür. Bu gibi durumlarda, Sonuç null olmayan bir şekilde başlar ve yalnızca özel bir özel durum işleyicisi bunu açıkça ( null işlenmemiş) olarak ayarlarsa özel durum konağa yayılır. Ayar Result'ü null'ye getirmek, bu gibi durumlarda iki senaryo için yararlı olabilir:

  1. Web API'dan önce/dışında kaydedilmiş özel durum işleme ara yazılımı ile OWIN tarafından barındırılan Web API.
  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 kaydedicileri hem de özel durum işleyicileri için, kaydedici veya işleyicinin kendisi bir özel durum oluşturursa, herhangi bir geri kazanım işlemi gerçekleştirilmez. (Özel durumun yayılmasına izin vermek dışında, daha iyi bir yaklaşımınız varsa bu sayfanın en altına geri bildirim bırakın.) Özel durum günlüğe kaydedicileri ve işleyicileri 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 kadar yayılır ve bu, ASP.NET'in sarı ekranı gibi bir HTML hatasının istemciye geri gönderilmesine yol açar (bu genellikle JSON veya XML bekleyen API çağıranları için tercih edilen bir 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 Mesajı İş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ıyorsanı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;
    }
}