Jak globalnie rejestrować wszystkie wyjątki dla aplikacji C # MVC4 WebAPI?

175

tło

Opracowuję warstwę usług API dla klienta i poproszono mnie o globalne wyłapywanie i rejestrowanie wszystkich błędów.

Tak więc, chociaż coś takiego jak nieznany punkt końcowy (lub akcja) można łatwo obsłużyć za pomocą ELMAH lub dodając coś takiego do Global.asax:

protected void Application_Error()
{
     Exception unhandledException = Server.GetLastError();
     //do more stuff
}

. . Nieobsługiwane błędy, które nie są związane z routingiem, nie są rejestrowane. Na przykład:

public class ReportController : ApiController
{
    public int test()
    {
        var foo = Convert.ToInt32("a");//Will throw error but isn't logged!!
        return foo;
    }
}

Próbowałem również ustawić [HandleError]atrybut globalnie, rejestrując ten filtr:

filters.Add(new HandleErrorAttribute());

Ale to również nie rejestruje wszystkich błędów.

Problem / pytanie

Jak przechwycić błędy, takie jak ten wygenerowany przez wywołanie /testpowyżej, aby móc je zarejestrować? Wydaje się, że ta odpowiedź powinna być oczywista, ale próbowałem wszystkiego, o czym przyszło mi do głowy.

Najlepiej byłoby dodać kilka rzeczy do rejestrowania błędów, takich jak adres IP użytkownika żądającego, data, godzina i tak dalej. Chcę również mieć możliwość automatycznego wysyłania e-maili do personelu pomocniczego w przypadku napotkania błędu. Wszystko to mogę zrobić, jeśli tylko zdołam przechwycić te błędy, gdy się pojawią!

ZDECYDOWANY!

Dzięki Darinowi Dimitrovowi, którego odpowiedź przyjąłem, zrozumiałem. Interfejs WebAPI nie obsługuje błędów w taki sam sposób, jak zwykły kontroler MVC.

Oto, co zadziałało:

1) Dodaj niestandardowy filtr do swojej przestrzeni nazw:

public class ExceptionHandlingAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        if (context.Exception is BusinessException)
        {
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent(context.Exception.Message),
                ReasonPhrase = "Exception"
            });

        }

        //Log Critical errors
        Debug.WriteLine(context.Exception);

        throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
        {
            Content = new StringContent("An error occurred, please try again or contact the administrator."),
            ReasonPhrase = "Critical Exception"
        });
    }
}

2) Teraz zarejestruj filtr globalnie w klasie WebApiConfig :

public static class WebApiConfig
{
     public static void Register(HttpConfiguration config)
     {
         config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{action}/{id}", new { id = RouteParameter.Optional });
         config.Filters.Add(new ExceptionHandlingAttribute());
     }
}

LUB możesz pominąć rejestrację i po prostu ozdobić pojedynczy kontroler [ExceptionHandling]atrybutem.

Matt Cashatt
źródło
Mam ten sam problem. Nieobsłużone wyjątki są przechwytywane w atrybucie filtru wyjątków dobrze, ale kiedy zgłaszam nowy wyjątek, nie jest on przechwytywany w atrybucie filtru wyjątków. Masz jakiś pomysł na ten temat?
daveBM
1
Nieznane wywołania kontrolera API, takie jak myhost / api / undefinedapicontroller, nadal nie są przechwytywane. Kod filtru błędu_aplikacji i wyjątku nie jest wykonywany. Jak je też złapać?
Andrus
1
Do WebAPI v2.1 dodano obsługę globalnych błędów. Zobacz moją odpowiedź tutaj: stackoverflow.com/questions/17449400/…
DarrellNorton
1
W niektórych przypadkach nie spowoduje to wykrycia błędów, takich jak „nie znaleziono zasobu” lub błędy w konstruktorze kontrolera. Zobacz tutaj: aspnet.codeplex.com/SourceControl/latest#Samples/WebApi/Elmah/…
Jordan Morris
Cześć @Matt. Napisałeś odpowiedź jako część pytania, ale nie jest to najlepsza praktyka w SO. Tutaj odpowiedzi powinny być oddzielone od pytania. Czy mógłbyś napisać to jako osobną odpowiedź (możesz użyć niebieskiego przycisku „Odpowiedz na własne pytanie” u dołu).
sashoalm

