Description
Problem
I recently deployed a new domain name for an existing site and found myself having to write a custom middleware to handle the transition. The site is still listening on both domains (e.g. olddomain.com
and newdomain.com
) but requests to olddomain.com
now redirect to newdomain.com
.
This feels like it would be a pretty common scenario (site listening on multiple hosts/domains but one is preferred).
ASP.NET Core contains multiple features related to URL rewriting/redirecting and host matching, but none of them actually address this scenario directly. The URL rewriting middleware only has first-class support for path rewriting (not host). Host filtering is about limiting which specific host names are allowed, but doesn't do redirection. The most similar scenario is HTTPS redirection for which we have a specific dedicated middleware for.
Proposal
We should consider adding a new middleware, PreferredHostRedirectionMiddleware
, that can be used to facilitate this scenario. This middleware can be added to the pipeline, much like the HTTPS redirection middleware, and it will redirect requests to a preferred host if one is configured.
The middleware should support direct configuration, e.g. passing a preferred host in as a string, or via the configuration/options system.
Example Usage
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UsePreferredHostRedirection("newdomain.com");
app.UseHttpsRedirection();
app.MapGet("/", () => "Hello, World!");
app.Run();
Strawman
Here's the implementation from the site I did this on recently. It doesn't use IOptions<T>
but rather it just looks at configuration directly but of course that can be changed. It also supports setting a query string flag on the new URL so that a message can be displayed on the site after the redirect. This likely wouldn't be something baked in, but perhaps a delegate could be set on the middleware options to allow modifying the URL being redirected to in order to enable scenarios like that.
using System.Net;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities;
namespace MySite.Middleware;
public class PreferredHostRedirectionMiddleware(ILogger<PreferredHostRedirectionMiddleware> logger, IConfiguration configuration) : IMiddleware
{
public const string PreferredHostKey = "PreferredHost";
public const string RedirectedKey = "r";
public const string TrueValue = "true";
private readonly ILogger<PreferredHostRedirectionMiddleware> _logger = logger;
private readonly string? _preferredHost = configuration[PreferredHostKey];
public Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!string.IsNullOrEmpty(_preferredHost) && !string.Equals(context.Request.Host.Host, _preferredHost, StringComparison.Ordinal))
{
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Request host '{RequestHost}' is not the preferred host. Redirecting to preferred host '{PreferredHost}'.",
context.Request.Host.Host, _preferredHost);
}
var redirectUriBuilder = new UriBuilder(context.Request.GetDisplayUrl()) { Host = _preferredHost };
if (context.GetEndpoint() is not null)
{
// Request is for an endpoint (e.g. a page) so set query string to ensure notification is displayed
var query = QueryHelpers.ParseQuery(redirectUriBuilder.Query);
query.Remove(RedirectedKey);
query.Add(RedirectedKey, TrueValue);
redirectUriBuilder.Query = QueryHelpers.AddQueryString("", query);
}
var redirectUrl = redirectUriBuilder.ToString();
if (_logger.IsEnabled(LogLevel.Debug))
{
_logger.LogDebug("Will redirect to URL '{RedirectUrl}'.", redirectUrl);
}
context.Response.Redirect(redirectUrl);
return Task.CompletedTask;
}
return next(context);
}
}
public static class PreferredHostRedirectionExtensions
{
public static IServiceCollection AddPreferredHostRedirection(this IServiceCollection services)
{
services.AddTransient<PreferredHostRedirectionMiddleware>();
return services;
}
public static IApplicationBuilder UsePreferredHostRedirection(this IApplicationBuilder app)
{
var logger = app.ApplicationServices.GetRequiredService<ILoggerFactory>().CreateLogger(nameof(PreferredHostRedirectionExtensions));
var configuration = app.ApplicationServices.GetRequiredService<IConfiguration>();
var preferredHost = configuration[PreferredHostRedirectionMiddleware.PreferredHostKey];
if (string.IsNullOrEmpty(preferredHost))
{
return app;
}
if (!Uri.CheckHostName(preferredHost).Equals(UriHostNameType.Dns))
{
if (logger.IsEnabled(LogLevel.Error))
{
logger.LogError("The preferred host is not a valid DNS name: {PreferredHost}", preferredHost);
}
return app;
}
app.UseMiddleware<PreferredHostRedirectionMiddleware>();
return app;
}
}
appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "olddomain.com;newdomain.com",
"PreferredHost": "newdomain.com"
}
appsettings.Development.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "127.0.0.1;localhost",
"PreferredHost": "localhost"
}