Skip to main content

Gracefully notifying end user about application error in .Net and .Net Core versions

We generally do error logging for the application to know what error might have occurred in the application. Notifying the end user about the error is equally important. Also, it is always better to implement this at first place rather than doing at a later stage. I have seen many big company's websites ending with .NET yellow page of death. Now, it is really good that the default behavior is removed from .Net Core framework with some generic error message.

We would look how to implement generically and use in older frameworks and some tips on .Net Core as well.

The idea is to create MVC Controller for error and then redirect it from any locations like Filters or Global Exception handling. Also, we would briefly look into the global AJAX error handling system since heavy usage of AJAX in current development trend.

The few steps that we are going to do.
- Represent application errors with enum as system/application have different ways to deal with errors like though Exception or status code.
- Domain model to generate respective error message based on Enum.
- Global exception handling to catch the exception and redirect to MVC Action.

Error Enum

This can be extended as you required. I would be using Unknown as a default initialization but could be set as zero to get initial value.

 /// <summary>  
 /// Error codes for requests  
 /// </summary>  
 public enum ErrorCode  
 {  
   /// <summary>  
   /// The HTTP request error  
   /// </summary>  
   HttpRequestError,  
   /// <summary>  
   /// The page not found  
   /// </summary>  
   PageNotFound,  
   /// <summary>  
   /// The unknown  
   /// </summary>  
   Unknown,  
   /// <summary>  
   /// The unauthorized user  
   /// </summary>  
   UnauthorizedUser,  
   /// <summary>  
   /// The anti forgery token  
   /// </summary>  
   AntiForgeryToken  
 }  

Domain model to generate message

This is pretty straight forward to initialize class based on ErrorCode enum which in turn gives us messages to display to the end users.

 /// <summary>  
 /// Error model  
 /// </summary>  
 public class ErrorModel  
 {  
   /// <summary>  
   /// Initializes a new instance of the <see cref="ErrorModel"/> class.  
   /// </summary>  
   /// <param name="errCode">The error code.</param>  
   public ErrorModel(ErrorCode errCode)  
   {  
     ErrorCode = errCode;  
     switch (errCode)  
     {  
       case ErrorCode.HttpRequestError:  
         ErrorHeader = "Invalid inputs given.";  
         ErrorMessage = "Request terminated. Invalid characters found. Please try putting correct values.";  
         break;  
       case ErrorCode.PageNotFound:  
         ErrorHeader = "Page Not Available";  
         ErrorMessage = "The requested page is either moved or invalid.";  
         break;  
       case ErrorCode.UnauthorizedUser:  
         ErrorHeader = "Unauthorized Access";  
         ErrorMessage = "You don't have permission to access this area.";  
         break;  
       case ErrorCode.AntiForgeryToken:  
         ErrorHeader = "Security Issue";  
         ErrorMessage = "Source and destination does not match each other. Please try again.";  
         break;  
       case ErrorCode.Unknown:  
       default:  
         ErrorHeader = "Unknown error";  
         ErrorMessage = "We are sorry but it appears that this page is missing or some error has occurred.";  
         break;  
     }  
   }  
   /// <summary>  
   /// Gets or sets the error code.  
   /// </summary>  
   /// <value>  
   /// The error code.  
   /// </value>  
   public ErrorCode ErrorCode { get; set; }  
   /// <summary>  
   /// Gets or sets the error header.  
   /// </summary>  
   /// <value>  
   /// The error header.  
   /// </value>  
   public string ErrorHeader { get; set; }  
   /// <summary>  
   /// Gets or sets the error message.  
   /// </summary>  
   /// <value>  
   /// The error message.  
   /// </value>  
   public string ErrorMessage { get; set; }  
 }  

Error Controller along with View

