Is it possible to make an ASP.NET MVC route based on a subdomain?
Asked Answered
O

10

244

Is it possible to have an ASP.NET MVC route that uses subdomain information to determine its route? For example:

  • user1.domain.example goes to one place
  • user2.domain.example goes to another?

Or, can I make it so both of these go to the same controller/action with a username parameter?

Outsoar answered 10/11, 2008 at 18:4 Comment(6)
I implemented a similar sort of thing for multi-tenanted applications, but using an abstract base Controller rather than a custom Route class. My blog post on it is here.Hexagon
Be sure to consider this approach: http://blog.tonywilliams.me.uk/asp-net-mvc-2-routing-subdomains-to-areas I found it to be better for introducing multitenancy to my app than the other answers, because MVC areas are a nice way to introduce tenant-specific controllers and views in an organized way.Labored
@Labored - I think you should add it as an answer, this is what I ended up using as the basis for my solution.Hurwit
@Hurwit - Thanks. It was an answer, but a moderator converted it to a comment for reasons I cannot understand.Labored
Tony's like was broken. Here's one that worked for me: blog.tonywilliams.me.uk/…Nucleolated
Is it possible in asp.net core latest?Blate
H
170

You can do it by creating a new route and adding it to the routes collection in RegisterRoutes in your global.asax. Below is a very simple example of a custom Route:

public class ExampleRoute : RouteBase
{

    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        var url = httpContext.Request.Headers["HOST"];
        var index = url.IndexOf(".");

        if (index < 0)
            return null;

        var subDomain = url.Substring(0, index);

        if (subDomain == "user1")
        {
            var routeData = new RouteData(this, new MvcRouteHandler());
            routeData.Values.Add("controller", "User1"); //Goes to the User1Controller class
            routeData.Values.Add("action", "Index"); //Goes to the Index action on the User1Controller

            return routeData;
        }

        if (subDomain == "user2")
        {
            var routeData = new RouteData(this, new MvcRouteHandler());
            routeData.Values.Add("controller", "User2"); //Goes to the User2Controller class
            routeData.Values.Add("action", "Index"); //Goes to the Index action on the User2Controller

            return routeData;
        }

        return null;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        //Implement your formating Url formating here
        return null;
    }
}
Hildick answered 12/2, 2009 at 14:30 Comment(8)
Thanks for the detailed sample but I'm not following how to execute the .Add from Global.asax.Nonconductor
I called the route SubdomainRoute and added it as the first route like this: routes.Add(new SubdomainRoute());Sheriesherif
Does this approach requires hard-coding a list of possible sub-domains?Rosemari
No, you can add a database field called something like "subdomain" that you will be what you're expecting the subdomain to be for a particular user, or whatever else, then just do a lookup on the subdomain.Locate
Could anybody recommend a webforms version of this?Valentin
This guy has a better approach: benjii.me/2015/02/subdomain-routing-in-asp-net-mvcDactyl
Please recommend for Web ApiCoadjutor
Is it possible in asp.net core latest?Blate
F
55

To capture the subdomain while retaining the standard MVC5 routing features, use the following SubdomainRoute class derived from Route.

Additionally, SubdomainRoute allows the subdomain optionally to be specified as a query parameter, making sub.example.com/foo/bar and example.com/foo/bar?subdomain=sub equivalent. This allows you to test before the DNS subdomains are configured. The query parameter (when in use) is propagated through new links generated by Url.Action, etc.

The query parameter also enables local debugging with Visual Studio 2013 without having to configure with netsh or run as Administrator. By default, IIS Express only binds to localhost when non-elevated; it won't bind to synonymous hostnames like sub.localtest.me.

class SubdomainRoute : Route
{
    public SubdomainRoute(string url) : base(url, new MvcRouteHandler()) {}

    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.Params["subdomain"]; // A subdomain specified as a query parameter takes precedence over the hostname.
        if (subdomain == null) {
            string host = httpContext.Request.Headers["Host"];
            int index = host.IndexOf('.');
            if (index >= 0)
                subdomain = host.Substring(0, index);
        }
        if (subdomain != null)
            routeData.Values["subdomain"] = subdomain;
        return routeData;
    }

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        object subdomainParam = requestContext.HttpContext.Request.Params["subdomain"];
        if (subdomainParam != null)
            values["subdomain"] = subdomainParam;
        return base.GetVirtualPath(requestContext, values);
    }
}

For convenience, call the following MapSubdomainRoute method from your RegisterRoutes method just as you would plain old MapRoute:

static void MapSubdomainRoute(this RouteCollection routes, string name, string url, object defaults = null, object constraints = null)
{
    routes.Add(name, new SubdomainRoute(url) {
        Defaults = new RouteValueDictionary(defaults),
        Constraints = new RouteValueDictionary(constraints),
        DataTokens = new RouteValueDictionary()
    });
}

