¿Es posible hacer una ruta ASP.NET MVC basada en un subdominio?

235

¿Es posible tener una ruta ASP.NET MVC que use información de subdominio para determinar su ruta? Por ejemplo:

  • user1 .domain.com va a un lugar
  • user2 .domain.com va a otro?

¿O puedo hacerlo para que ambos vayan al mismo controlador / acción con un usernameparámetro?

Dan Esparza
fuente
Implementé un tipo similar de cosas para aplicaciones multi-tenanted, pero usando un controlador base abstracto en lugar de una clase de ruta personalizada. Mi publicación de blog está aquí .
Luke Sampson el
66
Asegúrese de considerar este enfoque: http://blog.tonywilliams.me.uk/asp-net-mvc-2-routing-subdomains-to-areas Encontré que es mejor para introducir la propiedad múltiple a mi aplicación que las otras respuestas , porque las áreas MVC son una buena manera de introducir controladores y vistas específicos de inquilino de una manera organizada.
trebormf
2
@trebormf: creo que debería agregarlo como respuesta, esto es lo que terminé usando como base para mi solución.
Shagglez
@Shagglez - Gracias. Fue una respuesta, pero un moderador lo convirtió en un comentario por razones que no puedo entender.
trebormf
55
El estilo de Tony estaba roto. Aquí hay uno que funcionó para mí: blog.tonywilliams.me.uk/…
Ronnie Overby

Respuestas:

168

Puede hacerlo creando una nueva ruta y agregándola a la colección de rutas en RegisterRoutes en su global.asax. A continuación se muestra un ejemplo muy simple de una ruta personalizada:

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
fuente
1
Gracias por la muestra detallada, pero no estoy siguiendo cómo ejecutar .Add desde Global.asax.
justSteve
44
Llamé a la ruta SubdomainRoute y la agregué como la primera ruta de esta manera: routes.Add (new SubdomainRoute ());
Jeff Handley
66
¿Este enfoque requiere codificar una lista de posibles subdominios?
Maxim V. Pavlov
2
No, puede agregar un campo de base de datos llamado algo así como "subdominio", que será lo que espera que sea el subdominio para un usuario en particular, o cualquier otra cosa, luego simplemente haga una búsqueda en el subdominio.
Ryan Hayes
1
¿Alguien podría recomendar una versión web de esto?
MatthewT
52

Para capturar el subdominio mientras se conservan las características de enrutamiento MVC5 estándar , use la siguiente SubdomainRouteclase derivada de Route.

Además, SubdomainRoutepermite que el subdominio se especifique opcionalmente como un parámetro de consulta , creación sub.example.com/foo/bary example.com/foo/bar?subdomain=subequivalente. Esto le permite probar antes de configurar los subdominios DNS. El parámetro de consulta (cuando está en uso) se propaga a través de nuevos enlaces generados por Url.Action, etc.

El parámetro de consulta también permite la depuración local con Visual Studio 2013 sin tener que configurarlo con netsh o ejecutarlo como Administrador . Por defecto, IIS Express solo se une a localhost cuando no está elevado; no se unirá a nombres de host como 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);
    }
}

Para mayor comodidad, llame al siguiente MapSubdomainRoutemétodo desde su RegisterRoutesmétodo tal como lo haría con el viejo 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()
    });
}

Finalmente, para acceder convenientemente al subdominio (ya sea desde un subdominio verdadero o un parámetro de consulta), es útil crear una clase base de Controlador con esta Subdomainpropiedad:

protected string Subdomain
{
    get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
Edward Brey
fuente
1
Actualicé el código para que el subdominio siempre esté disponible como un valor de ruta. Esto simplifica el acceso al subdominio.
Edward Brey el
Me gusta esto. Muy simple y más que suficiente para mi proyecto.
SoonDead
Esta es una respuesta genial. ¿Hay alguna manera de que esto funcione con los atributos de ruta? Estoy tratando de hacer que esto funcione para rutas como "subdominio.domain.com/portal/register" y el uso de atributos lo haría más fácil.
perfect_element
@perfect_element: las rutas de atributos no son extensibles como las rutas basadas en convenciones. La única forma de hacer algo así sería construir su propio sistema de enrutamiento de atributos.
NightOwl888
23

Este no es mi trabajo, pero tuve que agregarlo en esta respuesta.

Aquí hay una gran solución a este problema. Maartin Balliauw escribió un código que crea una clase DomainRoute que puede usarse de manera muy similar al enrutamiento normal.

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

El uso de la muestra sería así ...

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
fuente
55
Hay un problema con esta solución. Digamos que desea manejar subdominios como diferentes usuarios: rutas.Agregar ("SD", nuevo DomainRoute ("user} .localhost", "", new {controller = "Home", action = "IndexForUser", user = "u1 "})); También almacena en caché la página de inicio. Esto se debe a la expresión regular que se genera. Para solucionar esto, puede hacer una copia del método CreateRegex en DomainRoute.cs, asígnele el nombre CreateDomainRegex, cambie el * en esta línea a +: source = source.Replace ("}", @ "> ([a- zA-Z0-9 _] *)) "); y use este nuevo método para el dominio regx en el método GetRouteData: domainRegex = CreateDomainRegex (Domain);
Gorkem Pacaci
No sé por qué no puedo ejecutar este código ... Acabo de recibir un SERVER NOT FOUNDerror ... significa que el código no funciona para mí ... ¿está configurando alguna otra configuración o algo así?
Dr. TJ
He creado un Gist de mi versión de este gist.github.com/IDisposable/77f11c6f7693f9d181bb
IDisposable el
1
@IDisposable ¿Qué es MvcApplication.DnsSuffix?
HaBo
Simplemente exponemos el dominio DNS base en web.config ... el valor típico sería .example.org
IDisposable el
4

Para capturar el subdominio cuando se usa la API web , anule el selector de acción para inyectar un subdomainparámetro de consulta. Luego use el parámetro de consulta de subdominio en las acciones de sus controladores de esta manera:

public string Get(string id, string subdomain)

Este enfoque hace que la depuración sea conveniente ya que puede especificar el parámetro de consulta manualmente cuando usa localhost en lugar del nombre de host real (consulte la respuesta de enrutamiento MVC5 estándar para obtener más detalles). Este es el código para el Selector de acciones:

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

Reemplace el selector de acción predeterminado agregando esto a WebApiConfig.Register:

config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Edward Brey
fuente
¿Alguien que tiene problemas donde los datos de la ruta no aparecen en el controlador de API web e inspecciona Request.GetRouteData dentro del controlador no muestra valores?
Alan Macdonald el
3

Sí, pero debe crear su propio controlador de ruta.

Por lo general, la ruta no conoce el dominio porque la aplicación podría implementarse en cualquier dominio y la ruta no importaría de una forma u otra. Pero en su caso, desea basar el controlador y la acción fuera del dominio, por lo que tendrá que crear una ruta personalizada que conozca el dominio.

Nick Berardi
fuente
3

Creé una biblioteca para el enrutamiento del subdominio, que puede crear dicha ruta. Actualmente está trabajando para .NET Core 1.1 y .NET Framework 4.6.1, pero se actualizará en un futuro próximo. Así es como funciona:
1) Mapa de ruta de subdominio en 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) Controladores / HomeController.cs

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

3) Esa biblioteca también te permitirá generar URL y formularios. Código:

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

Generará <a href="http://user1.localhost:54575/Home/Index">User home</a> URL generada también dependerá de la ubicación actual anfitrión y el esquema.
También puede usar html helpers para BeginFormy UrlHelper. Si lo desea, también puede usar una nueva función llamada ayudantes de etiqueta ( FormTagHelper, AnchorTagHelper)
Esa biblioteca todavía no tiene documentación, pero hay algunas pruebas y proyectos de muestra, así que no dude en explorarla.

Mariusz
fuente
2

En ASP.NET Core , el host está disponible a través de Request.Host.Host. Si desea permitir anular el host mediante un parámetro de consulta, primero verifique Request.Query.

Para hacer que un parámetro de consulta de host se propague a nuevas URL basadas en rutas, agregue este código a la app.UseMvcconfiguración de la ruta:

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

Y definir HostPropagationRouterasí:

/// <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
fuente
1

Después de definir un nuevo controlador de ruta que vería el host pasado en la URL , puede ir con la idea de un controlador base que conozca el sitio al que se está accediendo. Se parece a esto:

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 es una interfaz simple:

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

Os recomiendo que vayan al blog de Luke Sampson

Amirhossein Mehrvarzi
fuente
1

Si está buscando dar capacidades de MultiTenancy a su proyecto con diferentes dominios / subdominios para cada inquilino, debe echar un vistazo a SaasKit:

https://github.com/saaskit/saaskit

Los ejemplos de código se pueden ver aquí: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy

Algunos ejemplos que usan el núcleo ASP.NET: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/

EDITAR: si no desea utilizar SaasKit en su proyecto principal de ASP.NET, puede echar un vistazo a la implementación de Maarten de enrutamiento de dominio para MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain -rutamiento-y-resolución-actual-inquilino-con-aspnet-mvc-6-aspnet-5.html

Sin embargo, esos Gists no se mantienen y deben modificarse para que funcionen con la última versión del núcleo ASP.NET.

Enlace directo al código: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs

Darxtar
fuente
No busco multitenencia, ¡pero gracias por la sugerencia!
Dan Esparza
0

Hace unos meses desarrollé un atributo que restringe los métodos o controladores a dominios específicos.

Es muy fácil de usar:

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

También puede aplicarlo directamente en un controlador.

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

Restricción: es posible que no pueda tener dos mismas rutas en diferentes métodos con diferentes filtros. Quiero decir, lo siguiente puede arrojar una excepción para rutas duplicadas:

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

[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}
Vaquero
fuente