This has a just few logic to check if it is AJAX call and response accordingly. Just in case of Unauthorized it redirects to login page.

 public class ErrorController  
   : Controller  
 {  
   /// <summary>  
   /// Indexes the specified error code.  
   /// </summary>  
   /// <param name="id">The error code.</param>  
   /// <returns></returns>  
   [AllowAnonymous]  
   public ActionResult Index(ErrorCode id)  
   {  
     var error = new ErrorModel(id);  
     if (Request.IsAjaxRequest())  
     {  
       return Content($"{error.ErrorHeader} - {error.ErrorMessage}");  
     }  
     else if (id == ErrorCode.UnauthorizedUser)  
     {  
       return RedirectToAction("Index", "Login", new { area = "Account" });  
     }  
     return View(error);  
   }  
   /// <summary>  
   /// Error view based on Context Error code.  
   /// </summary>  
   /// <param name="statusCode">The status code.</param>  
   /// <returns>Redirects to Index</returns>  
   [AllowAnonymous]  
   [Route("Error/Code/{statusCode}")]  
   public ActionResult Code(int statusCode)  
   {  
     var errCode = ErrorCode.Unknown;  
     switch (statusCode)  
     {  
       case 404:  
         errCode = ErrorCode.PageNotFound;  
         break;  
       case 401:  
         errCode = ErrorCode.UnauthorizedUser;  
         break;  
     }  
     return RedirectToAction(nameof(Index), new { id = errCode });  
   }  
 }  

In the case of  .NET Core, IsAjaxRequest won't be available. So, you can use this extension method.

 public static bool IsAjaxRequest(this HttpRequest request)  
 {  
   if (request == null)  
   {  
     throw new ArgumentNullException(nameof(request));  
   }  
   if (request.Headers != null)  
   {  
     return request.Headers["X-Requested-With"] == "XMLHttpRequest";  
   }  
   return false;  
 }  

View for Action: Index.cshtml

 @model ErrorModel  
 @{  
   ViewBag.Title = "Error";  
   //Layout = "~/Views/Shared/_Layout.cshtml";  
 }  
 <h2 class="text-danger">@Model.ErrorHeader</h2>  
 <div class="text-info">  
   @Model.ErrorMessage  
 </div>  

Global exception handling and redirection: Global.asax.cs

This is not applicable in .Net Core we would have another section for same.
Based on certain condition it is composing correct ErrorCode enum and redirecting to ErrorController.

 protected void Application_Error(object sender, EventArgs e)  
 {  
   // Skip error logging and page redirect if application is running on local machine  
   if (Request.IsLocal)  
   {  
     return;  
   }  
   // Get exception  
   var ex = Server.GetLastError();  
   if (ex == null)  
   {  
     return;  
   }  
   Server.ClearError();  
   ErrorCode errCode = ErrorCode.Unknown;  
   // Form error code.  
   if (ex is HttpRequestValidationException)  
   {  
     errCode = ErrorCode.HttpRequestError;  
   }  
   else if ((ex as HttpException) != null &&  
         (ex as HttpException).GetHttpCode() == 404)  
   {  
     errCode = ErrorCode.PageNotFound;  
   }  
   else if (ex as System.Web.Mvc.HttpAntiForgeryException != null)  
   {  
     errCode = ErrorCode.AntiForgeryToken;  
   }
   Log.Error(String.Format("{0} error:", errCode.ToString()), ex);  
   Response.Redirect("/error?id=" + errCode);  
 }  

Since it won't work in .Net Core there have to be few extra things required.

.Net Core exception handling

A while back I had written an article Global exception handling and custom logging in AspNet Core with MongoDB. Under GlobalExceptionFilter we can hook this up and the same thing is done as well in that article.

The 401 or some others does not get properly redirected through Global exception filter in .Net Core. Maybe due to Pipeline of filters is directly associated with MVC. We can overcome by using this under Startup.cs along with GlobalExceptionFilter.

 app.UseStatusCodePagesWithReExecute("/error/code/{0}");  

Client side notification

This actually depends on the approach that you might have taken for clients side scripting. If there is common functions for get, post, AJAX requests then you could hook up there. In Angular these can be hooked on observable.