Finally, to conveniently access the subdomain (either from a true subdomain or a query parameter), it is helpful to create a Controller base class with this Subdomain property:

protected string Subdomain
{
    get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
Freakish answered 8/3, 2013 at 5:50 Comment(4)
I updated the code to make the subdomain always available as a route value. This simplifies access to the subdomain.Freakish
I like this. Very simple, and more than enough for my project.Lieselotteliestal
This is a great answer. Is there a way for this to work with route attributes? I'm trying to make this work for paths like "subdomain.domain.com/portal/register" and using attributes would make this easier.Hourigan
@Hourigan - Attribute routes are not extensible like convention based routes are. The only way to do something like that would be to build your own attribute routing system.Tarr
M
23

This is not my work, but I had to add it on this answer.

Here is a great solution to this problem. Maartin Balliauw wrote code that creates a DomainRoute class that can be used very similarly to the normal routing.

http://blog.maartenballiauw.be/post/2009/05/20/ASPNET-MVC-Domain-Routing.aspx

Sample use would be like this...

routes.Add("DomainRoute", new DomainRoute( 
    "{customer}.example.com", // Domain with parameters 
    "{action}/{id}",    // URL with parameters 
    new { controller = "Home", action = "Index", id = "" }  // Parameter defaults 
))

;

Mcclelland answered 27/4, 2010 at 18:22 Comment(7)
There is a problem with this solution. Say, you want to handle subdomains as different users: routes.Add("SD", new DomainRoute("user}.localhost", "", new { controller = "Home", action = "IndexForUser", user="u1" } )); It caches the homepage as well. This is because of the regex that's generated. In order to fix this, you can make a copy of the CreateRegex method in DomainRoute.cs, name it CreateDomainRegex, change the * on this line to +: source = source.Replace("}", @">([a-zA-Z0-9_]*))"); and use this new method for domain regx in GetRouteData method: domainRegex = CreateDomainRegex(Domain);Mazy
I don't know why I can't run this code... I just receive SERVER NOT FOUND error... means the code is not working for me... are you setting any other configuration or something?!Gravely
I've created a Gist of my version of this gist.github.com/IDisposable/77f11c6f7693f9d181bbMoneymaking
@Moneymaking what is MvcApplication.DnsSuffix?Newfangled
We just expose the base DNS domain in web.config... typical value would be .example.orgMoneymaking
I have a question please, In the answer where route is defined. {customer}.example.com in this route what would be the value of customer?? anything before .example will be the value customer?Helldiver
Also how can I test it with localhost , Visual studio solution.? What would be the url for localhost if my project runs on localhost:42323 ???Helldiver
F
4

To capture the subdomain when using Web API, override the Action Selector to inject a subdomain query parameter. Then use the subdomain query parameter in your controllers' actions like this:

public string Get(string id, string subdomain)

This approach makes debugging convenient since you can specify the query parameter by hand when using localhost instead of the actual host name (see the standard MVC5 routing answer for details). This is the code for Action Selector:

class SubdomainActionSelector : IHttpActionSelector
{
    private readonly IHttpActionSelector defaultSelector;

    public SubdomainActionSelector(IHttpActionSelector defaultSelector)
    {
        this.defaultSelector = defaultSelector;
    }

    public ILookup<string, HttpActionDescriptor> GetActionMapping(HttpControllerDescriptor controllerDescriptor)
    {
        return defaultSelector.GetActionMapping(controllerDescriptor);
    }

    public HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
    {
        var routeValues = controllerContext.Request.GetRouteData().Values;
        if (!routeValues.ContainsKey("subdomain")) {
            string host = controllerContext.Request.Headers.Host;
            int index = host.IndexOf('.');
            if (index >= 0)
                controllerContext.Request.GetRouteData().Values.Add("subdomain", host.Substring(0, index));
        }
        return defaultSelector.SelectAction(controllerContext);
    }
}

Replace the default Action Selector by adding this to WebApiConfig.Register:

config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Freakish answered 10/9, 2013 at 19:33 Comment(1)
Anyone having issues where the route data doesn't appear on the web API controller and inspecting the Request.GetRouteData inside the controller is showing no values?Newmown
S
3

Yes but you have to create your own route handler.

Typically the route is not aware of the domain because the application could be deployed to any domain and the route would not care one way or another. But in your case you want to base the controller and action off the domain, so you will have to create a custom route that is aware of the domain.

Senility answered 10/11, 2008 at 18:13 Comment(0)
H
3

I created library for subdomain routing which you can create such a route. It is working currently for a .NET Core 1.1 and .NET Framework 4.6.1 but will be updated in near future. This is how is it working:
1) Map subdomain route in Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    var hostnames = new[] { "localhost:54575" };

    app.UseMvc(routes =>
    {
        routes.MapSubdomainRoute(
            hostnames,
            "SubdomainRoute",
            "{username}",
            "{controller}/{action}",
            new { controller = "Home", action = "Index" });
    )};

