Asp net application error

I want to use Application_Error with my MVC project, but i can't get it to work. I add the following to my Global.asax file: protected void Application_Error(object sender, EventArgs e) { ...

So firstly remember that global error handling should be a last resort, and controller classes have a specific error method for errors;

protected virtual bool OnError(string actionName, 
    System.Reflection.MethodInfo methodInfo, Exception exception)

Within this you can redirect to the standard shared error view;

protected override bool OnError(string actionName, 
    System.Reflection.MethodInfo methodInfo, Exception exception)
{
   RenderView("Error", exception);
   return false;
}

The problem you have in the global application error is that it has no concept of views or controllers, so if you want to redirect in there then you must use a known URL

protected void Application_Error(object sender, EventArgs e)
{
    Exception exception = Server.GetLastError();
    System.Diagnostics.Debug.WriteLine(exception);
    Response.Redirect("/Home/Error");
}

but you don’t need to do this. If you set the default error page in web.config then you don’t need that redirect

<customErrors defaultRedirect="Home/Error" />

However, unless you’ve added an error view to your Home controller that doesn’t exist, so add the following to the home controller

public ActionResult Error()
{
    return View();
}

Then (if you’re sensible) you’d put the error handling code in the Error() method, as that’s where all unhandled errors will end up.

public ActionResult Error()
{
    Exception exception = Server.GetLastError();
    System.Diagnostics.Debug.WriteLine(exception);
    return View();
}

And finally remember that by default you don’t see custom errors if you are connecting to localhost! So you need to change that behaviour

<customErrors mode="On" defaultRedirect="/Home/Error" />

There is a pragmatic approach to error handling in ASP.NET MVC. This is the idea of recovering gracefully from those unhandled exception errors where remedial action cannot be taken under the current context, by passing control to a specified URL that is designed to deal with a particular category of application-specific error. Dino Esposito elaborates on a pattern that prevents unhandled exceptions from bubbling up well beyond the intended scope

There are many ways to handle errors and exceptions in an ASP.NET MVC application. I summarized all of them in an old article I wrote for Simple Talk. You can find it here: Handling Errors Effectively in ASP.NET MVC. That article offered a comprehensive view of all the possible techniques a developer can adopt including using the HandleError attribute on controller methods and providing an explicit implementation for the OnException method in a controller class. All these techniques work on top of any preliminary validation you can make over your data to prevent errors and any try/catch block you want to use to trap unwanted but expected exceptions.

In this article, I’ll take a slightly different, and wildly more pragmatic, route and focus exclusively on application-level recovery from unhandled exceptions. As I was saying in several books and articles, including the aforementioned Simple Talk article, every application should always include an application-level recovery procedure that serves as a sort of safety net for users and also saves the reputation of the coders. The world is full of examples of failures and disasters due to unhandled exceptions that bubbled up well beyond the intended scope, thereby causing undesired and unpredictable effects. So let’s just make it a rule that just every application should have its own Application_Error method right in global.asax.

What kind of code should we have there and to do what exactly? Let’s start by putting Application_Error into perspective.

The Application_Error Method

Application_Error is the conventional name for the routine that ASP.NET (not just ASP.NET MVC) calls right before displaying its own error screen—the notorious yellow screen of death. If you don’t have such a routine or if for some reason you let some events slip out of it your users will get the standard error page or the page that for common HTTP error codes (like 404 or 500) you may have defined in the web.config file.

Therefore, Application_Error is a sort of catch-all place where a number of unpleasant application events find their way. The overall role of Application_Error is quite controversial and it’s common to find companies where development teams have strong opinions about having or not such a centralized handler of application errors. Some find it just an ideal single place where handling all possible errors and exceptions. Others find the use of Application_Error a bit unprofessional as if it were the result of poor coding practices.

Technically speaking, Application_Error is a dumb event-handler that passes no specific information about the error event that just occurred.

void Application_Error(object sender, EventArgs e)

To write an effective handler, you need to put some very specific code in action. This code is expected to do at least a couple of things. First, it is expected to learn as much as possible about what has just happened. Second, it has to decide what to do; whether to just log the error or to redirect it to a safe place where the user can resume the session. Another option is to run whatever compensation logic makes sense, given the error that occurred.

So far, I have deliberately used the two words “error” and “exception” interchangeably. However, defining errors and exceptions is merely the first step towards an effective, yet pragmatic, error handling strategy.

Errors vs. Exceptions

In the context of a web application, I define an “error” as being an action that the user attempts to perform, but which fails under the total control of the application code. A good example is when the user that edits the URL in the address bar is then stopped by authorization rules, missing endpoints or inaccurate and invalid data. I define an “exception” as being a failure that happen outside the control of the application code. A good example is a network failure or a database error. For example, when your server-code places a remote call to an HTTP endpoint, it may fail for a number of reasons that may not depend on your code. There’s no validation that you can perform beforehand to ensure the call will always succeed. So in these cases it is a safe practice to wrap the network call in a try/catch block. In a way, this becomes an “expected” exception. Depending on the nature of the expected exception there are a few things you can do. For one thing, you can compensate for the effects of the exception in the catch block; for example you can retry the call. As an alternative, you swallow the exception and return some feedback to the caller or you can just throw a different exception with more generic or more specific information. Finally, you can simply log the exception in some way and let ASP.NET do the job of bubbling up the exception until a handler is found. An error is an action that cannot be taken under the current conditions. In this regard, an error requires a strong reaction from the system such as a popup message or a redirection to a landing page.

To deal with errors and exceptions I suggest the following guidelines.

  • Wrap in try/catch blocks any calls that can possibly generate an “expected” exception. In the catch block, you may log and/or swallow in some way the exception or, when allowed, just implement some compensation logic right in the block.
  • Always have an Application_Error method, so that unexpected exceptions are stopped before they reach the outermost shell of ASP.NET MVC code and render as yellow screens of death.
  • Use large chunks of validation logic to prevent errors as much as possible. As mentioned, errors originate from violated business rules and you are supposed to know them very well. When users find a way to bypass validation and the outlined user interface (i.e., they type an invalid URL on the browser address bar) you throw yourself an exception and redirect the application flow to Application_Error.

As I have experienced sometimes, the option of throwing an exception to redirect the code to Application_Error is one that some developers don’t like much.

Use of Exceptions

A golden rule of exception handling is that you should not use exceptions as control flow statements. I pretty much agree with the general meaning of the statement but in case of web applications and limited to errors I’m just doing that. When I detect an error—say, a violation of a well-known business rule—I throw an application exception and send user to a contextual view through the Application_Error handler.

Let’s be pragmatic. How would you react when you find that your controller action is being invoked with erroneous parameters? Or you find a logical error in a deeper layer of code that prevents the action from being completed? In the context of a web application, it is not an option to display a message box until it’s all JavaScript code: Worse yet, a controller method has to return some HTML views.

If you force yourself not to use exceptions as control flow statements, you then must have an error sub view in each and every view that can possibly face an error. And maybe a different sub view for each possible error in the view. I’m going to deny the purity of such an approach; but I find it a bit impractical. Throwing an application-specific exception every time you detect a logical error helps keeping your controller code as lean as possible. In addition, deeper layers of code—such as application logic but even more domain logic—get to have a cleaner design as their methods would simply reject through exceptions whatever scenario they can’t handle. The throw statement if not caught at some level bubbles up and is ultimately handled in Application_Error.

public ActionResult Schedule([Bind(Prefix=«y»)] int year = 0)

{

   if (year <= 0)

       throw new YourApplicationException(«You can’t view the page without indicating a year»);

   // Rest of the code

}

The deal is, cleaner code in controllers and one single place where all errors can be handled. With proper code in Application_Error you can redirect users to an appropriate error page.

Unless the exception is expected and you know how to handle it, it is not advisable to catch exceptions in every page, especially if you’re dealing with all of them in the same way. Most of the error handling code I’ve seen is about logging an error to some local or remote database and perhaps email the site admin. There’s no reason for repeating this code, even in the compact form of an attribute, in each controller method. By the way, this is the reason that made ELMAH a powerful and widely used tool for error handling in ASP.NET.

