È possibile creare una route MVC ASP.NET in base a un sottodominio?


235

È possibile disporre di una route ASP.NET MVC che utilizza le informazioni del sottodominio per determinarne la route? Per esempio:

  • user1 .domain.com va in un posto
  • user2 .domain.com va ad un altro?

Oppure, posso farlo in modo che entrambi vadano allo stesso controller / azione con un usernameparametro?


Ho implementato una cosa simile per le applicazioni multi-tenant, ma utilizzando un controller di base astratto anziché una classe Route personalizzata. Il mio post sul blog è qui .
Luke Sampson,

6
Assicurati di considerare questo approccio: http://blog.tonywilliams.me.uk/asp-net-mvc-2-routing-subdomains-to-areas L' ho trovato migliore per l'introduzione della multitenancy nella mia app rispetto alle altre risposte , poiché le aree MVC sono un modo efficace per introdurre controller e viste specifici per tenant in modo organizzato.
trebormf,

2
@trebormf - Penso che dovresti aggiungerlo come risposta, questo è quello che ho finito per usare come base per la mia soluzione.
Shagglez,

@Shagglez - Grazie. Era una risposta, ma un moderatore l'ha convertita in un commento per motivi che non riesco a capire.
trebormf,

5
Tony è come se fosse rotto. Eccone uno che ha funzionato per me: blog.tonywilliams.me.uk/…
Ronnie Overby

Risposte:


168

Puoi farlo creando una nuova rotta e aggiungendola alla raccolta delle rotte in RegisterRoutes in global.asax. Di seguito è riportato un esempio molto semplice di un percorso personalizzato:

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

1
Grazie per l'esempio dettagliato ma non sto seguendo come eseguire .Add da Global.asax.
JustSteve

4
Ho chiamato la route SubdomainRoute e l'ho aggiunta come prima route in questo modo: route.Add (new SubdomainRoute ());
Jeff Handley,

6
Questo approccio richiede la codifica definitiva di un elenco di possibili sottodomini?
Maxim V. Pavlov,

2
No, puoi aggiungere un campo di database chiamato qualcosa come "sottodominio" che sarà quello che ti aspetti dal sottodominio per un determinato utente o qualsiasi altra cosa, quindi fai una ricerca sul sottodominio.
Ryan Hayes,

1
Qualcuno potrebbe raccomandare una versione di questo modulo web?
Matteo

52

Per acquisire il sottodominio mantenendo le funzionalità di routing MVC5 standard , utilizzare la seguente SubdomainRouteclasse derivata Route.

Inoltre, SubdomainRouteconsente facoltativamente di specificare il sottodominio come parametro di query , di fabbricazione sub.example.com/foo/bare example.com/foo/bar?subdomain=subequivalente. Ciò consente di eseguire il test prima di configurare i sottodomini DNS. Il parametro di query (quando in uso) viene propagato attraverso nuovi collegamenti generati da Url.Action, ecc.

Il parametro query abilita anche il debug locale con Visual Studio 2013 senza dover configurare con netsh o eseguire come amministratore . Per impostazione predefinita, IIS Express si lega a localhost solo se non elevato; non si legherà a nomi host come 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);
    }
}

Per comodità, chiama il seguente MapSubdomainRoutemetodo dal tuo RegisterRoutesmetodo come faresti con il vecchio 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()
    });
}

Infine, per accedere comodamente al sottodominio (da un sottodominio vero o da un parametro di query), è utile creare una classe base del controller con questa Subdomainproprietà:

protected string Subdomain
{
    get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}

1
Ho aggiornato il codice per rendere il sottodominio sempre disponibile come valore di route. Questo semplifica l'accesso al sottodominio.
Edward Brey,

Mi piace questo. Molto semplice e più che sufficiente per il mio progetto.
Presto, il

Questa è un'ottima risposta C'è un modo per farlo funzionare con gli attributi di route? Sto cercando di farlo funzionare per percorsi come "subdomain.domain.com/portal/register" e l'utilizzo di attributi renderebbe tutto più semplice.
perfect_element

@perfect_element: le route degli attributi non sono estensibili come le route basate su convenzioni. L'unico modo per fare qualcosa del genere sarebbe costruire il proprio sistema di routing degli attributi.
NightOwl888

23

Questo non è il mio lavoro, ma ho dovuto aggiungerlo a questa risposta.

Ecco un'ottima soluzione a questo problema. Maartin Balliauw ha scritto codice che crea una classe DomainRoute che può essere utilizzata in modo molto simile al normale routing.

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

L'uso del campione sarebbe come questo ...

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

;


5
Si è verificato un problema con questa soluzione. Supponiamo che tu voglia gestire i sottodomini come utenti diversi: route.Add ("SD", new DomainRoute ("user} .localhost", "", new {controller = "Home", action = "IndexForUser", user = "u1 "})); Memorizza anche la homepage. Ciò è dovuto alla regex generata. Per risolvere questo problema, puoi creare una copia del metodo CreateRegex in DomainRoute.cs, denominarlo CreateDomainRegex, modificare il * su questa riga in +: source = source.Replace ("}", @ "> ([a- zA-Z0-9 _] *)) "); e utilizzare questo nuovo metodo per il dominio regx nel metodo GetRouteData: domainRegex = CreateDomainRegex (Domain);
Gorkem Pacaci,

