left-icon

ASP.NET Multitenant Applications Succinctly®
by Ricardo Peres

Previous
Chapter

of
A
A
A

CHAPTER 5

ASP.NET MVC

ASP.NET MVC


Introduction

ASP.NET MVC introduces a very different development model. Most people would agree that it is more testable, promotes a separation of responsibilities - views for user interface, controllers for business logic – and is much closer to the HTTP protocol, avoiding some magic that Web Forms employs, which, although useful, can result in increased complexity and decrease of performance.

Note: Nick Harrison wrote ASP.NET MVC Succinctly for the Succinctly series; be sure to read it for a good introduction to MVC.

Branding

Here we will explore three branding mechanisms offered by MVC:

  • Page layouts: The equivalent to Web Forms’ master pages, where we shall have a different one for each tenant.
  • View locations: Each tenant will have its views stored in a specific folder.
  • CSS bundles: Each tenant will have its own CSS bundle (a collection of tenant-specific .css files).

Page layouts

MVC’s views have a mechanism similar to Web Forms’ master pages, by the name of Page Layouts. A page layout specifies the global structure of the HTML content, leaving “holes” to be filled by pages that use it. By default, ASP.NET MVC uses the “~/Views/Shared/_Layout.cshtml” layout for C# views, and “_Layout.vbhtml” for Visual Basic views.

The only way to use a page layout is to explicitly set it in a view, like in this example, using Razor:

Code Sample 52

@{

    Layout = "~/Views/Shared/Layout.cshtml";

}

In order to avoid repeating code over and over just to set the page layout, we can use one of MVC’s extensibility points, the View Engine. Because we will be using Razor as the view engine of choice, we need to subclass RazorViewEngine, in order to inject our tenant-specific page layout.

Our implementation will look like this:

Code Sample 53

public sealed class MultitenantRazorViewEngine : RazorViewEngine

{

     public override ViewEngineResult FindView(ControllerContext ctx, 

String viewName, String masterName, Boolean useCache)

     {

          var tenant = TenantsConfiguration.GetCurrentTenant();

          //the third parameter to FindView is the page layout

          return base.FindView(controllerContext, viewName, 

               tenant.MasterPage, useCache);

     }

}

In order to use our new view engine, we need to replace the existing one in the ViewEngines.Engines collection; this should be done in some method spawned from Application_Start:

Code Sample 54

ViewEngines.Engines.Clear();

ViewEngines.Engines.Add(new MultitenantRazorViewEngine());

Finally, we must make sure our views set the page layout (Layout property) from ViewBag.Layout, which is mapped to whatever is returned in the third parameter of FindView:

Code Sample 55

@{

    Layout = ViewBag.Layout;

}

View locations

By default, the view engine will look for views (.cshtml or .vbhtml) in a pre-defined collection of virtual paths. What we would like to do is look for a view in a tenant-specific path first, and if it’s not found there, fall back to the default locations. Why would we want that? Well, this way, we can design our views to have a more tenant-specific look, more than we could do just by changing page layouts.

We will make use of the same view engine introduced in the previous page, and will adapt it for this purpose:

Code Sample 56

public sealed class MultitenantRazorViewEngine : RazorViewEngine

{

     private Boolean pathsSet = false;

 

     public MultitenantRazorViewEngine() : this(false) { }

          public MultitenantRazorViewEngine(Boolean usePhysicalPathsPerTenant)

     {

          this.UsePhysicalPathsPerTenant = usePhysicalPathsPerTenant;

     }

 

     public Boolean UsePhysicalPathsPerTenant { getprivate set; }

 

     private void SetPaths(ITenantConfiguration tenant)

     {

          if (this.UsePhysicalPathsPerTenant)

          {

               if (!this.pathsSet)

               {    

                    this.ViewLocationFormats = new String[]

                    {

                         String.Concat("~/Views/",

                          tenant.Name, "/{1}/{0}.cshtml"),

                         String.Concat("~/Views/",

                           tenant.Name, "/{1}/{0}.vbhtml")

                    }            .Concat(this.ViewLocationFormats).ToArray();

                    this.pathsSet = true;

               }

          }

     }

 

     public override ViewEngineResult FindView(ControllerContext ctx, 

          String viewName, String masterName, Boolean useCache)

     {

           var tenant = TenantsConfiguration.GetCurrentTenant();

          //the third parameter to FindView is the page layout

          return base.FindView(controllerContext, viewName, 

               tenant.MasterPage, useCache);

          }

     }

This requires that the view engine is configured with the usePhysicalPathsPerTenant set:

Code Sample 57

ViewEngines.Engines.Add(new MultitenantRazorViewEngine(true));

CSS Bundles

CSS Bundling and Minification is integrated in the two major flavors of ASP.NET: Web Forms and MVC. In Web Forms, because it has the themes mechanism (see Themes and ), it is probably not much used for branding, but MVC does not have an analogous mechanism.

