Follow up - Making Redirects with more performant code

Follow up - Making Redirects with more performant code

Calculating...

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 Loading with Asynchronous Initialization
    • Lazy<Task<ConcurrentDictionary<string, string>>> ensures that the redirects are loaded asynchronously and only when first needed.
  • Concurrent Dictionary:
    • ConcurrentDictionary<string, string> is used to ensure thread-safe access to the redirects.
  • Asynchronous File I/O:
    • await File.ReadAllLinesAsync(filePath) reads the file asynchronously to avoid blocking threads.
  • Minimize Logging:
    • Logging is kept to a minimum, and you can adjust the logging level based on the environment (e.g., more verbose in development, less in production).

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