Lets say I am using MVC for my web application and I have an area that contains multiple controllers... MyController1, MyController2 and MyController3
These controllers are used by users in certain groups: UserGroup1, UserGroup2 and UserGroup3. I will store the group id in the session.
I would like the client requests to look generic like this: www.mysite.com/MyArea/MyController/SomeAction
So, how do I assign the respective controller based on the group id variable stored in session?
some pseudo code:
var id = HttpContext.Current.Session["GroupId"];
if id == 1
use MyController1
else if id == 2
use MyController2
else if id == 3
use MyController3
I know I could hit a controller and perform a redirect, but is there somewhere higher up in the stack that I can have more control over the controller assignment.
After reading an article on MSDN https://msdn.microsoft.com/en-us/library/cc668201(v=vs.110).aspx I came up with the following solution:
Implement a custom MvcHandler that will handle the logic of choosing
a controller
Implement an IRouteHandler
Attach the IRouteHandler to the route that is registered in the AreaRegistration
public class MyRouteHandler : IRouteHandler
{
IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
{
return new MyMvcHandler(requestContext);
}
}
public class MyMvcHandler : MvcHandler, IHttpHandler
{
public MyMvcHandler(RequestContext requestContext) : base(requestContext)
{
}
private string GetControllerName(HttpContextBase httpContext)
{
string controllerName = this.RequestContext.RouteData.GetRequiredString("controller");
var groupId = httpContext.Session["GroupId"] as string;
if (!String.IsNullOrEmpty(groupId) && !String.IsNullOrEmpty(controllerName))
{
controllerName = groupId + controllerName;
}
return controllerName;
}
protected override IAsyncResult BeginProcessRequest(HttpContextBase httpContext, AsyncCallback callback, object state)
{
RequestContext.RouteData.Values["controller"] = this.GetControllerName(httpContext);
return base.BeginProcessRequest(httpContext, callback, state);
}
}
and finally, register the RouteHandler:
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"default",
"{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
).RouteHandler = new MyRouteHandler();
}
Related
Is it possible to have an ASP.NET MVC route that uses subdomain information to determine its route? For example:
user1.domain.example goes to one place
user2.domain.example goes to another?
Or, can I make it so both of these go to the same controller/action with a username parameter?
You can do it by creating a new route and adding it to the routes collection in RegisterRoutes in your global.asax. Below is a very simple example of a custom Route:
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;
}
}
To capture the subdomain while retaining the standard MVC5 routing features, use the following SubdomainRoute class derived from Route.
Additionally, SubdomainRoute allows the subdomain optionally to be specified as a query parameter, making sub.example.com/foo/bar and example.com/foo/bar?subdomain=sub equivalent. This allows you to test before the DNS subdomains are configured. The query parameter (when in use) is propagated through new links generated by Url.Action, etc.
The query parameter also enables local debugging with Visual Studio 2013 without having to configure with netsh or run as Administrator. By default, IIS Express only binds to localhost when non-elevated; it won't bind to synonymous hostnames like 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);
}
}
For convenience, call the following MapSubdomainRoute method from your RegisterRoutes method just as you would plain old 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()
});
}
Finally, to conveniently access the subdomain (either from a true subdomain or a query parameter), it is helpful to create a Controller base class with this Subdomain property:
protected string Subdomain
{
get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
This is not my work, but I had to add it on this answer.
Here is a great solution to this problem. Maartin Balliauw wrote code that creates a DomainRoute class that can be used very similarly to the normal routing.
http://blog.maartenballiauw.be/post/2009/05/20/ASPNET-MVC-Domain-Routing.aspx
Sample use would be like this...
routes.Add("DomainRoute", new DomainRoute(
"{customer}.example.com", // Domain with parameters
"{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
))
;
To capture the subdomain when using Web API, override the Action Selector to inject a subdomain query parameter. Then use the subdomain query parameter in your controllers' actions like this:
public string Get(string id, string subdomain)
This approach makes debugging convenient since you can specify the query parameter by hand when using localhost instead of the actual host name (see the standard MVC5 routing answer for details). This is the code for 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);
}
}
Replace the default Action Selector by adding this to WebApiConfig.Register:
config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Yes but you have to create your own route handler.
Typically the route is not aware of the domain because the application could be deployed to any domain and the route would not care one way or another. But in your case you want to base the controller and action off the domain, so you will have to create a custom route that is aware of the domain.
I created library for subdomain routing which you can create such a route. It is working currently for a .NET Core 1.1 and .NET Framework 4.6.1 but will be updated in near future. This is how is it working:
1) Map subdomain route 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) Controllers/HomeController.cs
public IActionResult Index(string username)
{
//code
}
3) That lib will also allow you to generate URLs and forms. Code:
#Html.ActionLink("User home", "Index", "Home" new { username = "user1" }, null)
Will generate User home
Generated URL will also depend on current host location and schema.
You can also use html helpers for BeginForm and UrlHelper. If you like you can also use new feature called tag helpers (FormTagHelper, AnchorTagHelper)
That lib does not have any documentation yet, but there are some tests and samples project so feel free to explore it.
In ASP.NET Core, the host is available via Request.Host.Host. If you want to allow overriding the host via a query parameter, first check Request.Query.
To cause a host query parameter to propagate into to new route-based URLs, add this code to the app.UseMvc route configuration:
routes.Routes.Add(new HostPropagationRouter(routes.DefaultHandler));
And define HostPropagationRouter like this:
/// <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);
}
After defining a new Route handler that would look at the host passed in the URL, you can go with the idea of a base Controller that is aware of the Site it’s being accessed for. It looks like this:
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 is a simple interface:
public interface ISiteProvider {
void Initialise(string host);
Site GetCurrentSite();
}
I refer you go to Luke Sampson Blog
If you are looking at giving MultiTenancy capabilities to your project with different domains/subdomains for each tenant, you should have a look at SaasKit:
https://github.com/saaskit/saaskit
Code examples can be seen here: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy
Some examples using ASP.NET core: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/
EDIT:
If you do no want to use SaasKit in your ASP.NET core project you can have a look at Maarten's implementation of domain routing for MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain-routing-and-resolving-current-tenant-with-aspnet-mvc-6-aspnet-5.html
However those Gists are not maintained and need to be tweaked to work with the latest release of ASP.NET core.
Direct link to the code: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs
Few month ago I have developed an attribute that restricts methods or controllers to specific domains.
It is quite easy to use:
[IsDomain("localhost","example.com","www.example.com","*.t1.example.com")]
[HttpGet("RestrictedByHost")]
public IActionResult Test(){}
You can also apply it directly on a 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
}
}
Restriction:
you may not be able to have two same routes on different methods with different filters
I mean the following may throw an exception for duplicate route:
[IsDomain("test1.example.com")]
[HttpGet("/Test")]
public IActionResult Test1(){}
[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}
Suppose that we want to transform an old version of asp.net web forms into mvc architecture.
But some users have bookmarked our urls or they have link it on other sites.
Now we don't want to loose that urls.instead we want to create a custom route that manages the old-typed urls.
This class gets the old url from user and creates a new route and redirects the user to the new page.
This is my custom route class:
public class LegacyRoute:RouteBase
{
private string[] urls;
public LegacyRoute(params string[] targetUrls)
{
urls = targetUrls;
}
public override RouteData GetRouteData(HttpContextBase httpContext)
{
RouteData result = null;
string requestedURL =
httpContext.Request.AppRelativeCurrentExecutionFilePath;
if (urls.Contains(requestedURL,StringComparer.OrdinalIgnoreCase))
{
result = new RouteData(this, new MvcRouteHandler());
result.Values.Add("controller", "Legacy");
result.Values.Add("action", "GetLegacyUrl");
result.Values.Add("legacyUrl", requestedURL);
}
return result;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
and this is my RegisterRoutes function:
public static void RegisterRoutes(RouteCollection routes)
{
// routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.Add(new LegacyRoute("~/articles/windows_3.1_Overview.html", "~/old/.NET_1.0_Class_Library"));
routes.MapRoute(null, "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
I have also created "Legacy" controller and "GetLegacyUrl" action.
But when I run the website and type
"localhost:14786/articles/windows_3.1_Overview.html"
in my browser I get the 404 Not Found error.
Where is the problem? Why I get this error?
I solved this problem (in Restful Routing) by creating a global attribute that looks at the route and then executes a redirect if it realizes the route is a "redirect route". In your case it would be a LegacyRoute.
public class RedirectFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.IsChildAction)
return;
var redirect = filterContext.RouteData.Route as RedirectRoute;
if (redirect != null)
{
var helper = new UrlHelper(filterContext.RequestContext);
var values = new RouteValueDictionary(filterContext.RequestContext.RouteData.Values);
var merged = new RouteValueDictionary(redirect.DataTokens["new_path"] as RouteValueDictionary);
// keep the values we specified, and add the other routeValues
// that we we didn't have overrides for.
foreach (var key in values.Keys.Where(key => !merged.ContainsKey(key)))
merged.Add(key, filterContext.RouteData.Values[key]);
var url = helper.RouteUrl(filterContext.RouteData.Values);
filterContext.Result = new RedirectResult(url, redirect.IsPermanent);
}
}
}
I know this is like a month old, but in your post, you did not mention the view. What view do you expect the user to arrive at? What is the view that the controller is returning as a result of GetLegacyUrl? If there is view being retured by GetLegacyUrl, then you will not get a 404. The original code you posted is fine.
My goal is to find a controller from it's name and area. I have successfully done this if my current httpContext is within the same Area as the to-be-found controller. However, I cannot get my call to the ControllerFactory to take Area into consideration. Here's my code:
public static ControllerBase GetControllerByName(this HtmlHelper htmlHelper, string controllerName)
{
IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
IController controller = factory.CreateController(htmlHelper.ViewContext.RequestContext, controllerName);
if (controller == null)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture, "The IControllerFactory '{0}' did not return a controller for the name '{1}'.", factory.GetType(), controllerName));
}
return (ControllerBase)controller;
}
Since it's taking a RequestContext as a parameter, I've added an route value of "area" to it but with no change. Is there something I can do with the requestContext to some how take area into consideration? Do I need to override the controller factory--and if so, what in particular handles Area distinction?
Update:
Here is an example of a AreaRegistration I have:
public class StoresAreaRegistration : AreaRegistration
{
public override string AreaName { get { return "Stores"; } }
public override void RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
AreaName,
AreaName + "/{controller}/{action}/{id}",
new { area = AreaName, controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
The Area and Namespaces used to locate a controller are in the RouteData of the RequestContext. They are populatd by default based off of the request you are currently serving up, if you need to change them you have to do so before calling CreateController. You may get an exception when a controller cannot be found so you'll have to account for that as well.
UPDATE: Note, you MUST create a new RequestContext. If you reuse the existing one it will mess with the resolution of actions & views later on down the line in this request.
var tempRequestContext = new RequestContext(Request.RequestContext.HttpContext, new RouteData());
tempRequestContext.RouteData.DataTokens["Area"] = "";
tempRequestContext.RouteData.DataTokens["Namespaces"] = "YourCompany.Controllers";
var controller = ControllerBuilder.Current.GetControllerFactory()
.CreateController(tempRequestContext, "ControllerName");
if(controller != null)
{
//TODO: Implement your logic here
}
I have found a solution for implement multi-tenant in my asp.net mvc project and
I want know if it's correct or exist a better way.
I want organize more customers using the same application handling the web request, for example:
http://mysite/<customer>/home/index //home is controller and index the action
For this reason i changed the default maproute:
routes.MapRoute(
name: "Default",
url: "{customername}/{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
and I implemented a custom ActionFilterAttribute:
public class CheckCustomerNameFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting( ActionExecutingContext filterContext )
{
var customerName = filterContext.RouteData.Values["customername"];
var customerRepository = new CustomerRepository();
var customer = customerRepository.GetByName( customerName );
if( customer == null )
{
filterContext.Result = new ViewResult { ViewName = "Error" };
}
base.OnActionExecuting( filterContext );
}
}
and using it:
public class HomeController : Controller
{
[CheckCustomerNameFilterAttribute]
public ActionResult Index()
{
var customerName = RouteData.Values["customername"];
// show home page of customer with name == customerName
return View();
}
}
With this solution i can switch customer using customer name and correctly accept requests like this:
http://mysite/customer1
http://mysite/customer2/product/detail/2
...................................
This solution works well but I don't know if the best approach.
Does anyone know a better way?
You can model bind the customer name, and not have to pull it from route values:
public ActionResult Index(string customerName)
{
}
Is it possible to have an ASP.NET MVC route that uses subdomain information to determine its route? For example:
user1.domain.example goes to one place
user2.domain.example goes to another?
Or, can I make it so both of these go to the same controller/action with a username parameter?
You can do it by creating a new route and adding it to the routes collection in RegisterRoutes in your global.asax. Below is a very simple example of a custom Route:
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;
}
}
To capture the subdomain while retaining the standard MVC5 routing features, use the following SubdomainRoute class derived from Route.
Additionally, SubdomainRoute allows the subdomain optionally to be specified as a query parameter, making sub.example.com/foo/bar and example.com/foo/bar?subdomain=sub equivalent. This allows you to test before the DNS subdomains are configured. The query parameter (when in use) is propagated through new links generated by Url.Action, etc.
The query parameter also enables local debugging with Visual Studio 2013 without having to configure with netsh or run as Administrator. By default, IIS Express only binds to localhost when non-elevated; it won't bind to synonymous hostnames like 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);
}
}
For convenience, call the following MapSubdomainRoute method from your RegisterRoutes method just as you would plain old 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()
});
}
Finally, to conveniently access the subdomain (either from a true subdomain or a query parameter), it is helpful to create a Controller base class with this Subdomain property:
protected string Subdomain
{
get { return (string)Request.RequestContext.RouteData.Values["subdomain"]; }
}
This is not my work, but I had to add it on this answer.
Here is a great solution to this problem. Maartin Balliauw wrote code that creates a DomainRoute class that can be used very similarly to the normal routing.
http://blog.maartenballiauw.be/post/2009/05/20/ASPNET-MVC-Domain-Routing.aspx
Sample use would be like this...
routes.Add("DomainRoute", new DomainRoute(
"{customer}.example.com", // Domain with parameters
"{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = "" } // Parameter defaults
))
;
To capture the subdomain when using Web API, override the Action Selector to inject a subdomain query parameter. Then use the subdomain query parameter in your controllers' actions like this:
public string Get(string id, string subdomain)
This approach makes debugging convenient since you can specify the query parameter by hand when using localhost instead of the actual host name (see the standard MVC5 routing answer for details). This is the code for 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);
}
}
Replace the default Action Selector by adding this to WebApiConfig.Register:
config.Services.Replace(typeof(IHttpActionSelector), new SubdomainActionSelector(config.Services.GetActionSelector()));
Yes but you have to create your own route handler.
Typically the route is not aware of the domain because the application could be deployed to any domain and the route would not care one way or another. But in your case you want to base the controller and action off the domain, so you will have to create a custom route that is aware of the domain.
I created library for subdomain routing which you can create such a route. It is working currently for a .NET Core 1.1 and .NET Framework 4.6.1 but will be updated in near future. This is how is it working:
1) Map subdomain route 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) Controllers/HomeController.cs
public IActionResult Index(string username)
{
//code
}
3) That lib will also allow you to generate URLs and forms. Code:
#Html.ActionLink("User home", "Index", "Home" new { username = "user1" }, null)
Will generate User home
Generated URL will also depend on current host location and schema.
You can also use html helpers for BeginForm and UrlHelper. If you like you can also use new feature called tag helpers (FormTagHelper, AnchorTagHelper)
That lib does not have any documentation yet, but there are some tests and samples project so feel free to explore it.
In ASP.NET Core, the host is available via Request.Host.Host. If you want to allow overriding the host via a query parameter, first check Request.Query.
To cause a host query parameter to propagate into to new route-based URLs, add this code to the app.UseMvc route configuration:
routes.Routes.Add(new HostPropagationRouter(routes.DefaultHandler));
And define HostPropagationRouter like this:
/// <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);
}
After defining a new Route handler that would look at the host passed in the URL, you can go with the idea of a base Controller that is aware of the Site it’s being accessed for. It looks like this:
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 is a simple interface:
public interface ISiteProvider {
void Initialise(string host);
Site GetCurrentSite();
}
I refer you go to Luke Sampson Blog
If you are looking at giving MultiTenancy capabilities to your project with different domains/subdomains for each tenant, you should have a look at SaasKit:
https://github.com/saaskit/saaskit
Code examples can be seen here: http://benfoster.io/blog/saaskit-multi-tenancy-made-easy
Some examples using ASP.NET core: http://andrewlock.net/forking-the-pipeline-adding-tenant-specific-files-with-saaskit-in-asp-net-core/
EDIT:
If you do no want to use SaasKit in your ASP.NET core project you can have a look at Maarten's implementation of domain routing for MVC6: https://blog.maartenballiauw.be/post/2015/02/17/domain-routing-and-resolving-current-tenant-with-aspnet-mvc-6-aspnet-5.html
However those Gists are not maintained and need to be tweaked to work with the latest release of ASP.NET core.
Direct link to the code: https://gist.github.com/maartenba/77ca6f9cfef50efa96ec#file-domaintemplateroutebuilderextensions-cs
Few month ago I have developed an attribute that restricts methods or controllers to specific domains.
It is quite easy to use:
[IsDomain("localhost","example.com","www.example.com","*.t1.example.com")]
[HttpGet("RestrictedByHost")]
public IActionResult Test(){}
You can also apply it directly on a 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
}
}
Restriction:
you may not be able to have two same routes on different methods with different filters
I mean the following may throw an exception for duplicate route:
[IsDomain("test1.example.com")]
[HttpGet("/Test")]
public IActionResult Test1(){}
[IsDomain("test2.example.com")]
[HttpGet("/Test")]
public IActionResult Test2(){}