Typical Code to Have in Application_Error

In summary, Application_Error plays two main roles. It offers a chance to recover gracefully from any sort of unexpected exceptions like 404 errors, model binding errors, route errors or generic internal errors. In addition, it acts as a dispatcher of error views so that each error presents the user a friendly message as well as a list of options or links to resume the session. Here’s some sample code that’s useful to have in the Application_ Error method.

void Application_Error(object sender, EventArgs e)

{

   // Grab information about the last error occurred

   var exception = Server.GetLastError();

   // Clear the response stream

   var httpContext = ((HttpApplication)sender).Context;

   httpContext.Response.Clear();

   httpContext.ClearError();

   httpContext.Response.TrySkipIisCustomErrors = true;

   // Manage to display a friendly view

   InvokeErrorAction(httpContext, exception);

}

As obvious as it may sound, if you handle exceptions you should ideally try to do more than just logging it somewhere. Doing more than just logging means isolating a few classes of unexpected events for which you define a compensation policy. A 404 is clearly one kind of unexpected event you want to handle along with authorization issues such as when users try to reach pages they’re not authorized to see or invoke endpoints they’re should not be calling. Internal errors (HTTP status code 500) should be split into multiple categories and the best way to do that in my view of the world is through application-specific exceptions.

In your code, you define a base exception class and make it expose a friendly and articulated informative content as well as a collection property storing feasible links to recover from. The InvokeErrorAction subroutine in the above code snippet will then do the rest.

void InvokeErrorAction(HttpContext httpContext, Exception exception)

{

var routeData = new RouteData();

routeData.Values[«controller»] = «home»;

routeData.Values[«action»] = «error»;

routeData.Values[«exception»] = exception;

using (var controller = new HomeController())

{

((IController)controller).Execute(

new RequestContext(new HttpContextWrapper(httpContext), routeData));

}

}

All that the method does is to invoke a controller action programmatically. The final effect is to display a custom view based on the information associated with the exception. Admittedly, such a code is a bit unusual to see and, of course, it is not the only possible way to just display a HTML view. However, I commonly opt for this code for one particular reason: the view is displayed in the context of the same HTTP request. In a way, it is the ASP.NET MVC counterpart of Server.Transfer you would use in ASP.NET Web Forms in the same scenario. Using a redirect to some URL like /home/error would achieve the same but at the cost of serializing in some way the exception. Unless you opt for serializing the exception core data somewhere on the server—preserving affinity in case of web farms—you lose that information or should pack it in some special places such as query string or headers.

Why not simply calling the method Error (or whatever other method) on the Home controller (or whatever other controller)? The reason is that a direct call to the Error method won’t trigger the view engine and won’t actually generate the expected HTML view. Calling the Execute method on the base controller interface triggers the ASP.NET MVC action invoker component that first calls the controller method and then executes the action result it gets from the controller action method. If you don’t much like to see HomeController in the code, the best you can do is to have an application-specific base controller class that exposes an Error method. Here’s a sample implementation.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

public ActionResult Error(Exception exception)

{

   var code = GetStatusCode(exception);

   var message = String.Format(Strings_Errors.Error500, code);

   var subtitle = «»;

   var appSpecific = (exception is YourApplicationBaseException);

   if (code == 404)

      message = Strings_Errors.Error404;

   if (code == 500)

   {

      if (appSpecific)

         message = exception.Message;

      else

         subtitle = exception.Message;

   }

   var model = new ErrorViewModel(message, appSpecific)

   {

       ErrorOccurred = { StatusCode = code, Subtitle = subtitle }

   };

}

The net effect is that, if the exception is an application-specific exception in the case of an HTTP 500 error, then you get the exception message: Otherwise you get a generic HTTP 500 error message plus the system generated unfiltered exception message which might even contain sensitive data. Finally, the exception might be associated with a list of links to be displayed in the error view for points in the application where the user can be safely redirected to continue the session. For example, the home page.

A Word or Two About ASP.NET Core

Everything in this article works for the current ASP.NET MVC 5.x. The interesting thing is that the approach to error handling in ASP.NET Core is pretty much the same approach that I described here as a pragmatic approach. In ASP.NET Core, when you register services in the startup of the application you typically use the following code:

app.UseExceptionHandler(«/Home/Error»);

The effect is that in case of unhandled exceptions the control is moved to the specified URL. Whether you go there through a redirect or an internal re-route it doesn’t change the emerging perspective of error handling in web applications. Throwing application-specific exceptions allows to show precise messages and breaks up the flow, saving you from dealing with many branches of code. More, most of the handling logic is in a single place (or a in just a few places) and this doesn’t even prevent logging or tracing.

Результатом выброшенного, но необработанного исключения, является прекращение работы веб-приложения. Выводимая при этом дополнительная информация, как правило, не предназначена для обычных пользователей. Посмотрим вариант как сделать её более понятной для них.

Ядро ASP.NET предоставляет стандартный механизм для обработки исключений, которые до этого не были перехвачены ни в одном из методов. Его использование крайне желательно:

  • Для ведения журнала и анализа исключительных ситуаций.
  • Для сокрытия информации об ошибке, если в web.config параметр customErrors равен «Off».
  • С точки зрения пользовательского интерфейса, лучше выводить контекстно-зависимые сообщения с понятным пользователю описанием ошибки и вариантами дальнейших действий.

В ASP.NET для указанной цели служит метод Application_Error(), размещенный в файле Global.asax. Давайте рассмотрим его на примере обработки исключения HttpRequestValidationException.

Реализация обработки в ASP.NET проекте

Создадим новый проект типа «ASP.NET Web Application». Откроем файл Default.aspx и в любом его месте добавим литерал, поле ввода и кнопку. Например, вот так:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="ExceptionDemo._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>
<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>
        Welcome to ASP.NET, <asp:Literal ID="txtValue" runat="server"></asp:Literal>!
    </h2>

    <asp:TextBox ID="tbValue" runat="server"></asp:TextBox>
    <asp:Button ID="btnSubmit" runat="server" Text="Submit" 
        onclick="btnSubmit_Click" />
    
</asp:Content>

Создадим простейший обработчик нажатия кнопки:

namespace ExceptionDemo
{
    using System;

    public partial class _Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
        }

        protected void btnSubmit_Click(object sender, EventArgs e)
        {
            txtValue.Text = tbValue.Text;
        }
    }
}

Запустим созданное веб-приложение. В поле ввода можно ввести любое имя, которое после нажатия кнопки «Submit» появится в заголовке страницы.

Как известно, ядро ASP.NET содержит механизм контроля получаемых запросов. При подозрении в попытке XSS или другой атаки выполнение веб-приложения будет прервано исключением. Например, если ввести текст, содержащий HTML теги, то результатом будет выброс HttpRequestValidationException.

Для его обработки перейдем к Application_Error(), расположенному в файле Global.asax. Сейчас это просто пустая заготовка. Для получения экземпляра Exception с данными об произошедшем событии вызовем метод Server.GetLastError(). Затем, проверив тип исключения, обработаем его:

void Application_Error(object sender, EventArgs e)
{
    Exception ex = Server.GetLastError();

    if (ex is HttpRequestValidationException) {
        Server.Transfer("RequestError.aspx");
    }
}

Обратите внимание на использование метода Transfer(). Он не только осуществляет переадресацию на указанную страницу, но и сохраняет текущий контекст. Поэтому в коде страницы RequestError.aspx метод Server.GetLastError() вернет тот же экземпляр Exception, что позволит продолжить анализ ситуации. Чтобы указать что ошибка обработана, необходимо сбросить её вызовом метода Server.ClearError().

Осталось создать страницу RequestError.aspx с сообщение об ошибке:

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
    CodeBehind="RequestError.aspx.cs" Inherits="ExceptionDemo.RequestError" %>

<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
    <p>
        I'm sorry, but HTML tags are not allowed on that page.</p>
    <p>
        Please make sure that your entries do not contain any angle brackets like &lt; or
        &gt;.</p>
    <p>
        <a href='javascript:history.go(-1);'>Go back</a></p>
</asp:Content>

Код RequestError.aspx тогда будет следующий:

namespace ExceptionDemo
{
    using System;

