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);
    }
}