Whenever a request is received by MVC, it is the job of the routing engine to match the request URL with the registered routes. After finding the matched route, the MvcRouteHandler
is called to provide a suitable handler for the request. It is the MvcHandler
which is tasked with the generation of response for the ongoing request being processed. In its ProcessRequestInit()
method, a call is made to to get the controller factory, which returns an instance of the DefaultControllerFactory
class. Using this instance of the DefaultControllerFactory
class, the CreateController()
method is invoked to return a controller. You can know more about MvcRouteHandler
and MvcHandler
in my previous post.
In this post, let's explore the process of controller instantiation by DefaultControllerFactory
class.
The DefaultControllerFactory
class implements IControllerFactory
interface. It is the responsibility of the controller factory for the creation of an instance of controller class that needs to be invoked inorder to generate the response.
IControllerFactory Interface
The IControllerFactory
interface exposes methods which when implemented by an any class makes the class to behave as a controller factory. We can create our custom controller factory by implementing IControllerFactory
and registering it in the Application_Start()
event by calling the SetControllerFactory()
method. This is how IControllerFactory
looks like :
namespace System.Web.Mvc {
using System.Web.Routing;
using System.Web.SessionState;
public interface IControllerFactory {
IController CreateController(RequestContext requestContext, string controllerName);
SessionStateBehavior GetControllerSessionBehavior(
RequestContext requestContext, string controllerName);
void ReleaseController(IController controller);
}
}
It is the CreateController()
method, to which the implementing class gives a body in order to create an instance of the matched controller. The ReleaseController()
method is used for the disposal of the controller instance.
DefaultControllerFactory Class
The DefaultControllerFactory
implements the methods of IControllerFactory
as virtual. As default, MVC registers the DefaultControllerFactory
as the factory for creating controllers in the ControllerBuilder
class constructor.
DefaultControllerFactory() constructor
In DefaultControllerFactory
class, apart from the default constructor, it has a parametrized constructor accepting the IControllerActivator
type. This allows one to create a custom controller activator with DefaultControllerFactory
class.
Note : Entire code for this class can be found in codeplex.
public DefaultControllerFactory()
: this(null, null, null) {
}
public DefaultControllerFactory(IControllerActivator controllerActivator)
: this(controllerActivator, null, null) {
}
internal DefaultControllerFactory(IControllerActivator controllerActivator,
IResolver<IControllerActivator> activatorResolver,
IDependencyResolver dependencyResolver) {
if (controllerActivator != null) {
_controllerActivator = controllerActivator;
}
else {
_activatorResolver = activatorResolver ??
new SingleServiceResolver<IControllerActivator>(
() => null,
new DefaultControllerActivator(dependencyResolver),
"DefaultControllerFactory contstructor"
);
}
}
Both the default and parametrized constructor call another internal constructor. It is the responsibility of this internal controller to create an instance of the controller activator in case if it is not received by it. Now, if a controller activator is being received by it, that means that there is a custom implementation for controller activator that is being provided by the user, if a null value is received, it checks whether there is any service that resolves controller activator using the activatorResolver
argument. The activatorResolver
is of type IResolver<IControllerActivator>
, it specifies a resolver that knows how to locate a IControllerActivator
type. If both controllerActivator
and activatorResolver
values are null, then an instance of the DefaultControllerActivator
is created. DefaultControllerActivator
is an inner class inside the DefaultControllerFactory
class. This is how the inner class looks :
private class DefaultControllerActivator : IControllerActivator {
Func<IDependencyResolver> _resolverThunk;
public DefaultControllerActivator()
: this(null) {
}
public DefaultControllerActivator(IDependencyResolver resolver) {
if (resolver == null) {
_resolverThunk = () => DependencyResolver.Current;
}
else {
_resolverThunk = () => resolver;
}
}
public IController Create(RequestContext requestContext, Type controllerType) {
try {
return (IController)(_resolverThunk().GetService(controllerType) ??
Activator.CreateInstance(controllerType));
}
catch (Exception ex) {
throw new InvalidOperationException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.DefaultControllerFactory_ErrorCreatingController,
controllerType),
ex);
}
}
}
In order to create an instance of DefaultControllerActivator
class, a call is made to the SingleServiceResolver()
method, which delegates the call to GetSingleService()
method in DependencyResolver
class, which creates an instance of the IControllerActivator
type using its default constructor.
CreateController() method
This is how DefaultControllerFactory
class gives body to the implemented CreateController()
method along with its required internal methods.
private IControllerActivator ControllerActivator {
get {
if (_controllerActivator != null) {
return _controllerActivator;
}
_controllerActivator = _activatorResolver.Current;
return _controllerActivator;
}
}
public virtual IController CreateController(RequestContext requestContext, string controllerName) {
if (requestContext == null) {
throw new ArgumentNullException("requestContext");
}
if (String.IsNullOrEmpty(controllerName)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
}
Type controllerType = GetControllerType(requestContext, controllerName);
IController controller = GetControllerInstance(requestContext, controllerType);
return controller;
}
protected internal virtual IController GetControllerInstance(
RequestContext requestContext, Type controllerType) {
if (controllerType == null) {
throw new HttpException(404,
String.Format(
CultureInfo.CurrentCulture,
MvcResources.DefaultControllerFactory_NoControllerFound,
requestContext.HttpContext.Request.Path));
}
if (!typeof(IController).IsAssignableFrom(controllerType)) {
throw new ArgumentException(
String.Format(
CultureInfo.CurrentCulture,
MvcResources.DefaultControllerFactory_TypeDoesNotSubclassControllerBase,
controllerType),
"controllerType");
}
return ControllerActivator.Create(requestContext, controllerType);
}
protected internal virtual Type GetControllerType(
RequestContext requestContext, string controllerName) {
if (String.IsNullOrEmpty(controllerName)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
}
object routeNamespacesObj;
Type match;
if (requestContext != null &&
requestContext.RouteData.DataTokens.TryGetValue(
"Namespaces", out routeNamespacesObj)) {
IEnumerable<string> routeNamespaces = routeNamespacesObj as IEnumerable<string>;
if (routeNamespaces != null && routeNamespaces.Any()) {
HashSet<string> nsHash =
new HashSet<string>(routeNamespaces, StringComparer.OrdinalIgnoreCase);
match = GetControllerTypeWithinNamespaces(
requestContext.RouteData.Route, controllerName, nsHash);
if (match != null || false.Equals(
requestContext.RouteData.DataTokens["UseNamespaceFallback"])) {
return match;
}
}
}
if (ControllerBuilder.DefaultNamespaces.Count > 0) {
HashSet<string> nsDefaults = new HashSet<string>(
ControllerBuilder.DefaultNamespaces, StringComparer.OrdinalIgnoreCase);
match = GetControllerTypeWithinNamespaces(
requestContext.RouteData.Route, controllerName, nsDefaults);
if (match != null) {
return match;
}
}
return GetControllerTypeWithinNamespaces(
requestContext.RouteData.Route, controllerName, null );
}
It is inside the MvcHandler
class, ProcessRequestInit()
method where an instance of DefaultControllerFactory
class is created. After creation of the instance a call is made to the CreateController()
method. The responsibilities of CreateController()
method are as such
- Searches for controller types that matches the controller name.
- If it finds a single controller type that matches the name, instantiates that type and returns its instance.
- If it finds more than one controller type matches the name then it throws an ambiguous exception.
- If not even a single controller type is found, then it gives a http 404 response.
The above work to be done by CreateController()
method is split into two virtual methods namely GetControllerType()
and GetControllerInstance()
methods.
Taking a closer look at the GetControllerType()
method, we can conclude that, the first place the factory looks for the controller type is in the namespaces assigned in the RouteData
's
DataTokens
property. When we create a route we can pass the namespaces of the controllers that will handle the requests. Such as,
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new[] { "MyMvcApp.Controllers" }
);
In order to check for the presence and get the namespaces the TryGetValue()
method of DataTokens
is called. A hashset is created out of the result of TryGetValue()
method, inorder to get unique namespaces. Using the matched route for the controller, hashset and controller name a call is made to GetControllerTypeWithinNamespaces()
method. This is how it looks :
private Type GetControllerTypeWithinNamespaces(RouteBase route,
string controllerName, HashSet<string> namespaces) {
ControllerTypeCache.EnsureInitialized(BuildManager);
ICollection<Type> matchingTypes =
ControllerTypeCache.GetControllerTypes(controllerName, namespaces);
switch (matchingTypes.Count) {
case 0:
return null;
case 1:
return matchingTypes.First();
default:
throw CreateAmbiguousControllerException(
route, controllerName, matchingTypes);
}
}
DefaultControllerFactory
class uses reflection to discover the controller types in the assemblies. Reflection itself is a costly affair. So, inorder to avoid searching controllers every-time, the factory caches the discovered controller types. The ControllerTypeCache
class is used for this purpose. All found controller types are stored as an xml file named as MVC-ControllerTypeCache.xml
. The first thing the GetControllerTypeWithinNamespaces()
method does is, it makes a call to the EnsureInitialized()
method of ControllerTypeCache
class. The result is, all the stored controller types are read from the xml file and are stored in a dictionary. After that a call is made to the GetControllerTypes()
method of ControllerTypeCache
class, passing the controller name and hashset of namespaces. The output is all the controller types that matches the name in the hashset of namespaces.
Depending upon the count of the matchingTypes
, the return type is decided. If there no matching controller type for the hashset of namespaces send, then it returns a null value. If there is a single entry, then it returns the matching controller type. Otherwise if there is more than one, matching controller type, then a call is made to CreateAmbiguousControllerException()
method which raises an InvalidOperationException
exception. This is how the CreateAmbiguousControllerException()
method looks like :
internal static InvalidOperationException CreateAmbiguousControllerException(
RouteBase route, string controllerName, ICollection<Type> matchingTypes) {
StringBuilder typeList = new StringBuilder();
foreach (Type matchedType in matchingTypes) {
typeList.AppendLine();
typeList.Append(matchedType.FullName);
}
string errorText;
Route castRoute = route as Route;
if (castRoute != null) {
errorText = String.Format(CultureInfo.CurrentCulture,
MvcResources.DefaultControllerFactory_ControllerNameAmbiguous_WithRouteUrl,
controllerName, castRoute.Url, typeList);
}
else {
errorText = String.Format(CultureInfo.CurrentCulture,
MvcResources.DefaultControllerFactory_ControllerNameAmbiguous_WithoutRouteUrl,
controllerName, typeList);
}
return new InvalidOperationException(errorText);
}
After getting the matched controller type, the control is back to the GetControllerType()
method of DefaultConrollerFactory
class.
Now the question is, what if there is no namespaces assigned in the RouteData's DataToken
property or if it is unable to find a single controller type that matches the namespace; then it moves to the second place, that is inside the namespaces that are 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 also through the ControllerBuilder
class. For example,
ControllerBuilder.Current.DefaultNamespaces.Add("MyMvcApp.Controllers");
Again for these defined namespaces, the same above process will be followed, that is creation of a hashset of namespaces, making a call to GetControllerTypeWithinNamespaces()
method to determine a controller type and returning the matching controller type, if there is one.
What if, it again fails for the above case, that is, there is no namespaces assigned in the RouteData's DataToken
property (or namespaces are there but it is unable to find a single controller type that matches the namespace assigned in the RouteData's DataToken
property) and there is no namespaces that are assigned in the DefaultNamespaces
property of ControllerBuilder
at global level (or namespace are there but it is unable to find a single controller type that matches the namespace assigned in the DefaultNamespaces
property of ControllerBuilder
). In such a case, it searches in all the namespaces of the current executing assembly as well as the referenced assemblies. In order to do this it makes a call to the GetControllerTypeWithinNamespaces()
method with the null value for namespace argument, who forwards this null value to ControllerTypeCache
class, GetControllerTypes()
method. A null value tells the GetControllerTypes()
method to search in all current executing assembly as well as the referenced assemblies.
NOTE : Now suppose if we want to prevent searching for controllers in other namespaces, which happens by default if it is not found in the specified namespace. We can do so by setting DataToken
property UseNamespaceFallback
value to false. This will stop checking of controller type in other namespaces, and will return a null value for the controller type.
Finally, the control is back to the CreateController()
method, after getting a matched controller type. Using this controller type a call is made to the GetControllerInstance()
method to create an instance of the matched type. If the controller type returned is null, then an HttpException
exception is raised with http status code of 404. Otherwise another check is made to ensure that the matched controller type is assignable to IController
interface. If everything goes fine using the ControllerActivator
property we get a reference to the DefaultControllerActivator
class (which was created by DefaultControllerFactory
class constructor). Using the DefaultControllerActivator
reference a call is made to its Create()
method, which creates an instance of the controller, using the controller type default constructor.
ReleaseController()
The ReleaseController()
method implementation, in the DefaultControllerFactory
class looks like this :
public virtual void ReleaseController(IController controller) {
IDisposable disposable = controller as IDisposable;
if (disposable != null) {
disposable.Dispose();
}
}
The name itself gives a clear indication that the method is used to release the controller instance.
The ReleaseController()
method is called in the ProcessRequest()
method of MvcHandler
class, after the call to Execute()
method for the controller has been completed. It marks the end of the process of response generation, with the release of the controller. In order to release the controller the Dispose()
method of the Controller
class is called (as every controller in ASP.NET MVC derives from Controller
class). Dispose()
method calls the SuppressFinalize()
method of GC
class, for the object it is disposing. SuppressFinalize tells the garbage collector that the object was cleaned up properly and doesn't need to go onto the the finalizer queue.
GetControllerSessionBehavior()
The third and last implementation of IControllerFactory
interface is the GetControllerSessionBehavior()
method. It is called inside the GetSessionStateBehavior()
method of MvcRouteHandler
class. The method body is as such :
private static readonly ConcurrentDictionary<Type, SessionStateBehavior>
_sessionStateCache = new ConcurrentDictionary<Type, SessionStateBehavior>();
SessionStateBehavior IControllerFactory.GetControllerSessionBehavior(
RequestContext requestContext, string controllerName) {
if (requestContext == null) {
throw new ArgumentNullException("requestContext");
}
if (String.IsNullOrEmpty(controllerName)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "controllerName");
}
Type controllerType = GetControllerType(requestContext, controllerName);
return GetControllerSessionBehavior(requestContext, controllerType);
}
protected internal virtual SessionStateBehavior GetControllerSessionBehavior(
RequestContext requestContext, Type controllerType) {
if (controllerType == null) {
return SessionStateBehavior.Default;
}
return _sessionStateCache.GetOrAdd(
controllerType,
type =>
{
var attr = type.GetCustomAttributes(typeof(SessionStateAttribute), inherit: true)
.OfType<SessionStateAttribute>()
.FirstOrDefault();
return (attr != null) ? attr.Behavior : SessionStateBehavior.Default;
}
);
}
This method is used to manage the session state behaviour for the controller. By default, Asp.Net MVC supports session state. Session is used to store data values across requests. Whether you store some data values with in the session or not Asp.Net MVC manages the session state for all the controllers in your application. In order to manage the session state for the controller type, it checks for the presence of, SessionStateAttribute
on given controller type, if it finds one, then it returns the SessionStateBehavior
enumeration value set in the SessionStateAttribute
, or else it returns the default session state i.e., SessionStateBehavior.Default
. The SessionStateBehavior.Default
enumeration value specifies the usage of default ASP.NET behaviour, which is to determine the session state configuration from HttpContext
.
The SessionStateBehavior
for the controller type gets stored in a static object of ConcurrentDictionary
class, which makes it accessible by multiple request threads. It acts like a cache of all matched controller types with their corresponding SessionStateBehavior
value.
With this we are done with the complete explanation of purpose and working of the DefaultControllerFactory
class.