I am using ASP.NET MVC to develop an application framework. Essentially, the end goal is to be able to log into an admin interface, create a new tenant with custom settings, enable the modules they want (blog, shopping basket, etc)... job done - satisfied customer with new website. I'm not using separate applications because there will be a lot of shared code and it would be easier to maintain this way, and also because it would be pretty easy to bring a new, identical node online at peak times.
Depending on what modules are loaded for the tenant, different routes are applicable for each tenant. As I see it, there are three options:
Have all tenants share the same route collection - however if there are a lot of modules it'll be searching through a lot of routes it doesn't need to, and some modules may well have conflicting routes.
Add the necessary routes for each tenant to the global route collection and extend the route class to look at the domain as well - but this could quickly end up with hundreds of routes as more tenants are added.
Work out what tenant is being accessed first and then only search their own private route collection - this would be ideal, but I've searched for hours and have absolutely no idea how to do it!
So can anyone point me in the correct direction for the third option or explain why either of the first two aren't really that bad?
How will each website be distinguished in your app? If we assume each tenant will be identified by a unique domain name or subdomain name, then you can accomplish your routing with one route and some RouteConstraints
. Create two constraints, one for controllers, the other for actions. Assuming that you will have tables in your database which list the available controllers/actions for a specific tenant, your constraints would be as follows:
using System;
using System.Web;
using System.Web.Routing;
namespace ExampleApp.Extensions
{
public class IsControllerValidForTenant : IRouteConstraint
{
public IsControllerValidForTenant() { }
private DbEntities _db = new DbEntities();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// determine domain
var domainName = httpContext.Request.Url.DnsSafeHost;
var siteId = _db.Sites.FirstorDefault(s => s.DomainName == domainName).SiteId;
// passed constraint if this controller is valid for this tenant
return (_db.SiteControllers.Where(sc => sc.Controller == values[parameterName].ToString() && sc.SiteId == siteId).Count() > 0);
}
}
public class IsActionValidForTenant : IRouteConstraint
{
public IsActionValidForTenant() { }
private DbEntities _db = new DbEntities();
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
// determine domain
var domainName = httpContext.Request.Url.DnsSafeHost;
var siteId = _db.Sites.FirstorDefault(s => s.DomainName == domainName).SiteId;
// passed constraint if this action is valid for this tenant
return (_db.SiteActions.Where(sa => sa.Action == values[parameterName].ToString() && sa.SiteId == siteId).Count() > 0);
}
}
}
Then, in Global.asax.cs, define your route as follows:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
new { controller = new IsControllerValidForTenant(), action = new IsActionValidForTenant(),}
);
}
When a request comes in, the constraints will examine whether the controller and action are valid for the domain, so that only valid controllers and actions for that tenant will pass the RouteConstraints.