2) Controllers/HomeController.cs

public IActionResult Index(string username)
{
    //code
}

3) That lib will also allow you to generate URLs and forms. Code:

@Html.ActionLink("User home", "Index", "Home" new { username = "user1" }, null)

Will generate <a href="http://user1.localhost:54575/Home/Index">User home</a> Generated URL will also depend on current host location and schema.
You can also use html helpers for BeginForm and UrlHelper. If you like you can also use new feature called tag helpers (FormTagHelper, AnchorTagHelper)
That lib does not have any documentation yet, but there are some tests and samples project so feel free to explore it.

Hoofer answered 17/10, 2017 at 17:8 Comment(0)
F
2

In ASP.NET Core, the host is available via Request.Host.Host. If you want to allow overriding the host via a query parameter, first check Request.Query.

To cause a host query parameter to propagate into to new route-based URLs, add this code to the app.UseMvc route configuration:

routes.Routes.Add(new HostPropagationRouter(routes.DefaultHandler));

And define HostPropagationRouter like this:

/// <summary>
/// A router that propagates the request's "host" query parameter to the response.
/// </summary>
class HostPropagationRouter : IRouter
{
    readonly IRouter router;

    public HostPropagationRouter(IRouter router)
    {
        this.router = router;
    }

    public VirtualPathData GetVirtualPath(VirtualPathContext context)
    {
        if (context.HttpContext.Request.Query.TryGetValue("host", out var host))
            context.Values["host"] = host;
        return router.GetVirtualPath(context);
    }

    public Task RouteAsync(RouteContext context) => router.RouteAsync(context);
}
Freakish answered 12/10, 2016 at 10:34 Comment(0)
A
1

After defining a new Route handler that would look at the host passed in the URL, you can go with the idea of a base Controller that is aware of the Site it’s being accessed for. It looks like this:

public abstract class SiteController : Controller {
    ISiteProvider _siteProvider;

    public SiteController() {
        _siteProvider = new SiteProvider();
    }

    public SiteController(ISiteProvider siteProvider) {
        _siteProvider = siteProvider;
    }

    protected override void Initialize(RequestContext requestContext) {
        string[] host = requestContext.HttpContext.Request.Headers["Host"].Split(':');

        _siteProvider.Initialise(host[0]);

        base.Initialize(requestContext);
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext) {
        ViewData["Site"] = Site;

        base.OnActionExecuting(filterContext);
    }

    public Site Site {
        get {
            return _siteProvider.GetCurrentSite();
        }
    }

}

ISiteProvider is a simple interface:

public interface ISiteProvider {
    void Initialise(string host);
    Site GetCurrentSite();
}

I refer you go to Luke Sampson Blog

Amylene answered 19/6, 2015 at 8:10 Comment(0)
R
1

If you are looking at giving MultiTenancy capabilities to your project with different domains/subdomains for each tenant, you should have a look at SaasKit:

https://github.com/saaskit/saaskit

Code examples can be seen here: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy

Some examples using ASP.NET core: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/

EDIT: If you do no want to use SaasKit in your ASP.NET core project you can have a look at Maarten's implementation of domain routing for MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain-routing-and-resolving-current-tenant-with-aspnet-mvc-6-aspnet-5.html

However those Gists are not maintained and need to be tweaked to work with the latest release of ASP.NET core.

Direct link to the code: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs

Rivkarivkah answered 5/10, 2016 at 22:35 Comment(1)
Not looking for multitenancy -- but thanks for the tip!Outsoar
R
1

Few month ago I have developed an attribute that restricts methods or controllers to specific domains.

It is quite easy to use:

[IsDomain("localhost","example.com","www.example.com","*.t1.example.com")]
[HttpGet("RestrictedByHost")]
public IActionResult Test(){}

You can also apply it directly on a controller.

public class IsDomainAttribute : Attribute, Microsoft.AspNetCore.Mvc.Filters.IAuthorizationFilter
{

    public IsDomainAttribute(params string[]  domains)
    {
        Domains = domains;
    }

    public string[] Domains { get; }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        var host = context.HttpContext.Request.Host.Host;
        if (Domains.Contains(host))
            return;
        if (Domains.Any(d => d.EndsWith("*"))
                && Domains.Any(d => host.StartsWith(d.Substring(0, d.Length - 1))))
            return;
        if (Domains.Any(d => d.StartsWith("*"))
                && Domains.Any(d => host.EndsWith(d.Substring(1))))
            return;

        context.Result = new Microsoft.AspNetCore.Mvc.NotFoundResult();//.ChallengeResult
    }
}

Restriction: you may not be able to have two same routes on different methods with different filters I mean the following may throw an exception for duplicate route:

[IsDomain("test1.example.com")]
[HttpGet("/Test")]
public IActionResult Test1(){}

[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}
Ramtil answered 22/1, 2019 at 16:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.