    public partial class RequestError : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            Exception ex = Server.GetLastError();
 
            .........

            Server.ClearError()
        }
    }
}

Как можно заметить, теперь пользователю вводится более понятное сообщение об ошибке. Разумеется, в реальном веб-проекте здесь может быть:

  • ведение журнала;
  • разделение обработки по типам исключений;
  • более подробный анализ ситуации, приведшей к исключению;
  • локализованные сообщения об ошибке;
  • экстренное уведомление администратора сайта по электронной почте или sms, например при подозрении в атаке на сервер;
  • и т.д.

Использование Application_Error() для обработки исключений в ASP.NET MVC

Каркас ASP.NET MVC позволяет использовать этот же подход при обработке исключений. Однако, есть несколько отличий, которые существенно меняют сценарий:

Во-первых, необходимо вручную добавить метод Application_Error() в файле Global.asax.

Во-вторых, вместо метода Server.Transfer() используется метод Redirect() с указанием необходимого Контроллера и Действия в виде строки URL. Другой вариант – Response.RedirectToRoute() с последующим Server.ClearError(). В любом случае контекст будет утерян, поэтому необходимо:

  • или осуществить всю обработку в методе Application_Error(), используя Контроллер и  Представление только для уведомления пользователя;
  • или сохранить данные в хранилище и передать в Контроллер код, для их получения обратно.

Пример такого решения:

void Application_Error(object sender, System.EventArgs e)
{
    System.Exception ex = Server.GetLastError();

    if (ex is System.Web.HttpRequestValidationException) {
        // Response.Redirect("/UserProfiles/Error");

        int recordId = LogReposistory.Store(ex);

        Response.RedirectToRoute(
            "User",
            new {
                controller = "Errors",
                action = "HttpError",
                id = recordId
            });
        Server.ClearError();
    }
}

При этом Контроллер будет содержать примерно вот такой код:

namespace MVCDemo.Controllers
{
    public class ErrorsController : Controller
    {
        // GET: /Errors/HttpError/{id}
        public ActionResult HttpError (int id)
        {
            .........
            Exception ex = LogReposistory.Load(id); 
            errorProcessor.Process(ex);

            return this.View("_Error");
        }
    }
}

Такой подход крайне неудобен в применении с точки зрения MVC. Поэтому каркас ASP.NET MVC реализует возможность создания фильтров исключений. Их работа соответствует идеологи шаблона, а получаемая информация содержит дополнительные подробности. Это позволяет осуществить более детальный анализ произошедшей ситуации. С выходом 3 версии появилась возможность задавать фильтры исключений не только для Контроллеров, но и глобальные. Данная тема будет подробно рассмотрена в одной из статей серии «ASP.NET MVC 3 в деталях».

  • Download demo — 269.4 KB

Introduction

When an unhandled exception occurs in my application, I want my application to give the user a «graceful» response. Regardless of the error, I do not want the user to see an unfriendly technical error messages generated by IIS or ASP.NET. At the same time, I want to receive email notification for every unhandled exception.

This article describes a simple and comprehensive solution to this problem.

The Problem

When I have no error handling configured for my application, my users might see any one of three different error pages, depending on the type of error.

If a user requests a static resource that does not exist (for example, an HTML or JPG file), then the user sees the default HTTP error message generated by IIS:

Image 1

If a user requests a dynamic resource that does not exist (for example, an ASPX file), then the user sees the default server error message generated by ASP.NET for HTTP 404 errors:

Image 2

If an unhandled exception occurs in the application, then the user sees the default server error message generated by ASP.NET for HTTP 500 errors:

Image 3

ASP.NET web application developers sometimes call these the «Blue Screen of Death» (BSOD) and the «Yellow Screen of Death» (YSOD).

The Solution

When a user sees an error message in my application, I want the error message to match the layout and style of my application, and I want every error message to follow the same layout and style.

Step 1: Integrated Pipeline Mode

As a first step, I set my application to use an application pool that is configured for Integrated managed pipeline mode.

Image 4

Microsoft Internet Information System (IIS) version 6.0 (and previous versions) integrates ASP.NET as an ISAPI extension, alongside its own processing model for HTTP requests. In effect, this gives two separate server pipelines: one for native components and one for managed components. Managed components execute entirely within the ASP.NET ISAPI extension — and only for requests specifically mapped to ASP.NET. IIS version 7.0 and above integrates these two pipelines so that services provided by both native and managed modules apply to all requests, regardless of the HTTP handler.

For more information on Integrated Pipeline mode, refer to the following Microsoft article:

  • How to Take Advantage of the IIS 7.0 Integrated Pipeline

Step 2: Application Configuration Settings

Next, I add error pages to my application for 404 and 500 error codes, and I update the application configuration file (web.config) with settings that instruct IIS to use my custom pages for these error codes.

<system.webServer>
  <httpErrors errorMode="Custom" existingResponse="Replace">
    <remove statusCode="404"/>
    <remove statusCode="500"/>
    <error statusCode="404" responseMode="ExecuteURL"
    path="/Pages/Public/Error404.aspx"/>
    <error statusCode="500" responseMode="ExecuteURL"
    path="/Pages/Public/Error500.aspx"/>
  </httpErrors>
</system.webServer>

Now, when a user requests a static resource that does not exist, the user sees the error message generated by my custom page:

Image 5

Similarly, if a user requests a dynamic resource that does not exist, then the user sees the error message generated by my custom page:

Image 6

And finally, if an unhandled exception occurs in the application, then the user sees the error message generated by my custom page:

Image 7

Step 3: Exception Details

The first part of my problem is now solved: the error messages seen by my users are rendered inside a page that is consistent with the layout and style of my application, and the error messages themselves are consistent regardless of the underlying cause of the unhandled exception.

However, in this configuration, when an unhandled exception occurs and IIS executes Error500.aspx, the code behind this page has no details for the exception itself. When an unhandled exception occurs, I need a crash report saved to the file system on the server and sent to me by email. The crash report needs to include the exception details and a stack trace so that I can find and fix the cause of the error.

Some articles suggest that I can identify the unhandled exception using Server.GetLastError, but I get a null value whenever I attempt this.

Exception ex = HttpContext.Current.Server.GetLastError(); 

Note: I have been unable to find a clear explanation for this in Microsoft’s documentation. (Please drop me a note if you can direct me to the documentation that explains this behaviour.)

I can solve this by adding an HTTP module with an event handler for application errors. For example, after I add this code to Global.asax, my custom error page can access the current cache for the exception details.

protected void Application_Error(object sender, EventArgs e)
{
    Exception ex = HttpContext.Current.Server.GetLastError();
    CrashReport report = CrashReporter.CreateReport(ex, null);
    HttpContext.Current.Cache[Settings.Names.CrashReport] = report;
}

Image 8

It is important to note that if I add code at the end of my event handler to invoke Server.ClearError(), my custom error message is NOT displayed to the user (and neither is the default server error message). In fact, if I invoke ClearError() here, then the error message becomes a blank page, with no HTML in the output rendered to the browser. Remember, the purpose of the event handler in this configuration is to store exception details in the current cache (or in the session state) so that it is accessible to the Error500.aspx page. The purpose is NOT to handle the exception itself, and this is the reason the error is not cleared here.

Note: Referring to my earlier point, if I have not cleared the error here, because it is required in order to ensure that my custom error page is executed, then it is not obvious why the call to Server.GetLastError() in the custom error page returns a null value. If you have an explanation for this, then please post a comment.

Improving the Solution

My solution needs to write a crash report to the file system (so we have a permanent record of the event) and it needs to send an email notification (so we are immediately alerted to the event). At any given time, my company is actively developing dozens of applications for various customers, so a reusable solution is important.

Code added to Global.asax is not easily reused across multiple applications, so I created an HTTP module (i.e., a class that inherits from System.Web.IHttpModule), which I can subsequently add to a library and then reference from many different applications.

In order for this solution to work, I add the following settings to the system.webServer element in my web application configuration file (Web.config):

<modules>
    <add name="ApplicationErrorModule" type="Demo.Classes.ApplicationErrorModule" />
</modules>

