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