I’ve been faced with a daunting challenge the last few months which is how to effectively create a multi-tenant architecture utilizing asp.net MVC 4. Creating an architecture like this can work several different ways depending on what you want to do. My particular project has a few goals.
First, the application should support tenants as “sites”. The entire project is hosted on one IIS configuration with one application pool. I’m not really going to get into the benefits of Multi-tenant architectures but one inherent benefit is that any number of sites can run on one code base. That is the approach I want to take.
Second, I’m basically converting an existing project to be multi tenant. What my approach does is introduce a Tenant table in the database which exposes a few basic properties about each tenant including a TenantID. This TenantID needs to be introduced into any table you want to be able to segregate based on tenant. You’ll find at least three schools of thought how to segregate tenant data in a database. You can create tables for each tenant making it easy to back up and restore only one tenant. You can also even create separate databases. My method is keeping all the data together and segregating by TenantID.
Third, I need a custom view engine to facilitate the view organization I desire. The structure I would like to end up with is View -> Tenant Name -> Views. I also want a View -> Global -> Views folder which is available if the view is not found in the Tenant folder. This allows me to share similar views such as in my project eCommerce shopping cart, check out, and payment code. This image below gives a nice image of what I’m talking about. More on the view engine in a bit.

Handling Static Files
You can also see I’ve restructured the /Content folder creating Global Folders for images and styles, then breaking out another Tenants folder for tenant specific resources. One issue you will run into quite quickly is how to deal with files such as robots.txt or favicon.ico. These two files are common on most any site (including many more) and must have copies for each tenant. My solution is to utilize the IISrewrite feature storing the rewrite rules directly in my web.config. An example below routes the favicon for site 1 to the proper folder. This isn’t ideal in my opinion since the web.config can get large quickly, but it does work quite well.
<rule name="Site1-FILE-favicon.ico">
<match url="^favicon.ico$" ignoreCase="true" />
<conditions>
<add input="{HTTP_HOST}" pattern="^site1." />
</conditions>
<action type="Rewrite" url="/Content/tenants/site1/favicon.ico" appendQueryString="true" />
</rule> |
Storing the Tenant List in Memory
As you will soon see we need to know the list of tenants every request to determine which tenant is requesting the page. In order to do this I load the current list of tenants at the application_start(). This is a simple FetchAll() into a list using Entity Framework 5. I’m not an expert on thread safety but once this data is loaded it will only be read from this point on.
public class MvcApplication : System.Web.HttpApplication
{
public static List<tenant> Tenants;
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
System.Web.Mvc.ViewEngines.Engines.Clear();
System.Web.Mvc.ViewEngines.Engines.Add(new MulitTenantRazorViewEngine());
Tenants = tenant.FetchAll();
}
} |
Determining the Tenant at the Controller
It is useful for several reasons to always know which tenant you are dealing with at the controller level. For this reason I overloaded Controller and created my own MultiTenantController that inherits Controller. All my controllers now inherit from MultiTenantController. I can do a few useful things now including intercepting the MasterName property of the ViewResult and setting that manually if needed. More importantly this is where I determine the tenant based on the domain name.
public class MultiTenantController : Controller
{
public tenant CurrentTenant;
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult != null)
{
viewResult.MasterName = "_Layout";
}
Debug.Assert(filterContext.HttpContext.Request.Url != null, "filterContext.HttpContext.Request.Url != null");
CurrentTenant = GetCurrentTenant(filterContext.HttpContext.Request.Url.Host.ToLower());
}
internal static tenant GetCurrentTenant(string host)
{
if (host == null)
{
host = "";
}
var Tenant = MvcApplication.Tenants.Where(p => //This Tenants Loaded in memory on Application_start()
{
var match = p.FolderName + "."; //p.FolderName holds "site1" or "site2" etc...
return host.StartsWith(match); //is it http://site1.com?
}).FirstOrDefault();
if (Tenant == null)
{
Tenant = MvcApplication.Tenants.Where(p =>
{
var match = p.FolderName + ".";
return host.Contains("." + match); //is it http://www.site1.com?
}).FirstOrDefault();
}
return Tenant ?? MvcApplication.Tenants[0];
} |
At this point each controller has access to CurrentTenant which hold the current tenant that is requesting the View. This is really useful because now we can create views based on which tenant. You could also swap key information on the page based on which tenant is looking at the page. Finally current tenant can be passed further down the application creating whatever behavior you need.
Custom View Engine
This is something that might not fit your needs exactly, but this is a pretty generic approach to handling views. As I discussed above I want a global folder which allows me to create views that are shared across all tenants. I also want to be able to specify views for specific tenants since each site can have different features.
Here is sort of what I based my approach off of.
http://weblogs.asp.net/imranbaloch/archive/2011/06/27/view-engine-with-dynamic-view-location.aspx
This view engine is pretty straight forward. I’m using razor in my project so this extends RazorViewEngine. The key part is that I’m pulling out of the controllerContext the controllerContext.Controller and casting as my MultiTenantController. Once I do this step I can now access my CurrentTenant variable we just talked about. At this point the %1 is simply replaced with the CurrentTenant folder name.
public class MulitTenantRazorViewEngine : RazorViewEngine
{
public MulitTenantRazorViewEngine()
{
AreaViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
AreaMasterLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
AreaPartialViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml"
};
ViewLocationFormats = new[] {
"~/Views/%1/{1}/{0}.cshtml",
"~/Views/%1/{1}/{0}.vbhtml",
"~/Views/%1/Shared/{0}.cshtml",
"~/Views/%1/Shared/{0}.vbhtml",
"~/Views/Global/{1}/{0}.cshtml",
"~/Views/Global/{1}/{0}.vbhtml",
"~/Views/Global/Shared/{0}.cshtml",
"~/Views/Global/Shared/{0}.vbhtml"
};
MasterLocationFormats = new[] {
"~/Views/%1/{1}/{0}.cshtml",
"~/Views/%1/{1}/{0}.vbhtml",
"~/Views/%1/Shared/{0}.cshtml",
"~/Views/%1/Shared/{0}.vbhtml",
"~/Views/Global/{1}/{0}.cshtml",
"~/Views/Global/{1}/{0}.vbhtml",
"~/Views/Global/Shared/{0}.cshtml",
"~/Views/Global/Shared/{0}.vbhtml"
};
PartialViewLocationFormats = new[] {
"~/Views/%1/{1}/{0}.cshtml",
"~/Views/%1/{1}/{0}.vbhtml",
"~/Views/%1/Shared/{0}.cshtml",
"~/Views/%1/Shared/{0}.vbhtml",
"~/Views/Global/{1}/{0}.cshtml",
"~/Views/Global/{1}/{0}.vbhtml",
"~/Views/Global/Shared/{0}.cshtml",
"~/Views/Global/Shared/{0}.vbhtml"
};
}
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
var PassedController = controllerContext.Controller as MultiTenantController;
Debug.Assert(PassedController != null, "PassedController != null");
return base.CreatePartialView(controllerContext, partialPath.Replace("%1", PassedController.CurrentTenant.FolderName));
}
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var PassedController = controllerContext.Controller as MultiTenantController;
Debug.Assert(PassedController != null, "PassedController != null");
return base.CreateView(controllerContext, viewPath.Replace("%1", PassedController.CurrentTenant.FolderName), masterPath.Replace("%1", PassedController.CurrentTenant.FolderName));
}
protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
var PassedController = controllerContext.Controller as MultiTenantController;
Debug.Assert(PassedController != null, "PassedController != null");
return base.FileExists(controllerContext, virtualPath.Replace("%1", PassedController.CurrentTenant.FolderName));
}
} |
The Final Result
At this point you can have any number of tenants hosted under one code base. On my particular project this allows me to maintain much less code while any enhancements I make to say checkout pages or features shared across tenants are all echoed immediately. I also have the ability to create totally different views and experiences for each tenant while still sharing key parts of my application. From an eCommerce standpoint my orders now funnel into one administration area making it much simpler to manage.
Further Considerations
Here are some other things I will be considering during this project
SSL
For an eCommerce site it is important to have a means to SSL secure. Since the site is hosted under one application in IIS it is not possible to use separate SSL certificates for each domain name. My strategy involves a wildcard SSL certificate for your “main” site. This can be difficult if your tenants maintain no relation at all, but mine do. Because of the way I route static files, and test domain name my system already works for sub domain based SSL. If you need SSL on site2 you would simply do https://site2.site1.com assuming site one is the “main” site you want to create the certificate for. The routes and domain test are set up to only read the information before the first, so site2 is flagged as the CurrentTenant.
Modular Design
This is more a design consideration. When creating features for multi tenant sites it is wise to create features that are modular. Perhaps visualize something you can turn on and off arbitrarily for each tenant. By doing so you can create features that can be easily shared among current or future tenants.
Tagged: .net 4.5, asp.net, multi tenant, mvc 4