Exception Handling (3), In ASP.NET Core MVC

Exception handling is required in any application. It is a very interesing issue where different apps have their own various way(s) to handle that. I plan to write a series of articles to discuss this issue

In this article, we will be discussing various ways of handling an exception in ASP.NET Core MVC.

Introduction

In Part I of this article seriers, we discussed Exception handling for ASP.NET MVC, where we may have three ways to handle exceptions,

For ASP.NET Core MVC, we have similar situation or discussion, but, with major differences:

  1. We will not discuss the Try-Catch-Finally approach, because it is language related issue;
  2. Due to Exception Filter, the approach is just secondary importance in ASP.NET Core app, we will just make brief discussion at the end.

This will be the order inwhich we will discuss the topic today:

  • A: Exception Handling in Development Environment for ASP.NET Core MVC
    • UseDeveloperExceptionPage
  • B: Exception Handling in Production Environment for ASP.NET Core MVC
    • Approach 1: UseExceptionHandler
      • 1: Exception Handler Page
      • 2: Exception Handler Lambda
    • Approach 2: UseStatusCodePages
      • 1: UseStatusCodePages, and with format string, and with Lambda
      • 2: UseStatusCodePagesWithRedirects
      • 3: UseStatusCodePagesWithReExecute
    • Approach 3: Exception Filter
      • Local
      • Global

A: Exception Handling in Developer Environment

The ASP.NET Core starup templates generate the following code,

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
 {  
     if (env.IsDevelopment())  
     {  
         app.UseDeveloperExceptionPage();  
     }  
     else 
     ......
 } 

The UseDeveloperExceptionPage extension method adds middleware into the request pipeline. The Developer Exception Page displays developer friendly detailed information about request exceptions. This helps developers in tracing errors that occur during development phase.

As this middleware displays sensitive information, it is advisable to add it only in development environment. The developer environment is a new feature in .NET Core. We will demostrate this below.

Step 1 - Create an ASP.NET Core MVC application

We use the current version of Visual Studio 2019 16.8 and .NET 5.0 SDK to build the app.

  1. Start Visual Studio and select Create a new project.
  2. In the Create a new project dialog, select ASP.NET Core Web Application > Next.
  3. In the Configure your new project dialog, enter ErrorHandlingSample for Project name.
  4. Select Create.
  5. In the Create a new ASP.NET Core web application dialog, select,
    1. .NET Core and ASP.NET Core 5.0 in the dropdowns.
    2. ASP.NET Core Web App (Model-View-Controller).
    3. Create

Step 2 - Change code in Home Controller

Replace the Index method in the HomeController with the code below:

public IActionResult Index(int? id = null)  
{  
    if (id.HasValue)  
    {  
        if (id == 1)  
        {  
            throw new FileNotFoundException("File not found exception thrown in index.chtml");  
        }  
        else if (id == 2)  
        {  
            return StatusCode(500);  
        }  
    }  
    return View();  
} 

Step 3 - Change code in Index view

Add the code in the bottom of Home/Index view, i.e., the file Index.cshtml in Views/home directory, 

<br />  
  
<div class="text-left">  
    <p>  
        <a href="/NoSuchPage">  
            Request an endpoint that doesn't exist. Trigger a 404  
        </a>.  
    </p>  
    <p><a href="/home/index/1">Trigger an exceptionn</a>.</p>  
    <p><a href="/home/index/2">Return a 500 error.</a>.</p>  
</div> 

Step 4 - Run app and Test

Run the app,

Click "Trigger an exception." you will get,

This is the Developer Exception Page that includes the following information about the exception and the request,

  • Stack trace
  • Query string parameters if any
  • Cookies if any
  • Headers
  • Routing

For examples: Headers, and Routing

B: Exception Handling in Production Environment

ASP.NET Core configures app behavior based on the runtime environment that is determined in launchSettings.json file:

Note

The launchSettings.json file:

  • Is only used on the local development machine.
  • Is not deployed.
  • contains profile settings.

Now, we switch environment from Development to Production,

