É possível criar uma rota do ASP.NET MVC com base em um subdomínio?

235

É possível ter uma rota ASP.NET MVC que usa informações de subdomínio para determinar sua rota? Por exemplo:

  • O usuário1 .domínio.com vai para um local
  • user2 .domínio.com vai para outro?

Ou posso fazê-lo para que ambos passem para o mesmo controlador / ação com um usernameparâmetro?

Dan Esparza
fonte
Eu implementei um tipo de coisa semelhante para aplicativos com vários arrendatários, mas usando um Controller base abstrato em vez de uma classe Route personalizada. Meu blog está aqui .
6339 Luke Sampson
6
Certifique-se de considerar esta abordagem: http://blog.tonywilliams.me.uk/asp-net-mvc-2-routing-subdomains-to-areas Achei melhor para a introdução de multitenancy no meu aplicativo do que as outras respostas , porque as áreas MVC são uma boa maneira de introduzir controladores e visualizações específicos de inquilino de maneira organizada.
Trebormf
2
@ trebormf - Eu acho que você deve adicioná-lo como resposta, foi isso que acabei usando como base para a minha solução.
Shagglez
@ Shagglez - Obrigado. Foi uma resposta, mas um moderador o converteu em um comentário por razões que não consigo entender.
trebormf
5
O de Tony estava quebrado. Aqui está um que trabalhou para mim: blog.tonywilliams.me.uk/...
Ronnie Overby

Respostas:

168

Você pode fazer isso criando uma nova rota e adicionando-a à coleção de rotas em RegisterRoutes no seu global.asax. Abaixo está um exemplo muito simples de uma rota 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
fonte
1
Obrigado pelo exemplo detalhado, mas não estou acompanhando como executar o .Add do Global.asax.
precisa
4
Chamei a rota SubdomainRoute e a adicionei como a primeira rota assim: routes.Add (new SubdomainRoute ());
9139 Jeff Handley
6
Essa abordagem requer a codificação embutida de uma lista de possíveis subdomínios?
Maxim V. Pavlov
2
Não, você pode adicionar um campo de banco de dados chamado algo como "subdomínio" que será o que você espera que o subdomínio seja para um usuário específico, ou qualquer outra coisa, basta fazer uma pesquisa no subdomínio.
Ryan Hayes
1
Alguém poderia recomendar uma versão webforms disso?
precisa
52

Para capturar o subdomínio enquanto mantém os recursos de roteamento MVC5 padrão , use a seguinte SubdomainRouteclasse derivada deRoute .

Além disso, SubdomainRoutepermite que o subdomínio seja opcionalmente especificado como um parâmetro de consulta , making sub.example.com/foo/bare example.com/foo/bar?subdomain=subequivalente. Isso permite que você teste antes da configuração dos subdomínios DNS. O parâmetro de consulta (quando em uso) é propagado por novos links gerados porUrl.Action etc.

O parâmetro query também habilita a depuração local com o Visual Studio 2013 sem precisar configurar com netsh ou executar como administrador . Por padrão, o IIS Express se liga apenas ao host local quando não é elevado; não será vinculado a nomes de host sinônimos 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);
    }
}

Por conveniência, chame o seguinte MapSubdomainRoutemétodo a partir do seu RegisterRoutesmétodo, exatamente como você faria 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()
    });
}

Por fim, para acessar convenientemente o subdomínio (de um subdomínio verdadeiro ou de um parâmetro de consulta), é útil criar uma classe base do Controller com esta Subdomainpropriedade:

protected string Subdomain
{
    get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
Edward Brey
fonte
1
Atualizei o código para tornar o subdomínio sempre disponível como um valor de rota. Isso simplifica o acesso ao subdomínio.
perfil completo de Eduardo Brey
Eu gosto disso. Muito simples e mais do que suficiente para o meu projeto.
SoonDead
Esta é uma ótima resposta. Existe uma maneira de isso funcionar com atributos de rota? Estou tentando fazer isso funcionar para caminhos como "subdomínio.domínio.com/portal/register" e o uso de atributos tornaria isso mais fácil.
perfect_element
@perfect_element - As rotas de atributos não são extensíveis, como as rotas baseadas em convenções. A única maneira de fazer algo assim seria criar seu próprio sistema de roteamento de atributos.
quer
23

Este não é o meu trabalho, mas tive que adicioná-lo nesta resposta.

Aqui está uma ótima solução para esse problema. Maartin Balliauw escreveu um código que cria uma classe DomainRoute que pode ser usada de maneira muito semelhante ao roteamento normal.

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

O uso da amostra seria assim ...

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
fonte
5
Há um problema com esta solução. Digamos que você queira manipular subdomínios como usuários diferentes: routes.Add ("SD", new DomainRoute ("user} .localhost", "", new {controller = "Home", action = "IndexForUser", user = "u1 "})); Ele também armazena em cache a página inicial. Isso ocorre por causa do regex gerado. Para corrigir isso, você pode fazer uma cópia do método CreateRegex em DomainRoute.cs, denomine CreateDomainRegex, altere o * nesta linha para +: source = source.Replace ("}", @ "> ([a- zA-Z0-9 _] *)) "); e use esse novo método para o domínio regx no método GetRouteData: domainRegex = CreateDomainRegex (Domain);
Gorkem Pacaci 28/06/10
Não sei por que não consigo executar esse código ... Acabei de receber SERVER NOT FOUNDerro ... significa que o código não está funcionando para mim ... você está definindo alguma outra configuração ou algo assim ?!
Dr TJ
Eu criei um Gist da minha versão deste gist.github.com/IDisposable/77f11c6f7693f9d181bb
IDisposable
1
@IDisposable, o que é o MvcApplication.DnsSuffix?
HABO
Nós apenas expor o domínio DNS de base em web.config ... valor típico seria .example.org
IDisposable
4

Para capturar o subdomínio ao usar a API da Web , substitua o Seletor de ações para injetar um subdomainparâmetro de consulta. Em seguida, use o parâmetro de consulta de subdomínio nas ações de seus controladores como este:

public string Get(string id, string subdomain)

Essa abordagem torna a depuração conveniente, pois você pode especificar manualmente o parâmetro de consulta ao usar o host local em vez do nome do host real (consulte a resposta de roteamento MVC5 padrão para obter detalhes). Este é o código do 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);
    }
}

