Multi-Tenant Architecture for MVC 4

In ASP.NET MVC the concept of convention over configuration (CoC) is very important. MVC relies on the common agreement (convention) such as folder structures and naming conventions.

When looking at a MVC project that was created by visual studio templates, you will see the structure of folders and files. They are following the predefined rules and naming conventions which is pretty simple for a single site. But is it possible to create/publish a complex MVC site to several clients, each with mostly shared functionality but also custom stuff, such as client specific controllers / views / business logic etc?

Yes. Here is a simple way.

  1. In folders “Content”, “Controllers” and “Views”, add sub-folders for each client. Then put content/controller/view files in them as normal MVC CoC rules.
  2. Add folder “ViewEngines” with 2 files to override MVC Razor and WebForm view engines

16-01-2014 2-25-59 PM

RazorViewEngine code:

public class MTRazorViewEngine : RazorViewEngine

{
public MTRazorViewEngine()
{
AreaViewLocationFormats = new[] {
“~/Areas/{2}/Views/%1/{1}/{0}.cshtml”,
“~/Areas/{2}/Views/%1/{1}/{0}.vbhtml”,
“~/Areas/{2}/Views/%1/Shared/{0}.cshtml”,
“~/Areas/{2}/Views/%1/Shared/{0}.vbhtml”
};

AreaMasterLocationFormats = new[] {
“~/Areas/{2}/Views/%1/{1}/{0}.cshtml”,
“~/Areas/{2}/Views/%1/{1}/{0}.vbhtml”,
“~/Areas/{2}/Views/%1/Shared/{0}.cshtml”,
“~/Areas/{2}/Views/%1/Shared/{0}.vbhtml”
};

AreaPartialViewLocationFormats = new[] {
“~/Areas/{2}/Views/%1/{1}/{0}.cshtml”,
“~/Areas/{2}/Views/%1/{1}/{0}.vbhtml”,
“~/Areas/{2}/Views/%1/Shared/{0}.cshtml”,
“~/Areas/{2}/Views/%1/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”
};

MasterLocationFormats = new[] {
“~/Views/%1/{1}/{0}.cshtml”,
“~/Views/%1/{1}/{0}.vbhtml”,
“~/Views/%1/Shared/{0}.cshtml”,
“~/Views/%1/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”
};
}

protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
var nameSpace = controllerContext.Controller.GetType().Namespace;
return base.CreatePartialView(controllerContext, partialPath.Replace(“%1”, nameSpace));
}

protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var nameSpace = controllerContext.Controller.GetType().Namespace;
return base.CreateView(controllerContext, viewPath.Replace(“%1”, nameSpace), masterPath.Replace(“%1”, nameSpace));
}

protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
var nameSpace = controllerContext.Controller.GetType().Namespace;
return base.FileExists(controllerContext, virtualPath.Replace(“%1”, nameSpace));
}

// Comment out following method when release which is useCache=true by default because it makes the application runs faster.
// If useCache=flase then every time system have to find a view that is already found, it would have to scan the file system looking for a match to the view name.
// But during the dev and UAT with multi-tenants senarios, we will need useCache=flase.

public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{

return base.FindView(controllerContext, viewName, masterName, false);

}

WebFormViewEngine code:

public class MTWebFormViewEngine : WebFormViewEngine
{
public MTWebFormViewEngine()
{
MasterLocationFormats = new[] {
“~/Views/%1/{1}/{0}.master”,
“~/Views/%1/Shared/{0}.master”
};

AreaMasterLocationFormats = new[] {
“~/Areas/{2}/Views/%1/{1}/{0}.master”,
“~/Areas/{2}/Views/%1/Shared/{0}.master”
};

ViewLocationFormats = new[] {
“~/Views/%1/{1}/{0}.aspx”,
“~/Views/%1/{1}/{0}.ascx”,
“~/Views/%1/Shared/{0}.aspx”,
“~/Views/%1/Shared/{0}.ascx”
};

AreaViewLocationFormats = new[] {
“~/Areas/{2}/Views/%1/{1}/{0}.aspx”,
“~/Areas/{2}/Views/%1/{1}/{0}.ascx”,
“~/Areas/{2}/Views/%1/Shared/{0}.aspx”,
“~/Areas/{2}/Views/%1/Shared/{0}.ascx”
};

PartialViewLocationFormats = ViewLocationFormats;
AreaPartialViewLocationFormats = AreaViewLocationFormats;
}

protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
{
var nameSpace = controllerContext.Controller.GetType().Namespace;
return base.CreatePartialView(controllerContext, partialPath.Replace(“%1”, nameSpace));
}

protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
{
var nameSpace = controllerContext.Controller.GetType().Namespace;
return base.CreateView(controllerContext, viewPath.Replace(“%1”, nameSpace), masterPath.Replace(“%1”, nameSpace));
}

protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
{
var nameSpace = controllerContext.Controller.GetType().Namespace;
return base.FileExists(controllerContext, virtualPath.Replace(“%1”, nameSpace));
}

// Comment out following method when release which is useCache=true by default because it makes the application runs faster.
// If useCache=flase then every time system have to find a view that is already found, it would have to scan the file system looking for a match to the view name.
// But during the dev and UAT with multi-tenants senarios, we will need useCache=flase.

public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{

return base.FindView(controllerContext, viewName, masterName, false);

}
}

By this design, the solution can support any styling framework without mixing each other, but still benefit from shared service and business logic.

This entry was posted in Information Technology, MVC and tagged , , , , . Bookmark the permalink.

1 Response to Multi-Tenant Architecture for MVC 4

  1. Carlos says:

    I like the simplicity of the solution you describe. However, if each tenant had different names they want to display for a model’s property (say “Zip Code”, “Zip”, “Postal Code” as display names on an address model), tenant-specific views will not satisfy this… would they? How would you go about implementing this?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s