In the case of jQuery, AJAX calls can be globally registered.
Sample:

   $(document).ajaxError(  
     function (e, xhr, settings) {  
       if (xhr.status == 401) {  
         // Notify user for redirection and redirect  
       }  
     });  





Comments

Popular posts from this blog

Elegantly dealing with TimeZones in MVC Core / WebApi

In any new application handling TimeZone/DateTime is mostly least priority and generally, if someone is concerned then it would be handled by using DateTime.UtcNow on codes while creating current dates and converting incoming Date to UTC to save on servers. Basically, the process is followed by saving DateTime to UTC format in a database and keep converting data to native format based on user region or single region in the application's presentation layer. The above is tedious work and have to be followed religiously. If any developer misses out the manual conversion, then that area of code/view would not work. With newer frameworks, there are flexible ways to deal/intercept incoming or outgoing calls to simplify conversion of TimeZones. These are steps/process to achieve it. 1. Central code for storing user's state about TimeZone. Also, central code for conversion logic based on TimeZones. 2. Dependency injection for the above class to ...

Kendo MVC Grid DataSourceRequest with AutoMapper

Kendo Grid does not work directly with AutoMapper but could be managed by simple trick using mapping through ToDataSourceResult. The solution works fine until different filters are applied. The problems occurs because passed filters refer to view model properties where as database model properties are required after AutoMapper is implemented. So, the plan is to intercept DataSourceRequest  and modify names based on database model. To do that we are going to create implementation of  CustomModelBinderAttribute to catch calls and have our own implementation of DataSourceRequestAttribute from Kendo MVC. I will be using same source code from Kendo but will replace column names for different criteria for sort, filters, group etc. Let's first look into how that will be implemented. public ActionResult GetRoles([MyDataSourceRequest(GridId.RolesUserGrid)] DataSourceRequest request) { if (request == null) { throw new Argume...

Using Redis distributed cache in dotnet core with helper extension methods

Redis cache is out process cache provider for a distributed environment. It is popular in Azure Cloud solution, but it also has a standalone application to operate upon in case of small enterprises application. How to install Redis Cache on a local machine? Redis can be used as a local cache server too on our local machines. At first install, Chocolatey https://chocolatey.org/ , to make installation of Redis easy. Also, the version under Chocolatey supports more commands and compatible with Official Cache package from Microsoft. After Chocolatey installation hit choco install redis-64 . Once the installation is done, we can start the server by running redis-server . Distributed Cache package and registration dotnet core provides IDistributedCache interface which can be overrided with our own implementation. That is one of the beauties of dotnet core, having DI implementation at heart of framework. There is already nuget package available to override IDistributedCache i...

Trim text in MVC Core through Model Binder

