Consider adding a middleware to redirect to a preferred host
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 6h
- Merged PRs (30d)
- 290
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](https://learn.microsoft.com/aspnet/core/fundamentals/url-rewriting) only has first-class support for path rewriting (not host). [Host filtering](https://learn.microsoft.com/aspnet/core/fundamentals/servers/kestrel/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
```csharp
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` 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.
```csharp
using System.Net;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.WebUtilities;
namespace MySite.Middleware;
public class PreferredHostRedirectionMiddleware(ILogger logger, IConfiguration configuration) : IMiddleware
{
public const string PreferredHostKey = "PreferredHost";
public const string RedirectedKey = "r";
public const string TrueValue = "true";
private readonly ILogger _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();
return services;
}
public static IApplicationBuilder UsePreferredHostRedirection(this IApplicationBuilder app)
{
var logger = app.ApplicationServices.GetRequiredService().CreateLogger(nameof(PreferredHostRedirectionExtensions));
var configuration = app.ApplicationServices.GetRequiredService();
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();
return app;
}
}
```
*appsettings.json*
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "olddomain.com;newdomain.com",
"PreferredHost": "newdomain.com"
}
```
*appsettings.Development.json*
```json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "127.0.0.1;localhost",
"PreferredHost": "localhost"
}
```
Contributor guide
Assessment
This issue has not been assessed yet.