The code to wireup handling for an application error is simple:

public void Init(HttpApplication application)
{
    application.Error += Application_Error;
}

private void Application_Error(Object sender, EventArgs e)
{
    if (!Settings.Enabled)
        return;

    Exception ex = HttpContext.Current.Server.GetLastError();

    if (UnhandledExceptionOccurred != null)
        UnhandledExceptionOccurred(ex);

    ExceptionOccurred(ex);
}

The code to process the exception itself is basically the same as the code I originally added to the global application event handler, but here I also add code to save the crash report to the file system, and to send a copy to me by email.

private static void ExceptionOccurred(Exception ex)
{
    
    

    HttpRequest request = HttpContext.Current.Request;
    if (Regex.IsMatch(request.Url.AbsolutePath, ErrorPagePattern))
        return;

    

    HttpResponse response = HttpContext.Current.Response;
    CrashReport report = new CrashReport(ex, null);

    
    

    if (HttpContext.Current.Cache != null)
        HttpContext.Current.Cache[Settings.Names.CrashReport] = report;

    

    String path = SaveCrashReport(report, request, null);

    

    SendEmail(report, path);

    
    

    if (!ReplaceResponse)
    {
        HttpContext.Current.Server.ClearError();

        try
        {
            response.Clear();
            response.StatusCode = 500;
            response.StatusDescription = "Server Error";
            response.TrySkipIisCustomErrors = true;
            response.Write(report.Body);
            response.End();
        }
        catch { }
    }
}

The last part of this function is especially important. If, for some reason, I forget to include the httpErrors section in my webserver configuration element, then I want the body of my crash report rendered to the browser and not the default Yellow Screen of Death (YSOD) that ASP.NET shows when a server error occurs.

Points of Interest

There are many good articles on the topic of ASP.NET application error handling, and there are many good products that are helpful in the development of solutions. Here are just a few references for more information:

  • IIS Configuration for HTTP Errors
  • Creating User-Friendly 404 Pages
  • Gracefully Responding to Unhandled Exceptions
  • Error Handling in ASP.NET
  • ASP.NET Error Logging Modules and Handlers (ELMAH) 

History

June 1, 2013: When the BaseErrorPage is loaded on a PostBack request, the response is assigned an HTTP status code of 200 (OK). This enables the «Submit Quick Error Report» feature on the error page.

June 5, 2013: Modified the code to save the crash report for an unhandled exception using a session-safe key. This corrects for the scenario in which multiple concurrent users encounter different exceptions at the same time.

Взялся осваивать ASP.NET MVC. Лучше поздно, чем никогда.

Ну и стал смотреть, как в нём обрабатывать ошибки, точнее исключения. В ASP.NET Web Forms в глобальном классе Global.asax.cs был метод Application_Error, где можно было ловить все необработанные исключения. В ASP.NET MVC это всё тоже есть, но переход на контроллеры и представления меня несколько сбил.

Статьи по теме уже есть в достаточном количестве, но, как обычно, с ходу разобраться у меня не получилось, потому я напишу, к чему пришёл, когда постиг.

Точнее, с перехватом исключений проблем не было, тут всё то же самое — выловил, записал в журнал или в базу (отправил письмо админам):

protected void Application_Error(object sender, EventArgs e)
{
    // пишем в журнал
    write2log(string.Format("Произошла неизвестная ошибка. {0}", Server.GetLastError().Message));
}

Но я захотел возвращать также свои представления, а не стандартные “жёлтые” страницы ошибок. И вот что для этого нужно было сделать.

Во-первых, в web.config‘е я ничего не правил. В некоторых статьях пишут, что нужно добавить секции обработки кастомных ошибок, но мне это не понадобилось.

Вот какие файлы/классы потребовалось изменить (и создать):

Файлы, отвечающие за обработку ошибок

Контроллер

Идея такова: в контроллере Controllers/ErrorController.cs будет несколько методов, и каждый будет возвращать какое-то своё представление:

public class ErrorController : Controller
{
    public ViewResult Index()
    {
    	// представление по умолчанию, для исключений по моей вине
        return View();
    }

    public ViewResult AccessDenied()
    {
    	// представление для кода 403
        return View("AccessDenied");
    }

    public ViewResult NotFound()
    {
    	// представление для кода 404
        return View("NotFound");
    }

    public ViewResult HttpError()
    {
    	// представление всех остальных кодов HTTP
        return View("HttpError");
    }
}

Представления

Как видно, всего у меня 4 представления, но все я описывать не буду, хватит и одного. Возьмём представление по умолчанию для исключений, выбрасываемых по моей вине — это дефолтное Views/Error/Index.cshtml. Я изменил его следующим образом (оригинал взял из ответа на вопрос How to make custom error pages work in ASP.NET MVC 4):

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Неизвестная ошибка</h2>
<p>При выполнении операции произошла непредвиденная ошибка. Сообщите администратору приложенную ниже информацию.</p>

<img src="~/images/500.png" style="margin-top:20px; margin-bottom:20px;">

@if (Model != null && HttpContext.Current.IsDebuggingEnabled)
{
    <div>
        <p>
            <b>Controller:</b> @Model.ControllerName<br />
            <b>Action:</b> @Model.ActionName<br />
            <b>Exception:</b> @Model.Exception.Message
        </p>
        <p><b>StackTrace:</b></p>
        <div style="overflow:scroll">
            <pre>@Model.Exception.StackTrace</pre>
        </div>
    </div>
}

Я знаю, что крайне не рекомендуется показывать пользователю StackTrace и прочее, но у нас ресурс внутренний, нам можно.

И да, в макете ~/Views/Shared/_Layout.cshtml у меня ещё есть разметка главной страницы (её будет видно на последнем скриншоте) — то есть возвращаемые представления будут не сами по себе страничками, а в составе общего оформления портала, и это очень круто, так как меньше стресса для пользователя :) да и вообще так приятней выглядит.

Глобальный класс приложения

В метод Application_Error класса Global.asax.cs записываю следующее (взял из статьи Exception Handling in ASP.NET MVC):

protected void Application_Error(Object sender, EventArgs e)
{
    var httpContext = ((MvcApplication)sender).Context;
    var currentController = string.Empty;
    var currentAction = string.Empty;
    var currentRouteData = RouteTable.Routes.GetRouteData(new HttpContextWrapper(httpContext));

    if (currentRouteData != null)
    {
        if (!string.IsNullOrEmpty(currentRouteData.Values["controller"].ToString()))
        {
            currentController = currentRouteData.Values["controller"].ToString();
        }

        if (!string.IsNullOrEmpty(currentRouteData.Values["action"].ToString()))
        {
            currentAction = currentRouteData.Values["action"].ToString();
        }
    }

    // пойманное исключение
    var ex = Server.GetLastError();
    
    // тут запись в мой журнал, в этой же точке можно отправлять письма админам
    logger.Error(ex.Message);
    
    // ну а дальше подготовка к вызову подходящего метода контроллера ошибок
    var controller = new ErrorController();
    var routeData = new RouteData();
    // метод по умолчанию в контроллере
    var action = "Index";

    // если это ошибки HTTP, а не моего кода, то для них свои представления
    if (ex is HttpException)
    {
        switch (((HttpException)ex).GetHttpCode())
        {
            case 403:
                action = "AccessDenied";
                break;
            case 404:
                action = "NotFound";
                break;
            default:
                action = "HttpError";
                break;
            // можно добавить свои методы контроллера для любых кодов ошибок
        }
    }

    httpContext.ClearError();
    httpContext.Response.Clear();
    httpContext.Response.StatusCode = ex is HttpException ? ((HttpException)ex).GetHttpCode() : 500;
    httpContext.Response.TrySkipIisCustomErrors = true;

    routeData.Values["controller"] = "Error";
    routeData.Values["action"] = action;

    controller.ViewData.Model = new HandleErrorInfo(ex, currentController, currentAction);
    ((IController)controller).Execute(new RequestContext(new HttpContextWrapper(httpContext), routeData));
}

Ну и всё. Теперь если где-то я пропущу исключение, оно попадёт в этот метод, он определит подходящий метод контроллера ошибок и вызовет его, а тот вернёт нужное представление.