Substitua o Seletor de Ação padrão adicionando isto a WebApiConfig.Register:

config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Edward Brey
fonte
Alguém com problemas em que os dados da rota não aparecem no controlador de API da web e inspecionando o Request.GetRouteData dentro do controlador não mostra valores?
Alan Macdonald
3

Sim, mas você precisa criar seu próprio manipulador de rotas.

Normalmente, a rota não está ciente do domínio porque o aplicativo pode ser implantado em qualquer domínio e a rota não se importa com uma maneira ou outra. Mas, no seu caso, você deseja basear o controlador e a ação fora do domínio, portanto, será necessário criar uma rota personalizada que esteja ciente do domínio.

Nick Berardi
fonte
3

Eu criei uma biblioteca para roteamento de subdomínio, que você pode criar essa rota. Atualmente, ele está trabalhando para o .NET Core 1.1 e o .NET Framework 4.6.1, mas será atualizado em breve. É assim que funciona:
1) Mapeie a rota do subdomínio no 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) Essa lib também permitirá gerar URLs e formulários. Código:

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

Irá gerar <a href="http://user1.localhost:54575/Home/Index">User home</a> URL gerado também dependerá da localização e esquema atuais do host.
Você também pode usar ajudantes html para BeginForme UrlHelper. Se você quiser, também pode usar o novo recurso chamado tag helpers ( FormTagHelper, AnchorTagHelper)
Essa lib ainda não possui nenhuma documentação, mas existem alguns testes e amostras de projeto, então fique à vontade para explorá-lo.

Mariusz
fonte
2

No ASP.NET Core , o host está disponível via Request.Host.Host. Se você deseja permitir a substituição do host por meio de um parâmetro de consulta, verifique primeiro Request.Query.

Para fazer com que um parâmetro de consulta de host seja propagado para novos URLs baseados em rota, adicione este código à app.UseMvcconfiguração de rota:

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

E defina HostPropagationRouterassim:

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

Depois de definir um novo manipulador de rota que examinaria o host transmitido na URL , você pode ter a idéia de um controlador base que esteja ciente do site para o qual está sendo acessado. Se parece com isso:

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 é uma interface simples:

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

Eu indico que você vá para o Luke Sampson Blog

Amirhossein Mehrvarzi
fonte
1

Se você deseja fornecer recursos de MultiTenancy ao seu projeto com diferentes domínios / subdomínios para cada inquilino, consulte o SaasKit:

https://github.com/saaskit/saaskit

Exemplos de código podem ser vistos aqui: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy

Alguns exemplos usando o núcleo do ASP.NET: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/

EDIT: Se você não deseja usar o SaasKit em seu projeto principal do ASP.NET, pode dar uma olhada na implementação de Maarten do roteamento de domínio para MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain -routing-and-resolving-current-tenant-with-aspnet-mvc-6-aspnet-5.html

No entanto, essas listas não são mantidas e precisam ser aprimoradas para funcionar com a versão mais recente do núcleo do ASP.NET.

Link direto para o código: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs

Darxtar
fonte
Não procura multitenancy - mas obrigado pela dica!
21816 Dan Esparza
0

Há alguns meses, desenvolvi um atributo que restringe métodos ou controladores a domínios específicos.

É bastante fácil de usar:

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

Você também pode aplicá-lo diretamente em um 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
    }
}

Restrição: talvez você não consiga ter duas rotas iguais em métodos diferentes com filtros diferentes. Quero dizer que o seguinte pode gerar uma exceção para rota duplicada:

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

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