Inspired by question on StackOverflow. Some parts of code are copied from MvcRouteHandler.
Note:
Your area names should not match any controller or method name. Otherwise, routing will not work correctly.
Step 1: Custom router class
We have to create class, which will split url into parts and use the first part as area. Original MvcRouteHandler is used here as a base class. Also, some code from it is used in RouteAsync
method. Calling base method somewhy don’t work.
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
namespace Sith.Main
{
public class AreaRouter : MvcRouteHandler, IRouter
{
private string[] _allowedSubdomains = { "Vpn", "Password" };
//These are actualy copies of same values from base class. Some of them are used later.
private IActionContextAccessor _actionContextAccessor;
private IActionInvokerFactory _actionInvokerFactory;
private IActionSelector _actionSelector;
private ILogger _logger;
private DiagnosticSource _diagnosticSource;
public AreaRouter(
IActionInvokerFactory actionInvokerFactory,
IActionSelector actionSelector,
DiagnosticSource diagnosticSource,
ILoggerFactory loggerFactory)
: this(actionInvokerFactory, actionSelector, diagnosticSource, loggerFactory, actionContextAccessor: null)
{
}
public AreaRouter(IActionInvokerFactory actionInvokerFactory, IActionSelector actionSelector, DiagnosticSource diagnosticSource,
ILoggerFactory loggerFactory, IActionContextAccessor actionContextAccessor)
: base(actionInvokerFactory, actionSelector, diagnosticSource,
loggerFactory, actionContextAccessor)
{
_actionContextAccessor = actionContextAccessor;
_actionInvokerFactory = actionInvokerFactory;
_actionSelector = actionSelector;
_diagnosticSource = diagnosticSource;
_logger = loggerFactory.CreateLogger<MvcRouteHandler>();
}
public new Task RouteAsync(RouteContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
string url = context.HttpContext.Request.Headers["HOST"];
string firstDomain = url.Split('.')[0];
//Areas usually start from uooer-case letter.
string subDomain = char.ToUpper(firstDomain[0]) + firstDomain.Substring(1);
//check if our app knows subdomain
if(_allowedSubdomains.Contains(subDomain))
context.RouteData.Values.Add("area", subDomain);
//All the next code is copied from base class
var candidates = _actionSelector.SelectCandidates(context);
if (candidates == null || candidates.Count == 0)
{
return TaskCache.CompletedTask;
}
var actionDescriptor = _actionSelector.SelectBestCandidate(context, candidates);
if (actionDescriptor == null)
{
return TaskCache.CompletedTask;
}
context.Handler = (c) =>
{
var routeData = c.GetRouteData();
var actionContext = new ActionContext(context.HttpContext, routeData, actionDescriptor);
if (_actionContextAccessor != null)
{
_actionContextAccessor.ActionContext = actionContext;
}
var invoker = _actionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null)
{
throw new InvalidOperationException();
}
return invoker.InvokeAsync();
};
return TaskCache.CompletedTask;
}
}
}
Step 2: Setting up Startup.cs
Firstly, we need to create AreaRouter which will be later used:
public void ConfigureServices(IServiceCollection services)
{
//some other stuff
services.AddSingleton<AreaRouter>();
}
Now we have working instance of area router and can configure the routes(note the change of the signature of the method):
//pass area router using dependancy injection
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, AreaRouter areaRouter)
{
//some other stuff
app.UseMvc(routes =>
{
routes.DefaultHandler = areaRouter;
routes.MapRoute("areaRoute", "{area:exists}/{controller=Admin}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Step 3(only IIS):
Open .vs\config\applicationHost.config
in project folder. Inside it find line similar to these:
<bindings>
<binding protocol="http" bindingInformation="*:5252:localhost" />
</bindings>
Add your subdomains here. They should look like <binding protocol="http" bindingInformation="*:5252:contoso.localhost" />
. Start Visual Studio with admin rights and test your application. IIS will not start working with subdomains without admin rights.
Finally:
This example can be improved in some ways and it has some drawbacks(read Note on the top), but it is working. Complete project is available here.