Например, у меня есть некий контроллер Controllers/UniversalController.cs, а в нём в методе fail специально допущено деление на нуль. Если я его вызову, то мне вернётся следующее представление:

Пример кастомного представления на возникшее исключение

Introduction

No matter how proficiently you developed your application there are chances that your code may not work as expected and will generate an error at runtime. Users may enter some invalid data, mathematical calculations can go wrong, some network level fault may cause errors and more. That is why it is always a good idea to implement a robust error handling mechanism in your web application. To that end ASP.NET MVC offers several techniques that help you build such an error handling mechanism. This article discusses them with examples.

Exception Handling Techniques for ASP.NET MVC

Before we get into the actual error handling techniques offered by ASP.NET MVC, let’s quickly enumerate them here:

  • try…catch
  • Overriding OnException method
  • Using the [HandleError] attribute on actions and controllers
  • Setting a global exception handling filter
  • Handling Application_Error event

The first technique listed above is not specific to ASP.NET MVC; it is applicable to any piece of C# code. However, we will still glance over it for the sake of understanding. If you have ever developed ASP.NET Web Forms applications, you might be aware of the Page_Error event available at the page level. Since ASP.NET MVC doesn’t follow the page life cycle events as such, obviously this event is not available to your application. Something analogous is, however, available through the OnException() method. More on that later. The [HandleError] attribute is possibly the most simple way to deal with errors in an ASP.NET MVC application. This technique doesn’t involve any special controller code other than this attribute. All you need is a custom error page in the form of a View. The last couple of techniques are global level techniques that are applicable to the whole ASP.NET MVC application and not to a particular action or controller. 

Now that you know the error handling techniques available to your application, let’s discuss each of them with a code sample.

To begin with, create a new ASP.NET MVC application. Add an ADO.NET Entity Data Model for the Customers table of Northwind database to the Models folder. The following figure shows the Customer entity:

The Customer Entity
The Customer Entity

Then add the Home controller in the Controllers folder.

Using the Try…Catch Statement

To illustrate the try..catch technique, you will deliberately cause some database related exception. Add the following code in the Index() action method of the HomeController class.

public ActionResult Index()
{
  try
  {
    NorthwindEntities db = new NorthwindEntities();
    Customer obj = new Customer();
    obj.CustomerID = "ABCDEFGHIJK";
    obj.CompanyName = "Company Name 1";
    obj.ContactName = "Contact Name 1";
    obj.Country = "USA";
    db.Customers.Add(obj);
    db.SaveChanges();
  }
  catch(Exception ex)
  {
    return View("Error");
  }
  return View();
}

The above piece of code attempts to insert a Customer whose CustomerID is more than five characters long – a limit defined for the column at the database level. Obviously, at SaveChanges() an exception is thrown. The exception is handled by the catch block. The above code doesn’t handle different exceptions using different catch blocks (which you are likely to do in a real world application), rather it just handles all the possible exceptions using the generic catch block. The catch block simply returns the Error view to the browser. The Error view is intended to display a generic friendly error message to the end user. To add the Error view, create a subfolder named Shared under the Views folder and then add a View (Error.cshtml) inside the Shared folder. This way you can use the same error view for all the controllers of the application. Of course, you could have also placed it in individual view folders if you wanted. The Error view in this case contains the following markup:

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Error</title>
</head>
<body>
    <h3>Unexpected error! Please contact the Administrator.</h3>
</body>
</html>

If you run the application you will see the Error view rendered in the browser like this:

 Error View
Error View

Overriding OnException Method

The try…catch technique discussed in the previous section allows you to trap errors at code level. However, this also means that you should identify all the places in your code that can potentially throw an exception. This may not be always possible and you may want to trap errors at the whole controller level. That’s what the OnException() method allows you to do. In this technique you override the OnException() method of the Controller base class and then write the exception handling code. This method is called whenever there is an unhandled error in the controller. The following code snippet shows how OnException() can be overridden in the HomeController class.

protected override void OnException(ExceptionContext filterContext)
{
  if (!filterContext.ExceptionHandled)
  {
    string controller = filterContext.RouteData.Values["controller"].ToString();
    string action = filterContext.RouteData.Values["action"].ToString();
    Exception ex = filterContext.Exception;
    //do something with these details here
    RedirectToAction("Error", "Home");
  }
}

The OnException() method receives the filterContext parameter that gives more information about the exception. Inside, you check the ExceptionHandled property to see whether the exception has been handled already by some other part of the controller or not. If this property returns false you go ahead and grab the controller and action name that caused the exception. Notice how RouteData.Values is used to retrieve the controller name and the action name. To get the actual Exception that was thrown you use the Exception property. Although not shown in the above code, you can use these pieces of information for logging or deciding a further course of action. In this example you simply redirect the control to the Error action method so that the Error view can be sent to the browser. The Error action method looks like this:

public ActionResult Error()
{
    return View();
}

Using HandleError Attribute

The [HandleError] attribute is possibly the simplest error handling technique. It requires that you decorate either the action methods or the controller with the [HandleError] attribute and create an Error view. If you place [HandleError] on top of action methods then any unhandled exceptions raised from that action cause the Error view to be sent to the browser. By default [HandleError] assumes that you have a view named Error either in the specific Views > <controller_name> folder or inside the Shared folder. You can also customize this view name using one of the properties of the [HandleError].

The following code shows how [HandleError] can be used with action methods as well as controllers:

[HandleError]
public ActionResult Index()
{
  ...
  return View();
}
[HandleError]
public class HomeController : Controller
{
  ...
}

If you add [HandleError] to the whole controller, unhandled exceptions arising in any of its action methods are handled by it and the Error view is displayed. Obviously, if you place [HandleError] at the controller level you don’t need to place it on top of each and every action method.

One tricky thing to remember is that [HandleError] requires custom errors enabled in the web.config. So, ensure that you have the following markup inside web.config:

<customErrors mode="On"></customErrors>

Before you run the application make sure to comment out the try…catch block as well as the OnException() method you wrote earlier and then test the working of the [HandleError] attribute.

The ErrorHandlerAttribute class has ExceptionType and View properties that can be used to customize the behavior of [HandleError]. The ExceptionType property can be used to specify a specific exception type that you wish to handle rather than generic exceptions. The View property can be used to specify a view acting as an error view.

Setting HandleError Attribute as a Global Filter

In the previous example you used the [HandleError] attribute at the action or controller level. You can register the same attribute class (HandleErrorAttribute) as a global error handling filter. To do so, open Global.asax and add this code in the Application_Start event handler:

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();
    RouteConfig.RegisterRoutes(RouteTable.Routes);

    GlobalFilters.Filters.Add(new HandleErrorAttribute());
}

Here, you add HandleErrorAttribute to the GlobalFilters.Filters collection so as to set it as a global error handler. Any unhandled exception that takes place within the boundary of the MVC application will now be handled by this global error handler.

To test this global handler, comment out the [HandleError] attribute from the action or the controller and then run the application.

Handling Application_Error Event

The last exception handling technique discussed here is the Application_Error event. If you ever worked with ASP.NET Web Forms chances are you already know about this event. The Application_Error event is raised whenever  there is any unhandled exception in the application. That means an exception is not handled by any of the other techniques discussed earlier, it eventually gets bubbled up to the Application_Error event. Inside this event handler you can do tasks such as error logging and take some alternate path of execution. The following code shows how Application_Error can be added to Global.asax:

protected void Application_Error()
{
    Server.ClearError();
    Response.Redirect("/home/error");
}

The Application_Error event handler calls Server.ClearError() so as to convey to ASP.NET that the exception has been handled and that there is no longer an exception in the application. This way if you have set a custom error page in the web.config, it won’t be displayed. Then the code redirects the user to /home/error so that the Error view is displayed in the browser.

Summary

Error handling is an important consideration in any web application. ASP.NET MVC offers several error handling techniques in addition to try…catch that you can use. They include – overriding OnException() method, [HandleError] attribute, HandleErrorAttribute as a global filter and Application_Error event. Which of these techniques to use depends on the granularity of exception handling you need in an application.

  • Проблема
  • Решение
  • Сообщить администратору
  • При помощи комбинации вышеперечисленных методов
  • Альтернатива

