Categories
Hard Skills

DotNet Core Domain Redirect Custom Middleware

A few days ago, I was asked to create a custom middleware, that can redirect from one domain to another from code, in an asp.net core 3 application. In this article, we are going to recreate that middleware step by step.

A few days ago, I was asked to create a custom middleware, that can redirect from one domain to another from code, in an asp.net core 3 application. In this article, we are going to recreate that middleware step by step.
You can download or view the example code on GitHub to follow along.

Introduction

In this situation, we hosted the application in Azure App Services, and for some reason, the application did not get redirected from the URL that Azure gave us to the public domain the IT team set up. I do not have to mention that we solved that problem through the IT team at the end, but if I already wrote that middleware, I thought I will share it with you.

You might want to use this middleware if:

  • You have no access to your CDN settings
  • You cannot solve this through IIS or any other hosting settings:
  • Your redirection rules are based on complex and dynamic data, where you have to define your settings in runtime. In that case, you need to make some changes to the example I give you. If you struggle with that, let me know, and I can give you some instructions where you should be headed with your code.
  • Or you just simply want to show the world, you can do it by code, no IT config needed 😉

You do not want to use this middleware if:

  • You have any other option to set up your redirects.

Let’s get started

Note: I created an example application for this demo, that you can download or find on GitHub to follow along.

If you go to the commits section on GitHub, you can see we haven’t got many commits, so I will step you through those.

Commits link here.

custom domain redirect github commits screenshot

Next, I walk you through the steps. I created those commits so you can follow along if you want to implement those in your application.

First and Initial Commit

In the first 2 commits, I created this repository on GitHub and uploaded a brand new asp.net core 3.1 application with the React and Redux Template. You can create any type of asp.net core application; I prefer the React template.

Add configuration to appsettings.json

Commit link here.

Somewhere we need to tell our application which domain do we want to redirect to where.

If you have a Development, QA, Staging, or Production environment as well, each deployed and hosted on their subdomain; you can safely set them up in appsettings.json. (Learn more about appsettings.json in the Microsoft Docs)

In this example, we are going to set up the Development Environment in appsettings.Development.json.

"RedirectToPublicDomainConfiguration": {
    "RedirectFromHost": "localhost",
    "RedirectToHost": "https://botond.dev",
    "IsPermanent": false
  }

Here you can see 3 values:

  • RedirectFromHost: In this example, we redirect from localhost. If you host your application somewhere, you might want to use your generated domain, given by your host service. Here you do not want to specify the protocol, like http or https.
  • RedirectToHost: Usually, this will be your public domain. In this example, we are going to redirect to my blog’s host, since I do not host this app publicly. You want to redirect to your new public domain. Here you should specify the protocol, like http or https.
  • IsPermanent: If you want your redirect to be permanent with code 301 (your client’s browser will cache the redirect), set it to true. If you do not want the client to cache your redirect request, it will be a temporary redirect with code 302. I would suggest to set it to false while you are testing because it will be challenging to force your new redirect on the client once it caches it.

Model for options pattern

Commit link here.

In the next step, we add a Model Class, that represents the settings we set up in the appsettings.Development.json file. We need this class to easily access the settings in our middleware using Microsoft’s options pattern.

public class RedirectToPublicDomainConfiguration
{
    public string RedirectFromHost { get; set; }
 
    public string RedirectToHost { get; set; }
 
    public bool IsPermanent { get; set; }
}

Configure Services

Commit link here.

Next, we have to tell Asp.Net Core to map the JSON values into our new model on start-up. Doing that, the dependency injection will help us out when we are writing the custom middleware code.

In your Startup.cs ConfigureServices method, add the following line of code:

services.Configure<RedirectToPublicDomainConfiguration>(Configuration.GetSection("RedirectToPublicDomainConfiguration")); 

In this line of code, we map the “RedirectToPublicDomainConfiguration” section of appsettings.json into a new RedirectToPublicDomainConfiguration object.

