In order to create an instance of a controller, the DefaultControllerFactory
class, uses .NET reflection. It searches through the different assemblies, in order to find out a matching controller type. Using this controller type it instantiates the controller class. You can know more about working of DefaultControllerFactory
class in my previous post.
Now, reflection itself is a very costly affair, you may refer this SO discussion to get a fair idea about the overhead it causes. Hence to avoid this overhead, MVC framework, uses an efficient caching mechanism. In order to avoid searching for controllers types every time, it remembers the discovered controllers types each time. In order to remember the discovered controllers, it creates a XML file, at the application start and goes on adding each newly discovered controller type to it, on subsequent requests. After knowing about it, you must be interested in having a look at the dynamically generated cache file. You will find it here, C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\NAMEOFYOURAPP\xxxxx\xxxxxxxx\UserCache\
folder. You will find two XML files over there, the one we are interested in, is named as MVC-ControllerTypeCache.xml
. The other one, MVC-AreaRegistrationTypeCache.xml
which is used to cache areas. MVC-ControllerTypeCache.xml looks likes this :
="1.0" ="UTF-8"
-->
<typeCache mvcVersionId="23916767-7e3a-4fab-ad85-0bb94082b2dd" lastModified="3/31/2013 12:03:09 PM">
<assembly name="MyMVCApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
<module versionId="6c6e3d2b-46c9-4a49-842d-c1cee44511ca">
<type>MyMVCApp.Controllers.HomeController</type>
<type>MyMVCApp.Controllers.AccountController</type>
</module>
</assembly>
</typeCache>
The whole process of creation and maintenance of cache file is handled by three classes :
- ControllerTypeCache
- TypeCacheUtil
- TypeCacheSerializer
Lets look into each of these classes to understand the role they play.
ControllerTypeCache & TypeCacheUtil class
ControllerTypeCache
class is place where controller types is retrieved from cache using TypeCacheUtil
class, grouping controller types by its name to retrieve a requested controller from given namespaces.
The first call that is made to the ControllerTypeCache
class, is from inside the GetControllerTypeWithinNamespaces()
method of DefaultControllerFactory
class. The call is made to the EnsureInitialized()
method of the ControllerTypeCache
class, passing a reference of BuildManagerWrapper
class. This how the method looks :
Note : The entire code for this class can be found in Codeplex.
private const string _typeCacheName = "MVC-ControllerTypeCache.xml";
private Dictionary<string, ILookup<string, Type>> _cache;
private object _lockObj = new object();
public void EnsureInitialized(IBuildManager buildManager) {
if (_cache == null) {
lock (_lockObj) {
if (_cache == null) {
List<Type> controllerTypes = TypeCacheUtil.GetFilteredTypesFromAssemblies(_typeCacheName, IsControllerType, buildManager);
var groupedByName = controllerTypes.GroupBy(
t => t.Name.Substring(0, t.Name.Length - "Controller".Length),
StringComparer.OrdinalIgnoreCase);
_cache = groupedByName.ToDictionary(
g => g.Key,
g => g.ToLookup(t => t.Namespace ?? String.Empty, StringComparer.OrdinalIgnoreCase),
StringComparer.OrdinalIgnoreCase);
}
}
}
}
EnsureInitialized()
method uses a double checked locking to ensure that initialization of the _cache
dictionary is performed only once for the entire lifetime of the application and it takes place when the first request hits the IIS. The _cache
dictionary, stores the controller type and the namespace in which it is found, against the controller name which acts as the key. In order to extract the controller types from the xml file, a call is made to the GetFilteredTypesFromAssemblies()
method of TypeCacheUtil
class. It is the sole public method of this class. The method looks like this :
Note : The whole code for this class can be found in Codeplex.
public static List<Type> GetFilteredTypesFromAssemblies(string cacheName, Predicate<Type> predicate, IBuildManager buildManager) {
TypeCacheSerializer serializer = new TypeCacheSerializer();
List<Type> matchingTypes = ReadTypesFromCache(cacheName, predicate, buildManager, serializer);
if (matchingTypes != null) {
return matchingTypes;
}
matchingTypes = FilterTypesInAssemblies(buildManager, predicate).ToList();
SaveTypesToCache(cacheName, matchingTypes, buildManager, serializer);
return matchingTypes;
}
The TypeCacheUtil
class is the one which actually operates on the xml cache file. The first thing the
GetFilteredTypesFromAssemblies()
method does is creates an instance of the TypeCacheSerializer
class, as the name suggests this class is used to write the discovered controller types into the xml cache and and later read it back from the xml file, when required. We will see more about this in a few moments. Afer that, an attempt is made to read the cache file, by calling the internal method ReadTypesFromCache()
and passing to it the cache file name, a reference of BuildManagerWrapper
class, the TypeCacheSerializer
reference and a delegate to the IsControllerType()
method of ControllerTypeCache
class. The ReadTypesFromCache()
method looks like this :
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")]
internal static List<Type> ReadTypesFromCache(string cacheName, Predicate<Type> predicate, IBuildManager buildManager, TypeCacheSerializer serializer) {
try {
Stream stream = buildManager.ReadCachedFile(cacheName);
if (stream != null) {
using (StreamReader reader = new StreamReader(stream)) {
List<Type> deserializedTypes = serializer.DeserializeTypes(reader);
if (deserializedTypes != null && deserializedTypes.All(type => TypeIsPublicClass(type) && predicate(type))) {
return deserializedTypes;
}
}
}
}
catch {
}
return null;
}
The first thing ReadTypesFromCache()
method does is, using BuildManagerWrapper
class reference, it makes a call to the ReadCachedFile()
method. BuildMangerWrapper
class implements IBuildManager
interface. Apart from the wide variety of job that this class does, the current job which it does, is reading the cahe file using ReadCachedFile()
method. It returns a file stream, which is then deserialized using TypeCacheSerializer
class. After deserialization it checks for each type, whether its public, of class type and not abstract in nature. Apart from these checks a call is also made to the IsControllerType()
method referenced by the predicate. The IsControllerType()
looks like this :
internal static bool IsControllerType(Type t) {
return
t != null &&
t.IsPublic &&
t.Name.EndsWith("Controller", StringComparison.OrdinalIgnoreCase) &&
!t.IsAbstract &&
typeof(IController).IsAssignableFrom(t);
}
The IsControllerType()
method does a vital check, it confirms whether the controller name is suffixed with "Controller"
or not. As every controller in MVC needs to be suffixed with "Controller"
string, if we are using the DefaultControllerfactory
class. If it passes through all the checks, then all the controler types are returned to the GetFilteredTypesFromAssemblies()
method. Otherwise, if the reading from the cache fails due to some reason, then it starts looking through all the assemblies. In order to do that it makes a call to the FilterTypesInAssemblies()
method.
private static IEnumerable<Type> FilterTypesInAssemblies(IBuildManager buildManager, Predicate<Type> predicate) {
IEnumerable<Type> typesSoFar = Type.EmptyTypes;
ICollection assemblies = buildManager.GetReferencedAssemblies();
foreach (Assembly assembly in assemblies) {
Type[] typesInAsm;
try {
typesInAsm = assembly.GetTypes();
}
catch (ReflectionTypeLoadException ex) {
typesInAsm = ex.Types;
}
typesSoFar = typesSoFar.Concat(typesInAsm);
}
return typesSoFar.Where(type => TypeIsPublicClass(type) && predicate(type));
}
FilterTypesInAssemblies()
uses BuildManagerWrapper
class, GetReferencedAssemblies()
method to get all the referenced assemblies needed for all page compilations. Then it loops through all of the extracted assemblies, gets its types and at last filters the types based on the call to TypeIsPublicClass()
and IsControllerType()
method.
So now we have all the matching types that are present in the assembly. In order to make use of these found types, the framework saves all these types again in the xml file. Hence a call is made to the SaveTypesToCache()
method, passing to it the cache file name, all matching types, reference to the buildManager and serializer class.
[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Cache failures are not fatal, and the code should continue executing normally.")]
internal static void SaveTypesToCache(string cacheName, IList<Type> matchingTypes, IBuildManager buildManager, TypeCacheSerializer serializer) {
try {
Stream stream = buildManager.CreateCachedFile(cacheName);
if (stream != null) {
using (StreamWriter writer = new StreamWriter(stream)) {
serializer.SerializeTypes(matchingTypes, writer);
}
}
}
catch {
}
}
SaveTypesToCache()
uses BuilManagerWrapper
's CreateCachedFile()
method to create a cache file. Then using the SerializeTypes()
of the TypeCacheSerializer
class to write down the types to the xml cache file. With this, the job of the GetFilteredTypesFromAssemblies()
method as well as the TypeCacheUtil
class completes and the found matching types are returned to the EnsureInitialized()
method. EnsureInitialized()
groups the types and saves it in the _cache
dictionary.
This ends the execution of EnsureInitialized()
method, called by GetControllerTypeWithinNamespaces(
)
of DefaultControllerFactory
class. Now we have all the controller types; we need to find out which controller type falls in the namespace. In order to find this, GetControllerTypes()
of ControllerTypeCache
class is called passing to it the controller name and namespaces.
public ICollection<Type> GetControllerTypes(string controllerName, HashSet<string> namespaces) {
HashSet<Type> matchingTypes = new HashSet<Type>();
ILookup<string, Type> nsLookup;
if (_cache.TryGetValue(controllerName, out nsLookup)) {
if (namespaces != null) {
foreach (string requestedNamespace in namespaces) {
foreach (var targetNamespaceGrouping in nsLookup) {
if (IsNamespaceMatch(requestedNamespace, targetNamespaceGrouping.Key)) {
matchingTypes.UnionWith(targetNamespaceGrouping);
}
}
}
}
else {
foreach (var nsGroup in nsLookup) {
matchingTypes.UnionWith(nsGroup);
}
}
}
return matchingTypes;
}
internal static bool IsNamespaceMatch(string requestedNamespace, string targetNamespace) {
if (requestedNamespace == null) {
return false;
}
else if (requestedNamespace.Length == 0) {
return true;
}
if (!requestedNamespace.EndsWith(".*", StringComparison.OrdinalIgnoreCase)) {
return String.Equals(requestedNamespace, targetNamespace, StringComparison.OrdinalIgnoreCase);
}
else {
requestedNamespace = requestedNamespace.Substring(0, requestedNamespace.Length - ".*".Length);
if (!targetNamespace.StartsWith(requestedNamespace, StringComparison.OrdinalIgnoreCase)) {
return false;
}
if (requestedNamespace.Length == targetNamespace.Length) {
return true;
}
else if (targetNamespace[requestedNamespace.Length] == '.') {
return true;
}
else {
return false;
}
}
}
GetControllerTypes()
searches for the controller name in the _cache
dictionary. After finding a matched entry in the dictionary it loops through the controller type collection to find the controller type that is present in the provided namespaces. In order to do a namespace match it sends the provided namespace and the namespace of the controller type to the IsNamespaceMatch()
match method. A hashset is formed out of all the controller types that qualify, to be returned. If no namespace is provided to the GetControllerTypes()
method, then it returns all the controller type found.
All the matching namespaces are then returned to the caller method i.e the GetControllerTypeWithinNamespaces()
method of DefaultControllerFactory
class. Using this controller type, DefaultControllerFactory
class instantiates the controller. With this we are done exploring the ControllerTypeCache
class as well.
BuildManagerWrapper class
BuildManagerWrapper
class implements the IBuildManager
interface, it contains methods which use BuildManager
class in System.web.compilation
namespace for compiling the code file at runtime and returning its type, getting all referenced assemblies, creating and reading cache files and checking the file exists or not by getting the object factory from specified virtual path. The class look like this :
Note : The entire code for this class can be found in Codeplex.
namespace System.Web.Mvc {
using System.Collections;
using System.IO;
using System.Web.Compilation;
internal sealed class BuildManagerWrapper : IBuildManager {
bool IBuildManager.FileExists(string virtualPath) {
return BuildManager.GetObjectFactory(virtualPath, false) != null;
}
Type IBuildManager.GetCompiledType(string virtualPath) {
return BuildManager.GetCompiledType(virtualPath);
}
ICollection IBuildManager.GetReferencedAssemblies() {
return BuildManager.GetReferencedAssemblies();
}
Stream IBuildManager.ReadCachedFile(string fileName) {
return BuildManager.ReadCachedFile(fileName);
}
Stream IBuildManager.CreateCachedFile(string fileName) {
return BuildManager.CreateCachedFile(fileName);
}
}
}
This is the class that is used by the TypeCacheUtil
class, in order to do various work. It uses CreateCachedFile()
method to create a cache file, in order to cache data, we write the contents of the cache file to Stream
object that is returned by this method. ReadCachedFile()
method is used for reading the contents of the cache file, we do so by reading from the Stream
object that is returned by this method. The GetReferencedAssemblies()
method is used for getting all the referenced assemblies that are used for successfull compilation of controller type. The GetCompiledType()
method compiles the code file available at the virtual path and returns its compiled type.
TypeCacheSerializer Class
In order to write down the contents of the cache file TypeCacheUtil
class uses these two methods, SerializeTypes()
and DeserializeTypes()
of TypeCacheSerializer
class, which are the only methods contained in the class. The method body looks like this :
Note : The entire code for this class can be found in Codeplex.
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This is an instance method for consistency with the SerializeTypes() method.")]
public List<Type> DeserializeTypes(TextReader input) {
XmlDocument doc = new XmlDocument();
doc.Load(input);
XmlElement rootElement = doc.DocumentElement;
Guid readMvcVersionId = new Guid(rootElement.Attributes["mvcVersionId"].Value);
if (readMvcVersionId != _mvcVersionId) {
return null;
}
List<Type> deserializedTypes = new List<Type>();
foreach (XmlNode assemblyNode in rootElement.ChildNodes) {
string assemblyName = assemblyNode.Attributes["name"].Value;
Assembly assembly = Assembly.Load(assemblyName);
foreach (XmlNode moduleNode in assemblyNode.ChildNodes) {
Guid moduleVersionId = new Guid(moduleNode.Attributes["versionId"].Value);
foreach (XmlNode typeNode in moduleNode.ChildNodes) {
string typeName = typeNode.InnerText;
Type type = assembly.GetType(typeName);
if (type == null || type.Module.ModuleVersionId != moduleVersionId) {
return null;
}
else {
deserializedTypes.Add(type);
}
}
}
}
return deserializedTypes;
}
public void SerializeTypes(IEnumerable<Type> types, TextWriter output) {
var groupedByAssembly = from type in types
group type by type.Module into groupedByModule
group groupedByModule by groupedByModule.Key.Assembly;
XmlDocument doc = new XmlDocument();
doc.AppendChild(doc.CreateComment(MvcResources.TypeCache_DoNotModify));
XmlElement typeCacheElement = doc.CreateElement("typeCache");
doc.AppendChild(typeCacheElement);
typeCacheElement.SetAttribute("lastModified", CurrentDate.ToString());
typeCacheElement.SetAttribute("mvcVersionId", _mvcVersionId.ToString());
foreach (var assemblyGroup in groupedByAssembly) {
XmlElement assemblyElement = doc.CreateElement("assembly");
typeCacheElement.AppendChild(assemblyElement);
assemblyElement.SetAttribute("name", assemblyGroup.Key.FullName);
foreach (var moduleGroup in assemblyGroup) {
XmlElement moduleElement = doc.CreateElement("module");
assemblyElement.AppendChild(moduleElement);
moduleElement.SetAttribute("versionId", moduleGroup.Key.ModuleVersionId.ToString());
foreach (Type type in moduleGroup) {
XmlElement typeElement = doc.CreateElement("type");
moduleElement.AppendChild(typeElement);
typeElement.AppendChild(doc.CreateTextNode(type.FullName));
}
}
}
doc.Save(output);
}
The SerializeTypes(
) method implementation is quite simple, it groups the controller types received by Module
and the Assembly
in which they are found and writes it down to the xml file.
Similarly the DeserializeTypes()
method, first checks the mvcVersionId
value to ensure that the cache file was created by the same version of MVC, then it reads the contents of the xml file, creates a list out of it and returns it.