Non so perché non riesco a eseguire questo codice ... Ricevo solo un SERVER NOT FOUNDerrore ... significa che il codice non funziona per me ... stai impostando un'altra configurazione o qualcosa del genere ?!
Dr TJ,

Ho creato un Sintesi della mia versione di questo gist.github.com/IDisposable/77f11c6f7693f9d181bb
IDisposable

1
@IDisposable cos'è MvcApplication.DnsSuffix?
HaBo,

Esponiamo semplicemente il dominio DNS di base in web.config ... il valore tipico sarebbe .example.org
IDisposable

4

Per acquisire il sottodominio quando si utilizza l' API Web , sovrascrivere il Selettore azioni per iniettare un subdomainparametro di query. Quindi utilizzare il parametro di query del sottodominio nelle azioni dei controller in questo modo:

public string Get(string id, string subdomain)

Questo approccio semplifica il debug poiché è possibile specificare manualmente il parametro di query quando si utilizza localhost anziché il nome host effettivo (vedere la risposta di routing MVC5 standard per i dettagli). Questo è il codice per 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);
    }
}

Sostituisci il Selettore azioni predefinito aggiungendolo a WebApiConfig.Register:

config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));

Qualcuno ha problemi in cui i dati del percorso non vengono visualizzati sul controller API Web e l'ispezione di Request.GetRouteData all'interno del controller non mostra valori?
Alan Macdonald,

3

Sì, ma devi creare il tuo gestore di percorsi.

In genere la route non è a conoscenza del dominio perché l'applicazione potrebbe essere distribuita a qualsiasi dominio e la route non si occuperebbe in un modo o nell'altro. Ma nel tuo caso vuoi basare il controller e l'azione fuori dal dominio, quindi dovrai creare un percorso personalizzato che sia a conoscenza del dominio.


3

Ho creato una libreria per l'instradamento del sottodominio che puoi creare un tale percorso. Funziona attualmente per .NET Core 1.1 e .NET Framework 4.6.1 ma verrà aggiornato nel prossimo futuro. Ecco come funziona:
1) Mappa il percorso del sottodominio 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) Controller / HomeController.cs

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

3) Quella lib ti permetterà anche di generare URL e moduli. Codice:

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

Genererà <a href="http://user1.localhost:54575/Home/Index">User home</a> URL generato dipenderà anche da posizione host corrente e schema.
Puoi anche usare gli helper HTML per BeginForme UrlHelper. Se ti piace puoi anche usare una nuova funzione chiamata tag helpers ( FormTagHelper, AnchorTagHelper)
Che lib non ha ancora documentazione, ma ci sono alcuni test e progetti di esempio, quindi sentiti libero di esplorarlo.


2

In ASP.NET Core , l'host è disponibile tramite Request.Host.Host. Se si desidera consentire l'override dell'host tramite un parametro di query, verificare innanzitutto Request.Query.

Per fare in modo che un parametro di query host si propaghi in nuovi URL basati su route, aggiungi questo codice alla app.UseMvcconfigurazione della route:

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

E definire HostPropagationRoutercosì:

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

1

Dopo aver definito un nuovo gestore di route che guarderebbe l'host passato nell'URL , puoi andare con l'idea di un controller di base che è a conoscenza del sito a cui si accede. Sembra così:

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 è una semplice interfaccia:

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

Vi rimando, andate al blog di Luke Sampson


1

Se stai cercando di offrire funzionalità MultiTenancy al tuo progetto con domini / sottodomini diversi per ciascun inquilino, dovresti dare un'occhiata a SaasKit:

https://github.com/saaskit/saaskit

Esempi di codice sono disponibili qui: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy

Alcuni esempi usando il core ASP.NET: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/

EDIT: Se non vuoi usare SaasKit nel tuo progetto principale ASP.NET puoi dare un'occhiata all'implementazione di Maarten del routing del dominio per MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain -routing-e-risolvere-corrente-tenant-con-ASPNET-MVC-6-ASPNET-5.html

Tuttavia, questi Gist non vengono mantenuti e devono essere modificati per funzionare con l'ultima versione del core ASP.NET.

Link diretto al codice: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs


Non cercare la multi-tenancy - ma grazie per la punta!
Dan Esparza,

0

Qualche mese fa ho sviluppato un attributo che limita metodi o controller a domini specifici.

È abbastanza facile da usare:

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

Puoi anche applicarlo direttamente su un 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
    }
}

Limitazione: potresti non essere in grado di avere due stessi percorsi su metodi diversi con filtri diversi Intendo che quanto segue può generare un'eccezione per il percorso duplicato:

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

[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.