Add the using statement for the model if you need to on the top of your Startup.cs file.

using dotnetcore_domain_redirect_custom_middleware_blog_example.Configuration;

Implement DomainRedirectMiddleware 

Commit link here.

In this commit, we are going to set up multiple things:

  1. Install the Microsoft.ApplicationInsights.AspNetCore NuGet package. We are going to use an extension method from the NuGet package to read data from HttpContext.
  2. Implement the DomainRedirectMiddleware class.
  3. Create an extension method for IApplicationBuilder to make use of the new middleware in a single line of code.
  4. Add that single line of code, to activate the new middleware in Startup.cs

Install NuGet dependencies

In order to have access to all the needed data in the HTTP context, we need some (one specific) extension method that is implemented in the Microsoft.ApplicationInsights.AspNetCore NuGet package.

In Visual Studio go to:

Tools -> NuGet Package Manager -> Manage Nuget Packages for Solution

find nuget package manager in visual studio

On the NuGet – Solution tab:

  1. Search for “Microsoft.ApplicationInsights.AspNetCore”
  2. Select the appropriate result
  3. Select the project you want to install it for
  4. Click the install button

In the image below you can see the steps as written above.

install Microsoft.ApplicationInsights.AspNetCore NuGet package

Implement the DomainRedirectMiddleware class

First, I am going to paste the class here, and then I walk you through it step by step.

using dotnetcore_domain_redirect_custom_middleware_blog_example.Configuration;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using System.Threading.Tasks;
 
namespace dotnetcore_domain_redirect_custom_middleware_blog_example.CustomMiddleware
{
    public class DomainRedirectMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly IOptions<RedirectToPublicDomainConfiguration> _configuration;
 
        public DomainRedirectMiddleware(RequestDelegate next, IOptions<RedirectToPublicDomainConfiguration> configuration)
        {
            _next = next;
            _configuration = configuration;
        }
 
        public async Task InvokeAsync(HttpContext context)
        {
            var redirected = false;
            if (_configuration.Value.RedirectFromHost != null && _configuration.Value.RedirectToHost != null)
            {
                var redirectFromHost = _configuration.Value.RedirectFromHost;
                if (context.Request.GetUri().Host.Contains(redirectFromHost))
                {
                    var redirectToHost = _configuration.Value.RedirectToHost;
                    var path = context.Request.Path.Value;
                    var isPermanent = _configuration.Value.IsPermanent;
 
                    context.Response.Redirect($"{redirectToHost}{path}", isPermanent);
                    redirected = true;
                }
            }
 
            if (redirected == false)
            {
                await _next(context);
            }
        }
    }
}

Let’s take it apart and discuss them one by one.

Constructor and needed services

We will need the following 2 services to make our middleware work:

  1. The RequestDelegate, so we can pass the context execution to other middlewares on start-up. We only have to do this if the middleware should not redirect.
  2. The configuration we set up in the appsettings.json. We need those values to determine if we have to redirect and to where.
        private readonly RequestDelegate _next;
        private readonly IOptions<RedirectToPublicDomainConfiguration> _configuration;
 
        public DomainRedirectMiddleware(RequestDelegate next, IOptions<RedirectToPublicDomainConfiguration> configuration)
        {
            _next = next;
            _configuration = configuration;
        }

In the constructor, you can see, we do not write any other logic, other than saving the requested services. Those services will get injected by asp.net core’s dependency injection, we set up earlier in Startup.cs.

Middleware’s logic

The middleware’s logic needs to be implemented in a method called public async Task InvokeAsync(HttpContext context).

The function name is required to follow that signature to work as a middleware.

NOTE: If you noticed, in the class, we do not implement any interface. I just realized that while I was writing that part of the article. Quickly I found the IMiddleware interface (https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.imiddleware?view=aspnetcore-3.1), implemented it, and for my surprise, the application stopped working.

This is the exception I received:

“An unhandled exception occurred while processing the request.
InvalidOperationException: No service for type 'dotnetcore_domain_redirect_custom_middleware_blog_example.CustomMiddleware.DomainRedirectMiddleware' has been registered.”

I went back to check the tutorial for Middlewares written by Microsoft (https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-3.1), and it seems they did not use it as well. I might try to implement the middleware using the interface again, but for now, let us go without that, as it is in the Microsoft tutorial.

The logic implemented in this function is straightforward:

First, we create a variable to know if we redirected the request to a new domain.
var redirected = false;

 For now, the default value is false, but we will overwrite it after the redirect method will get called. We will leave it false if the did not redirect it. It is essential to know because if we got redirected, we should not call the next(context) delegate. If we call it, de redirection will not work.
I did not dive deep on the why, but my first guess would be that the http status code and our redirection request will be overwritten by other parts of the middleware chain.

Next, in the first if statement, we check if we have the configuration from the appsettings.json.

It can happen that we did not set up the needed values in app settings for our specific environment. In that case, we should abandon the redirect middleware and pass the context to the next middleware.

if (_configuration.Value.RedirectFromHost != null && _configuration.Value.RedirectToHost != null)

If we have the needed configuration, we also have to check if the domain in the request is the one we need to redirect from. The context.Request.GetUri() is the function we need from the “Microsoft.ApplicationInsights.AspNetCore” NuGet package.

var redirectFromHost = _configuration.Value.RedirectFromHost;
if (context.Request.GetUri().Host.Contains(redirectFromHost)){...}

If the statement is true, we copy those values into humanly easily readable variables and pass them into the Redirect extension method.

var redirectToHost = _configuration.Value.RedirectToHost;
var path = context.Request.Path.Value;
var isPermanent = _configuration.Value.IsPermanent;
 
context.Response.Redirect($"{redirectToHost}{path}", isPermanent);
redirected = true;

Before we leave the if block, we save the fact that we sent a redirect request in the response.
Note that we do not simply redirect to another domain, but we also preserve the path it should follow. If you want, you can go further and pass through the query parameters as well, but for this example, I wanted to keep this simple. Also, our application did not need to redirect those values.

At the end of the function, we check if we redirected the request or not

If we did not redirect, we simply send the context down the middleware call chain, and we let the application run on the originally requested domain.

if (redirected == false)
{
    await _next(context);
}

Create an extension method

To make use of the new middleware in a single, easily understandable line of code, we are going to create an extension method for IApplicationBuilder.

Implementing an extension method is really simple, as you can see it in the code snippet.

public static class CustomMiddlewareExtensions
{
    public static IApplicationBuilder UseDomainRedirectMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<DomainRedirectMiddleware>();
    }
}

The benefits are subtle, but:

  • When you have multiple custom middlewares, it is simpler to find your needed middleware because of IntelliSense.
  • If your middleware is more complex and needs some special code preparation, you can do that in that static function. This function, in that case, would be used as a Façade pattern. It means that your new method will hide some of the complexities of your code and make it simpler to use your new Façade method when you need it.

Activate the new middleware in Startup.cs

In the final step, you only have to insert that single line of code into your Configure method in the Startup.cs.

app.UseDomainRedirectMiddleware();

Note:

While we were trying out the new middleware in Azure, we found that we received a “too many redirect” error. It probably was caused by multiple factors, but the idea come up; it might have to do something with the app.UseHttpsRedirection(); middleware. Because that one also redirects, we moved our middleware after that like:

app.UseHttpsRedirection();
app.UseDomainRedirectMiddleware();

Because of this change, we passed that error.

And that is it. You can now run your application, and if you set up your configuration correctly, the middleware will redirect your application wherever you wanted it to go.

Thank you

Thank you for reading to the end 🙂

By Botond Bertalan

I am the founder of www.botond.dev
I started my programming studies in 2010 and started to work professionally in 2012 before graduation.
I love programming and architecting code that solves real business problems and gives value for the end-user.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.