4

I'm building an appplication where I want to give a client company a unique url like 'clientcompany.app.com' or 'app.com/clientcompany'.

When a user signs up I want to let them pick their subdomain, and they should be able to invite other users to work under that subdomain as well. The subdomain/route should be the 'parent' that all users are grouped under.

How would I achieve something like this with MVC 4 routing?

502502
  • 437
  • 1
  • 5
  • 15
  • The app.com/clientcompany subdomain has different views, controllers, implementation for each client? If yes, you should use Areas! – Fals Jun 24 '13 at 17:25
  • No, I'm simply trying to give my (groups of) customers their own subdomain to work under. People from CompanyA can go to companya.myapp.com and people from CompanyB can go to companyb.myapp.com. Their data will be private under their particular domain. – 502502 Jun 24 '13 at 17:34

4 Answers4

3

This can be achieved by creating a custom domain route:

public class DomainRoute : Route
{
    private Regex domainRegex;
    private Regex pathRegex;

    public string Domain { get; set; }

    public DomainRoute(string domain, string url, RouteValueDictionary defaults)
        : base(url, defaults, new MvcRouteHandler())
    {
        Domain = domain;
    }

    public DomainRoute(string domain, string url, RouteValueDictionary defaults,      IRouteHandler routeHandler)
        : base(url, defaults, routeHandler)
    {
        Domain = domain;
    }

    public DomainRoute(string domain, string url, object defaults)
        : base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
    {
        Domain = domain;
    }

    public DomainRoute(string domain, string url, object defaults, IRouteHandler routeHandler)
        : base(url, new RouteValueDictionary(defaults), routeHandler)
    {
        Domain = domain;
    }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        // Build regex
        domainRegex = CreateRegex(Domain);
        pathRegex = CreateRegex(Url);

        // Request information
        string requestDomain = httpContext.Request.Headers["host"];

        if (!string.IsNullOrEmpty(requestDomain))
        {
            if (System.Diagnostics.Debugger.IsAttached == false)
            {
                if (requestDomain.IndexOf(":") > 0)
                {
                    requestDomain = requestDomain.Substring(0, requestDomain.IndexOf(":"));
                }
            }

            // Strip Multiple Subdomains
            if (requestDomain.Split('.').Length > 3)
            {
                string[] split = requestDomain.Split('.');

                requestDomain = String.Join(".", split, split.Length - 3, 3);

                string url = String.Format("{0}://{1}/", httpContext.Request.Url.Scheme, requestDomain);

                if (System.Diagnostics.Debugger.IsAttached == true)
                {
                    httpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache);
                    httpContext.Response.CacheControl = "no-cache";
                }

                httpContext.Response.RedirectPermanent(url, true);
            }
        }
        else
        {
            requestDomain = httpContext.Request.Url.Host;
        }



        string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;

        // Match domain and route
        Match domainMatch = domainRegex.Match(requestDomain);
        Match pathMatch = pathRegex.Match(requestPath);

        // Route data
        RouteData data = null;
        if (domainMatch.Success && pathMatch.Success)
        {
            data = new RouteData(this, RouteHandler);

            // Add defaults first
            if (Defaults != null)
            {
                foreach (KeyValuePair<string, object> item in Defaults)
                {
                    data.Values[item.Key] = item.Value;
                }
            }

            // Iterate matching domain groups
            for (int i = 1; i < domainMatch.Groups.Count; i++)
            {
                Group group = domainMatch.Groups[i];
                if (group.Success)
                {
                    string key = domainRegex.GroupNameFromNumber(i);

                    if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
                    {
                        if (!string.IsNullOrEmpty(group.Value))
                        {
                            data.Values[key] = group.Value;
                        }
                    }
                }
            }

            // Iterate matching path groups
            for (int i = 1; i < pathMatch.Groups.Count; i++)
            {
                Group group = pathMatch.Groups[i];
                if (group.Success)
                {
                    string key = pathRegex.GroupNameFromNumber(i);

                    if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
                    {
                        if (!string.IsNullOrEmpty(group.Value))
                        {
                            data.Values[key] = group.Value;
                        }
                    }
                }
            }
        }

        return data;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        return base.GetVirtualPath(requestContext, RemoveDomainTokens(values));
    }

    public DomainData GetDomainData(RequestContext requestContext, RouteValueDictionary values)
    {
        // Build hostname
        string hostname = values.Aggregate(Domain, (current, pair) => current.Replace("{" + pair.Key + "}", pair.Value.ToString()));

        // Return domain data
        return new DomainData
        {
            Protocol = "http",
            HostName = hostname,
            Fragment = ""
        };
    }

    private Regex CreateRegex(string source)
    {
        // Perform replacements
        source = source.Replace("/", @"\/?");
        source = source.Replace(".", @"\.?");
        source = source.Replace("-", @"\-?");
        source = source.Replace("{", @"(?<");
        source = source.Replace("}", @">([a-zA-Z0-9_]*))");

        return new Regex("^" + source + "$");
    }

    private RouteValueDictionary RemoveDomainTokens(RouteValueDictionary values)
    {
        Regex tokenRegex = new Regex(@"({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?");
        Match tokenMatch = tokenRegex.Match(Domain);
        for (int i = 0; i < tokenMatch.Groups.Count; i++)
        {
            Group group = tokenMatch.Groups[i];
            if (group.Success)
            {
                string key = group.Value.Replace("{", "").Replace("}", "");
                if (values.ContainsKey(key))
                    values.Remove(key);
            }
        }

        return values;
    }
}     

public class DomainData
{
    public string Protocol { get; set; }
    public string HostName { get; set; }
    public string Fragment { get; set; }
}

Global.asax:

routes.Add(
                "DomainRoute", new DomainRoute(
                "{subdomain}.yoururl.com",     // Domain with parameters 
                "{controller}/{action}",    // URL with parameters 
                new { controller = "Home", action = "Index", subdomain = UrlParameter.Optional }  // Parameter defaults 
            ));

http://subdomain.app.com will then add the parameter, 'subdomain' to your RouteValueDictionary.

Also, make sure you create a wildcard DNS record.

Jeff Odle
  • 71
  • 3
2

If you want to give each client company their own subdomain, for example clientcompany.app.com, you'd have to create the DNS entries for the client company when the first user signs up. Point each subdomain to your MVC4 app, but ensure the IIS settings for your app allow for multiple/wildcard hosts (this happens by default).

After that, you can check the domain host that the client requested during the controllers Request object, parse the domain (eg. pick clientcompany from the domain) and use that as your group.

Alternatively, if you want the client company to simply be part of the URL path (ie. constant domain), like www.app.com/clientcompany/, then you can create a route such as:

{company}/{controller}/{action}

Then where you care about the company, you can add a company parameter or member to your model and read it as needed.

Matt Houser
  • 33,983
  • 6
  • 70
  • 88
  • Thank you! I think I will go with the URL Path option so I don't have to bother with wildcard hosts and just let my code deal with it. – 502502 Jun 24 '13 at 18:04
1

You will need a custom IRouteConstraint to handle the Subdomain behavior. There's a post here that covers this exactly the way you want!

Take a look here MVC 3 Subdomain Routing

Hope This help you!

Community
  • 1
  • 1
Fals
  • 6,813
  • 4
  • 23
  • 43
1

Honestly, this has nothing to do with routing and everything to do with authorization. Whether you use a subdomain or a directory-style path, you will essentially treat the "clientcompany" part as a slug -- using it to look up a "group". You'll then verify user/group ownership through relationships on that "group", and if the user is not allowed to access it, you return a 403 Forbidden response. Otherwise, you allow the view to render.

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444