Introduction
Exception is a common issue in web projects. Exception loggers are quite important to track such exceptions to improve the quality of the solution. But in my case, I wanted to track little extra information rather than just the stack trace. So here, we will see how we can trace the whole web request object in ASP.NET web service.
Background
The intention was started when I first wrote about the Log all parameters that were passed to some method in C#. But I wanted to avoid such number of try
/catch
blocks, and use more centralized code. So let’s see how we can do this in ASP.NET MVC.
Let’s say we want to collect error data as:
public class ErrorModel
{
public string ControllerName { get; set; }
public string ActionName { get; set; }
public bool? HasSession { get; set; }
public string Session { get; set; }
public Dictionary<string, string> RequestHeader { get; set; }
public string RequestData { get; set; }
public Exception Exception { get; set; }
public DateTime CreatedDateTime { get; set; }
}
Where Can We Find the Errors?
- In Action
- In Controller for all of its actions
- For all controllers actions, using a base controller
- For whole application
In Action
This is a simple try
/catch
block in action. An exception would be found at the catch
scope.
public ActionResult Create(string name)
{
try
{
throw new Exception("Error to Reset.");
}
catch (Exception exception)
{
var aException = exception;
throw new Exception("Error.", exception);
}
}
In Controller for All of Its Actions
Using OnException
on a controller means whenever an exception would be thrown from any action of the controller, this would fire each time.
protected override void OnException(ExceptionContext filterContext)
{
var aException = filterContext.Exception;
base.OnException(filterContext);
}
For All Controllers Actions, Using a Base Controller
Using a base controller for each controller of the project, as I did in Session in ASP.NET MVC and adding placing the OnException
at the BaseController
means whenever an error would be found at any actions of the SubControllers
, the BaseController
’s OnException
would be fired.
For Whole Application
Adding Application_Error
method at the Global.asax:
protected void Application_Error(Object sender, EventArgs e)
{
Exception aException = HttpContext.Current.Server.GetLastError();
var httpContext = ((MvcApplication)sender).Context;
aException = httpContext.Server.GetLastError();
}
What Do We Need More?
Session
Object HttpRequest
Object
Session Object
It’s not important to have a session
object. The Log utility can create logs without the session too. But it is important to know if the user was logged in when the exception was thrown.
Do let’s add an inline session
object in controller like this:
protected override void Initialize(RequestContext requestContext)
{
base.Initialize(requestContext);
Session["logOnUserId"] = "User-101";
}
HttpRequest Object
Which may come from any browser or via any request. Here, we can use any of those to our error logger utility.
HttpRequest request = System.Web.HttpContext.Current.Request;
HttpRequestBase requestBase = new HttpRequestWrapper(request);
Let’s Start Building the Error Log Utility
Here is the Extensions
method, which will process the HttpRequest
object, to trace request header and params.
public static class HttpRequestExtensions
{
public static string ParamsToString(this HttpRequestBase request)
{
request.InputStream.Seek(0, SeekOrigin.Begin);
return new StreamReader(request.InputStream).ReadToEnd();
}
public static Dictionary<string, string> ToRaw(this HttpRequest request)
{
return new HttpRequestWrapper(request).ToRaw();
}
public static Dictionary<string, string> ToRaw(this HttpRequestBase requestBase)
{
Dictionary<string, string> writer = new Dictionary<string, string>();
WriteStartLine(requestBase, ref writer);
WriteHeaders(requestBase, ref writer);
WriteBody(requestBase, ref writer);
return writer;
}
private static void WriteStartLine(HttpRequestBase request, ref Dictionary<string, string> writer)
{
writer.Add("HttpMethod", request.HttpMethod);
writer.Add("Url", request.Url.ToString());
writer.Add("ServerVariables", request.ServerVariables["SERVER_PROTOCOL"]);
}
private static void WriteHeaders(HttpRequestBase request, ref Dictionary<string, string> writer)
{
foreach (string key in request.Headers.AllKeys)
{
writer.Add(key, request.Headers[key]);
}
}
private static void WriteBody(HttpRequestBase request, ref Dictionary<string, string> writer)
{
StreamReader reader = new StreamReader(request.InputStream);
try
{
string body = reader.ReadToEnd();
writer.Add("Body", body);
}
finally
{
reader.BaseStream.Position = 0;
}
}
}
Here is the Utility
class to provide us the ErrorModel
:
public class ErrorLogUtility
{
public readonly HttpRequestBase RequestBase;
public ErrorLogUtility(HttpRequestBase requestBase)
{
if (requestBase == null)
{
throw new NullReferenceException("requestBase is null at ErrorLogUtility.");
}
RequestBase = requestBase;
}
public ErrorLogUtility(HttpRequest request)
: this(new HttpRequestWrapper(request))
{
}
public ErrorModel GetErrorModel(Exception exception)
{
var errorModel = new ErrorModel();
SetRouteDetail(ref errorModel);
SetRequestDetail(ref errorModel);
SetExceptionDetail(ref errorModel, exception);
return errorModel;
}
public ErrorModel GetErrorModel(Exception exception, object session)
{
var errorModel = GetErrorModel(exception);
SetSessionDetail(ref errorModel, session);
return errorModel;
}
private void SetRequestDetail(ref ErrorModel errorModel)
{
errorModel.RequestHeader = RequestBase.ToRaw();
errorModel.RequestData = RequestBase.ParamsToString();
}
private void SetRouteDetail(ref ErrorModel errorModel)
{
Func<string, string> routeDataValue = delegate(string indexName)
{
bool hasValue = RequestBase.RequestContext.RouteData.Values[indexName] != null;
return hasValue ? RequestBase.RequestContext.RouteData.Values[indexName].ToString() : "";
};
errorModel.ControllerName = routeDataValue("controller");
errorModel.ActionName = routeDataValue("action");
}
private void SetExceptionDetail(ref ErrorModel errorModel, Exception exception)
{
errorModel.Exception = exception;
errorModel.CreatedDateTime = DateTime.Now;
}
private void SetSessionDetail(ref ErrorModel errorModel, object session)
{
errorModel.HasSession = session != null;
errorModel.Session = (session != null)? session.ToString() : "";
}
}
Using the Code
In Controller for All of Its Actions / For All Controllers Actions, Using a Base Controller
protected override void OnException(ExceptionContext filterContext)
{
var errorModelUsingRequestBase = new ErrorLogUtility
(filterContext.RequestContext.HttpContext.Request).GetErrorModel(filterContext.Exception);
var errorModelUsingRequest = new ErrorLogUtility
(System.Web.HttpContext.Current.Request).GetErrorModel(filterContext.Exception);
var errorModelWithSession = new ErrorLogUtility(System.Web.HttpContext.Current.Request)
.GetErrorModel(filterContext.Exception, Session["logOnUserId"]);
base.OnException(filterContext);
}
For Whole Application
protected void Application_Error(Object sender, EventArgs e)
{
var httpContext = ((MvcApplication)sender).Context;
Exception exception = HttpContext.Current.Server.GetLastError();
var errorModelUsingRequestBase = new ErrorLogUtility(httpContext.Request).GetErrorModel(exception);
var errorModelUsingRequest = new ErrorLogUtility
(System.Web.HttpContext.Current.Request).GetErrorModel(exception);
var errorModelWithSession = new ErrorLogUtility(System.Web.HttpContext.Current.Request)
.GetErrorModel(exception, Session["logOnUserId"]);
}
In Action
public ActionResult Create(string name)
{
try
{
throw new Exception("Error to Reset.");
}
catch (Exception exception)
{
var errorModelUsingRequest = new ErrorLogUtility
(System.Web.HttpContext.Current.Request).GetErrorModel(exception);
var errorModelWithSession = new ErrorLogUtility(System.Web.HttpContext.Current.Request)
.GetErrorModel(exception, Session["logOnUserId"]);
throw new Exception("Error.", exception);
}
}
Limitations
- I am still working with it. There could be some errors too. If you find any, just let me know.
- If you are using custom models of the session, make sure you override the
ToString
method.
class Person
{
public long Id { get; set; }
public string Name { get; set; }
public override string ToString()
{
return string.Format("Id: {0}, Name: {1}", Id, Name);
}
}
- Try to avoid repeated uses of this log process for a particular exception. (In action/ in controller/ base controller/ global.asax) and once.
- Have a look at the comment section "A good idea, a couple of comments about the implementation" where John Brett pointed some good things.
Sample JSONs
Here, I have converted the ErrorModel
into json.
As you can see:
RequestData
is pointing the data for which such exception was found. RequestHeader
points little extra information too.
{
"ControllerName": "InControllerLog",
"ActionName": "Edit",
"HasSession": true,
"Session": "User-101",
"RequestHeader": {
"HttpMethod": "POST",
"Url": "http://localhost:1999/InControllerLog/Edit",
"ServerVariables": "HTTP/1.1",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Pragma": "no-cache",
"Content-Length": "143",
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.5",
"Cookie": "ASP.NET_SessionId=xs32x1xeelllzuycaz01lpn3",
"Host": "localhost:1999",
"Referer": "http://localhost:1999/InControllerLog",
"User-Agent": "Mozilla/5.0 (Windows NT 6.2; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0",
"X-Requested-With": "XMLHttpRequest",
"Body": ""
},
"RequestData": "{\"id\":1,
\"student\":{\"id\":1,\"name\":\"Student_1\"},
\"subjects\":[{\"id\":1,\"name\":\"Subject_1\"},
{\"id\":2,\"name\":\"Subject_2\"},{\"id\":3,
\"name\":\"Subject_3\"}]}",
"Exception": {
"ClassName": "System.Exception",
"Message": "Error to Edit.",
"Data": {},
"InnerException": null,
"HelpURL": null,
"StackTraceString": " at ErrorRequestLog.Controllers.InControllerLogController.Edit
(UInt64 id, Student student, List`1 subjects) in
c:\\Users\\Dipon Roy\\Desktop\\ErrorRequestLog\\ErrorRequestLog\\ErrorRequestLog\\Controllers\\
InControllerLogController.cs:line 35\r\n at lambda_method(Closure , ControllerBase ,
Object[] )\r\n at System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller,
Object[] parameters)\r\n at System.Web.Mvc.ReflectedActionDescriptor.Execute
(ControllerContext controllerContext, IDictionary`2 parameters)\r\n at
System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext,
ActionDescriptor actionDescriptor, IDictionary`2 parameters)\r\n
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass42.
<BeginInvokeSynchronousActionMethod>b__41()\r\n at System.Web.Mvc.Async.AsyncResultWrapper.
<>c__DisplayClass8`1.<BeginSynchronous>b__7(IAsyncResult _)\r\n
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.End()\r\n at
System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethod
(IAsyncResult asyncResult)\r\n
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass37.<>c__DisplayClass39.
<BeginInvokeActionMethodWithFilters>b__33()\r\n
at System.Web.Mvc.Async.AsyncControllerActionInvoker.
<>c__DisplayClass4f.<InvokeActionMethodFilterAsynchronously>b__49()\r\n
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass37.
<BeginInvokeActionMethodWithFilters>b__36(IAsyncResult asyncResult)\r\n
at System.Web.Mvc.Async.AsyncResultWrapper.WrappedAsyncResult`1.End()\r\n
at System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethodWithFilters
(IAsyncResult asyncResult)\r\n at System.Web.Mvc.Async.AsyncControllerActionInvoker.
<>c__DisplayClass25.<>c__DisplayClass2a.<BeginInvokeAction>b__20()\r\n
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass25.
<BeginInvokeAction>b__22(IAsyncResult asyncResult)",
"RemoteStackTraceString": null,
"RemoteStackIndex": 0,
"ExceptionMethod": "8\nEdit\nErrorRequestLog, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null\nErrorRequestLog.Controllers.InControllerLogController
\nSystem.Web.Mvc.JsonResult Edit(UInt64, ErrorRequestLog.Model.Student,
System.Collections.Generic.List`1[ErrorRequestLog.Model.Subject])",
"HResult": -2146233088,
"Source": "ErrorRequestLog",
"WatsonBuckets": null
},
"CreatedDateTime": "2014-07-16T13:15:56.5202454-08:00"
}
Use this json at http://www.jsoneditoronline.org/ to see how it would look like. Or use the model object as you want to use.
Find VS2012 MVC4 project solution in the attachment. Rebuild, let nugget install the required packages.