Quantcast
Channel: Development With A Dot
Viewing all articles
Browse latest Browse all 404

Loading ASP.NET MVC Controllers and Views From an Assembly

$
0
0

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:privatereadonly 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:publicoverride 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:publicclass AssemblyVirtualPathProvider : VirtualPathProvider
   2: {
   3:privatereadonly Assembly assembly;
   4:privatereadonly 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:publicoverride 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:publicoverride Boolean FileExists(String virtualPath)
  27:     {
  28:foreach (var provider inthis.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:publicoverride VirtualFile GetFile(String virtualPath)
  42:     {
  43:         var resource = nullas Stream;
  44:  
  45:foreach (var provider inthis.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:publicclass AssemblyVirtualFile : VirtualFile
  81: {
  82:privatereadonly Assembly assembly;
  83:privatereadonly 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:publicoverride Stream Open()
  92:     {
  93:return (this.assembly.GetManifestResourceStream(this.resourceName));
  94:     }
  95: }
  96:  
  97:publicclass AssemblyCacheDependency : CacheDependency
  98: {
  99:privatereadonly 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:publicstaticclass AssemblyRoute
   2: {
   3:publicstaticvoid 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


Viewing all articles
Browse latest Browse all 404

Trending Articles