Asp net core: mapping areas to subdomains

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.

Share Comments
comments powered by Disqus