Writing an ASP.NET MVC Controller Inspector

asp.net mvc, asp.net 0 comments suggest edit

99.99999% of the time (yes, I measured it), a controller in ASP.NET MVC is a type, and an action is a method — with reflection as the glue that holds it all together. For most folks, that’s the best way to view how ASP.NET MVC works.

But some folks like to dig deeper and get their hands dirty a bit by taking a peek under the hood. Doing so reveals that while the reflection based mapping of controllers types and actions to methods is true by default, it can be easily changed to something else entirely.

ASP.NET MVC contains powerful abstractions for the controllers and actions via the ControllerDescriptor and ActionDescriptor classes. These abstractions make it possible to implement completely different underlying implementations of a controller and action. For example, one could implement a version of ASP.NET MVC on top of a dynamic language using the DLR such as the IronRuby ASP.NET MVC I wrote about a long time ago.

Using these abstractions, we can implement something useful like a Controller Inspector, a nice complement to the Route Debugger I wrote a while back.

Installing the Controller Inspector

Inspector-120x120The Controller Inspector is available as a NuGet package with the package id MvcHaack.ControllerInspector (my Paint.net skills are top notch!).

Install-Package MvcHaack.ControllerInspector

After installing the package, visit any URL in your application rendered by a controller action. For example, here’s a standard request for a boring action.

index-action

With the package installed and while running the site on localhost (it won’t work when the site is deployed), append the query string parameter ?inspect. For example, in my sample, I just visit: http://localhost:38249/?inspect and voila!

controller-inspector

I nicely formatted page that displays information about the controller and each of its actions. If you’re wondering, “hey, isn’t this like Glimpse!” please skip to the end of this blog post where I address that.

Here’s a look at an action method.

contorller-inspector-action-view

Notice that it conveniently shows the HTTP verbs next to the name to differentiate action methods of the same name. If an action method accepts a complex type as a parameter, the inspector displays details about that type (though not recursively yet).

action-with-model

Accessing controller metadata

The ControllerDescriptor class provides ways to get at metadata about the controller. The interesting members include:

  • ControllerName
  • ControllerType
  • GetCanonicalActions
  • GetCustomAttributes

The first two properties are self evident. The method GetCanonicalActions returns an enumeration of ActionDescriptor instances, each of which describes an action. GetCustomAttributes returns attributes applied to the controller. These are typically the filters applied to the controller itself.

In the case of the default controller descriptor, ReflectedControllerDescriptor, the filters returned by GetCustomAttributes are retrieved via reflection. But a custom descriptor could load those filters from elsewhere (as is the case with the IronRuby implementation).

The ActionDescriptor also has a few interesting properties.

  • ActionName
  • ControllerDescriptor
  • GetCustomAttributes
  • GetFilters
  • GetParameters
  • GetSelectors

Despite what you might expect, you can’t obtain everything you’d want to know from an ActionDescriptor. For example, if you’re interested in the return type of an action method, the ActionDescriptor won’t help? Why not? Well, it may be impossible to tell you that. For example, the type might not be known ahead of time because it requires the action method to be invoked first, as would be the case in a dynamically typed language.

So these abstractions were carefully designed not to assume too much about the underlying implementation of an action/controller.

But as we learned before, 99.99999% of the time we’re dealing with the default reflection based approach. So what the Controller Inspector does is to try and cast the ActionDescriptor to ReflectedActionDescriptor and if that succeeds, it can reflect over the action method normally to provide a lot more details.

Hooking itself up

To hook the code up that outputs all this information, I made use of David Ebbo’s WebActivator package. This allows me to run a bit of code at startup that replaces the current controller factory with an InspectorControllerFactory.

[assembly: PostApplicationStartMethod(typeof(AppStart), "Start")]
namespace MvcHaack.ControllerInspector {
  public static class AppStart {
    public static void Start() {
      var factory = ControllerBuilder.Current.GetControllerFactory();
      ControllerBuilder.Current.SetControllerFactory(
        new InspectorControllerFactory(factory));
      }
  }
}

The InspectorControllerFactory wraps the existing controller factory. All it does is call into the existing factory to create a controller, and if the request is a local request with the proper “inspect” query string parameter, it sets its invoker to be an InspectorActionInvoker. This way, for normal requests, there is pretty much no overhead.

public IController CreateController(RequestContext requestContext, 
    string controllerName) {
  var controller = _controllerFactory.CreateController(requestContext,
    controllerName);

  if (IsInspectorRequest(requestContext.HttpContext.Request)) {
    var normalController = controller as Controller;
    var invoker = normalController.ActionInvoker;
    normalController.ActionInvoker = new InspectorActionInvoker(invoker);
  }
  return controller;
}

private static bool IsInspectorRequest(HttpRequestBase httpRequest) {
  return httpRequest.IsLocal
    && httpRequest.QueryString.Keys.Count > 0
    && httpRequest.QueryString.GetValues(null).Contains("inspect");
}

What the InspectorActionInvoker does is build up a model of all the information we want to display and passes it to a precompiled Razor template using the approach I wrote about recently in Text Templating using Razor the easy way. I simply build up a huge anonymous class and pass it to a dynamic model in the template. Note that this probably won’t work in medium trust, but I can easily fix that later by either using an ExpandoObject or by using a strongly typed model. I was just being lazy as this is a proof of concept.

