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:
- 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! ![]()