Можно ли сделать маршрут ASP.NET MVC на основе поддомен?

235

Возможно ли иметь маршрут ASP.NET MVC, который использует информацию о поддомене для определения своего маршрута? Например:

  • user1 .domain.com идет в одно место
  • user2 .domain.com переходит к другому?

Или я могу сделать так, чтобы оба они шли к одному контроллеру / действию с usernameпараметром?

Дэн Эспарза
источник
Я реализовал аналогичную вещь для многопользовательских приложений, но с использованием абстрактного базового контроллера, а не пользовательского класса Route. Мой пост в блоге об этом здесь .
Люк Сэмпсон
6
Обязательно рассмотрите этот подход: http://blog.tonywilliams.me.uk/asp-net-mvc-2-routing-subdomains-to-areas Мне показалось, что лучше ввести многопользовательскую среду в мое приложение, чем другие ответы. потому что области MVC - хороший способ организованно представить специфичные для арендатора контроллеры и представления.
trebormf
2
@trebormf - я думаю, что вы должны добавить это как ответ, это то, что я в конечном итоге использовал в качестве основы для моего решения.
Шагглз
@Shagglez - Спасибо. Это был ответ, но модератор преобразовал его в комментарий по причинам, которые я не могу понять.
Trebormf
5
Тони, как был сломан. Вот тот, который работал для меня: blog.tonywilliams.me.uk/…
Ронни Оверби

Ответы:

168

Вы можете сделать это, создав новый маршрут и добавив его в коллекцию маршрутов в RegisterRoutes в вашем global.asax. Ниже приведен очень простой пример пользовательского маршрута:

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
Спасибо за подробный пример, но я не понимаю, как выполнить .Add из Global.asax.
JustSteve
4
Я вызвал маршрут SubdomainRoute и добавил его в качестве первого маршрута следующим образом: rout.Add (new SubdomainRoute ());
Джефф Хэндли
6
Требует ли этот подход жесткого кодирования списка возможных поддоменов?
Максим Павлов
2
Нет, вы можете добавить поле базы данных, называемое чем-то вроде «субдомена», которое будет таким, каким вы ожидаете, что субдомен будет для конкретного пользователя, или чем-то еще, затем просто выполните поиск на субдомене.
Райан Хейс
1
Кто-нибудь может порекомендовать версию этого веб-формы?
От Матфея
52

Чтобы захватить субдомен при сохранении стандартных функций маршрутизации MVC5 , используйте следующий SubdomainRouteкласс, полученный из Route.

Кроме того, SubdomainRouteпозволяет субдомен при желании указывать в качестве параметра запроса , делая sub.example.com/foo/barи example.com/foo/bar?subdomain=subэквивалентный. Это позволяет вам тестировать до того, как настроены субдомены DNS. Параметр запроса (при использовании) распространяется по новым ссылкам, созданным и Url.Actionт. Д.

Параметр запроса также включает локальную отладку в Visual Studio 2013 без необходимости настройки с помощью netsh или запуска с правами администратора . По умолчанию IIS Express привязывается только к localhost, если не повышен; он не будет привязываться к синонимичным именам хостов, таким как 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);
    }
}

Для удобства, вызовите следующий MapSubdomainRouteметод из вашего RegisterRoutesметода так же, как вы бы просто старый 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()
    });
}

Наконец, для удобного доступа к субдомену (из истинного субдомена или параметра запроса) полезно создать базовый класс Controller с этим Subdomainсвойством:

protected string Subdomain
{
    get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
Эдвард Брей
источник
1
Я обновил код, чтобы сделать поддомен всегда доступным в качестве значения маршрута. Это упрощает доступ к поддомену.
Эдвард Брей
Мне это нравится. Очень просто и более чем достаточно для моего проекта.
Скоро,
Это отличный ответ. Есть ли способ для этого работать с атрибутами маршрута? Я пытаюсь заставить это работать для путей как "subdomain.domain.com/portal/register", и использование атрибутов сделало бы это проще.
perfect_element
@perfect_element - Маршруты атрибутов не расширяемы, как маршруты, основанные на соглашениях. Единственный способ сделать что-то подобное - создать собственную систему маршрутизации атрибутов.
NightOwl888
23

Это не моя работа, но я должен был добавить это к этому ответу.

Вот отличное решение этой проблемы. Maartin Balliauw написал код, который создает класс DomainRoute, который может использоваться очень похоже на обычную маршрутизацию.

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

Пример использования будет таким ...

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
Существует проблема с этим решением. Скажем, вы хотите обрабатывать субдомены как разных пользователей: rout.Add ("SD", new DomainRoute ("user} .localhost", "", new {controller = "Home", action = "IndexForUser", user = "u1"). "})); Он также кэширует домашнюю страницу. Это из-за сгенерированного регулярного выражения. Чтобы это исправить, вы можете сделать копию метода CreateRegex в DomainRoute.cs, назвать его CreateDomainRegex, изменить * в этой строке на +: source = source.Replace ("}", @ "> ([a- Za-z0-9 _] *)) "); и используйте этот новый метод для домена regx в методе GetRouteData: domainRegex = CreateDomainRegex (Domain);
Gorkem Pacaci
Я не знаю, почему я не могу запустить этот код ... Я просто получаю сообщение SERVER NOT FOUNDоб ошибке ... означает, что код не работает для меня ... Вы устанавливаете какую-либо другую конфигурацию или что-то ?!
Доктор Ти Джей
Я создал Gist своей версии этого gist.github.com/IDisposable/77f11c6f7693f9d181bb
IDisposable
1
@IDisposable Что такое MvcApplication.DnsSuffix?
HaBo
Мы просто выставляем базовый DNS-домен в web.config ... типичное значение будет .example.org
IDisposable
4

Чтобы захватить поддомен при использовании веб-API , переопределите селектор действий, чтобы ввести subdomainпараметр запроса. Затем используйте параметр запроса поддоменов в действиях ваших контроллеров следующим образом:

public string Get(string id, string subdomain)

Такой подход делает отладку удобной, поскольку вы можете указать параметр запроса вручную при использовании localhost вместо фактического имени хоста (подробности см. В стандартном ответе о маршрутизации MVC5 ). Это код для выбора действий:

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

Замените селектор действий по умолчанию, добавив его в WebApiConfig.Register:

config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Эдвард Брей
источник
У кого-то возникают проблемы, когда данные о маршруте не отображаются на контроллере веб-API, и проверка Request.GetRouteData внутри контроллера не показывает никаких значений?
Алан Макдональд
3

Да, но вы должны создать свой собственный обработчик маршрута.

Обычно маршрут не знает о домене, потому что приложение может быть развернуто в любом домене, и маршрут не будет заботиться так или иначе. Но в вашем случае вы хотите основать контроллер и действие вне домена, поэтому вам придется создать собственный маршрут, который знает о домене.

Ник Берарди
источник
3

Я создал библиотеку для маршрутизации поддоменов, по которой вы можете создать такой маршрут. В настоящее время он работает для .NET Core 1.1 и .NET Framework 4.6.1, но будет обновлен в ближайшем будущем. Вот как это работает:
1) Карта маршрута субдомена в 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) Контроллеры / HomeController.cs

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

3) Эта библиотека также позволит вам создавать URL-адреса и формы. Код:

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

<a href="http://user1.localhost:54575/Home/Index">User home</a> Сгенерированный сгенерированный URL будет также зависеть от текущего местоположения хоста и схемы.
Вы также можете использовать помощники HTML для BeginFormи UrlHelper. Если вам нравится, вы также можете использовать новую функцию, называемую tag helpers ( FormTagHelper, AnchorTagHelper).
В lib еще нет документации, но есть некоторые тесты и примеры проектов, поэтому вы можете их изучить.

Мариуш
источник
2

В ASP.NET Core хост доступен через Request.Host.Host. Если вы хотите разрешить переопределение хоста с помощью параметра запроса, сначала проверьте Request.Query.

Чтобы параметр запроса хоста распространялся на новые URL-адреса на основе маршрута, добавьте этот код в app.UseMvcконфигурацию маршрута:

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

И определите HostPropagationRouterтак:

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

После определения нового обработчика маршрута, который будет проверять хост, переданный в URL , вы можете исходить из идеи базового контроллера, который знает о сайте, к которому он обращается. Это выглядит так:

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 это простой интерфейс:

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

Я имею в виду, вы идете в блог Люка Сэмпсона

Амирхосейн Мехрварзи
источник
1

Если вы хотите предоставить возможности MultiTenancy вашему проекту с разными доменами / поддоменами для каждого арендатора, вам стоит взглянуть на SaasKit:

https://github.com/saaskit/saaskit

Примеры кода можно увидеть здесь: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy

Некоторые примеры использования ядра ASP.NET: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/

РЕДАКТИРОВАТЬ: Если вы не хотите использовать SaasKit в вашем основном проекте ASP.NET, вы можете взглянуть на реализацию Maarten доменной маршрутизации для MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain прокладка маршрута-и-решения-вольт-арендатора-с-ASPnet-6-MVC-ASPnet-5.html

Однако эти Gists не поддерживаются и должны быть настроены для работы с последней версией ядра ASP.NET.

Прямая ссылка на код: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs

Darxtar
источник
Не ищу мультитенюса - но спасибо за совет!
Дэн Эспарза
0

Несколько месяцев назад я разработал атрибут, ограничивающий методы или контроллеры конкретными доменами.

Это довольно легко использовать:

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

Вы также можете применить его непосредственно на контроллере.

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

Ограничение: вы не можете иметь два одинаковых маршрута в разных методах с разными фильтрами. Я имею в виду, что следующее может вызвать исключение для дублированного маршрута:

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

[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}
джинсовый
источник