An efficient way of handling multiple actions through one route

Published: Thursday 13 June 2019

Recently, I wrote about a way that you can use one route to direct traffic to different controllers and actions in ASP.NET Core MVC.

In that example, I mentioned that there were six different types of pages that would be handled by one route.

This is great, but the problem I experienced was the data for each different types of page was housed in six different database tables.

I couldn't just potentially call up to six different database queries to try and match the type of page against the URL being pulled in. The website performance would have been compared to the speed of a slug.

Of course I could have done some funky UNION join against all six tables, but that would have still of meant a call to the database.

The solution

The idea I came up with is using a singleton within dependency injection.

For anyone that doesn't know what a singleton is, it's a class that keeps it's properties and variables set for the lifetime of the application.

So when running a website application, the singleton will be created when the website application starts, and the singleton will be destroyed once the website application stops. Each thread to the website application would be able to access the same values for each of the properties from the singleton.

The singleton would call the database to get a list of all the potential URL's on creation and when one of the types of pages is updated. This would be locked down using the "lock" keyword so no other threads can access the function whilst it's doing it's job.

The advantages of this is that the database is only involved when the website application starts, or a change has been made to one of the types of pages.

The singleton class would store a list of all the URL's and which controller and action each URL would go to. These would be stored in a ConcurrentDictionary class (rather than the Dictionary class) to support multiple users accessing it at the same time. This dictionary is effectively "the cache".

The "key" property of the ConcurrentDictionary would be the URL hashed into a number (the GetHashedCode function that appears on all objects). I did this to speed up the lookup of the URL.

As an addition, when a user updates one of the types of pages, it fires a new thread to update the URL's. This means that the user who has updated the page doesn't have to hang around for the background service to complete it's job in updating the URL's.

An example on how to do this is below:

// BackgroundService.cs
using System.Collections.Concurrent;

namespace MyProgram.Routes
{
	public partial class BackgroundService : IBackgroundService
	{
		protected ConcurrentDictionary<int, TypeRoute> _types;

		public BackgroundService()
		{
			UpdateURLs();
		}

		public virtual TypeRoute GetTypeRoute(string url)
		{
			// Use the URL of the page to get the appropriate "type" record
			return _types.ContainsKey(url.GetHashCode()) ? _types[url.GetHashCode()] : null;
		}

		public virtual void UpdateURLs()
		{
			lock (this)
			{
				// Lock so no other thread can access whilst updating the URL's

				// Use your DB query to get the different types

				// Create new variable of "types" and store it against the class specific variable once all the data has been added.
				var types = new ConcurrentDictionary<int, TypeRoute>();
				types.AddOrUpdate(("type1").GetHashCode(), new TypeRoute { Url = "type1", Controller = "Type", Action = "Type1" }, (k2, l) => new TypeRoute { Url = "type1", Controller = "Type", Action = "Type1" });

				_types = types;
			}
		}
	}

	public partial interface IBackgroundService
	{
		TypeRoute GetTypeRoute(string url);

		void UpdateURLs();
	}

	public partial class TypeRoute
	{
		public virtual string Url { get; set; }
		public virtual string Controller { get; set; }
		public virtual string Action { get; set; }

	}
}
// Startup.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace MyProgram
{
	public class Startup
	{
		public Startup(IConfiguration configuration)
		{
			Configuration = configuration;
		}

		public IConfiguration Configuration { get; }

		// This method gets called by the runtime. Use this method to add services to the container.
		public void ConfigureServices(IServiceCollection services)
		{

			services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

			// Add singleton service to dependency injection
			services.AddSingleton<IBackgroundService, BackgroundService>();

		}
	}
}
// TypeInsertUpdate.cs
using System.Threading;
using System.Threading.Tasks;

namespace MyProgram.Routes
{
	public class TypeInsertUpdate
	{
		protected readonly IBackgroundService _backgroundService;

		public TypeInsertUpdate(IBackgroundService backgroundService)
		{
			_backgroundService = backgroundService;
		}

		public virtual void Update()
		{
			// Do update task
			var cancellationTokenSource = new CancellationTokenSource();
			var cancellationToken = cancellationTokenSource.Token;

			Task.Factory.StartNew(() =>
			{
				// Async thread meaning the user does not need to hang around to finish.
				_backgroundService.UpdateURLs();

			}, cancellationToken);
		}
	}
}