Est-il possible de créer une route ASP.NET MVC basée sur un sous-domaine?

235

Est-il possible d'avoir une route ASP.NET MVC qui utilise les informations de sous-domaine pour déterminer sa route? Par exemple:

  • user1 .domaine.com va à un seul endroit
  • user2 .domain.com passe à un autre?

Ou, puis-je faire en sorte que les deux vont au même contrôleur / action avec un usernameparamètre?

Dan Esparza
la source
J'ai implémenté une sorte de chose similaire pour les applications multi-locataires, mais en utilisant un contrôleur de base abstrait plutôt qu'une classe Route personnalisée. Mon article de blog à ce sujet est ici .
Luke Sampson
6
Assurez-vous de considérer cette approche: http://blog.tonywilliams.me.uk/asp-net-mvc-2-routing-subdomains-to-areas J'ai trouvé que c'était mieux pour introduire la mutualisation dans mon application que les autres réponses , car les zones MVC sont un bon moyen d'introduire des contrôleurs et des vues spécifiques au locataire de manière organisée.
trebormf
2
@trebormf - Je pense que vous devriez l'ajouter comme réponse, c'est ce que j'ai fini par utiliser comme base pour ma solution.
Shagglez
@Shagglez - Merci. C'était une réponse, mais un modérateur l'a convertie en commentaire pour des raisons que je ne comprends pas.
trebormf
5
Le style de Tony était cassé. En voici une qui a fonctionné pour moi: blog.tonywilliams.me.uk/…
Ronnie Overby

Réponses:

168

Vous pouvez le faire en créant un nouvel itinéraire et en l'ajoutant à la collection d'itinéraires dans RegisterRoutes dans votre global.asax. Voici un exemple très simple d'un itinéraire personnalisé:

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;
    }
}
Jon Cahill
la source
1
Merci pour l'exemple détaillé mais je ne suis pas en train de suivre comment exécuter le .Add à partir de Global.asax.
justSteve
4
J'ai appelé la route SubdomainRoute et l'ai ajoutée comme première route comme celle-ci: routes.Add (new SubdomainRoute ());
Jeff Handley
6
Cette approche nécessite-t-elle de coder en dur une liste de sous-domaines possibles?
Maxim V. Pavlov
2
Non, vous pouvez ajouter un champ de base de données appelé quelque chose comme "sous-domaine" que vous serez ce que vous attendez du sous-domaine pour un utilisateur particulier, ou quoi que ce soit d'autre, puis faites simplement une recherche sur le sous-domaine.
Ryan Hayes
1
Quelqu'un pourrait-il recommander une version de formulaires Web de cela?
MatthewT
52

Pour capturer le sous-domaine tout en conservant les fonctionnalités de routage MVC5 standard , utilisez la SubdomainRouteclasse suivante dérivée de Route.

En outre, SubdomainRoutepermet éventuellement de spécifier le sous-domaine en tant que paramètre de requête , faisant sub.example.com/foo/baret example.com/foo/bar?subdomain=subéquivalent. Cela vous permet de tester avant de configurer les sous-domaines DNS. Le paramètre de requête (lorsqu'il est utilisé) est propagé via de nouveaux liens générés par Url.Action, etc.

Le paramètre de requête permet également le débogage local avec Visual Studio 2013 sans avoir à configurer avec netsh ou à exécuter en tant qu'administrateur . Par défaut, IIS Express ne se lie à localhost que lorsqu'il n'est pas élevé; il ne se liera pas aux noms d'hôte comme 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);
    }
}

Pour plus de commodité, appelez la MapSubdomainRouteméthode suivante à partir de votre RegisterRoutesméthode, comme vous le feriez tout simplement 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()
    });
}

Enfin, pour accéder facilement au sous-domaine (à partir d'un vrai sous-domaine ou d'un paramètre de requête), il est utile de créer une classe de base Controller avec cette Subdomainpropriété:

protected string Subdomain
{
    get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
Edward Brey
la source
1
J'ai mis à jour le code pour que le sous-domaine soit toujours disponible en tant que valeur d'itinéraire. Cela simplifie l'accès au sous-domaine.
Edward Brey
J'aime ça. Très simple et largement suffisant pour mon projet.
SoonDead
C'est une excellente réponse. Existe-t-il un moyen pour que cela fonctionne avec les attributs de route? J'essaie de faire en sorte que cela fonctionne pour des chemins comme "subdomain.domain.com/portal/register" et l'utilisation d'attributs rendrait cela plus facile.
perfect_element
@perfect_element - Les routes d'attributs ne sont pas extensibles comme les routes basées sur les conventions. La seule façon de faire quelque chose comme ça serait de créer votre propre système de routage d'attributs.
NightOwl888
23

Ce n'est pas mon travail, mais j'ai dû l'ajouter à cette réponse.

Voici une excellente solution à ce problème. Maartin Balliauw a écrit du code qui crée une classe DomainRoute qui peut être utilisée de manière très similaire au routage normal.

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

Exemple d'utilisation serait comme ça ...

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

;

Jim Blake
la source
5
Il y a un problème avec cette solution. Supposons que vous souhaitiez gérer les sous-domaines en tant qu'utilisateurs différents: routes.Add ("SD", new DomainRoute ("user} .localhost", "", new {controller = "Home", action = "IndexForUser", user = "u1 "})); Il met également en cache la page d'accueil. C'est à cause de l'expression régulière qui est générée. Pour résoudre ce problème, vous pouvez faire une copie de la méthode CreateRegex dans DomainRoute.cs, nommez-le CreateDomainRegex, changez le * sur cette ligne en +: source = source.Replace ("}", @ "> ([a- zA-Z0-9 _] *)) "); et utilisez cette nouvelle méthode pour le domaine regx dans la méthode GetRouteData: domainRegex = CreateDomainRegex (Domain);
Gorkem Pacaci
Je ne sais pas pourquoi je ne peux pas exécuter ce code ... Je reçois juste une SERVER NOT FOUNDerreur ... signifie que le code ne fonctionne pas pour moi ... définissez-vous une autre configuration ou quelque chose?!
Dr TJ
J'ai créé un résumé de ma version de ce gist.github.com/IDisposable/77f11c6f7693f9d181bb
IDisposable
1
@IDisposable qu'est-ce que MvcApplication.DnsSuffix?
HaBo
Nous exposons simplement le domaine DNS de base dans web.config ... la valeur typique serait .example.org
IDisposable
4

Pour capturer le sous-domaine lors de l'utilisation de l' API Web , remplacez le sélecteur d'action pour injecter un subdomainparamètre de requête. Utilisez ensuite le paramètre de requête de sous-domaine dans les actions de vos contrôleurs comme ceci:

public string Get(string id, string subdomain)

Cette approche rend le débogage pratique car vous pouvez spécifier le paramètre de requête à la main lorsque vous utilisez localhost au lieu du nom d'hôte réel (voir la réponse de routage MVC5 standard pour plus de détails). Voici le code du sélecteur d'action:

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);
    }
}

