Multi Tenant Architecture with Asp.net MVC 4

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.

 

 

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 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.

Update – A Sample Project

Ok, I get it everybody wants a sample project to try this. One of the reasons I haven’t yet is because this configuration isn’t something you can just open and hit run. You’re going to have to configure some custom settings to make the demo work, but here it is!

Keep in mind there are various ways to route to static files such as robots.txt/favicon.ico. The demo shows a pretty bare bones way of doing it, it could easily be tweaked to be slightly more maintainable such filtering each file run into one rule for each file.

Set Up the Hosts File

You need to add two entries to your local DNS server or HOST file in windows. If you don’t know how, read this

127.0.0.1 site1.com
127.0.0.1 site2.com

Setting Up the Sample Project

Download the sample project here

  1. You’ll need to run visual studio as administrator.  Right click Visual Studio > Run as Administrator
  2. Extract and open the project.  You might get an error about IIS.
  3. We need to set up an IIS site to run the project, we will not be using the built in web server.  Create a new site in IIS, map it to the folder of your project.   Add two bindings as show below.
    IIS Setup
  4. Head back into Visual Studio and goto Project > MvcMultiTenant Properties then set up as belowCapture2
  5. The previous step requires administrative access so that is why you need to start visual studio with Admin privileges.
  6. At this point you should be able to build the project and start/Debug

Running the Project

Browse to Site1.com and Site2.com you’ll notice the page changes style sheets and views. You can examine the views folder to see how this works. This behavior can be extended to your entire site. You’ll also notice you can browse http://site1.com/Home/Contact and it uses the view from the global folder and shares on both sites. If you browse to http://site1.com/robots.txt and http://site2.com/robots.txt you’ll notice they are serving different files as expected.

Capture4

Be Sociable, Share!
Tagged: , , ,

