Errors handling and logging in ASP.NET MVC

How to handle errors and exception in ASP.NET MVC applications the right way.
I want the following:
– All the errors and exceptions must be logged
– Logging should be easy to program and it should not influence main code flow
– Error log must contain call stack and time stamp
– User should receive a friendly error page.
Sample project for Visual Studio 2015 can be downloaded here https://github.com/mchudinov/AspMvcErrorHandler
error404

I use the following tools:
Application_Error method to catch all errors in ASP.NET application that were not caught inplace
ELMAH to log all errors
PostSharp to integrate logging as aspect. Read my blog post about using PostSharp for logging
NLog as logging framework. Read my blog post about using NLog.

1. Install and configure NLog

NLog or other logging framework. NLog is good because it can log asynchronously. Here is my blog post Logging in .NET Mono on Linux and Windows using NLog.

2. Add errors handling to Application_Error method and show custom error page

I need one single point in my application to handle exceptions. Exception must be logged and user will see an error page. Error page is in fact an Error view with Error controller behind.

– Remove all customErrors and httpErrors from Web.config
– Create Error view model in Models folder of ASP.NET MVC project

public class Error
{
	public string StatusCode { get; set; }

	public string StatusDescription { get; set; }

	public string Message { get; set; }

	public DateTime DateTime { get; set; }
}

– Create ErrorPageController

public class ErrorPageController : Controller
{
	public ActionResult Error(int statusCode, Exception exception)
	{
		Response.StatusCode = statusCode;
		var error = new Models.Error
		{
			StatusCode = statusCode.ToString(),
			StatusDescription = HttpWorkerRequest.GetStatusDescription(statusCode),
			Message = exception.Message,
			DateTime = DateTime.Now
		};
		return View(error);
	}
}

– Create Error view

@model MvcErrorHandler.Models.Error
@{
    ViewBag.Title = Model.StatusCode;
}

<h1 class="error">@Model.StatusCode</h1>

@Model.StatusDescription<br />
@Model.Message<br />
@Model.DateTime

– Add Application_Error method to Global.asax.cs
Application_Error will catch all uncatched exceptions, log exception message with call stack and session data and run ErrorPageController that will show the user friendly error page.

protected void Application_Error(object sender, EventArgs e)
{
	Exception ex = Server.GetLastError();
	if (ex != null)
	{
		StringBuilder err = new StringBuilder();
		err.Append("Error caught in Application_Error event\n");
		err.Append("Error in: " + (Context.Session == null ? string.Empty : Request.Url.ToString()));
		err.Append("\nError Message:" + ex.Message);
		if (null != ex.InnerException)
			err.Append("\nInner Error Message:" + ex.InnerException.Message);
		err.Append("\n\nStack Trace:" + ex.StackTrace);
		Server.ClearError();

		if (null != Context.Session)
		{
			err.Append($"Session: Identity name:[{Thread.CurrentPrincipal.Identity.Name}] IsAuthenticated:{Thread.CurrentPrincipal.Identity.IsAuthenticated}");
		}
		_log.Error(err.ToString());

		if (null != Context.Session)
		{
			var routeData = new RouteData();
			routeData.Values.Add("controller", "ErrorPage");
			routeData.Values.Add("action", "Error");
			routeData.Values.Add("exception", ex);

			if (ex.GetType() == typeof(HttpException))
			{
				routeData.Values.Add("statusCode", ((HttpException)ex).GetHttpCode());
			}
			else
			{
				routeData.Values.Add("statusCode", 500);
			}
			Response.TrySkipIisCustomErrors = true;
			IController controller = new ErrorPageController();
			controller.Execute(new RequestContext(new HttpContextWrapper(Context), routeData));
			Response.End();
		}
	}
}

Let’s test now what we did!
Throw an exception somewhere in the application (in a controller or wherever). This should generate a 500 internal server error and an Error page will be shown.
Run application in Visual Studio and open it in Internet Explorer. IE (v11) shows its own 500 error page:

500ie

Open same page in FireFox. Our custom error page will be shown:
500firefox

Navigate to a non existing path will generate a 404 error page:
404firefox

Note! Errors occurred in Application_Start will be logged but user will not be redirected to an Error page because Response object does not existed in Application_Start yet.

3. Use aspects for logging and exception handling