{  
  "iisSettings": {  
    "windowsAuthentication": false,  
    "anonymousAuthentication": true,  
    "iisExpress": {  
      "applicationUrl": "http://localhost:50957",  
      "sslPort": 44362  
    }  
  },  
  "profiles": {  
    "IIS Express": {  
      "commandName": "IISExpress",  
      "launchBrowser": true,  
      "environmentVariables": {  
        //"ASPNETCORE_ENVIRONMENT": "Development",  
        "ASPNETCORE_ENVIRONMENT": "Production"  
      }  
    },  
    "ErrorHandlingSample": {  
      "commandName": "Project",  
      "dotnetRunMessages": "true",  
      "launchBrowser": true,  
      "applicationUrl": "https://localhost:5001;http://localhost:5000",  
      "environmentVariables": {  
        //"ASPNETCORE_ENVIRONMENT": "Development",  
        "ASPNETCORE_ENVIRONMENT": "Production"  
      }  
    }  
  }  
} 

Approach 1: UseExceptionHandler

1: Exception Handler Page

For Production environment, startup file Configure method tells us: ASP.NET Core handles exception by calling UseExceptionHandler,

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler("/Home/Error");  
    } 
    ......
} 

Run the app, and Click Trigger an exception link in the home page, we got the Exception Handler Page, and by default Home/Error.cshtml.cs generated by the ASP.NET Core templates,

This exception handling middleware,

  • Catches and logs exceptions.
  • Re-executes the request in an alternate pipeline using the path indicated. The request isn't re-executed if the response has started. The template generated code re-executes the request using the /Home/Error path.

2: Exception Handler Lambda

An alternative to a custom exception handler page is to provide a lambda to UseExceptionHandler. Using a lambda allows access to the error before returning the response.

The following code uses a lambda for exception handling (startup file):

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler(errorApp =>  
        {  
            errorApp.Run(async context =>  
            {  
                context.Response.StatusCode = 500;  
                context.Response.ContentType = "text/html";  
  
                await context.Response.WriteAsync("<html lang=\"en\"><body>\r\n");  
                await context.Response.WriteAsync("ERROR!<br><br>\r\n");  
  
                var exceptionHandlerPathFeature =  
                    context.Features.Get<IExceptionHandlerPathFeature>();  
  
                if (exceptionHandlerPathFeature?.Error is FileNotFoundException)  
                {  
                    await context.Response.WriteAsync(  
                                              "File error thrown!<br><br>\r\n");  
                }  
  
                await context.Response.WriteAsync(  
                                              "<a href=\"/\">Home</a><br>\r\n");  
                await context.Response.WriteAsync("</body></html>\r\n");  
                await context.Response.WriteAsync(new string(' ', 512));   
            });  
        });  
        app.UseHsts();  
    }  
  
    app.UseHttpsRedirection();  
    app.UseStaticFiles();  
  
    app.UseRouting();  
  
    app.UseAuthorization();  
  
    app.UseEndpoints(endpoints =>  
    {  
        endpoints.MapRazorPages();  
    });  
} 

We got the result,

Note

For convenience, we keep the original startup file as startup.cs file, and make a new startup file with a class name and file name as startupLambda, the highlighted one in the graph below,

and in Program.cs, comment out Startup class, replace it by startupLambda class, like this,

public class Program  
{  
    public static void Main(string[] args)  
    {  
        CreateHostBuilder(args).Build().Run();  
    }  
  
    public static IHostBuilder CreateHostBuilder(string[] args) =>  
        Host.CreateDefaultBuilder(args)  
            .ConfigureWebHostDefaults(webBuilder =>  
            {  
                //webBuilder.UseStartup<Startup>();  
                webBuilder.UseStartup<StartupLambda>();  
                //webBuilder.UseStartup<StartupUseStatusCodePages>();  
                //webBuilder.UseStartup<StartupStatusLambda>();  
                //webBuilder.UseStartup<StartupFormat>();  
                //webBuilder.UseStartup<StartupSCredirect>();  
                //webBuilder.UseStartup<StartupSCreX>();  
            });  
} 

With similarity, we create several new startup classes as in the above graph, we will define them and use them in later discussions.

Approach 2: UseStatusCodePages