Remplacez le sélecteur d'action par défaut en l'ajoutant à WebApiConfig.Register:

config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Edward Brey
la source
Toute personne ayant des problèmes où les données de route n'apparaissent pas sur le contrôleur d'API Web et inspectant le Request.GetRouteData à l'intérieur du contrôleur n'affiche aucune valeur?
Alan Macdonald
3

Oui mais vous devez créer votre propre gestionnaire d'itinéraire.

En règle générale, la route ne connaît pas le domaine, car l'application peut être déployée sur n'importe quel domaine et la route s'en moque d'une manière ou d'une autre. Mais dans votre cas, vous voulez baser le contrôleur et l'action hors du domaine, vous devrez donc créer une route personnalisée qui connaît le domaine.

Nick Berardi
la source
3

J'ai créé une bibliothèque pour le routage de sous-domaine que vous pouvez créer un tel itinéraire. Il fonctionne actuellement pour un .NET Core 1.1 et .NET Framework 4.6.1 mais sera mis à jour dans un proche avenir. Voici comment cela fonctionne:
1) Mapper la route du sous-domaine dans 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) Contrôleurs / HomeController.cs

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

3) Cette bibliothèque vous permettra également de générer des URL et des formulaires. Code:

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

Générerons <a href="http://user1.localhost:54575/Home/Index">User home</a> URL générée dépendra également de l'emplacement de l' hôte actuel et le schéma.
Vous pouvez également utiliser des assistants html pour BeginFormet UrlHelper. Si vous le souhaitez, vous pouvez également utiliser une nouvelle fonctionnalité appelée helpers de balises ( FormTagHelper, AnchorTagHelper)
Cette bibliothèque n'a pas encore de documentation, mais il existe des tests et des exemples de projets, alors n'hésitez pas à l'explorer.

Mariusz
la source
2

Dans ASP.NET Core , l'hôte est disponible via Request.Host.Host. Si vous souhaitez autoriser la substitution de l'hôte via un paramètre de requête, vérifiez d'abord Request.Query.

Pour provoquer la propagation d'un paramètre de requête hôte vers de nouvelles URL basées sur l' app.UseMvcitinéraire , ajoutez ce code à la configuration de l' itinéraire:

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

Et définissez HostPropagationRoutercomme ceci:

/// <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);
}
Edward Brey
la source
1

Après avoir défini un nouveau gestionnaire de route qui examinerait l'hôte transmis dans l'URL , vous pouvez partir avec l'idée d'un contrôleur de base qui connaît le site auquel il accède. Cela ressemble à ceci:

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 est une interface simple:

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

Je vous réfère, allez sur le blog de Luke Sampson

Amirhossein Mehrvarzi
la source
1

Si vous cherchez à donner des capacités MultiTenancy à votre projet avec différents domaines / sous-domaines pour chaque locataire, vous devriez jeter un œil à SaasKit:

https://github.com/saaskit/saaskit

Des exemples de code peuvent être consultés ici: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy

Quelques exemples utilisant le noyau ASP.NET: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/

EDIT: Si vous ne souhaitez pas utiliser SaasKit dans votre projet principal ASP.NET, vous pouvez consulter la mise en œuvre de Maarten du routage de domaine pour MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain -routage-et-résolution-locataire-actuel-avec-aspnet-mvc-6-aspnet-5.html

Cependant, ces Gists ne sont pas maintenus et doivent être modifiés pour fonctionner avec la dernière version du noyau ASP.NET.

Lien direct vers le code: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs

Darxtar
la source
Vous ne recherchez pas la mutualisation - mais merci pour le conseil!
Dan Esparza
0

Il y a quelques mois, j'ai développé un attribut qui restreint les méthodes ou les contrôleurs à des domaines spécifiques.

Il est assez simple à utiliser:

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

Vous pouvez également l'appliquer directement sur un contrôleur.

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: vous ne pourrez peut-être pas avoir deux mêmes itinéraires sur des méthodes différentes avec des filtres différents. Je veux dire que les éléments suivants peuvent lever une exception pour l'itinéraire en double:

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

[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}
Jean
la source