One of the main design principles ASP.NET MVC has been designed with is extensibility. Everything (or most of) in the processing pipeline is replaceable so, if you don’t like the conventions (or lack of them) that ASP.NET MVC uses, you can create your own services to support your conventions and inject them into the main pipeline.
In this post I’m going to show 13 extensibility points that every ASP.NET MVC developer should know, starting from the beginning of the pipeline and going forward till the rendering of the view.
Usually you could put some constrains on url parameters using regular expressions, but if your constrains depend on something that is not only about the single parameter, you can implement the IRouteConstrains’s method and put your validation logic in it.
One example of this is the validation of a date: imagine an url that has year, month and date on different url tokens, and you want to be able to validate that the three parts make a valid date.
Not really specific to ASP.NET MVC, the RouteHandler is the component that decide what to do after the route has been selected. Obviously if you change the RouteHandler you end up handling the request without ASP.NET MVC, but this can be useful if you want to handle a route directly with some specific HttpHanlders or even with a classic WebForm.
The controller factory is the component that, based on the route, chooses which controller to instantiate and instantiate it. The default factory looks for anything that implements IController and whose name ends with Controller, and than create an instance of it through reflection, using the parameter-less constructor.
But if you want to use Dependency Injection you cannot use it, and you have to use a IoC aware controller factory: there are already controller factory for most of the IoC containers. You can find them in MvcContrib or having a look at the Ninject Controller Factory.
ActionInvoker is responsible for invoking the action based on it’s name. The default action invoker looks for the action based on the method name, the action name and possibly other selector attributes. Then it invokes the action method together with any filter defined and finally it executes the action result.
If you read carefully you probably understood that most of the execution pipeline is inside the logic of the default ControllerActionInvoker class. So if you want to change any of these conventions, from the action method’s selection logic, to the way http parameters are mapped to action parameters, to the way filters are chosen and executed, you have to extend that class and override the method you want to change.
A good example of this, is the NinjectActionInvoker I developed to allow injection of dependencies inside filters.
Actions, with the default action invoker, are selected based on their name, but you can finer tune the selection of actions implementing your own Method Selector. The framework already comes with the AcceptVerbs attribute that allows you to specify to which HTTP Verb an action has to respond to.
A possible scenario for a custom selector attribute is if you want to choose one action or another based on languages supported by the browser or based on the type of browser, for example whether it is a mobile browser or a desktop browser.
These kind of filters are executed before the action is executed, and their role is to make sure the request is “valid”.
There are already a few Authorization filters inside the framework, the most “famous” of which is the Authorize attribute that checks whether the current user is allowed to execute the action. Another is the the ValidateAntiForgeryToken that prevents CSRF attacks. If you want to implement your own authorization schema, the interface you have to implement is IAuthorizationFilter. An example could be the hour of the day.
Action Filters are executed before and after an action is executed. One of the core filters is the OutputCache filter, but you can find many other usages for this filter. This is the most likely extension point you are going to use, as, IMHO, it’s critical to a good componentization of views: the controller only has to do its main stuff, and all the other data needed by the view must be retrieved from inside action filters.
The default model binder maps HTTP parameters to action method parameters using their names: a http parameter named user.address.city will be mapped to the City property of the Address object that itself is a property of the method parameter named user. The DefaultModelBinder works also with arrays, and other list types.
But it can be pushed even further: for example you might use it to convert the id of the person directly to the Person object looking up on the database. This approach is explained better in the following post Timothy Khouri (aka SingingEels): Model Binders in ASP.NET MVC. The code is based on the preview 5, but the concept remains the same.
All controllers inherit from the base class Controller. A good way to encapsulate logic or conventions inside your actions is to create you own layer supertype and have all your controllers to inherit from it.
Like the ActionFiters, the ResultFilters are execute before and after the ActionResult is executed. Again, the OutputCache filter is an example of a ResultFilter. The usual example that is done to explain this filter is logging. If you want to log that a page has been returned to the user, you can write a custom RenderFilter that logs after the ActionResult has been executed.
ASP.NET MVC comes with many different kind of results to render views, to render JSON, plain text, files and to redirect to other actions. But if you need some different kind of result you can write your own ActionResult and implement the ExecuteResult method. For example, if you want to send a PDF file as result you could write your own ActionResult that use a PDF library to generate the PDF. Another possible scenario is the RSS feed: read more about how to write a RssResult in this post.
Look at implementing a custom action result when the only peculiarity is how the result is returned to the user.
Probably you are not going to write your own view engine, but there are a few that you might consider using instead of the default WebForm view engine. The most interesting one, IMHO, is Spark.
But if you really want to write your own view engine, have a look at this post by Brad Wilson: Partial Rendering & View Engines in ASP.NET MVC
Views must be very dumb and thin, and they should only have html markup and calls to HtmlHelpers. There should be no code inside the views, so helpers come very handy for extracting the code from the view and putting it into something that is testable. As Rob Conery says: “If there's an IF, make a Helper”.
What is an HtmlHelper? Basically it’s just an extension method of the HtmlHelper class, but that’s the only requirement.
You can read more about how HtmlHelpers are a great way to encapsulate code for view on Rob’s post: Avoiding Tag Soup.
Which one should you use in your applications?
As you might have guess, not all the application you write need you to extend the framework in all the 13 extension points above. The ones the are most likely needed in every applications are ActionFilters and HtmlHelpers. Then you might probably want to use extensions someone else wrote, like an alternative ControllerFactory to use a IoC container or ViewEngine to get rid of the WebForm taste.
But, it’s important to experiment on these extension points so that you know which are your options and you are ready to exploit their power when needed. In the next weeks I’m going to write a few posts on how some of these extension points can be used.
And if you want to read that topics covered more in detail you might consider buying one of the many books that are coming out about ASP.NET MVC, like, “Beginning ASP.NET MVC” (which I happen to be the author) or “Professional ASP.NET MVC” (written by the team that wrote the framework itself) or “ASP.NET MVC in Action” (by Jeffrey Palermo and Ben Scheirman)
Did I miss some extension point you think it’s important to use? Did you use any of the extension points I listed above? I’d love to hear from you which scenario you addressed.