The two techniques discussed so far deal with the unhandled exceptions arising from code. However, that's not the only source of errors. Many times errors are generated due to internal server errors, non existent pages, web server authorization issues and so on. These errors are reflected by the HTTP status codes such as 500, 404 and 401.

By default, an ASP.NET Core app doesn't provide a status code page for HTTP error status codes, such as 404 - Not Found. When the app encounters an HTTP 400-599 error status code that doesn't have a body, it returns the status code and an empty response body.

Click the link: Request an endpoint that doesn't exist. Trigger a 404 below,

We will get,

Whie clicking Run a 500 error,

To deal with such errors we can use UseStatusCodePages() method (status code pages middleware) to provide status code pages.

1: Default UseStatusCodePages, or with format string, or with Lambda

To enable default text-only handlers for common error status codes, call UseStatusCodePages in the Startup.Configure method:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler("/Home/Error");  
        app.UseHsts();  
    }  
  
    app.UseStatusCodePages();  
  
    ......
}  

We make this file (class) name as startupUseStatusCodePages, and  Remove the comments from webBuilder.UseStartup<StartupUseStatusCodePages>(); in Program.cs.

Run the app, the resullt will be:

for 404 error, and below for 500 error:

 

Again, we make startup file named as StartupFormat

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
 {  
     if (env.IsDevelopment())  
     {  
         app.UseDeveloperExceptionPage();  
     }  
     else  
     {  
         app.UseExceptionHandler("/Home/Error");  
         app.UseHsts();  
     }  
  
     app.UseStatusCodePages(  
         "text/plain", "Status code page, status code: {0}");  
     ...... 
 } 

Remove the comments from webBuilder.UseStartup<StartupFormat>(); in Program.cs. Run the app, the resullt will be shown:


and

The same, to make startup file named as StartupStatusLambda,

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
 {  
     if (env.IsDevelopment())  
     {  
         app.UseDeveloperExceptionPage();  
     }  
     else  
     {  
         app.UseExceptionHandler("/Home/Error");  
         app.UseHsts();  
     }  
  
     app.UseStatusCodePages(async context =>  
     {  
         context.HttpContext.Response.ContentType = "text/plain";  
  
         await context.HttpContext.Response.WriteAsync(  
             "Status code lambda, status code: " +  
             context.HttpContext.Response.StatusCode);  
     });  
     ...... 
 } 

Remove the comments from webBuilder.UseStartup<StartupStatusLambda>(); in Program.cs. Run the app, the resullt will be shown:


and

Note

UseStatusCodePages isn't typically used in production because it returns a message that isn't useful to users.

2: UseStatusCodePagesWithRedirects

The UseStatusCodePagesWithRedirects extension method:

  • Sends a status code to the client.
  • Redirects the client to the error handling endpoint provided in the URL template. The error handling endpoint typically displays error information and returns HTTP 200. 

Implementation

Step 1: Set up Startup file

Make startup file named as StartupSCredirect,

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler("/Home/Error");  
        app.UseHsts();  
    }  
  
    app.UseStatusCodePagesWithRedirects("/Home/MyStatusCode?code={0}");  
    ...... 
} 

Remove the comments from webBuilder.UseStartup<StartupSCredirect>(); in Program.cs.

Step 2:  Add an Action method in HomeController,

public IActionResult MyStatusCode(int code)  
{  
    if (code == 404)  
    {  
        ViewBag.ErrorMessage = "The requested page not found.";  
    }  
    else if (code == 500)  
    {  
        ViewBag.ErrorMessage = "My custom 500 error message.";  
    }  
    else  
    {  
        ViewBag.ErrorMessage = "An error occurred while processing your request.";  
    }  
  
    ViewBag.RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;  
    ViewBag.ShowRequestId = !string.IsNullOrEmpty(ViewBag.RequestId);  
    ViewBag.ErrorStatusCode = code;  
  
    return View();  
} 

Step 3

Create a view for the Action: View/Home/MyStatusCode.cshtml

@{   
    Layout = null;  // clean up F12 tool network tab   
}  
  
@{ ViewData["Title"] = "Status Code @ViewBag.ErrorStatusCode"; }  
<head>  
    <!-- prevent favicon.ico from being requested. -->  
    <link rel="icon" href="data:,">  
</head>  
  
