I am currently working on a side-project and have the basic foundation and architecture I want in place for the site. I wanted to get your opinion and thoughts on my current practices and methods before I get too deep down the rabbit hole and any potential issues would be difficult to remove.
The basic premise of the site is an ASP.NET MVC 4 site using Areas to separate each service of the site. Each service/area is allocated subdomains to represent that service. This was a major pain point for me to figure out how to handle both incoming routing for each subdomain/area, but also generating of links using the routing table and custom routing methods.
The routing can be described in the following way:
A custom
SubdomainRoute
inheriting 'Route' is created that takes an additional parameter 'sub'. It then overridesGetRouteData
and compares the current subdomain (either the subdomain, or a '?sub=blah' query param) against the routes specified subdomain.using System.IO; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; namespace Teknik { public class SubdomainRoute : Route { public string Subdomain { get; set; } public SubdomainRoute(string subdomain, string url, IRouteHandler handler) : base(url, handler) { this.Subdomain = subdomain; } public SubdomainRoute(string subdomain, string url, RouteValueDictionary defaults, IRouteHandler handler) : base(url, defaults, handler) { this.Subdomain = subdomain; } public SubdomainRoute(string subdomain, string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler handler) : base(url, defaults, constraints, handler) { this.Subdomain = subdomain; } public SubdomainRoute(string subdomain, string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler handler) : base(url, defaults, constraints, dataTokens, handler) { this.Subdomain = subdomain; } public override RouteData GetRouteData(HttpContextBase httpContext) { var routeData = base.GetRouteData(httpContext); if (routeData == null) return null; // Only look at the subdomain if this route matches in the first place. string subdomain = httpContext.Request.QueryString["sub"]; // A subdomain specified as a query parameter takes precedence over the hostname. if (subdomain == null) { string host = httpContext.Request.Headers["Host"]; subdomain = host.GetSubdomain(); } else { if (routeData.Values["sub"] == null) { routeData.Values["sub"] = subdomain; } else { subdomain = routeData.Values["sub"].ToString(); } } //routeData.Values["sub"] = subdomain; if (Subdomain == "*" || Subdomain == subdomain) { return routeData; } return null; } public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { object subdomainParam = requestContext.HttpContext.Request.QueryString["sub"]; if (subdomainParam != null && values["sub"] == null) values["sub"] = subdomainParam; return base.GetVirtualPath(requestContext, values); // we now have the route based on subdomain } } }
The routes are added practically the same as the normal route, with the addition of the sub param. I have one subdomain set aside for the dev site ('dev'), which is basically a copy of the routing of the normal subdomain with the Area tacked on as the first url section.
public override void RegisterArea(AreaRegistrationContext context) { context.MapSubdomainRoute( "About.Index", // Route name "dev", // Subdomain "About", // URL with parameters new { controller = "About", action = "Index" }, // Parameter defaults new[] { typeof(Controllers.AboutController).Namespace } ); context.MapSubdomainRoute( "About.Index", // Route name "about", // Subdomain "", // URL with parameters new { controller = "About", action = "Index" }, // Parameter defaults new[] { typeof(Controllers.AboutController).Namespace } ); }
To generate a link using the subdomain routing, I created a
UrlHelper
extension that will take the subdomain you want, the route name, and the route values you want to use. By specifying the route name, and defining a common naming convention for the names, I don't have to deal with parsing the route table to find a valid route, since I know exactly which route I want to use. This extension will then attach the correct subdomain or query param depending on the subdomain provided and the current subdomain of the current page.using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using System.Text.RegularExpressions; using System.Web; using System.Web.Mvc; using System.Web.Routing; using System.Web.UI; namespace Teknik { public static class UrlExtensions { public static string SubRouteUrl(this UrlHelper url, string sub, string routeName) { return url.SubRouteUrl(sub, routeName, null); } /// <summary> /// Generates a full URL given the specified sub domain and route name /// </summary> /// <param name="url"></param> /// <param name="sub"></param> /// <param name="routeName"></param> /// <param name="routeValues"></param> /// <returns></returns> public static string SubRouteUrl(this UrlHelper url, string sub, string routeName, object routeValues) { string host = url.RequestContext.HttpContext.Request.Url.Authority; string domain = host; string rightUrl = string.Empty; // get current subdomain string curSub = host.GetSubdomain(); var split = host.Split('.'); // split the host by '.' if (split.Count() > 2) { int index = host.IndexOf('.') + 1; if (index >= 0 && index < host.Length) domain = host.Substring(index, (host.Length - index)); } // Grab the sub from parameters if it exists string subParam = url.RequestContext.HttpContext.Request.QueryString["sub"]; // A subdomain specified as a query parameter takes precedence over the hostname. // If the param is not being used, we will use the curSub if (string.IsNullOrEmpty(subParam)) { string firstSub = (curSub == "dev") ? "dev" : sub; if (!string.IsNullOrEmpty(firstSub)) { routeName = firstSub + "." + routeName; domain = firstSub + "." + domain; } } else { string desiredSub = (subParam == "dev") ? "dev" : sub; routeName = desiredSub + "." + routeName; domain = host; } try { rightUrl = url.RouteUrl(routeName, routeValues); } catch (ArgumentException) { } string absoluteAction = string.Format("{0}://{1}{2}", url.RequestContext.HttpContext.Request.Url.Scheme, domain, rightUrl); if (!string.IsNullOrEmpty(subParam) && subParam != "dev") { absoluteAction = absoluteAction.SetUrlParameter("sub", sub); } return absoluteAction; } public static string GetUrlParameters(this string url) { Uri uri = new Uri(url); var queryParts = HttpUtility.ParseQueryString(uri.Query); return queryParts.ToString(); } public static string SetUrlParameter(this string url, string paramName, string value) { return new Uri(url).SetParameter(paramName, value).ToString(); } public static Uri SetParameter(this Uri url, string paramName, string value) { var queryParts = HttpUtility.ParseQueryString(url.Query); queryParts[paramName] = value; return new Uri(url.AbsoluteUriExcludingQuery() + '?' + queryParts.ToString()); } public static string AbsoluteUriExcludingQuery(this Uri url) { return url.AbsoluteUri.Split('?').FirstOrDefault() ?? String.Empty; } public static string GetSubdomain(this string host) { if (host.IndexOf(":") >= 0) host = host.Substring(0, host.IndexOf(":")); Regex tldRegex = new Regex(@"\.[a-z]{2,3}\.[a-z]{2}$"); host = tldRegex.Replace(host, ""); tldRegex = new Regex(@"\.[a-z]{2,4}$"); host = tldRegex.Replace(host, ""); if (host.Split('.').Length > 1) return host.Substring(0, host.IndexOf(".")); else return string.Empty; } } }
For authentication I am using FormsAuth for session management and my own user/groups/roles table for user information and permissions. I'm adding the roles for a user in the 'Application_PostAuthenticateRequest' function based on the groups the user is in.
protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
if (FormsAuthentication.CookiesSupported == true)
{
if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
{
//let us take out the username now
string username = FormsAuthentication.Decrypt(Request.Cookies[FormsAuthentication.FormsCookieName].Value).Name;
List<string> roles = new List<string>();
using (TeknikEntities entities = new TeknikEntities())
{
User user = entities.Users.Include("Groups").Include("Groups.Roles").SingleOrDefault(u => u.Username == username);
if (user != null)
{
foreach (Group grp in user.Groups)
{
foreach (Role role in grp.Roles)
{
if (!roles.Contains(role.Name))
{
roles.Add(role.Name);
}
}
}
}
}
//Let us set the Pricipal with our user specific details
HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(
new System.Security.Principal.GenericIdentity(username, "Forms"), roles.ToArray());
}
}
}
For the database backend, I'm using EntityFramework to connect to it with a code-first approach and auto-migration set up. I'm really liking it so far because I don't have to worry about a disconnect between the DB and my models.
I'm still fairly new to ASP.NET and MVC pattern in general, so any critique on my methods or any better alternatives would be appreciated. I'm concerned about the longevity of the current structure of separating the subdomains into Areas and cross area referencing when accessing shared resources (ex: ~/Areas/Profile/Views/Login accessed via all other areas in nav bar).
The source can be found here.
You can view a live demo here.