Controller lookup and default controller factory
Controller factory
As the name, the Controller factory is the component responsible for searching and creating controllers. Like many other components, ASP.NET MVC has a built-in factory named DefaultControllerFactory that's pretty much sufficient in most cases.
Searching a controller is a perform intensive job and the DefaultControllerFactory does some efficient caching mechanism to avoid looking for controllers every time when there is a need. In this article we are going to explore how the DefaultControllerFactory searches for a controller and what kind of strategies it uses to do the process efficiently.
Knowing how the DefaultControllerFactory works will help when we go for creating a custom factory. First we will see the basics then we slowly dive into the inner workings of the factory and its dependencies.
What makes a component controller factory?
Any class that implements IControllerFactory is eligible to call itself a controller factory. The IControllerFactory is a simple interface contains only three methods.
public interface IControllerFactory { IController CreateController(RequestContext requestContext, string controllerName); SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); void ReleaseController(IController controller); }
Listing 1. IControllerFactory interface
The methods are self explanatory. The most important one is of-course the CreateController and that's what we are going to see mostly in this article.
As you see the CreateController method takes couple of parameters. The second parameter controllerName is very important which is the key for the factory to find the controller.
DefaultControllerFactory
The DefaultControllerFactory implements the methods of IControllerFactory as virtual. It splits the CreateController method into two virtual methods namely GetControllerType and GetControllerInstance.
Creating a brand new controller factory is quite a complex job and many times instead of creating a brand new factory by implementing the IControllerFactory it's wise to extend the DefaultControllerFactory. The public/protected virtual methods in the factory class easily helps us to extend with only overriding the required methods.
As default, MVC registers the DefaultControllerFactory as the factory for creating controllers in the ControllerBuilder. We can set our own controller factory by calling the SetControllerFactory methods in Application_Start event.
For example,
ControllerBuilder.Current.SetControllerFactory(new MyControllerFactory());
or
ControllerBuilder.Current.SetControllerFactory(typeof(MyControllerFactory));
Listing 2. Registering custom controller factory
You can see the source code of DefaultControllerFactory here.
How the DefaultControllerFactory creates a controller?
At a higher level the DefaultControllerFactory does the following things,
- Searches for controller types that matches the name.
- If it finds a single type that matches the name, instantiates that type and returns the instance.
- If it finds more than one type matches the name then throws an ambiguous exception.
- If it not even finds a single type then throws a 404 exception.
Where are the places the controller factory searches?
The first place the factory looks for the controller types is in the namespaces assigned at the RouteData's DataTokens property. When we create a route we can pass the namespaces for the controllers that will handle the requests.
For example,
routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "MvcApp.Controllers" } // namespace of controllers );
Listing 3. Passing controllers namespaces while creating routes
The passed array of namespaces is stored in the DataTokens property of the Route class and this is the place where our default factory goes after first. If the factory couldn't able to find a single controller type that matches the name then it goes for the second place.
The second place is the namespaces that is assigned in the DefaultNamespaces property of ControllerBuilder. Like we pass the namespaces of controllers in routes we can also set the namespaces of the controllers at a global level through the ControllerBuilder.
For example,
ControllerBuilder.Current.DefaultNamespaces.Add("Reusables.Controllers");
Listing 4. Setting controllers namespaces in DefaultNamespaces property
If the factory fails to find a controller type that matches the controllerName in this place too.. it goes for searching all the namespaces of the current executing assembly as well as the referenced assemblies.
If the factory could able to succeed by finding a single controller type, it instantiates the type and return the instance. If it finds more than one types matching the name, it throws an ambiguous exception and sit silent. If it can't even find a single type then throws the 404 error.
Why & what caching strategy is used?
The factory has to use reflection to discover the controller types in the assemblies. Reflection is of-course a performance intensive stuff. So, to avoid searching controllers every-time, the factory caches the discovered controller types.
The caching strategy used is quite interesting one. At a simple level, I can say that all found controller types are stored as an xml file. The name of the cached file is MVC-ControllerTypeCache.xml which typically looks like below.
<?xml version="1.0" encoding="utf-8"?> <!--This file is automatically generated. Please do not modify the contents of this file.--> <typeCache lastModified="04/01/2012 16:35:03" mvcVersionId="3cff62e5-ef21-4e58-897f-d0f1eafd3beb"> <assembly name="MvcApp.Controllers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"> <module versionId="3fb0cce6-10dd-43d3-a44c-00046017b574"> <type>MvcApp.Controllers.HomeController</type> <type>MvcApp.Controllers.AccountController</type> </module> </assembly> <assembly name="MvcContrib, Version=2.0.36.0, Culture=neutral, PublicKeyToken=null"> <module versionId="889dd733-c7a0-4ae6-8f50-934f417174ea"> <type>MvcContrib.PortableAreas.EmbeddedResourceController</type> <type>MvcContrib.SubController</type> </module> </assembly> </typeCache>
Listing 5. The xml cache file that stores the controller types
If you look at the xml file you can see that there is a root element named typeCache that contains a group of assembly elements. Each assembly element contains one or more module elements and finally each module contains one or more controller types.
The cached xml file is created using the BuildManager class. Most of us not aware about this BuildManager class. This class exists in the System.Web.Compilation namespace which provides a bunch of methods for compilation and others.
Some of the interesting methods are,
- AddReferencedAssembly(Assembly assembly)
- This method is used to dynamically add a reference to an assembly.
- GetReferencedAssemblies()
- Returns the referenced assemblies as a collection.
- CreateCachedFile(string fileName)
- Creates a cached file.
- ReadCachedFile(string fileName)
- Reads the contents of the cached file.
There are also other interesting methods like CreateInstanceFromVirtualPath, GetCompiledAssembly and more. It's worth to spend some time to check this class for knowing its full capabilities. The couple of methods that we are interested are the last ones CreateCachedFile and ReadCachedFile.
The CreateCachedFile method is called to create our xml cache file. The important advantages we gain by creating the xml file this way are,
- The cache is preserved even the AppDomain reboots, if the controller types are stored in other ways like memory cache they will be lost when AppDomain recycles.
- The xml file is updated automatically. When we add new controllers or remove the existing ones, we probably go for a build, since the application is recompiled, at runtime the BuildManager recreates the xml file.
I hope, now you got some idea about the caching strategy used by the framework. Let see the components that supports the DefaultControllerFactory for searching the types and especially caching.
Supporting classes
The DefaultControllerFactory depends on following classes for controller lookup and caching.
- ControllerTypeCache
- TypeCacheUtil
- TypeCacheSerializer
Let see about each class and how they helps.
ControllerTypeCache
This is the class our DefaultControllerFactory directly interacts with. You can see the complete source code of this class here. This class mainly does couple of things.
- Maintains a temporary dictionary to store all the controller types extracted from the xml cache file.
- Returns the matching controller types to the controller factory based on the passed name in the passed namespaces.
You may think why we need a temporary dictionary to store the controller types since we already have the types cached in xml file. The reason is the controller factory has to make more than one call to ControllerTypeCache to get the controller type.
As I told before, the controller factory searches the controller type first in the namespaces available in the DataTokens and next in the DefaultNamespaces. If no type matches in both these places then it goes for searching every namespace. So it has to make more than one call to get ControllerTypeCache passing the controller name and namespaces, instead of reading the types from the xml file every time it would be wise to store the read types in memory and for that we need a dictionary.
Lookup dictionary
This is what the dictionary looks in ContollerTypeCache.
private volatile Dictionary<string, ILookup<string, Type>> _cache;
Listing 6. Temporary lookup dictionary that stores controller types
The dictionary contains keys as the controller names and the values as namespace/type mappings. The ControllerTypeCache contains couple of methods EnsureInitialized and GetControllerTypes. Both these methods are called by the factory.
- The EnsureInitialized method initializes the dictionary. It reads all the stored controller types from the xml file and store in the dictionary. Our default factory calls this method once before looking for the types.
- The GetControllerTypes is the method that returns the controller types that matches the name in the namespaces. This method is called by three times from the factory. First time it calls when it searches the type passing the namespaces set in the DataTokens property. Next it calls when it searches in the namespaces set in the DefaultNamespaces. Last time when it searches in all the available namespaces.
TypeCacheUtil
This is an utility class that interacts with the xml cache store. It contains a single public method GetFilteredTypesFromAssemblies which returns all the controller types that would be temporarily stored as dictionary in ControllerTypeCache.
The GetFilteredTypesFromAssemblies method first checks whether the xml cache returns types. If not, it gets all the controller types from the referenced assemblies and save it to the xml cache file.
This class uses BuildManager for read/write the xml cache file. To read the contents from the xml file it calls the ReadCachedFile method and for writing the contents it calls the WriteCachedFile method. You can see the complete source code of TypeCacheUtil right here.
TypeCacheSerializer
The name explains what it does! We have to serialize the discovered controller types into xml and later deserialize the xml back to types and that's what this class does. All it contains is a couple of methods Serialize and Deserialize.
This class is called by TypeCacheUtil to save and read the types from the cache file. You can see the source code here.
Big picture
I hope now you got a good understanding of how the DefaultControllerFactory does the controller lookup and saves the performance by caching. Let's recap our knowledge by seeing the big picture.
Controlling session behavior of controllers
A controller factory should also implement GetControllerSessionBehavior and ReleaseController methods. The session behavior of a controller is decided by the GetControllerSessionBehavior method. This method returns SessionStateBehavior which is an enumeration shown below,
public enum SessionStateBehavior { Default = 0, Required = 1, ReadOnly = 2, Disabled = 3 }
Listing 7. SessionStateBehavior enumeration
Default
In the default behavior, ASP.NET logic is used to determine the session state behavior. The default logic looks for the existence of marker session state interfaces on the System.Web.IHttpHandler.
Required
Full read-write session state behavior is enabled for the request. This setting overrides whatever session behavior would have been determined by inspecting the handler for the request.
ReadOnly
Read-only session state is enabled for the request. This means that session state cannot be updated. This setting overrides whatever session state behavior would have been determined by inspecting the handler for the request.
Disabled
Session state is disabled for processing the request. This setting overrides whatever session behavior would have been determined by inspecting the handler for the request.
The DefaultControllerFactory implements the GetControllerSessionBehavior such that the SessionStateBehavior is decided by the presence of SessionStateAttribute. The SessionStateAttribute takes the SessionStateBehavior in the constructor. For ex. to disable the session for a controller you can decorate the controller with the SessionStateAttribute passing SessionStateBehavior.Disabled in the constructor.
[SessionState(SessionStateBehavior.Disabled)] public class NoSessionController: Controller { }
Listing 8. Applying SessionStateAttribute to controllers
The DefaultControllerFactory checks if the controller is marked with the SessionStateAttribute and if yes it returns the SessionStateBehavior set in the attribute else return the default state i.e. SessionStateBehavior.Default.
Releasing controllers
The ReleaseController method is used to do cleanup. The DefaultControllerFactory checks if the controller implements IDisposable and if yes it calls the Dispose method.
Summary
In this article we explored how the DefaultControllerFactory performs controller lookup and how it applies caching mechanism to improve the performance. Sometimes the DefaultControllerFactory is not sufficient for our needs and we are forced to create a custom made one. At those times if we don't know how the DefaultControllerFactory works and what are the measurements it takes to speed-up the performance, we may end-up creating a simple factory which is not efficient.
Unfortunately the classes that supports DefaultControllerFactory for controller lookup and caching are mostly internal so we have to recreate those classes if we want to utilize them in a custom factory.