4
\$\begingroup\$

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

\$\endgroup\$
1
\$\begingroup\$

Braces

Be consistent with your curly braces. You should always use them. At the moment you are mixing and matching.

Commented code

You should be using source control so there isn't any need for commented code to be left in the source. It clutters it up and is a code smell.

var

Use var where possible, so if you come to change the type of a variable you only need to change it in one place.

List<string> roles = new List<string>();

becomes:

var roles = new List<string>();

Single or Default

Think about how you want your code to behave when your collection is empty. Single will throw an exception if there is more than one element in a list or if there isn't an element in a list. Using single will throw but removes the need for a null check.

If there are issues down the line, you don't necessarily want to swallow the lack of an item in a list, you want it to throw the exception.

Comments

When it comes to comments, comment on the why, not the how. We can see the how, that is the code you have written. If it's not clear, then you need to rethink how it is written so that become clear.

Summaries

Summaries are great! However blank param declarations should be removed, or completed. They should also be completed for methods that make sense.

Arrow Anti Pattern

The Application_PostAuthenticateRequest event handler suffers for the arrow anti pattern. Leave early and avoid nesting your statements. It would be better like this:

protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
{
    if (FormsAuthentication.CookiesSupported == false || Request.Cookies[FormsAuthentication.FormsCookieName] == null)
    {
        return;
    }

    var username = FormsAuthentication.Decrypt(Request.Cookies[FormsAuthentication.FormsCookieName].Value).Name;
    var roles = new List<string>();

    using (TeknikEntities entities = new TeknikEntities())
    {
        User user = entities.Users.Include("Groups").Include("Groups.Roles").Single(u => u.Username == username);

        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 GenericPrincipal(new GenericIdentity(username, "Forms"), roles.ToArray());
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.