Discussion

  1. Rickard says:

    Very cool, just what I needed!

    Thanks

  2. Yaron says:

    Thank!! You helped me a lot! Cheers!

  3. work.az says:

    thanksssss

  4. Sonal says:

    Plz anybody tell me definition of class “tenant” which is used in this code..

    • LoneTechie says:

      public partial class tenant
      {
      public int TenantID { get; set; }
      public string Name { get; set; }
      public string FolderName { get; set; }
      public string Email { get; set; }
      }

      • Sonal says:

        Thanks LoneTechie………………but there is a method “Fetchall()” which is also not define in class “tenant”. I want definition of this method.

        • Sonal says:

          Plz tell me how to run this application ..I mean what url hav to write to run this application…I m new to this..

          Thanks in Advance

        • LoneTechie says:

          This is the other part of the class

          public partial class tenant
          {
          public static List FetchAll()
          {
          using(var Db = new DbEntities())
          {
          return Db.tenants.ToList();
          }
          }

          }

          I didn’t include this because this assumes you are using entity framework and your tenants table is designed just like mine.

  5. Sonal says:

    Hey, I want to ask one more question…Plz tell me how to run this application ..I mean what url have to write to run this application…I m new to this..
    I am using MVC4 framework. Actully i try to put the url like site1:9045, but it does not work.

  6. Andrew says:

    Can i have working copy of this….please i really need this……

    • LoneTechie says:

      Sorry I just don’t have time at the moment to put one together, possibly in the future.

      • Jeff says:

        +1. It’s hard to evaluate this without a working project and code. I could create a new MVC 4 project on my own and apply the different classes you’ve listed here, but that still leaves the configuration to be set up, and even after that, there are many places where this could break down.

        A working sample shouldn’t be too hard to whip up, and would greatly help those like myself who are evaluating this approach.

        Thanks,

        Jeff

  7. Crob says:

    I have a very similar setup to what you have here with the biggest exception being my location formats, which I suspect is causing my problem. Here’s an example of how I have it setup:
    PartialViewLocationFormats = new[]
    {
    “~/Views/{1}/%1/{0}.cshtml”,
    “~/Views/{1}/{0}.cshtml”,
    “~/Views/Shared/%1/{0}.cshtml”,
    “~/Views/Shared/{0}.cshtml”
    };

    I run into any issues that would appear to be cache related for RenderAction helpers. I can reproduce the issue reliably at least for this example. In my _Layout file I have @{Html.RenderAction(“Header”, “Navigation”);}
    After I restart the webserver if I visit the original site, GLOBAL for you, but without path modification for me then pop over to one of the tenant sites it will throw an error saying “/Views/Navigation/tenantfolder/Header.cshtml” does not exist. However, if I restart the webserver and visit the tenant site first, then everything works fine. To avoid the issue completely I have to copy the file in question into the tenant folder even though there are no modifications.

    I’m just wondering if you have an insight into the mechanism that is causing this issue so I could possibly find a solution other than reworking the Location Formats. It only happens with RenderAction.

    Thanks!

    • LoneTechie says:

      Hard to say without seeing all the code. If you are sure the path is correct I would open perfmon/filemon and see exactly where it is looking for the view on disk.

      • Crob says:

        So I found the issue. I’m not sure if it was you or not but “The Lone Webbie” posted how to turn off caching for the view on this page: http://weblogs.asp.net/imranbaloch/archive/2011/06/27/view-engine-with-dynamic-view-location.aspx. I was looking through the source of VirtualPathProviderViewEngine.cs and found the FindView method as overridden by “The Lone Webbie” as well as a FindPartialView method, which I hadn’t overridden and thus the cache was still on for partial views. Anyway, figured I’d mention that here. Here’s both of them that ended up in my custom view engine class:

        public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
        {
        return base.FindView(controllerContext, viewName, masterName, false);
        }

        public override ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)
        {
        return base.FindPartialView(controllerContext, partialViewName, false);
        }

        • LoneTechie says:

          Nope not the same lone blogger 🙂 Thanks for the update though, makes sense!

        • LoneTechie says:

          I’m glad you followed up with this. I actually ran into this bug this week and this code solved it exactly. Not sure why it didn’t crop up before. I think it had to do with the arrangement of my views. Thanks!

        • Chad says:

          I too have been bitten by this caching issue, but I still don’t really understand the true mechanics of it so maybe someone can help me understand it. This is what I *think* the caching would do: A request is made for view @{Html.RenderAction(“Header”);} by tenant that doesn’t have a custom view so it resolves to “Views/Header.cshtml”. Now the next tenant does have a custom view for Header.cshtml…they request view in “Views/Tenant1/Header.cshtml”.

          Now is this where the caching comes in? I’d think it would’ve cached that a request for view “Header” -> “Views/Header.cshtml” and they bypass the lookup logic and just present Tenant 1 with the base Header? If this were the case then why is it throwing the error that it can’t find “Views/Tenant1/Header.cshtml” as it would never get to the point of attempting to use this path?

          So it must be caching something else instead of what I presumed. Any insights?

  8. Pratik Kagda says:

    Hello There,

    Excellent example of Multi tenant application, Can you please let me know how to deploy it ? I wanted to deploy in IIS 7.5 (not in a azure). As well my requirement something like I need to create multi tenant on the fly, so is there any way I can create multi instance without updating host file ?

    Thanks,
    Pratik

    • Ben Morris says:

      Yes, in a live environment instead of editing the hosts file you would simply point a domain name to your servers ip address.

      If you are in a development environment and don’t want to use a hosts file you might need to create a local DNS server and create a local wildcard like *.testserver.com. Then you won’t have to worry about creating a host entry every time you add a tenant.

      • Pratik Kagda says:

        Thanks a lot Ben,

        It would be really appreciated if you can please provide a sample example as I am pretty new in configuring DNS server.

        First of all I wanted to deploy multi tenant site in my local IIS and then I wanted to move it to production server.

  9. Pratik Kagda says:

    Thanks a lot Ben, yeah and agreed editing host file in development environment is a quite simple solution.

    It solved my queries. Thanks again

  10. David Cornelson says:

    This was very helpful. I’m creating a multi-tenant site and this completely solved the problems I was having with locating and naming views. The SSL issue isn’t an issue for me since all of the tenants will be directing their traffic to tenant.client.com. So the wildcard certificate for client.com is enough.

    I also added the paradigm for custom workflow with Models\Tenant\Workflow.cs classes and this can then be auto-wired through controllers and views. I’ll do something similar for client-side interactions.

    Thanks much.

    David C.

  11. In order to run this sample, be sure to install the url rewrite IIS extension first: http://www.iis.net/downloads/microsoft/url-rewrite

    I also had to manually enable IIS “Application Development Features” for IIS and even perform a aspnet_regiis -i before the sample worked for me.

    ++Alan

  12. Stephen Patten says:

    This has got me pointed in the right direction, thank you.

Add a Comment

*