Because ASP.NET MVC has been designed with extensibility as its design principle; almost every logical step of the processing pipeline can be replaced with your own implementation. In fact, the best way to develop applications with ASP.NET MVC is to extend the system, Simone starts a series that explains how to implement extensions to ASP.NET MVC, starting with the ones at the beginning of the pipeline (routing extensions) and finishing with the view extensions points.
If you are not extending, you are not doing it right: this is the main tenet that you must keep in mind while developing web applications with ASP.NET MVC.
I admit this is a strong statement, but over the next few months I'm going to explain why extending is the right way to develop with ASP.NET MVC, together with explaining when and where you can extend it, and, most importantly, how to do it. But not just yet: to start with, we will look at how a request is processed by the framework, and where during this processing you can replace the default behavior or inject your own logic. This obviously underpins everything we’ll be covering in later articles, so let’s make sure we understand this clearly.
Given that we will be discussing several advanced topics in the coming months, it’s worth making sure you have a working knowledge of object oriented programming, .NET, ASP.NET, and the basics of ASP.NET MVC. All of the code samples I give you will be written in C#, so you’ll need to be comfortable with this language as well. The code samples will also be developed on .NET 4 and Visual Studio 2010.
What is extensibility?
The English dictionary defines extensibility as:
"the capability of being extended"
In computer science terms, a software system is extensible when it can be expanded without the need for changing and recompiling the original source code. This is also referred to as "open/closed principle", taken from Bertrand Meyer’s quite well-known writings about object-oriented programming:
"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification."
- “Open for extension” means that an entity can be made to behave in different ways from the original requirements, and that new requirements can be added.
- “Closed for modification” means that the code of such an entity must not be changed once it is declared completed.
If a system does not meet these criteria, it will become very difficult to maintain and evolve, quickly leading to obsolescence.
These criteria are even more important for a commercial software framework that is somehow opinionated and adopts lots of conventions. If it weren’t extensible, then it wouldn’t be used by developers who didn't like the opinions or conventions adopted.
Because of that, ASP.NET MVC has been designed with extensibility as one of the main objectives; almost every step of the processing pipeline can be replaced with your own implementation. For example, if you don't like the default convention of having actions as public methods of a class, whose name is the name of the controller plus "Controller", you might swap it with your own IControllerFactory that implements the convention you think is most suitable for you or your team.
Chances are you’ve already extended ASP.NET MVC at some point or other. If you’ve used a view engine other than WebForm View Engine or Razor (such as Spark, for example), or if you’ve used a IoC container to create your controllers (such as Ninject or Unity), then congratulations! You’ve already used extensions, albeit other people’s.
Where Extensibility Points fit into the Processing Pipeline
At a very high level, the lifecycle of a request in ASP.NET MVC is:
- The browser sends the GET or POST HTTP request;
- The request is inspected and routed to the right controller and the right action;
- The action collects or creates the data that needs to be returned to the browser;
- This data is passed to the view that renders the response, which is sent back to the browser.
Hopefully you already knew this, but to give you a bit more context regarding where we can place all the various extensibility hooks, next we’re going to step through the processing pipeline in more detail, and with the help a flow diagram to visually identify it. I know that there’s a lot of ground to cover here, and it can appear quite dense if you’re not familiar with it, but take your time as you go through this material, and it should all be clear by the end.
The Routing part of the pipeline
The first macro area of the pipeline is the routing.
Note: The complete flow chart for these steps is available to download as a .PDF from the speech bubble at the top of the article, or from here.
Upon receiving the request, the Routing module tries to match the incoming URL with the routing table of the application. The matching is usually done by evaluating the routing URL pattern, or can be made more powerful by specifying Route constraints (IRouteConstraint), allowing you to apply more complex evaluation logic, and even talk to databases or webservices.
Once the Route has been selected, the corresponding IRouteHandler is called. The default one calls the MvcHandler, which starts the real processing inside ASP.NET MVC. Obviously, if you replace the default handler with something completely different, you also take over the processing of the request, and so all the steps that follow might not apply anymore.
Creation of the Controller
Based on the route parameters and the default naming conventions, the IControllerFactory creates an instance of the controller. If an IoC container has been configured, the controller factory retrieves the instance of the controller with the help of the IDependencyResolver.
The Action execution
Once the controller has been instantiated, the specified action has to be executed, and this is handled by the IActionInvoker. As with the IRouteHandler, in theory you could completely replace this component with your own implementation, but then you will be developing against your own conventions. This is certainly not a problem, but it just means that the next steps will not apply:
The first part of the process involves identifying the method to execute: in the simple scenario, the method which shares the same name as the action is chosen, but this behavior can be modified by applying an attribute of type ActionNameSelectorAttribute, which allows you to apply custom logic to this process. If more than one method is found, the correct one is chosen with the help of another kind of attribute applied to the methods: ActionMethodSelectorAttribute.
If the method selected for execution has one or more parameters, they have to be filled in with the right values. The Model Binder (or rather, the IModelBinder interface) is the component responsible for this: it not only deserializes the HTTP request data into CLR objects, but also validates that the values are correct. This is done based on the validation rules (if the default ones are not enough you can write your own rules and validation logic) returned by the Model Validation Provider. Alternatively, another way to validate an object is to implement the IValidatableObject for cases where you need a validation rule that acts on the whole object, rather than just individual properties.
to the action. These filters are discovered via another component that can be customized and replaced: the IFilterProvider. In case you are using IoC, the filters are also instantiated using the IDependencyResolver. Out of the four types of filters available, two of them are executed before the actual action method: Authorization filters and part of the Action filters (specifically, OnActionExecuting). Filters are a very powerful way to implement orthogonal or cross-cutting behaviors that have to be shared among many actions or controllers. Filters can be applied per action, per controller, or even per application.
If the filters don’t stop the execution the action method is executed and the parameters are filled in with the values that were previously discovered by the Model Binder
When the action method has completed its tasks, it returns a result of type ActionResult. This is executed to return the response back to the client that sent the original request. However, there might be other filters that can be applied before the response is returned: the OnActionExecuted and the OnResultExecuting.
Finally, the View
There are many action results: one simply returns a text string, another returns a binary file or a Json formatted result. Yet the most important Action Result is the ViewResult, which renders and returns an HTML page to the browser.
In ASP.NET MVC 3 there are two standard view engines: the WebForm view engine and the new Razor view engine, and their views are just normal .aspx pages (typically without a code-behind file) or ASP.NET Web Pages. However, more than one view engine can be registered, so the first step in the execution of the View Result involves selecting the appropriate engine to use and the view to render; which is all done via the IViewEngine interface of the view engine (which, despite name, takes care of finding the correct view).
Once the correct view engine and the view is chosen, it is instantiated, again with the help of the IDependencyResolver, and the model is then passed to the view.
Another type of validation that happens on the client is remote validation; this is needed when the validation rules require access to resources on the server. For example, if you have a form in which you need to check whether a username has already been taken, the validator needs to call back to the server to determine whether the field is valid.
Now, when writing the code for the view, you will use three libraries:
They contain helper methods to write input fields, create links based on the routes, AJAX-enabled forms, links and more. Moreover, extending them is very easy, and writing new methods for these libraries, as opposed to writing the code directly inline in the view, is also considered an ASP.NET MVC programming best practice.
Types of extensibility points
Now that we’ve gone over the process pipeline in more detail, you hopefully have a better idea of what’s happening under the hood, and you should also be starting to get a feel for the number of opportunities you have to extend your ASP.NET MVC application. However, as you might have already noticed, not all extensibility points need to be used with the same frequency:
Some customizations are integral parts of the development of a web application with ASP.NET MVC, and thus are used in every project. The most common extensibility points in this category are:
- Custom templates for the templated helpers
- Server-side validation rules
- Client-side validation rules
- HTML Helpers
Other extensibility points are not always needed, but if used can help make development more productive and less repetitive:
- A base controller to enforce your own conventions
- AJAX/URL Helpers
- Action/Result Filters
Next up, there are other extensibility points that you only need to use if you need to solve a specific problem:
- Route Constraint
- Route Handler
- Action Result
- Model Binders
Finally, there are the extensibility points that change a big part of ASP.NET MVC, and which you are not very likely to implement. These might even remove some of the other extensibility points and add their own. Nevertheless, there is a very high chance that you used a custom component for one of these points that was developed by someone else:
- Controller Factory
- Action Invoker
- View Engine
- Model Validation Provider
- Model Metadata Provider
How these Articles will be structured
During the course of these articles, I'm going to explain how to implement your extensions to ASP.NET MVC, starting with the ones at the beginning of the pipeline (routing extensions) and finishing with the view extensions points. I'll focus mainly on the first two categories (“Must Use” and “Productivity Enhancers”), but I'll also briefly touch upon the more rarely implemented ones, where there are useful modifications to be made.
For each point explained, a common structure will be followed:
- why your own custom implementation might be needed
- area of extensibility (routing, controller, views, etc)
- type of extensibility (must use, only when necessary, etc)
- the interfaces that have to be implemented
- what the default implementation (if it exists) does
- sample of already available implementations in the community (if any)
- sample implementation
Thus far we’ve seen an overview of the entire ASP.NET MVC processing pipeline, and should now have a feel for the huge number of opportunities available to extend and customize how the framework behaves. We’ve also seen that not all extensibility hooks are created equal, and there are some which are more useful than others. Now that we’re all on the same page, the next article will cover how the routing process can be extended.
The free wallchart, 'ASP.NET MVC Pipeline', that goes with this article is available as a PDF file from the speechbubble at the head of the article or from here. It is best printed on an A3 printer.