Multi-tenant ASP.NET MVC - Views
So far we have covered the basic premise of tenants and how they will be delegated. Now comes a big issue with multi-tenancy, the views. In some applications, you will not have to override views for each tenant. However, one of my requirements is to add extra views (and controller actions) along with overriding views from the core structure. This presents a bit of a problem in locating views for each tenant request. I have chosen quite an opinionated approach at the present but will coming back to the “views” issue in a later post.
What’s the deal?
The path I’ve chosen is to use precompiled Spark views. I really love Spark View Engine and was planning on using it in my project anyways. However, I ran across a really neat aspect of the source when I was having a look under the hood. There’s an easy way to hook in embedded views from your project. There are solutions that provide this, but they implement a special Virtual Path Provider. While I think this is a great solution, I would rather just have Spark take care of the view resolution. The magic actually happens during the compilation of the views into a bin-deployable DLL. After the views are compiled, the are simply pulled out of the views DLL. Each tenant has its own views DLL that just has “.Views” appended after the assembly name as a convention.
The list of reasons for this approach are quite long. The primary motivation is performance. I’ve had quite a few performance issues in the past and I would like to increase my application’s performance in any way that I can. My customized build of Spark removes insignificant whitespace from the HTML output so I can some some bandwidth and load time without having to deal with whitespace removal at runtime.
How to setup Tenants for the Host
In the source, I’ve provided a single tenant as a sample (Sample1). This will serve as a template for subsequent tenants in your application. The first step is to add a “PostBuildStep” installer into the project. I’ve defined one in the source that will eventually change as we focus more on the construction of dependency containers. The next step is to tell the project to run the installer and copy the DLL output to a folder in the host that will pick up as a tenant. Here’s the code that will achieve it (this belongs in Post-build event command line field in the Build Events tab of settings)
%systemroot%\Microsoft.NET\Framework\v4.0.30319\installutil "$(TargetPath)"
copy /Y "$(TargetDir)$(TargetName)*.dll" "$(SolutionDir)Web\Tenants\"
copy /Y "$(TargetDir)$(TargetName)*.pdb" "$(SolutionDir)Web\Tenants\"
The DLLs with a name starting with the target assembly name will be copied to the “Tenants” folder in the web project. This means something like MultiTenancy.Tenants.Sample1.dll and MultiTenancy.Tenants.Sample1.Views.dll will both be copied along with the debug symbols. This is probably the simplest way to go about this, but it is a tad inflexible. For example, what if you have dependencies? The preferred method would probably be to use IL Merge to merge your dependencies with your target DLL. This would have to be added in the build events. Another way to achieve that would be to simply bypass Visual Studio events and use MSBuild.
I also got a question about how I was setting up the controller factory. Here’s the basics on how I’m setting up tenants inside the host (Global.asax)
protected void Application_Start(){RegisterRoutes(RouteTable.Routes);// create a container just to pull in tenants
var topContainer = new Container();
topContainer.Configure(config =>{config.Scan(scanner =>{scanner.AssembliesFromPath(Path.Combine(Server.MapPath("~/"), "Tenants"));scanner.AddAllTypesOf<IApplicationTenant>();});});// create selectors
var tenantSelector = new DefaultTenantSelector(topContainer.GetAllInstances<IApplicationTenant>());
var containerSelector = new TenantContainerResolver(tenantSelector);
// clear view engines, we don't want anything other than spark
ViewEngines.Engines.Clear();// set view engine
ViewEngines.Engines.Add(new TenantViewEngine(tenantSelector));
// set controller factory
ControllerBuilder.Current.SetControllerFactory(new ContainerControllerFactory(containerSelector));
}
The code to setup the tenants isn’t actually that hard. I’m utilizing assembly scanners in StructureMap as a simple way to pull in DLLs that are not in the AppDomain. Remember that there is a dependency on the host in the tenants and a tenant cannot simply be referenced by a host because of circular dependencies.
Tenant View Engine
TenantViewEngine is a simple delegator to the tenant’s specified view engine. You might have noticed that a tenant has to define a view engine.
public interface IApplicationTenant{....IViewEngine ViewEngine { get; }
}
The trick comes in specifying the view engine on the tenant side. Here’s some of the code that will pull views from the DLL.
protected virtual IViewEngine DetermineViewEngine(){var factory = new SparkViewFactory();
var file = GetType().Assembly.CodeBase.Without("file:///").Replace(".dll", ".Views.dll").Replace('/', '\\');var assembly = Assembly.LoadFile(file);factory.Engine.LoadBatchCompilation(assembly);return factory;
}
This code resides in an abstract Tenant where the fields are setup in the constructor. This method (inside the abstract class) will load the Views assembly and load the compilation into Spark’s “Descriptors” that will be used to determine views. There is some trickery on determining the file location… but it works just fine.
Up Next
There’s just a few big things left such as StructureMap configuring controllers with a convention instead of specifying types directly with container construction and content resolution. I will also try to find a way to use the Web Forms View Engine in a multi-tenant way we achieved with the Spark View Engine without using a virtual path provider. I will probably not use the Web Forms View Engine personally, but I’m sure some people would prefer using WebForms because of the maturity of the engine. As always, I love to take questions by email or on twitter. Suggestions are always welcome as well! (Oh, and here’s another link to the source code).