<h1>MyStatusCode page</h1>  
<h2 class="text-danger">Status Code: @ViewBag.ErrorStatusCode</h2>  
<h2 class="text-danger"> @ViewBag.ErrorMessage</h2>  
  
@if (ViewBag.ShowRequestId)  
{   
<h3>Request ID</h3>  
                <p>  
                    <code>@ViewBag.RequestId</code>  
                </p>  
} 

Run the app, click either 400 or 500 errors, we got (for Error Code 400):

Note

The link is redirected to a new link that is endpoint provided.

3: UseStatusCodePagesWithReExecute

The UseStatusCodePagesWithReExecute extension method:

  • Returns the original status code to the client.
  • Generates the response body by re-executing the request pipeline using an alternate path.

Implementation

Step 1: Set up Startup file

Make startup file named as StartupSCreX:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)  
{  
    if (env.IsDevelopment())  
    {  
        app.UseDeveloperExceptionPage();  
    }  
    else  
    {  
        app.UseExceptionHandler("/Home/Error");  
        app.UseHsts();  
    }  
  
    app.UseStatusCodePagesWithReExecute("/Home/MyStatusCode2", "?code={0}");
    ...... 
} 

Remove the comments from webBuilder.UseStartup<StartupSCreX>(); in Program.cs.

Step 2:  Add an Action method in HomeController, 

public IActionResult MyStatusCode2(int code)  
{  
  
    var statusCodeReExecuteFeature = HttpContext.Features.Get<  
                                           IStatusCodeReExecuteFeature>();  
    if (statusCodeReExecuteFeature != null)  
    {  
        ViewBag.OriginalURL =  
            statusCodeReExecuteFeature.OriginalPathBase  
            + statusCodeReExecuteFeature.OriginalPath  
            + statusCodeReExecuteFeature.OriginalQueryString;  
    }  
  
    ViewBag.RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;  
    ViewBag.ShowRequestId = !string.IsNullOrEmpty(ViewBag.RequestId);  
    ViewBag.ShowOriginalURL = !string.IsNullOrEmpty(ViewBag.OriginalURL);  
    ViewBag.ErrorStatusCode = code;  
  
    return View();  
} 

Step 3

Create a view for the Action: View/Home/MyStatusCode2.cshtml 

@{  
    Layout = null;  // clean up F12 tool network tab  
}  
  
@{ ViewData["Title"] = "Status Code @ViewBag.ErrorStatusCode"; }  
  
<head>  
    <!-- prevent favicon.ico from being requested. -->  
    <link rel="icon" href="data:,">  
</head>  
  
<h1 class="text-danger">Status Code: @ViewBag.ErrorStatusCode</h1>  
<h2 class="text-danger">An error occurred while processing your request.</h2>  
  
@if (ViewBag.ShowRequestId)  
{  
<h3>Request ID</h3>  
                <p>  
                    <code>@ViewBag.RequestId</code>  
                </p>}  
  
@if (ViewBag.ShowOriginalURL)  
{  
<h3>Original URL</h3>  
                <p>  
                    <code>@ViewBag.OriginalURL</code>  
                </p>} 

Run the app, click either 400 or 500 errors, we got (for Error Code 400):

Note

The link is kept the same with original one.

Approach 3: Exception Filter

General Discussion

In MVC apps, exception filters can be configured globally or on a per-controller or per-action basis. In Razor Pages apps, they can be configured globally or per page model. These filters handle any unhandled exceptions that occur during the execution of a controller action or another filter. 

Exception filters are useful for trapping exceptions that occur within MVC actions, but they're not as flexible as the built-in exception handling middleware, UseExceptionHandler. Microsoft recommend using UseExceptionHandler, unless you need to perform error handling differently based on which MVC action is chosen. 

Difference from ASP.NET MVC

  1. In ASP.NET MVC, Exception Filter is the major approach for exception handling, while for ASP.NET Core MVC, as Microsoft suggested, the built-in exception hadling middleware, UseExceptionHandler, is more flexible and suitable.
  2. IExceptionFilter Interface for ASP.NET is derived by System.Web.Mvc.HandleErrorAttribute and System.Web.Mvc.Controller, therefore, we can either overriding OnException method from a class derived from HandleErrorAttribute class, or directly overriding OnException method from a controller. However, IExceptionFilter Interface for ASP.NET Core is only derived by Microsoft.AspNetCore.Mvc.Filters.ExceptionFilterAttribute, not by Controller any more. So, we have to implemente IExceptionFilter interface directly or from ExceptionFilterAttribute class, but not from Controller directly any more.

 ASP.NET