What about Glimpse?

As soon as I showed this to some co-workers they asked me why I was trying to re-implement Glimpse. If you haven’t heard of Glimpse, stop and go read this blog post. Glimpse is like a server-side Firebug for ASP.NET MVC applications. However, when I last checked, it didn’t have something exactly like this.

The point in writing this was to teach folks about the ControllerDescriptor and ActionDescriptor. I’ll make the code available later when I finish part three of this informal series and perhaps someone can help me turn this into a Glimpse plugin if that makes sense. In the meanwhile, I built the package with symbols so you can debug into it to see the source.

UPDATE: The code is available on Github!

In the third part, I blog about what originally lead me down this path to write about the descriptors, which in turn lead me down the path to write about the Razor Generator. Yes, I’m easily sidetracked!

As a reminder, to try it out, install the MvcHaack.ControllerInspector package then use the ?inspect query string parameter when viewing a page rendered by a controller.

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

11 responses

  1. Avatar for Braian
    Braian August 10th, 2011

    cool!

  2. Avatar for Nik
    Nik August 10th, 2011

    Looks great Phil! Of course we will help you turn this into a Glimpse plugin when the series is complete.
    Looking forward to it too - love me some MVC internals.

  3. Avatar for Justin
    Justin August 10th, 2011

    Phil, perhaps I am just a bit thick but I don't really see the benefit of this beyond being cool. I am sure I am wrong though...it is still early and I haven't had my coffee yet.

  4. Avatar for haacked
    haacked August 11th, 2011

    @Justin gives a quick view of what's happening with your controller at runtime. For example, what are all the action filters being applied to an action method? That includes those directly applied along with global filters etc.

  5. Avatar for Wouter Boevink
    Wouter Boevink August 11th, 2011

    If I put something other than ?inspect in the querystring (like ?a=1) I get an error:
    [ArgumentNullException: Value cannot be null.
    Parameter name: source]
    System.Linq.Enumerable.Contains(IEnumerable`1 source, TSource value, IEqualityComparer`1 comparer) +4212193
    System.Linq.Enumerable.Contains(IEnumerable`1 source, TSource value) +4206858
    MvcHaack.ControllerInspector.InspectorControllerFactory.IsInspectorRequest(HttpRequestBase httpRequest) +167
    MvcHaack.ControllerInspector.InspectorControllerFactory.CreateController(RequestContext requestContext, String controllerName) +146
    System.Web.Mvc.MvcHandler.ProcessRequestInit(HttpContextBase httpContext, IController& controller, IControllerFactory& factory) +196
    System.Web.Mvc.<>c__DisplayClass6.<BeginProcessRequest>b__2() +49
    System.Web.Mvc.<>c__DisplayClassb`1.<ProcessInApplicationTrust>b__a() +13
    System.Web.Mvc.SecurityUtil.<GetCallInAppTrustThunk>b__0(Action f) +7
    System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(Action action) +22
    System.Web.Mvc.SecurityUtil.ProcessInApplicationTrust(Func`1 func) +124
    System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, Object state) +98
    System.Web.Mvc.MvcHandler.BeginProcessRequest(HttpContext httpContext, AsyncCallback callback, Object state) +50
    System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData) +16
    System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +8920324
    System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +184

  6. Avatar for sgMarshall
    sgMarshall August 12th, 2011

    “99.99999% of the time....” I can see the headlines now, “Well-placed Microsoft insider publically states, MVC not 100%. ;-)

  7. Avatar for silverlight
    silverlight August 15th, 2011

    I don't really see the benefit of this beyond being cool.

  8. Avatar for Felix
    Felix August 23rd, 2011

    It seems that just adding the package breaks Intranet applications. For some reason it redirects to Account/Login?ReturnUrl=%2f and, obviously, I am getting "resource not found" (MVC404) error.
    Internet works fine - and it *doesn't* go to Account controller!

  9. Avatar for haacked
    haacked September 9th, 2011

    @Wouter I fixed the issue. Please try again.

  10. Avatar for aaron
    aaron October 13th, 2011

    If you are talking about a web page probably not that useful but for an API it's great. It's a bit like the /help pages in the WCF REST Kit. Which are extremely useful.

  11. Avatar for terry
    terry July 9th, 2012

    Phil what we need are tools that help us to find controllers that are throwing 404 errors and MOST importantly will give us a checklist of things to try to fix it. I have a app right now where one bad route broke routing for the entire application, had to spend hours and hours to learn that you need to add views as embedded resources because its a large multi-project mvc app where there is only one area defined, and now I have a controller that is returning 404 and the spelling, inheriting form Controller and implementing the IController interface all appear to be correct. The most critical thing is to expose the reasons for and solutions to the extremely brittle routing engine that throws lots of 404's errors for routes,controllers and views but give developers EXTREMELY unhelpful error messages and no suggestions on how to fix the errors that it reports. The developer experience for MVC routing needs desperate attention and it isn't fully baked yet. There is a reason devs are still insisting on sticking to asp.net 4.