Odpowiedzi:

56

Jeśli interfejs API sieci Web jest hostowany w aplikacji ASP.NET, Application_Errorzdarzenie zostanie wywołane dla wszystkich nieobsłużonych wyjątków w kodzie, w tym wyjątków w pokazanej akcji testowej. Więc wszystko, co musisz zrobić, to obsłużyć ten wyjątek wewnątrz zdarzenia Application_Error. W przykładowym kodzie, który pokazałeś, obsługujesz tylko wyjątek typu, HttpExceptionktóry oczywiście nie ma miejsca w przypadku Convert.ToInt32("a")kodu. Więc upewnij się, że logujesz i obsługujesz tam wszystkie wyjątki:

protected void Application_Error()
{
    Exception unhandledException = Server.GetLastError();
    HttpException httpException = unhandledException as HttpException;
    if (httpException == null)
    {
        Exception innerException = unhandledException.InnerException;
        httpException = innerException as HttpException;
    }

    if (httpException != null)
    {
        int httpCode = httpException.GetHttpCode();
        switch (httpCode)
        {
            case (int)HttpStatusCode.Unauthorized:
                Response.Redirect("/Http/Error401");
                break;

            // TODO: don't forget that here you have many other status codes to test 
            // and handle in addition to 401.
        }
        else
        {
            // It was not an HttpException. This will be executed for your test action.
            // Here you should log and handle this case. Use the unhandledException instance here
        }
    }
}

Obsługa wyjątków w internetowym interfejsie API może odbywać się na różnych poziomach. Oto detailed articlewyjaśnienie różnych możliwości:

  • niestandardowy atrybut filtru wyjątków, który można zarejestrować jako globalny filtr wyjątków

    [AttributeUsage(AttributeTargets.All)]
    public class ExceptionHandlingAttribute : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            if (context.Exception is BusinessException)
            {
                throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent(context.Exception.Message),
                    ReasonPhrase = "Exception"
                });
            }
    
            //Log Critical errors
            Debug.WriteLine(context.Exception);
    
            throw new HttpResponseException(new HttpResponseMessage(HttpStatusCode.InternalServerError)
            {
                Content = new StringContent("An error occurred, please try again or contact the administrator."),
                ReasonPhrase = "Critical Exception"
            });
        }
    }
  • wywołujący akcję niestandardową

    public class MyApiControllerActionInvoker : ApiControllerActionInvoker
    {
        public override Task<HttpResponseMessage> InvokeActionAsync(HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
        {
            var result = base.InvokeActionAsync(actionContext, cancellationToken);
    
            if (result.Exception != null && result.Exception.GetBaseException() != null)
            {
                var baseException = result.Exception.GetBaseException();
    
                if (baseException is BusinessException)
                {
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Error"
    
                    });
                }
                else
                {
                    //Log critical error
                    Debug.WriteLine(baseException);
    
                    return Task.Run<HttpResponseMessage>(() => new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent(baseException.Message),
                        ReasonPhrase = "Critical Error"
                    });
                }
            }
    
            return result;
        }
    }
Darin Dimitrov
źródło
Chciałbym, żeby to było takie proste, ale błąd nadal nie jest wychwytywany. Zaktualizowałem pytanie, aby uniknąć nieporozumień. Dzięki.
Matt Cashatt
@MatthewPatrickCashatt, jeśli ten wyjątek nie jest przechwytywany w Application_Errorzdarzeniu, oznacza to, że jakiś inny kod używa go wcześniej. Na przykład możesz mieć niestandardowe HandleErrorAttributes, niestandardowe moduły, ... Istnieją miliardy innych miejsc, w których wyjątki mogą być przechwytywane i obsługiwane. Ale najlepszym miejscem do tego jest zdarzenie Application_Error, ponieważ tam kończą się wszystkie nieobsłużone wyjątki.
Darin Dimitrov
Jeszcze raz dziękuję, ale bez względu na wszystko, /testprzykład nie został trafiony. Umieściłem punkt przerwania w pierwszej linii ( Exception unhandledException = . . .), ale nie mogę go osiągnąć w /testscenariuszu. Jeśli jednak wstawię fałszywy adres URL, zostanie osiągnięty punkt przerwania.
Matt Cashatt
1
@MatthewPatrickCashatt, masz całkowitą rację. Application_ErrorZdarzenie nie jest właściwe miejsce, aby obsłużyć wyjątki dla Web API, ponieważ nie zostanie uruchomiony we wszystkich przypadkach. Znalazłem bardzo szczegółowy artykuł wyjaśniający różne możliwości osiągnięcia tego celu: weblogs.asp.net/fredriknormen/archive/2012/06/11/…
Darin Dimitrov
1
@Darin Dimitrov Nieznane wywołania kontrolera API, takie jak błędy myhost / api / undefinedapi, nadal nie są przechwytywane. Kod filtru błędu_aplikacji i wyjątku nie jest wykonywany. Jak je też złapać?
Andrus
79

