Loading ASP.NET MVC Controllers and Views From an Assembly

Back to MVC land! This time, I wanted to be able to load controllers and views from an assembly other than my application. I know about the extensibility mechanisms that ASP.NET and MVC give provides, such as Virtual Path Providers and Controller Factories, so I thought I could use them.

First things first: we need a controller factory that can load a controller from another assembly:

   1: class AssemblyControllerFactory : DefaultControllerFactory
   2: {
   3:     private readonly IDictionary<String, Type> controllerTypes;
   4:  
   5:     public AssemblyControllerFactory(Assembly assembly)
   6:     {
   7:         this.controllerTypes = assembly.GetExportedTypes().Where(x => (typeof(IController).IsAssignableFrom(x) == true) && (x.IsInterface == false) && (x.IsAbstract == false)).ToDictionary(x => x.Name, x => x);
   8:     }
   9:  
  10:     public override IController CreateController(RequestContext requestContext, String controllerName)
  11:     {
  12:         var controller = base.CreateController(requestContext, controllerName);
  13:  
  14:         if (controller == null)
  15:         {
  16:             var controllerType = this.controllerTypes.Where(x => x.Key == String.Format("{0}Controller", controllerName)).Select(x => x.Value).SingleOrDefault();
  17:             var controllerActivator = DependencyResolver.Current.GetService(typeof (IControllerActivator)) as IControllerActivator;
  18:  
  19:             if (controllerType != null)
  20:             {
  21:                 if (controllerActivator != null)
  22:                 {
  23:                     controller = controllerActivator.Create(requestContext, controllerType);
  24:                 }
  25:                 else
  26:                 {
  27:                     controller = Activator.CreateInstance(controllerType) as IController;
  28:                 }
  29:             }
  30:         }
  31:  
  32:         return (controller);
  33:     }
  34: }

I inherited AssemblyControllerFactory from DefaultControllerFactory because this class has most of the logic we need, and I just override its CreateController method.

Next, we need to be able to load view files from an assembly, and a virtual path provider is just what we need for that:

   1: public class AssemblyVirtualPathProvider : VirtualPathProvider
   2: {
   3:     private readonly Assembly assembly;
   4:     private readonly IEnumerable<VirtualPathProvider> providers;
   5:  
   6:     public AssemblyVirtualPathProvider(Assembly assembly)
   7:     {
   8:         var engines = ViewEngines.Engines.OfType<VirtualPathProviderViewEngine>().ToList();
   9:  
  10:         this.providers = engines.Select(x => x.GetType().GetProperty("VirtualPathProvider", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(x, null) as VirtualPathProvider).Distinct().ToList();
  11:         this.assembly = assembly;
  12:     }
  13:  
  14:     public override CacheDependency GetCacheDependency(String virtualPath, IEnumerable virtualPathDependencies, DateTime utcStart)
  15:     {
  16:         if (this.FindFileByPath(this.CorrectFilePath(virtualPath)) != null)
  17:         {
  18:             return (new AssemblyCacheDependency(assembly));
  19:         }
  20:         else
  21:         {
  22:             return (base.GetCacheDependency(virtualPath, virtualPathDependencies, utcStart));
  23:         }
  24:     }
  25:  
  26:     public override Boolean FileExists(String virtualPath)
  27:     {
  28:         foreach (var provider in this.providers)
  29:         {
  30:             if (provider.FileExists(virtualPath) == true)
  31:             {
  32:                 return (true);
  33:             }
  34:         }
  35:  
  36:         var exists = this.FindFileByPath(this.CorrectFilePath(virtualPath)) != null;
  37:  
  38:         return (exists);
  39:     }
  40:  
  41:     public override VirtualFile GetFile(String virtualPath)
  42:     {
  43:         var resource = null as Stream;
  44:  
  45:         foreach (var provider in this.providers)
  46:         {
  47:             var file = provider.GetFile(virtualPath);
  48:  
  49:             if (file != null)
  50:             {
  51:                 try
  52:                 {
  53:                     resource = file.Open();
  54:                     return (file);
  55:                 }
  56:                 catch
  57:                 {
  58:                 }
  59:             }
  60:         }
  61:  
  62:         var resourceName = this.FindFileByPath(this.CorrectFilePath(virtualPath));
  63:  
  64:         return (new AssemblyVirtualFile(virtualPath, this.assembly, resourceName));
  65:     }
  66:  
  67:     protected String FindFileByPath(String virtualPath)
  68:     {
  69:         var resourceNames = this.assembly.GetManifestResourceNames();
  70:  
  71:         return (resourceNames.SingleOrDefault(r => r.EndsWith(virtualPath, StringComparison.OrdinalIgnoreCase) == true));
  72:     }
  73:  
  74:     protected String CorrectFilePath(String virtualPath)
  75:     {
  76:         return (virtualPath.Replace("~", String.Empty).Replace('/', '.'));
  77:     }
  78: }
  79:  
  80: public class AssemblyVirtualFile : VirtualFile
  81: {
  82:     private readonly Assembly assembly;
  83:     private readonly String resourceName;
  84:  
  85:     public AssemblyVirtualFile(String virtualPath, Assembly assembly, String resourceName) : base(virtualPath)
  86:     {
  87:         this.assembly = assembly;
  88:         this.resourceName = resourceName;
  89:     }
  90:  
  91:     public override Stream Open()
  92:     {
  93:         return (this.assembly.GetManifestResourceStream(this.resourceName));
  94:     }
  95: }
  96:  
  97: public class AssemblyCacheDependency : CacheDependency
  98: {
  99:     private readonly Assembly assembly;
 100:  
 101:     public AssemblyCacheDependency(Assembly assembly)
 102:     {
 103:         this.assembly = assembly;
 104:         this.SetUtcLastModified(File.GetCreationTimeUtc(assembly.Location));
 105:     }
 106: }

These three classes inherit from VirtualPathProvider, VirtualFile and CacheDependency and just override some of its methods. AssemblyVirtualPathProvider first checks with other virtual path providers if a file exists, and only if it doesn’t does it create the AssemblyVirtualFile. This looks up the virtual file name in the assembly’s resources, using a convention that translates slashes (/) per dots (.) and returns it. As for the AssemblyCacheDependency, we need it because otherwise ASP.NET MVC will think that the file exists in a directory and will try to monitor it, and because the directory and file do not exist, it will throw an exception at runtime.

We also need a bootstrapping class for setting up everything:

   1: public static class AssemblyRoute
   2: {
   3:     public static void MapRoutes(this RouteCollection routes, Assembly assembly)
   4:     {
   5:         ControllerBuilder.Current.SetControllerFactory(new AssemblyControllerFactory(assembly));
   6:         HostingEnvironment.RegisterVirtualPathProvider(new AssemblyVirtualPathProvider(assembly));
   7:     }
   8: }

Finally, for this to work, we need three things:

  • The controller must be public, have a parameterless constructor, and its name must end with Controller (the default convention);
  • View files must be compiled as embedded resources in the assembly:

image

  • And finally, we need to set this up in Global.asax.cs or RouteConfig.cs:
   1: routes.MapRoutes(typeof(MyController).Assembly);

By the way, the AssemblyVirtualPath provider, AssemblyVirtualFile and AssemblyCacheDependency are pretty generic, so you can use them in other scenarios.

That’s all, folks! Winking smile

                             

7 Comments

Add a Comment

As it will appear on the website

Not displayed

Your website