2

I'm trying to route to areas based on a subdomain while having the URL not include the Area parameter.

I'd like to be able to go the following routes for example

example1.domain.com/login

example1.domain.com/landingpage

example2.domain.com/login

example2.domain.com/landingpage

I'd like to have each subdomain route to a different Area. I've tried following this post Is it possible to make an ASP.NET MVC route based on a subdomain? which led me to http://benjii.me/2015/02/subdomain-routing-in-asp-net-mvc/. But I can't seem to figure out how to not have the Area parameter in the URL.

How can I get the correct URL schema I'm looking for? {subdomain}.domain.com/{action}/{id}

BinaryBoss
  • 64
  • 7

1 Answers1

2

To use a route in an area, you need to set the DataTokens["area"] parameter to the correct area in addition to doing the other subdomain routing. Here is an example:

SubdomainRoute

public class SubdomainRoute : Route
{
    // Passing a null subdomain means the route will match any subdomain
    public SubdomainRoute(string subdomain, string url, IRouteHandler routeHandler) 
        : base(url, routeHandler)
    {
        this.Subdomain = subdomain;
    }

    public string Subdomain { get; private set; }

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        // A subdomain specified as a query parameter takes precedence over the hostname.
        string subdomain = httpContext.Request.Params["subdomain"]; 
        if (subdomain == null)
        {
            string host = httpContext.Request.Headers["Host"];
            int index = host.IndexOf('.');
            if (index >= 0)
                subdomain = host.Substring(0, index);
        }
        // Check if the subdomain matches this route
        if (this.Subdomain != null && !this.Subdomain.Equals(subdomain, StringComparison.OrdinalIgnoreCase))
            return null;

        var routeData = base.GetRouteData(httpContext);
        if (routeData == null) return null; // The route doesn't match - exit early

        // Store the subdomain as a datatoken in case it is needed elsewhere in the app
        routeData.DataTokens["subdomain"] = subdomain;

        return routeData;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        // Read the current query string and cascade it to the current URL only if it exists
        object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
        if (subdomainParam != null)
            values["subdomain"] = subdomainParam;
        return base.GetVirtualPath(requestContext, values);
    }
}

RouteCollectionExtensions

These extension methods allow you to register routes in the non-area part of your application to work with subdomains.

public static class RouteCollectionExtensions
{
    public static SubdomainRoute MapSubdomainRoute(
        this RouteCollection routes, 
        string subdomain, 
        string url, 
        object defaults = null, 
        object constraints = null, 
        string[] namespaces = null)
    {
        return MapSubdomainRoute(routes, null, subdomain, url, defaults, constraints, namespaces);
    }

    public static SubdomainRoute MapSubdomainRoute(
        this RouteCollection routes,
        string name,
        string subdomain, 
        string url, 
        object defaults = null, 
        object constraints = null, 
        string[] namespaces = null)
    {
        var route = new SubdomainRoute(subdomain, url, new MvcRouteHandler())
        {
            Defaults = new RouteValueDictionary(defaults),
            Constraints = new RouteValueDictionary(constraints),
            DataTokens = new RouteValueDictionary()
        };
        if ((namespaces != null) && (namespaces.Length > 0))
        {
            route.DataTokens["Namespaces"] = namespaces;
        }
        routes.Add(name, route);
        return route;
    }
}

AreaRegistrationContextExtensions

These extension methods allow you to register area routes to work with subdomains.

public static class AreaRegistrationContextExtensions
{
    public static SubdomainRoute MapSubdomainRoute(
        this AreaRegistrationContext context, 
        string url, 
        object defaults = null, 
        object constraints = null, 
        string[] namespaces = null)
    {
        return MapSubdomainRoute(context, null, url, defaults, constraints, namespaces);
    }

    public static SubdomainRoute MapSubdomainRoute(
        this AreaRegistrationContext context, 
        string name, 
        string url, 
        object defaults = null, 
        object constraints = null, 
        string[] namespaces = null)
    {
        if ((namespaces == null) && (context.Namespaces != null))
        {
            namespaces = context.Namespaces.ToArray<string>();
        }
        var route = context.Routes.MapSubdomainRoute(name, 
            context.AreaName, url, defaults, constraints, namespaces);
        bool flag = (namespaces == null) || (namespaces.Length == 0);
        route.DataTokens["UseNamespaceFallback"] = flag;
        route.DataTokens["area"] = context.AreaName;
        return route;
    }
}

Usage

To get the URL pattern {subdomain}.domain.com/{action}/{id} to use a specific specific area, you just need to register it as part of the AreaRegistration.

public class AppleAreaRegistration : AreaRegistration
{
    public override string AreaName
    {
        get
        {
            return "Apple";
        }
    }

    public override void RegisterArea(AreaRegistrationContext context)
    {
        context.MapSubdomainRoute(
            url: "{action}/{id}", 
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
    }
}

NOTE: Your example URL has a limitation that you can only use a single controller per area. controller is a required route value, so you either need to supply it in the URL ({controller}/{action}/{id}) or default it as the above example - the latter case means you can only have 1 controller.

Of course, you will also need to setup the DNS server to use subdomains.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • Thanks for the solution. I'm using my host file to set the DNS entry and a local IIS to host the website. I can debug and step through the code and I see the correct values being assigned to the Datatokens, however, when I run the application I get a 404 resource not found. The route values look correct in the dictionary by the way. Am I missing something in the RouteConfig? In the global I am Registering all areas -AreaRegistration.RegisterAllAreas() and then RouteConfig.RegisterRoutes(RouteTable.Routes) (which only ignores a route). Thanks. – BinaryBoss Oct 16 '17 at 18:09
  • It is not clear why you are getting a 404. Does it work when you provide the subdomain as a query string parameter like `Index?subdomain=Apple`? Also, you should ensure the "normal" route config works before applying this one to ensure you have all of the controllers, actions, and views in the right place. – NightOwl888 Oct 17 '17 at 13:27
  • I did try providing the subdomain as a query string parameter, and I checked that the route configs worked. So I created an entirely new project, an viola it worked! Thank you for your effort! I have accepted your answer as the correct one. – BinaryBoss Oct 17 '17 at 18:03
  • If you want [AllowHtml] to work need to change this `string subdomain = httpContext.Request.Params["subdomain"];` to `string subdomain = httpContext.Request.Unvalidated.QueryString["subdomain"];` – brettm Oct 22 '17 at 00:36