Jako dodatek do poprzednich odpowiedzi.

Wczoraj został oficjalnie wydany ASP.NET Web API 2.1 .
Daje kolejną możliwość obsługi wyjątków na całym świecie.
Szczegóły podano w próbce .

Krótko mówiąc, dodajesz globalne rejestratory wyjątków i / lub globalną obsługę wyjątków (tylko jeden).
Dodajesz je do konfiguracji:

public static void Register(HttpConfiguration config)
{
  config.MapHttpAttributeRoutes();

  // There can be multiple exception loggers.
  // (By default, no exception loggers are registered.)
  config.Services.Add(typeof(IExceptionLogger), new ElmahExceptionLogger());

  // There must be exactly one exception handler.
  // (There is a default one that may be replaced.)
  config.Services.Replace(typeof(IExceptionHandler), new GenericTextExceptionHandler());
}

I ich realizacja:

public class ElmahExceptionLogger : ExceptionLogger
{
  public override void Log(ExceptionLoggerContext context)
  {
    ...
  }
}

public class GenericTextExceptionHandler : ExceptionHandler
{
  public override void Handle(ExceptionHandlerContext context)
  {
    context.Result = new InternalServerErrorTextPlainResult(
      "An unhandled exception occurred; check the log for more information.",
      Encoding.UTF8,
      context.Request);
  }
}
Vladimir
źródło
2
To działało doskonale. Loguję i obsługuję jednocześnie (ponieważ otrzymuję logID i przekazuję go z powrotem, aby użytkownik mógł dodać komentarz), więc ustawiam wynik na nowy ResponseMessageResult. Dręczyło mnie to od jakiegoś czasu, dzięki.
Brett,
8

Po co wrzucać ponownie itp.? To działa i sprawi, że usługa zostanie zwrócona o statusie 500 itd

public class LogExceptionFilter : ExceptionFilterAttribute
{
    private static readonly ILog log = LogManager.GetLogger(typeof (LogExceptionFilter));

    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {
        log.Error("Unhandeled Exception", actionExecutedContext.Exception);
        base.OnException(actionExecutedContext);
    }
}
Anders
źródło
2

czy myślałeś o zrobieniu czegoś w rodzaju filtru działania błędu obsługi, takiego jak

[HandleError]
public class BaseController : Controller {...}

możesz również utworzyć niestandardową wersję programu, za [HandleError]pomocą której możesz zapisywać informacje o błędach i wszystkie inne szczegóły do ​​logowania

NA ZIMNO
źródło
Dzięki, ale mam już to ustawione globalnie. Stwarza ten sam problem, co powyżej, nie wszystkie błędy są rejestrowane.
Matt Cashatt
1

Zawiń całość w try / catch i zarejestruj nieobsługiwany wyjątek, a następnie przekaż go dalej. Chyba że istnieje lepszy wbudowany sposób, aby to zrobić.

Oto odniesienie do wszystkich wyjątków (obsłużonych lub nieobsługiwanych)

(edytuj: oh API)

Tim
źródło
Na wszelki wypadek musiałby również odrzucić wyjątek.
DigCamara
@DigCamara Przepraszamy, właśnie to miałem na myśli, przekazując to dalej. rzucać; powinien sobie z tym poradzić. Początkowo powiedziałem „zdecyduj, czy zamknąć, czy ponownie załadować”, a potem zdałem sobie sprawę, że powiedział, że to API. W takim przypadku najlepiej pozwolić aplikacji zdecydować, co chce zrobić, przekazując ją dalej.
Tim
1
To zła odpowiedź, ponieważ spowoduje to mnóstwo zduplikowanego kodu w każdej akcji.
Jansky