Trimming text can be done on client side codes, but I believe it is most suitable on MVC Model Binder since it would be at one place on infrastructure level which would be free from any manual intervention of developer. This would allow every post request to be processed and converted to a trimmed string. Let us start by creating Model binder using Microsoft.AspNetCore.Mvc.ModelBinding; using System; using System.Threading.Tasks; public class TrimmingModelBinder : IModelBinder { private readonly IModelBinder FallbackBinder; public TrimmingModelBinder(IModelBinder fallbackBinder) { FallbackBinder = fallbackBinder ?? throw new ArgumentNullException(nameof(fallbackBinder)); } public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bin...

Interface based model binding to populate properties value automatically

Interface is great way to make consistency and re-useability of codes. In this tutorial, I am going to show the power of interface to populate view/domain models automatically and interchange values between models. Technologies or techniques used in this approach: - MVC. - Interface for models. - MVC model binder for associated model with interface. Through this model binder we will populate values. - T4 template to generate code to register model binder with all models associated with interface. - Generic extension method to interchange values. It might sounding bit complex to achieve small thing. Just hold on with me and see unfolding. This will result in easier maintenance and less repetitive code. First let's start with creation of Interface that need to be populate automatically. This interface would be implemented on models and values will be auto-populated on binder. /// <summary> /// Interface for storing entry related values /// </summar...

Centralized model validation both for MVC/WebApi and SPA client-side validation using FluentValidation

Validation is one of the crucial parts of any application. It has to validate on both client side and server side requests. What are target features or implementation from this article? Model validation for any given model. Centralized/One code for validation on both server-side and client-side. Automatic validation of model without writing any extra codes on/under actions for validation.  NO EXTRA/ANY codes on client-side to validate any form. Compatible with SPA. Can be compatible with any client-side validation framework/library. Like Angular Reactive form validation or any jquery validation libraries. Tools used in the implementation? FluentValidation : I feel DataAnnotation validation are excellent and simple to use, but in case of complex validation or writing any custom validations are always tricker and need to write a lot of codes to achieve whereas FluentValidations are simple even in case of complex validation. Generally, we need to validate inc...

OpenId Authentication with AspNet Identity Core

This is a very simple trick to make AspNet Identity work with OpenId Authentication. More of all both approach is completely separate to each other, there is no any connecting point. I am using  Microsoft.AspNetCore.Authentication.OpenIdConnect  package to configure but it should work with any other. Configuring under Startup.cs with IAppBuilder app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme, LoginPath = new PathString("/Account/Login"), CookieName = "MyProjectName", }) .UseIdentity() .UseOpenIdConnectAuthentication(new OpenIdConnectOptions { ClientId = "<AzureAdClientId>", Authority = String.Format("https://login.microsoftonline.com/{0}", "<AzureAdTenant>"), ResponseType = OpenIdConnectResponseType.IdToken, PostLogoutRedirectUri = "<my website url>", ...

LDAP with ASP.Net Identity Core in MVC with project.json

Lightweight Directory Access Protocol (LDAP), the name itself explain it. An application protocol used over an IP network to access the distributed directory information service. The first and foremost thing is to add references for consuming LDAP. This has to be done by adding reference from Global Assembly Cache (GAC) into project.json "frameworks": { "net461": { "frameworkAssemblies": { "System.DirectoryServices": "4.0.0.0", "System.DirectoryServices.AccountManagement": "4.0.0.0" } } }, These  System.DirectoryServices  and  System.DirectoryServices.AccountManagement  references are used to consume LDAP functionality. It is always better to have an abstraction for irrelevant items in consuming part. For an example, the application does not need to know about PrincipalContext or any other dependent items from those two references to make it extensible. So, we can begin wi...

Handling JSON DateTime format on Asp.Net Core

This is a very simple trick to handle JSON date format on AspNet Core by global settings. This can be applicable for the older version as well. In a newer version by default, .Net depends upon Newtonsoft to process any JSON data. Newtonsoft depends upon Newtonsoft.Json.Converters.IsoDateTimeConverter class for processing date which in turns adds timezone for JSON data format. There is a global setting available for same that can be adjusted according to requirement. So, for example, we want to set default formatting to US format, we just need this code. services.AddMvc() .AddJsonOptions(options => { options.SerializerSettings.DateTimeZoneHandling = "MM/dd/yyyy HH:mm:ss"; });

Global exception handling and custom logging in AspNet Core with MongoDB

In this, we would be looking into logging and global exception handling in the AspNet Core application with proper registration of logger and global exception handling. Custom logging The first step is to create a data model that we want to save into DB. Error log Data model These are few properties to do logging which could be extended or reduced based on need. public class ErrorLog { /// <summary> /// Gets or sets the Error log identifier. /// </summary> /// <value> /// The Error log identifier. /// </value> [BsonRepresentation(BsonType.ObjectId)] public ObjectId Id { get; set; /// <summary> /// Gets or sets the date. /// </summary> /// <value> /// The date. /// </value> public DateTime Date { get; set; } /// <summary> /// Gets or sets the thread. /// </summary> ...