I use to separate aspects for logging and for exception logging.
– Install PostSharp. Here is my blog post Logging in .NET with AOP using PostSharp

– Add exception log aspect

[Serializable]
public class LogExceptionAttribute : OnExceptionAspect
{
	private static readonly Logger log = LogManager.GetCurrentClassLogger();

	public LogExceptionAttribute()
	{
		AspectPriority = 10;
	}

	public override void OnException(MethodExecutionArgs args)
	{
		log.Error("Exception {0} in {1}.{2}()", args.Exception, args.Method.DeclaringType.Name, args.Method.Name);
	}
}

– Add log aspect.
It will logg all methods on entry and on leaving when log level is set to DEBUG. Can be quite helpful when troubleshooting.

[Serializable]
public class LogAttribute : OnMethodBoundaryAspect
{
	private static readonly Logger log = LogManager.GetCurrentClassLogger();

	public LogAttribute()
	{
		AspectPriority = 20;
	}

	public override void OnEntry(MethodExecutionArgs args)
	{
		log.Debug("Entering {0}.{1}({2})", args.Method.DeclaringType.Name, args.Method.Name, DisplayObjectInfo(args));
	}

	public override void OnExit(MethodExecutionArgs args)
	{
		log.Debug("Leaving {0}.{1}() Return value [{2}]", args.Method.DeclaringType.Name, args.Method.Name, args.ReturnValue);
	}

	static string DisplayObjectInfo(MethodExecutionArgs args)
	{
		StringBuilder sb = new StringBuilder();
		Type type = args.Arguments.GetType();
		FieldInfo[] fi = type.GetFields();
		if (fi.Length > 0)
		{
			foreach (FieldInfo f in fi)
			{
				sb.Append(f + " = " + f.GetValue(args.Arguments));
			}
		}
		else
			sb.Append("None");

		return sb.ToString();
	}
}

Use these aspects on every class that you need exceptions logging and debug logging:

[Log]
[LogException]
public class DataProvider
{
	private static readonly Random _rnd = new Random();

	public static string GetData()
	{
		throw new Exception("Hoho!");
		return "Hello!";
	}
}

AspectPriority modifier is needed to avoid execution priority conflict since both aspects implement OnException method.

Log example with on entry and on leaving events:

2016-03-03 14:47:38.5828 INFO Application started (AspMvcErrorHandler.MvcApplication.Application_Start)
2016-03-03 14:47:38.6993 DEBUG Entering DataProvider..cctor(PostSharp.Aspects.Arguments Empty = PostSharp.Aspects.Arguments) (AspMvcErrorHandler.LogAttribute.OnEntry)
2016-03-03 14:47:38.6993 DEBUG Leaving DataProvider..cctor() Return value [] (AspMvcErrorHandler.LogAttribute.OnExit)
2016-03-03 14:47:38.6993 DEBUG Entering DataProvider.GetData(PostSharp.Aspects.Arguments Empty = PostSharp.Aspects.Arguments) (AspMvcErrorHandler.LogAttribute.OnEntry)
2016-03-03 14:47:38.7583 DEBUG Leaving DataProvider.GetData() Return value [] (AspMvcErrorHandler.LogAttribute.OnExit)
2016-03-03 14:47:38.7813 ERROR Exception System.Exception: Hoho!
   at AspMvcErrorHandler.DataProvider.GetData() in D:\projects\AspMvcErrorHandler\AspMvcErrorHandler\DataProvider.cs:line 13 in DataProvider.GetData() (AspMvcErrorHandler.LogExceptionAttribute.OnException)
2016-03-03 14:47:41.7452 ERROR Error caught in Application_Error event
Error in: http://localhost:57686/
Error Message:Hoho!

Stack Trace:   at AspMvcErrorHandler.DataProvider.GetData() in D:\projects\AspMvcErrorHandler\AspMvcErrorHandler\DataProvider.cs:line 13
   at AspMvcErrorHandler.Controllers.HomeController.Index() in D:\projects\AspMvcErrorHandler\AspMvcErrorHandler\Controllers\HomeController.cs:line 10
   at lambda_method(Closure , ControllerBase , Object[] )
   at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters)

4. Use ELMAH

ELMAH is Error Logging Modules and Handlers. Here is a simple ELMAH tutorial. With ASP.NET MVC applications I use a special NuGet package Elmah.MVC. Just install and use it.