Во время выполнения приложения ASP.NET может возникнуть исключение, которое не обрабатывается в коде приложения, т.к. от ошибок или невнимательности никто не застрахован. 🙂 Но стандартная страница ASP.NET, сообщая об ошибке, выглядит достаточно пугающе для рядового пользователя. Решением может выступить создание для нее дружественного интерфейса.

Для этого придется внести изменения в Global.asax. Данный файл содержит методы, обрабатывающие события уровня приложения и сессии. Нам нужно будет работать с методом Application_Error. Кроме того, нужно будет создать страницу, назовем ее Error.aspx, сообщающую о возникшей ошибке.

Код Global.asax.cs

Здесь возможно несколько подходов к реализации процесса оповещения об ошибке.

1. Сообщить пользователю об ошибке и предоставить информацию о ней. В данном случае код выглядит так:

protected void Application_Error(Object sender, EventArgs e)
{
        try 
        {
                //ловим последнее возникшее исключение
                Exception lastError = Server.GetLastError();

                if (lastError != null)
                {
                        //Записываем непосредственно исключение, вызвавшее данное, в
                        //Session для дальнейшего использования
                        Session["ErrorException"] = lastError.InnerException;
                }

                // Обнуление ошибки на сервере
                Server.ClearError();

                // Перенаправление на свою страницу отображения ошибки
                Response.Redirect("Error.aspx");
        }
        catch (Exception) 
        {
                // если мы всёже приходим сюда - значит обработка исключения 
                // сама сгенерировала исключение, мы ничего не делаем, чтобы
                // не создать бесконечный цикл
                Response.Write("К сожалению произошла критическая ошибка. Нажмите кнопку 'Назад' в браузере и попробуйте ещё раз. ");
        }
}

2.1. При помощи электроннной почты

protected void Application_Error(Object sender, EventArgs e)
{
        try
        {
                try 
                {
                        System.Exception ex = Server.GetLastError();
                        // Собираем необходимые данные
                        String Message = "Main Error" + "nDate & Time: " + 
                        DateTime.Now.ToString("F") + "nnURL: " + Request.Path + 
                        "nnQUERY: " + Request.QueryString + "nnMESSAGE: " +
                         ex.Message + "nnBROWSER: " + Request.Browser.Browser +
                         "nnIP Address: " + Request.UserHostAddress;

                        //Добавляем информацию о предыдущей посещенной странице
                        if(Context.Request.UrlReferrer != null)
                        {
                                Message += "nnReferer: " +
                                 Context.Request.UrlReferrer.ToString();
                        }

                        //Добавляем информацию о пользователе, в случае если успешно прошел процесс аутентификации
                        if(Context.User.Identity.IsAuthenticated)
                        {
                                Message += "nnUser: " + Context.User.Identity.Name;
                        }

                        Message += "nnnnEXCEPTION: " + ex.ToString();
                        System.Web.Mail.MailMessage mail = new System.Web.Mail.MailMessage();
                        mail.To = "[e-mail адрес администратора]";
                        mail.Subject = "Error in the Site";
                        mail.Priority = System.Web.Mail.MailPriority.High; 
                        mail.BodyFormat = System.Web.Mail.MailFormat.Text;
                        mail.Body = Message;
                        // Здесь необходимо указать используемый SMTP сервер
                        System.Web.Mail.SmtpMail.SmtpServer="[адрес SMTP сервера]";
                        System.Web.Mail.SmtpMail.Send(mail);
                } 
                catch {}
                // Обнуление ошибки на сервере
                Server.ClearError();
                // Перенаправление на статическую html страницу, сообщающую об ошибке
                // никаких данных об произошедшей ошибке ей не передается
                Response.Redirect("Error.html");
        }
        catch
        {
                // если мы всёже приходим сюда - значит обработка исключения 
                // сама сгенерировала исключение, мы ничего не делаем, чтобы
                // не создать бесконечный цикл
                Response.Write("К сожалению произошла критическая ошибка. Нажмите кнопку 'Назад' в браузере и попробуйте ещё раз. ");
        }
}

2.2. При помощи записи сообщения в журнал событий Windows

protected void Application_Error(Object sender, EventArgs e)
{
        try
        {
                // Наименование ресурса, вызвавшего ошибку
                string EventSourceName = "ErrorSample";
                // Наименование LogView
                string logName = "Application";
                
                System.Exception ex = Server.GetLastError();

                System.Diagnostics.EventLog Log = new System.Diagnostics.EventLog(logName);
                Log.Source = EventSourceName;
                Log.WriteEntry(ex.InnerException.Message, System.Diagnostics.EventLogEntryType.Error);        
        
                // Обнуление ошибки на сервере
                Server.ClearError();
                // Перенаправление на статическую html страницу, сообщающую об ошибке
                // никаких данных об произошедшей ошибке ей не передается
                Response.Redirect("Error.html");
        }
        catch (Exception ex)
        {
                // если мы всёже приходим сюда - значит обработка исключения 
                // сама сгенерировала исключение, мы ничего не делаем, чтобы
                // не создать бесконечный цикл
                Response.Write("К сожалению произошла критическая ошибка. Нажмите кнопку 'Назад' в браузере и попробуйте ещё раз. ");
        }
}

Здесь следует заметить, что зачастую приложение ASP.NET имеет достаточно ограниченный набор прав (что, в принципе, правильно с точки зрения безопасности). В связи с этим мы не сможем программно создать Event Source или проверить его существование в журнале событий Windows, если не будем использовать имперсонацию (impersonate). Решением может выступать ручное создание Event Source. Для этого внесем в реестр новый ключ, воспользовавшись программой regedit.

Нам необходимо добавить новый ключ по адресу

HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesEventlogApplication.

Его имя должно совпадать с указанным в коде. В нашем случае это ErrorSample.

Страница Error.aspx

Страница Error.aspx, как уже говорилось выше, должна непосредственно выводить сообщение об ошибке. Для этого добавим на нее Label и назовем его lblMessage. Обработку исключения (напомню, что мы поместили его в сессию), будем производить в методе Page_Load. Его текст приведен ниже.

private void Page_Load(object sender, System.EventArgs e)
{
        try 
        {
                // возьмем информацию об исключении из сессии
                Exception exc=(Exception)Session["ErrorException"];
                string errorMsg = exc.Message;
                string pageErrorOccured = Context.Request.UrlReferrer.ToString();
                string exceptionType = exc.GetType().ToString();
                string stackTrace = exc.StackTrace;
                
                // очистим переменную сессии
                Session["ErrorException"] = null;

                //отобразим пользователю общее сообщение об ошибке
                lblMessage.Text = " К сожалению, произошла ошибка выполнения приложения.<br/><br/>";
                lblMessage.Text =String.Format("{0} Чтобы попробовать ещё раз,
                кликните <a href='{1}'>здесь</a>.<br/><br/>",lblMessage.Text,
                pageErrorOccured);

                //добавим конкретное сообщение об
                lblMessage.Text = lblMessage.Text +
                "Error Message: " + errorMsg +"n"+
                "Page Error Occurred: " + pageErrorOccured + "n"+
                "ExceptionType: " + exceptionType +"n"+
                "Stack Trace: " + stackTrace;
        }
        catch (Exception ex) 
        {
                //если исключение вызвано кодом, написанным выше
                //выведем сообщение об ошибке и StackTrace
                lblMessage.Text = ex.Message+" "+ex.StackTrace;
        }
}

Следует заметить, что намного эффективнее будет использовать один из параметров файла web.config. Это позволит быстро (без изменения кода), менять ссылку страницы с сообщениями об ошибке или вообще убрать ее, в случае необходимости. Сссылка на нее задается с помощью атрибута defaultRedirect. Кроме того, при использовании данного атрибута в коде, следует убрать строки

// Обнуление ошибки на сервере
Server.ClearError();

А необходимость в следующих строках просто теряется, так как перенаправление теперь происходит автоматически.

// Перенаправление на страницу сообщающую об ошибке
Response.Redirect("Error.aspx");

Пример настройки web.config:

