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.
In the case of .NET Core, IsAjaxRequest won't be available. So, you can use this extension method.
Based on certain condition it is composing correct ErrorCode enum and redirecting to ErrorController.
Since it won't work in .Net Core there have to be few extra things required.
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.
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
Post a Comment