/ #dev #csharp 

Routing by Subdomain in ASP.NET Core MVC

A long time ago I wanted to do some routing through a project using a wildcard subdomain, something like a simulated multi-tenanted setup for a service.

Custom router

The way to do this is to create a custom router. Create a class under /Services/CustomerCouter.cs with the following content:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;

namespace DNSRouting.Services
{
    public class CustomRouter : MvcRouteHandler, IRouter
    {
        private IActionContextAccessor _actionContextAccessor;
        private IActionInvokerFactory _actionInvokerFactory;
        private IActionSelector _actionSelector;
        private ILogger _logger;
        private DiagnosticSource _diagnosticSource;


        public CustomRouter(
            IActionInvokerFactory actionInvokerFactory,
            IActionSelector actionSelector,
            DiagnosticSource diagnosticSource,
            ILoggerFactory loggerFactory)
            : this(actionInvokerFactory, actionSelector, diagnosticSource, loggerFactory, actionContextAccessor: null)
        {
        }

        public CustomRouter(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));
            }

            // *****
            // ⚠️ This is the important part! ⚠️
            // *****
            string Host = context.HttpContext.Request.Host.Host;
            if (Host == "localhost") // Change this the to your usual host
            {
                // Do nothing, normal routing
            }
            else
            {
                // You can do pretty much anything here, but I chose to switch
                // to a different controller. ✅
                context.RouteData.Values["controller"] = "Alternate";
                context.RouteData.Values.Add("Host", Host); // Add a variable for fun
            }

            // All the next code is copied from base class
            var candidates = _actionSelector.SelectCandidates(context);
            if (candidates == null || candidates.Count == 0)
            {
                return Task.CompletedTask;
            }

            var actionDescriptor = _actionSelector.SelectBestCandidate(context, candidates);
            if (actionDescriptor == null)
            {
                return Task.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 Task.CompletedTask;
        }
    }
}

Configure in startup

Then you need to reference it in the Startup.cs file! Replace your configure method with the following:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, CustomRouter customRouter)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseStaticFiles();

    app.UseMvc(routes =>
    {
        routes.DefaultHandler = customRouter;
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Accessing in the Controller

If you wanna do what I did (with the alternate controller), here’s how you can access it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace DNSRouting.Controllers
{
    public class AlternateController : Controller
    {
        // GET: /<controller>/
        public string Index(string Host) // Host is set in CustomRouter
        {
            return String.Format("Alternate page, host = {0}", Host);
        }
    }
}

That’s about it! You could do some kind of lookup on the host to get some kind of info from a data connection, and work with it like you would as if it was it’s own web app! 🌐

I put everything into a repo as a full web app - you can find that here.

Header image is licensed under CC0 Creative Commons – source.

Author

Matt Crook

Futurist, technologist, and student at Auckland Uni