Tuesday, February 15, 2011

MVC3's GlobalFilters and HandleErrorAttribute

In MVC3 a GlobalFilterCollection has been added to the Application_Start. This allows you to register filters that will be applied to all controller actions in a single location. Also, MVC3 web applications now add an instance of HandleErrorAttribute to these GlobalFilters by default. This means that errors in the MVC pipeline will now be automatically handled by these attributes and never fire the HttpApplication's OnError event.

This is nice because it is another step away from the old ASP.NET way of doing things, and a step toward the newer cleaner MVC way of doing things. However, it did throw us a slight curve ball when updating CodeSmith Insight's HttpModule.

Side Note: The CodeSmith Insight MVC3 client assembly will be released next week (the week of 2/21/11).

Out With the Old

Our old HttpModule wired up to the HttpApplication's OnError event and used that to log unhandled exceptions in web applications. It didn't care if the error happened in or out of the MVC pipeline, either way it was going to bubble up and get caught in the module.

public virtual void Init(HttpApplication context)
{
   InsightManager.Current.Register();
   InsightManager.Current.Configuration.IncludePrivateInformation = true;
   context.Error += OnError;
}

private void OnError(object sender, EventArgs e)
{
   var context = HttpContext.Current;
   if (context == null)
       return;

   Exception exception = context.Server.GetLastError();
   if (exception == null)
       return;

   var abstractContext = new HttpContextWrapper(context);
   InsightManager.Current.SubmitUnhandledException(exception, abstractContext);
}

However, now the MVC HandleErrorAttribute may handle exceptions right inside of the MVC pipeline, meaning that they will never reach the HttpApplication and the OnError will never be fired. What to do, what to do...

In With the New

Now we need to work with both the attributes and the HttpApplication, ensuring that we will catch errors from both inside and outside of the MVC pipeline. This means that we need to find and wrap any instances of HandleErrorAttribute in the GlobalFilters, and still register our model to receive notifications from the HttpApplications OnError event.

The first thing we had to do was create a new HandleErrorAttribute. Please note that this example is simplified and only overrides the OnException method. If you want to do this "right", you'll have to override and wrap all of the virtual methods in HandleErrorAttribute.

public class HandleErrorAndReportToInsightAttribute : HandleErrorAttribute
{
   public bool HasWrappedHandler
   {
       get { return WrappedHandler != null; }
   }

   public HandleErrorAttribute WrappedHandler { get; set; }

   public override void OnException(ExceptionContext filterContext)
   {
       if (HasWrappedHandler)
           WrappedHandler.OnException(filterContext);
       else
           base.OnException(filterContext);

       if (filterContext.ExceptionHandled)
           InsightManager.Current.SubmitUnhandledException(filterContext.Exception, filterContext.HttpContext);
   }
}

Next we needed to update our HttpModule to find, wrap, and replace any instances of HandleErrorAttribute in the GlobalFilters.

public virtual void Init(HttpApplication context)
{
   InsightManager.Current.Register();
   InsightManager.Current.Configuration.IncludePrivateInformation = true;
   context.Error += OnError;

   ReplaceErrorHandler();
}

private void ReplaceErrorHandler()
{
   var filter = GlobalFilters.Filters.FirstOrDefault(f => f.Instance is HandleErrorAttribute);
   var handler = new HandleErrorAndReportToInsightAttribute();

   if (filter != null)
   {
       GlobalFilters.Filters.Remove(filter.Instance);
       handler.WrappedHandler = (HandleErrorAttribute) filter.Instance;
   }

   GlobalFilters.Filters.Add(handler);
}

In Conclusion

Now when we register the InsightModule in our web.config, we will start capturing all unhandled exceptions again.

<configuration>
 <configSections>
   <section name="codesmith.insight" type="CodeSmith.Insight.Client.Configuration.InsightSection, CodeSmith.Insight.Client.Mvc3" />
 </configSections>
 <codesmith.insight apiKey="XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" serverUrl="http://app.codesmithinsight.com/" />
 <system.web>
   <customErrors mode="On" />
   <httpModules>
     <add name="InsightModule" type="CodeSmith.Insight.Client.Web.InsightModule, CodeSmith.Insight.Client.Mvc3"/>
   </httpModules>
 </system.web>
</configuration>
Real Time Web Analytics