ASP.NET Core

Exception filters

The following sample exception filter uses a custom error view to display details about exceptions that occur when the app is in development:

Implementation

Step 1

Create an Custom Exception Filter: CustomExceptionFilter

using Microsoft.AspNetCore.Mvc;  
using Microsoft.AspNetCore.Mvc.Filters;  
using Microsoft.AspNetCore.Mvc.ModelBinding;  
using Microsoft.AspNetCore.Mvc.ViewFeatures;  
  
namespace ErrorHandlingSample.Filters  
{  
    public class CustomExceptionFilter : IExceptionFilter  
    {  
        private readonly IModelMetadataProvider _modelMetadataProvider;  
  
        public CustomExceptionFilter(IModelMetadataProvider modelMetadataProvider)  
        {  
            _modelMetadataProvider = modelMetadataProvider;  
        }  
  
        public void OnException(ExceptionContext context)  
        {  
            var result = new ViewResult { ViewName = "CustomError" };  
            result.ViewData = new ViewDataDictionary(_modelMetadataProvider, context.ModelState);  
            result.ViewData.Add("Exception", context.Exception);  
  
            // Here we can pass additional detailed data via ViewData  
            context.ExceptionHandled = true; // mark exception as handled  
            context.Result = result;  
        }  
    }  
} 

Step 2

Create a CustomError view: View/Shared/CustomError.cshtml

@{  
    ViewData["Title"] = "CustomError";  
    var exception = ViewData["Exception"] as Exception;  
}  
  
<h1>An Error has Occurred</h1>  
  
<p>@exception.Message</p>  

Step 3

Register in either locally in Controller level or Action level, e.g.

[TypeFilter(typeof(CustomAsyncExceptionFilter))]  
public IActionResult Failing()  
{  
    throw new Exception("Testing custom exception filter.");  
} 

or global level in startup.ConfigureService,

public void ConfigureServices(IServiceCollection services)  
{  
    services.AddControllersWithViews();  
  
    services.AddControllersWithViews(config => config.Filters.Add(typeof(CustomExceptionFilter)));  
} 

Run the app, and Test it: Click Trigger an exception (you must either register the Exception filter locally in Action or Controller or Globally):

C: the Discussion here is suitable for ASP.NET Core Web App.

Finally, I would like to make a point that our discussions in this article, Exception Handling for ASP.NET Core MVC, are suitable for ASP.NET Core Web app, because the structure of ASP.NET Core MVC app and ASP.NET Core Web app are quite similar. If we compare the startup file for both Core MVC app and Core Web app, we can see that:

There are only three differences in the startup codes, they are all not structure difference, they are all for views. The first difference indicates AddControllers() for web app, and AddControllersWithViews() for MVC: 

 The second one just directs the error handling page to diffrent places:

And the third one is related to endpoints, the routing:

app. UseEndpoints(endpoints app. UseEndpoint5(endpoint5 "default" , n;

Therefore, in exception handling, Web App and MVC App are the same, we can apply our discussion for MVC to Web App.

Summary

In this article, we had a comprehensive discussion about Exception handling for ASP.NET Core MVC (Also for .NET Core Web App), this is the summary:

  • A: Exception Handling in Development Environment for ASP.NET Core MVC
    • UseDeveloperExceptionPage
  • B: Exception Handling in Production Environment for ASP.NET Core MVC
    • Approach 1: UseExceptionHandler
      • 1: Exception Handler Page
      • 2: Exception Handler Lambda
    • Approach 2: UseStatusCodePages
      • 1: UseStatusCodePages, and with format string, and with Lambda
      • 2: UseStatusCodePagesWithRedirects
      • 3: UseStatusCodePagesWithReExecute
    • Approach 3: Exception Filter
      • Local
      • Global

References


Similar Articles