Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ControllerTypeCache and TypeCacheUtil in ASP.NET MVC framework

0.00/5 (No votes)
9 Jun 2013 1  
This article describes how ASP.NET MVC framework uses caching, during the retrival of controller type for controller instantiation, using the ControllerTypeCache, TypeCacheUtil and TypeCacheSerializer classes.

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 :

<?xml version="1.0" encoding="UTF-8"?>

<!--This file is automatically generated. Please do not modify the contents of this file.-->
<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.

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information. 
        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.

 // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information. 

        public static List<Type> GetFilteredTypesFromAssemblies(string cacheName, Predicate<Type> predicate, IBuildManager buildManager) {
            TypeCacheSerializer serializer = new TypeCacheSerializer();

            // first, try reading from the cache on disk
            List<Type> matchingTypes = ReadTypesFromCache(cacheName, predicate, buildManager, serializer);
            if (matchingTypes != null) {
                return matchingTypes;
            }

            // if reading from the cache failed, enumerate over every assembly looking for a matching type
            matchingTypes = FilterTypesInAssemblies(buildManager, predicate).ToList();

            // finally, save the cache back to disk
            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 :

 // Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.

        [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))) {
                            // If all read types still match the predicate, success!
                            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 :

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.
        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.

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.

        private static IEnumerable<Type> FilterTypesInAssemblies(IBuildManager buildManager, Predicate<Type> predicate) {
            // Go through all assemblies referenced by the application and search for types matching a 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.

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.

        [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.

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.

        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)) {
                // this friendly name was located in the cache, now cycle through namespaces
                if (namespaces != null) {
                    foreach (string requestedNamespace in namespaces) {
                        foreach (var targetNamespaceGrouping in nsLookup) {
                            if (IsNamespaceMatch(requestedNamespace, targetNamespaceGrouping.Key)) {
                                matchingTypes.UnionWith(targetNamespaceGrouping);
                            }
                        }
                    }
                }
                else {
                    // if the namespaces parameter is null, search *every* namespace
                    foreach (var nsGroup in nsLookup) {
                        matchingTypes.UnionWith(nsGroup);
                    }
                }
            }

            return matchingTypes;
        }

        internal static bool IsNamespaceMatch(string requestedNamespace, string targetNamespace) {
            // degenerate cases
            if (requestedNamespace == null) {
                return false;
            }
            else if (requestedNamespace.Length == 0) {
                return true;
            }

            if (!requestedNamespace.EndsWith(".*", StringComparison.OrdinalIgnoreCase)) {
                // looking for exact namespace match
                return String.Equals(requestedNamespace, targetNamespace, StringComparison.OrdinalIgnoreCase);
            }
            else {
                // looking for exact or sub-namespace match
                requestedNamespace = requestedNamespace.Substring(0, requestedNamespace.Length - ".*".Length);
                if (!targetNamespace.StartsWith(requestedNamespace, StringComparison.OrdinalIgnoreCase)) {
                    return false;
                }

                if (requestedNamespace.Length == targetNamespace.Length) {
                    // exact match
                    return true;
                }
                else if (targetNamespace[requestedNamespace.Length] == '.') {
                    // good prefix match, e.g. requestedNamespace = "Foo.Bar" and targetNamespace = "Foo.Bar.Baz"
                    return true;
                }
                else {
                    // bad prefix match, e.g. requestedNamespace = "Foo.Bar" and targetNamespace = "Foo.Bar2"
                    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.

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.

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.

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// See License.txt in the project root for license information.

       [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) {
                // The cache is outdated because the cache file was produced by a different version
                // of MVC.
                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) {
                            // The cache is outdated because we couldn't find a previously recorded
                            // type or the type's containing module was modified.
                            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.

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here