After yesterday's blog, I've had some really useful feedback from colleagues and Umbraco Community members and I thought I'd post the new improved code.
The first suggestion was to just use the generic ILogger rather than having the middleware dependent on Serilog logging. I had used Serilog because I thought, for some reason, the generic ILogger didn't save in to the logs in Umbraco. I was wrong and I've updated the code to reflect that.
Next up was about performance. It was suggested that I look at using a Lazy Property and I'll be honest, I've never heard of this before, so I asked Github Copilot to make my code more performant and here is what it suggested :
Lazy<Task<ConcurrentDictionary<string, string>>>
ensures that the redirects are loaded asynchronously and only when first needed.ConcurrentDictionary<string, string>
is used to ensure thread-safe access to the redirects.await File.ReadAllLinesAsync(filePath)
reads the file asynchronously to avoid blocking threads.Here is what the code now looks like :
using System.Collections.Concurrent;
namespace MySite.Web.Infrastructure.Middleware
{
public class RedirectMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RedirectMiddleware> _logger;
private readonly Lazy<Task<ConcurrentDictionary<string, string>>> _redirects;
public RedirectMiddleware(RequestDelegate next, ILogger<RedirectMiddleware> logger)
{
_next = next;
_logger = logger;
_redirects = new Lazy<Task<ConcurrentDictionary<string, string>>>(LoadRedirectsAsync);
}
public async Task InvokeAsync(HttpContext context)
{
var redirects = await _redirects.Value;
if (redirects.TryGetValue(context.Request.Path.Value, out var targetUrl))
{
var queryString = context.Request.QueryString.ToString();
if (!string.IsNullOrEmpty(queryString))
{
targetUrl = $"{targetUrl}{queryString}";
}
_logger.LogInformation($"Redirecting from {context.Request.Path} to {targetUrl}");
context.Response.Redirect(targetUrl, true);
return;
}
await _next(context);
}
private async Task<ConcurrentDictionary<string, string>> LoadRedirectsAsync()
{
var redirects = new ConcurrentDictionary<string, string>();
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "redirects.csv");
if (File.Exists(filePath))
{
var lines = await File.ReadAllLinesAsync(filePath);
foreach (var line in lines)
{
var parts = line.Split(',');
if (parts.Length == 2)
{
redirects.TryAdd(parts[0].Trim(), parts[1].Trim());
}
}
}
return redirects;
}
}
}
Updated on : 03 September 2024
Since the original blog, I've updated this code even more! It now uses memory cache so that I'm only reading the csv file once and there are a couple of other performance tweaks I've made.
public class RedirectMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RedirectMiddleware> _logger;
private readonly IMemoryCache _memoryCache;
public RedirectMiddleware(RequestDelegate next, ILogger<RedirectMiddleware> logger, IMemoryCache memoryCache)
{
_next = next;
_logger = logger;
_memoryCache = memoryCache;
}
public async Task InvokeAsync(HttpContext context)
{
if (_memoryCache.TryGetValue("redirects", out ConcurrentDictionary<string, string>? redirects) == false)
{
redirects = await LoadRedirectsAsync();
}
if (context.Request.Path.Value != null && redirects != null &&
redirects.TryGetValue(context.Request.Path.Value, out string targetUrl))
{
var queryString = context.Request.QueryString.ToString();
if (!string.IsNullOrEmpty(queryString))
{
targetUrl += queryString;
}
_logger.LogInformation($"Redirecting from {context.Request.Path} to {targetUrl}");
context.Response.Redirect(targetUrl, true);
return;
}
await _next(context);
}
private async Task<ConcurrentDictionary<string, string>> LoadRedirectsAsync()
{
var filePath = Path.Combine(Directory.GetCurrentDirectory(), "redirects.csv");
var redirects = new ConcurrentDictionary<string, string>();
if (File.Exists(filePath))
{
var lines = await File.ReadAllLinesAsync(filePath);
foreach (var line in lines)
{
var parts = line.Split(',');
if (parts.Length == 2)
{
redirects.TryAdd(parts[0].Trim(), parts[1].Trim());
}
}
}
return _memoryCache.Set("redirects", redirects);
}
}