<customErrors mode="On" defaultRedirect="error.aspx"/>

Так же, сужествует дополнительная возможность: перенаправление на определенную страницу в зависимости от кода HTTP ошибки. Это позволяет делать параметр “error“. Его атрибут statusCode задает код ошибки, а redirect задает страницу, на которую следует перенаправить пользователя. Например, в случае, если запрашиваемый ресурс не найден (код ошибки 404), перенаправим пользователя на страницу Error404.html, оповещающая пользователя о случившемся происшествии. Web.config будет выглядить так:

<customErrors mode="On" defaultRedirect="error.aspx">
  <error statusCode="404" redirect="Error404.html"/>
</customErrors>

This is the eighth of a new series of posts on ASP .NET Core 3.1 for 2020. In this series, we’ll cover 26 topics over a span of 26 weeks from January through June 2020, titled ASP .NET Core A-Z! To differentiate from the 2019 series, the 2020 series will mostly focus on a growing single codebase (NetLearner!) instead of new unrelated code snippets week.

Previous post:

  • Generic Host Builder in ASP .NET Core 3.1

NetLearner on GitHub :

  • Repository: https://github.com/shahedc/NetLearnerApp
  • v0.8-alpha release: https://github.com/shahedc/NetLearnerApp/releases/tag/v0.8-alpha

In this Article:

  • H is for Handling Errors
  • Exceptions with Try-Catch-Finally
  • Try-Catch-Finally in NetLearner
  • Error Handling for MVC
  • Error Handling for Razor Pages
  • Error Handling in Blazor
  • Logging Errors
  • Transient Fault Handling
  • References

H is for Handling Errors

Unless you’re perfect 100% of the time (who is?), you’ll most likely have errors in your code. If your code doesn’t build due to compilation errors, you can probably correct that by fixing the offending code. But if your application encounters runtime errors while it’s being used, you may not be able to anticipate every possible scenario.

Runtime errors may cause Exceptions, which can be caught and handled in many programming languages. Unit tests will help you write better code, minimize errors and create new features with confidence. In the meantime, there’s the good ol’ try-catch-finally block, which should be familiar to most developers.

NOTE: You may skip to the next section below if you don’t need this refresher.

try-catch-finally in C#

Exceptions with Try-Catch-Finally

The simplest form of a try-catch block looks something like this:

**try** { _ **// try something here** _} **catch** ( **Exception** ex){ _ **// catch an exception here** _}

You can chain multiple catch blocks, starting with more specific exceptions. This allows you to catch more generic exceptions toward the end of your try-catch code. In a string of _ catch _() blocks, only the caught exception (if any) will cause that block of code to run.

**try** { _ **// try something here** _} **catch** ( **IOException** ioex){ _ **// catch specific exception, e.g. IOException** _} **catch (Exception** ex){ _ **// catch generic exception here** _}

Finally, you can add the optional _ finally _block. Whether or not an exception has occurred, the finally block will always be executed.

**try** { _ **// try something here** _} **catch** ( **IOException** ioex){ _ **// catch specific exception, e.g. IOException** _} **catch (Exception** ex){ _ **// catch generic exception here** _} **finally** { _ **// always run this code** _}

Try-Catch-Finally in NetLearner

In the NetLearner sample app, the LearningResourcesController uses a LearningResourceService from a shared .NET Standard Library to handle database updates. In the overloaded Edit() method, it wraps a call to the Update() method from the service class to catch a possible DbUpdateConcurrencyException exception. First, it checks to see whether the ModelState is valid or not.

if (ModelState.IsValid){ **...** }

Then, it tries to update user-submitted data by passing a LearningResource object. If a DbUpdateConcurrencyException exception occurs, there is a check to verify whether the current LearningResource even exists or not, so that a 404 (NotFound) can be returned if necessary.

**try** { await _learningResourceService.Update(learningResource);} **catch** ( **DbUpdateConcurrencyException** ){ if (!LearningResourceExists(learningResource.Id)) { return NotFound(); } else { **throw** ; }}return RedirectToAction(nameof(Index));

In the above code, you can also see a throw keyword by itself, when the expected exception occurs if the Id is found to exist. In this case, the throw statement (followed immediately by a semicolon) ensures that the exception is rethrown, while preserving the stack trace.

Run the MVC app and navigate to the Learning Resources page after adding some items. If there are no errors, you should see just the items retrieved from the database.

Learning Resources UI (no errors)

If an exception occurs in the Update() method, this will cause the expected exception to be thrown. To simulate this exception, you can intentionally throw the exception inside the update method. This should cause the error to be displayed in the web UI when attempting to save a learning resource.

Exception details in Web UI

Error Handling for MVC

In ASP .NET Core MVC web apps, unhandled exceptions are typically handled in different ways, depending on whatever environment the app is running in. The default template uses the DeveloperExceptionPage middleware in a development environment but redirects to a shared Error view in non-development scenarios. This logic is implemented in the Configure () method of the Startup.cs class.

if (env. **IsDevelopment** ()){ app. **UseDeveloperExceptionPage** (); app. **UseDatabaseErrorPage** (); }else{ **app.UseExceptionHandler("/Home/Error");** _ **...** _}

The DeveloperExceptionPage middleware can be further customized with DeveloperExceptionPageOptions properties, such as FileProvider and SourceCodeLineCount.

var options = new **DeveloperExceptionPageOptions** { **SourceCodeLineCount** = 2}; app.UseDeveloperExceptionPage(options); 

Using the snippet shown above, the error page will show the offending line in red, with a variable number of lines of code above it. The number of lines is determined by the value of SourceCodeLineCount , which is set to 2 in this case. In this contrived example, I’m forcing the exception by throwing a new Exception in my code.

Customized lines of code within error page

For non-dev scenarios, the shared Error view can be further customized by updating the Error.cshtml view in the Shared subfolder. The ErrorViewModel has a ShowRequestId boolean value that can be set to true to see the RequestId value.

@model **ErrorViewModel** @{ ViewData["Title"] = "Error";}<h1 class="text-danger">Error.</h1><h2 class="text-danger">An error occurred while processing your request.</h2>@if (Model. **ShowRequestId** ){<p><strong>Request ID:</strong> <code>@Model. **RequestId** </code></p>}<h3>header content</h3><p>text content</p> 

In the MVC project’s Home Controller, the Error () action method sets the RequestId to the current Activity.Current.Id if available or else it uses HttpContext.TraceIdentifier. These values can be useful during debugging.

[**ResponseCache** (Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]public IActionResult **Error** (){ return View(new **ErrorViewModel** { **RequestId** = Activity.Current?.Id ?? HttpContext.TraceIdentifier });} 

But wait… what about Web API in ASP .NET Core? After posting the 2019 versions of this article in a popular ASP .NET Core group on Facebook, I got some valuable feedback from the admin:

Dmitry Pavlov: “For APIs there is a nice option to handle errors globally with the custom middleware https://code-maze.com/global-error-handling-aspnetcore – helps to get rid of try/catch-es in your code. Could be used together with FluentValidation and MediatR – you can configure mapping specific exception types to appropriate status codes (400 bad response, 404 not found, and so on to make it more user friendly and avoid using 500 for everything).”

For more information on the aforementioned items, check out the following resources:

  • Global Error Handling in ASP.NET Core Web API: https://code-maze.com/global-error-handling-aspnetcore/
  • FluentValidation • ASP.NET Integration: https://fluentvalidation.net/aspnet
  • MediatR Wiki: https://github.com/jbogard/MediatR/wiki
  • Using MediatR in ASPNET Core Apps: https://ardalis.com/using-mediatr-in-aspnet-core-apps

Later on in this series, we’ll cover ASP .NET Core Web API in more detail, when we get to “W is for Web API”. Stay tuned!

Error Handling for Razor Pages

Since Razor Pages still use the MVC middleware pipeline, the exception handling is similar to the scenarios described above. For starters, here’s what the Configure () method looks like in the Startup.cs file for the Razor Pages web app sample.

if (env. **IsDevelopment** ()){ app. **UseDeveloperExceptionPage** (); app. **UseDatabaseErrorPage** (); }else{ app. **UseExceptionHandler** ("/Error"); ...}

In the above code, you can see the that development environment uses the same DeveloperExceptionPage middleware. This can be customized using the same techniques outlined in the previous section for MVC pages, so we won’t go over this again.

As for the non-dev scenario, the exception handler is slightly different for Razor Pages. Instead of pointing to the Home controller’s Error () action method (as the MVC version does), it points to the to the / Error page route. This Error.cshtml Razor Page found in the root level of the Pages folder.

@ **page** @model **ErrorModel** @{ ViewData["Title"] = "Error";}<h1 class="text-danger">Error.</h1><h2 class="text-danger">An error occurred while processing your request.</h2>@if (Model. **ShowRequestId** ){ <p> <strong>Request ID:</strong> <code>@Model. **RequestId** </code> </p>}<h3>custom header text</h3><p>custom body text</p>

The above Error page for looks almost identical to the Error view we saw in the previous section, with some notable differences:

  • @ page directive (required for Razor Pages, no equivalent for MVC view)
  • uses ErrorModel (associated with Error page) instead of ErrorViewModel (served by Home controller’s action method)

Error Handling for Blazor

In Blazor web apps, the UI for error handling is included in the Blazor project templates. Consequently, this UI is available in the NetLearner Blazor sample as well. The _Host.cshtml file in the Pages folder holds the following < environment > elements:

<div id="blazor-error-ui"> < **environment** include="Staging,Production"> An error has occurred. This application may no longer respond until reloaded. </ **environment** > < **environment** include="Development"> An unhandled exception has occurred. See browser dev tools for details. </ **environment** > <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div>

The div identified by the id “blazor-error-ui” ensures that is hidden by default, but only shown when an error has occured. Server-side Blazor maintains a connection to the end-user by preserving state with the use of a so-called circuit.

Each browser/tab instance initiates a new circuit. An unhandled exception may terminate a circuit. In this case, the user will have to reload their browser/tab to establish a new circuit.

According to the official documentation, unhandled exceptions may occur in the following areas:

  1. Component instantiation : when constructor invoked
  2. Lifecycle methods : (see Blazor post for details)
  3. Upon rendering : during BuildRenderTree() in .razor component
  4. UI Events : e.g. onclick events, data binding on UI elements
  5. During disposal : while component’s .Dispose() method is called
  6. JavaScript Interop : during calls to IJSRuntime.InvokeAsync
  7. Prerendering : when using the Component tag helper

To avoid unhandled exceptions, use try-catch exception handlers within .razor components to display error messages that are visible to the user. For more details on various scenarios, check out the official documentation at:

  • Handle errors in ASP.NET Core Blazor apps: https://docs.microsoft.com/en-us/aspnet/core/blazor/handle-errors?view=aspnetcore-3.1

Logging Errors

To log errors in ASP .NET Core, you can use the built-in logging features or 3rd-party logging providers. In ASP .NET Core 2.x, the use of CreateDefaultBuilder () in Program.cs takes of care default Logging setup and configuration (behind the scenes).

public static IWebHostBuilder **CreateWebHostBuilder** (string[] args) => WebHost. **CreateDefaultBuilder** (args) .UseStartup<Startup>();

The Web Host Builder was replaced by the Generic Host Builder in ASP .NET Core 3.0, so it looks slightly different now. For more information on Generic Host Builder, take a look at the previous blog post in this series: Generic Host Builder in ASP .NET Core.

public static IHostBuilder **CreateHostBuilder** (string[] args) => Host. **CreateDefaultBuilder** (args) . **ConfigureWebHostDefaults** (webBuilder => { webBuilder. **UseStartup** (); });

The host can be used to set up logging configuration, e.g.:

public static IHostBuilder **CreateHostBuilder** (string[] args) => Host. **CreateDefaultBuilder** (args) . **ConfigureLogging** ((hostingContext, logging) => { logging.ClearProviders(); logging.AddConsole(options => options.IncludeScopes = true); logging.AddDebug(); }) . **ConfigureWebHostDefaults** (webBuilder => { webBuilder. **UseStartup** (); });

To make use of error logging (in addition to other types of logging) in your web app, you may call the necessary methods in your controller’s action methods or equivalent. Here, you can log various levels of information, warnings and errors at various severity levels.

As seen in the snippet below, you have to do the following in your _ MVC Controller _ that you want to add Logging to:

  1. Add using statement for Logging namespace
  2. Add a private readonly variable for an ILogger object
  3. Inject an ILogger object into the constructor
  4. Assign the private variable to the injected variable
  5. Call various log logger methods as needed.
... **using Microsoft.Extensions.Logging;** public class **MyController** : Controller{ ... **private**  **readonly ILogger _logger;** public **MyController** (..., ILogger< **MyController** > logger) { ... **_logger = logger;** } public IActionResult MyAction(...) { _logger. **LogTrace** ("log trace"); _logger. **LogDebug** ("log debug"); _logger. **LogInformation** ("log info"); _logger. **LogWarning** ("log warning"); _logger. **LogError** ("log error"); _logger. **LogCritical** ("log critical"); }}

In Razor Pages, the logging code will go into the Page’s corresponding Model class. As seen in the snippet below, you have to do the following to the _ Model class that corresponds to a Razor Page _:

  1. Add using statement for Logging namespace
  2. Add a private readonly variable for an ILogger object
  3. Inject an ILogger object into the constructor
  4. Assign the private variable to the injected variable
  5. Call various log logger methods as needed.
... **using Microsoft.Extensions.Logging;** public class **MyPageModel** : PageModel{ ... **private readonly ILogger _logger;** public **MyPageModel** (..., **ILogger<MyPageModel> logger** ) { ... **_logger = logger;** } ... public void **MyPageMethod** () { ... _logger. **LogInformation** ("log info"); _logger. **LogError** ("log error"); ... }} 

You may have noticed that Steps 1 through 5 are pretty much identical for MVC and Razor Pages. This makes it very easy to quickly add all sorts of logging into your application, including error logging.

Transient fault handling

Although it’s beyond the scope of this article, it’s worth mentioning that you can avoid transient faults (e.g. temporary database connection losses) by using some proven patterns, practices and existing libraries. To get some history on transient faults, check out the following article from the classic “patterns & practices”. It describes the so-called “Transient Fault Handling Application Block”.

  • Classic Patterns & Practices: https://docs.microsoft.com/en-us/previous-versions/msp-n-p/dn440719(v=pandp.60)

More recently, check out the docs on Transient Fault Handling:

  • Docs: https://docs.microsoft.com/en-us/aspnet/aspnet/overview/developing-apps-with-windows-azure/building-real-world-cloud-apps-with-windows-azure/transient-fault-handling

And now in .NET Core, you can add resilience and transient fault handling to your .NET Core HttpClient with Polly!

  • Adding Resilience and Transient Fault handling to your .NET Core HttpClient with Polly: https://www.hanselman.com/blog/AddingResilienceAndTransientFaultHandlingToYourNETCoreHttpClientWithPolly.aspx
  • Integrating with Polly for transient fault handling: https://www.stevejgordon.co.uk/httpclientfactory-using-polly-for-transient-fault-handling
  • Using Polly for .NET Resilience with .NET Core: https://www.telerik.com/blogs/using-polly-for-net-resilience-and-transient-fault-handling-with-net-core

You can get more information on the Polly project on the official Github page:

  • Polly on Github: https://github.com/App-vNext/Polly

References

  • try-catch-finally: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/try-catch-finally
  • Handle errors in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling
  • Use multiple environments in ASP.NET Core: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments
  • UseDeveloperExceptionPage: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageextensions.usedeveloperexceptionpage
  • DeveloperExceptionPageOptions: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.developerexceptionpageoptions
  • Logging: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging
  • Handle errors in ASP.NET Core Blazor apps: https://docs.microsoft.com/en-us/aspnet/core/blazor/handle-errors?view=aspnetcore-3.1

Понравилась статья? Поделить с друзьями:
  • Asp mvc error 500
  • Asmmap64 sys как исправить ошибку
  • Asmb8 ikvm java error
  • Asm syntax error
  • Asko ошибка f10 посудомойка