A CSS bundle consists of a name and a set of .css files located in any number of folders. We will create, for each of the registered tenants, an identically-named bundle containing all of the files in a folder named from the tenant configuration’s Theme property under the folder ~/Content. Here’s how:

Code Sample 58

public static void RegisterBundles(BundleCollection bundles)

{

     foreach (var tenant in TenantsConfiguration.GetTenants())

     {

          var virtualPath = String.Format("~/{0}", tenant.Name);

          var physicalPath = String.Format("~/Content/{0}",

               tenant.Theme);

          if (!BundleTable.Bundles.Any(b => b.Path == virtualPath))

          {

               var bundle = new StyleBundle(virtualPath)

                    .IncludeDirectory(physicalPath, "*.css");

               BundleTable.Bundles.Add(bundle);

          }

     }

}

This method needs to be called after the tenants are registered:

Code Sample 59

RegisterBundles(BundleTable.Bundles);

Tenant “abc.com” will get a CSS bundle called “~/abc.com” containing all .css files under “~/Content/abc.com”.

This is not all, however; in order to actually add the CSS bundle to a view, we need to add an explicit call on the view, like this one:

Code Sample 60

@Styles.Render("~/abc.com")

However, if we are to hardcode the tenant’s name, this won’t scale. This would be okay for tenant-specific layout pages, but not for generic views. What we need is a mechanism to automatically supply, on each request, the right bundle name. Fortunately, we can achieve this with an action filter:

Code Sample 61

public sealed class MultitenantActionFilter : IActionFilter

{

     void IActionFilter.OnActionExecuted(ActionExecutedContext ctx) { }

     void IActionFilter.OnActionExecuting(ActionExecutingContext ctx)

     {

          var tenant = TenantsConfiguration.GetCurrentTenant();

          ctx.Controller.ViewBag.Tenant = tenant.Name;   

          ctx.Controller.ViewBag.TenantCSS = String.Concat("~/"

               filterContext.Controller.ViewBag.Tenant);

     }

}

This action filter implements the IActionFilter interface, and all it does is inject two values in the ViewBag collection, the tenant name (Tenant), and the CSS bundle’s name (TenantCSS). Action filters can be registered in a number of ways, one of which is the GlobalFilters.Filters collection:

Code Sample 62

GlobalFilters.Filters.Add(new MultitenantActionFilter());

With this on, all we need to add custom tenant-specific bundles on a view is:

Code Sample 63

@Styles.Render(ViewBag.TenantCSS)

Security

We might want to restrict some controller actions for some tenant. MVC offers a hook called an authorization filter that can be used to allow or deny access to a given controller or action method. There’s a base implementation, AuthorizeAttribute, which grants access only if the current user is authenticated. The implementation allows extensibility, and that’s what we are going to do:

Code Sample 64

[Serializable]

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 

     AllowMultiple = false, Inherited = true)]

public sealed class AllowedTenantsAttribute : AuthorizeAttribute

{

     public AllowedTenantsAttribute(params String [] tenants)

     {

          this.Tenants = tenants;

     }

 

     public IEnumerable<String> Tenants { getprivate set; }

     protected override Boolean AuthorizeCore(HttpContextBase ctx)

     {

          var tenant = TenantsConfiguration.GetCurrentTenant();

          return this.Tenants.Any(x => x == tenant.Name);

     }

}

When applied to a controller method or class with one or more tenants as parameters, it will only grant access to the specified tenants:

Code Sample 65

public class SomeController : Controller

{

     [AllowedTenants("abc.com")]

     public ActionResult SomeThing()

     {

          //this can only be accessed by abc.com

          //return something     

     }

     [AllowedTenants("xyz.net", "abc.com")]

     public ActionResult OtherThing()

     {

          //this can be accessed by abc.com and xyz.net

          //return something

     }

}

The next chapter describes another technique for restricting access to resources without the need for code.

Unit Testing

Testing MVC controllers and actions is straightforward. In the context of multitenant applications, as we have been talking in this book, we just need to set up our test bench according to the right tenant identification strategy. For example, if you are using NUnit, you would do it before each test, in a method decorated with the SetUpFixtureAttribute:

Code Sample 66

[SetUpFixture]

public static class MultitenantSetup

{

     [SetUp]

     public static void Setup()

     {

          var req = new HttpRequest(

               filename: String.Empty,

               url: "http://abc.com/",

               queryString: String.Empty);

          req.Headers["HTTP_HOST"] = "abc.com";

          //add others as needed

          var response = new StringBuilder();

          var res = new HttpResponse(new StringWriter(response));

 

          var ctx = new HttpContext(req, res);

          var principal = new GenericPrincipal(

               new GenericIdentity("Username"), new [] { "Role" });

          var culture = new CultureInfo("pt-PT");

 

          Thread.CurrentThread.CurrentCulture = culture;

          Thread.CurrentThread.CurrentUICulture =  culture;

 

          HttpContext.Current = ctx;

          HttpContext.Current.User = principal;

     }

}

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.