Skip to content

Editing A Beginner's Guide To Implementing CQRS ES Part 7: Interchangeable Storage with Repositories

cdmdotnet edited this page Feb 20, 2018 · 3 revisions

This is Part 7 of a series describing how to build an application in .NET using the Command-Query Responsibility Segregation and Event Sourcing patterns, as well as the [CQRS.NET]. Click here for Part 1.

We've built a basic console application in Part 1 through to Part 5 as well as moving to the web in Part 6. Now we'll move onto the topic of how to use plug-able technology so as an application grows we can move from SQL products/servers to NoSQL products

Adding Repositories and Strategies

Repositories are the building blocks on queries for our read-stores. They are built in tandem with two other components Query Strategies and Query Strategy Builders. In short, a query strategy abstracts away the complexities of SQL etc away from everyday developers into business logic, while a query strategy builder allows our technology specialist with advanced knowledge of SQL and NoSQL, to convert our business friendly query strategy into something that can be executed. The advantage is that we can change how we store and query our data-stores as we need to without a major rebuild of the application.

Let's start by adding a query strategy.

using System;
using Cqrs.Repositories.Queries;

public class MovieQueryStrategy : QueryStrategy
{
	public virtual MovieQueryStrategy WithRsn(Guid rsn)
	{
		QueryPredicate = And(IsNotLogicallyDeleted());
		QueryPredicate = And(BuildQueryPredicate(WithRsn, rsn));

		return this;
	}

	public virtual MovieQueryStrategy WithSuitableForYoungAdults()
	{
		QueryPredicate = And(IsNotLogicallyDeleted());
		QueryPredicate = And(BuildQueryPredicate(WithSuitableForYoungAdults));

		return this;
	}
}

This example has several different method that demonstrate what a business may define as a filter or business rule, while totally ignoring how the data is stored. We have no idea of how age ratings are stored, and nor should we. We are playing the role of a developer focused on business rules... not SQL or storage problems.

Let's look at the matching query strategy builder:

using System;
using System.Collections.Generic;
using System.Linq;
using Cqrs.Configuration;
using Cqrs.Repositories.Queries;

public class MovieQueryStrategyBuilder : QueryBuilder<MovieQueryStrategy, MovieEntity>
{
	public MovieQueryStrategyBuilder(IDataStoreFactory dataStoreFactory, IDependencyResolver dependencyResolver)
		: base(dataStoreFactory.GetMovieEntityDataStore(), dependencyResolver)
	{
	}

	protected override IQueryable<MovieEntity> GeneratePredicate(QueryPredicate queryPredicate, IQueryable<MovieEntity> leftHandQueryable = null)
	{
		MovieQueryStrategy queryStrategy = GetNullQueryStrategy();
		SortedSet<QueryParameter> parameters = queryPredicate.Parameters;

		/*
		if (queryPredicate.Name == GetFunctionName(queryStrategy.WithSuitableForYoungAdults))
			return GeneratePredicateWithSuitableForYoungAdults(parameters, leftHandQueryable);
		*/
		if (queryPredicate.Name == GetFunctionName<Guid>(queryStrategy.WithRsn))
			return GeneratePredicateWithRsn(parameters, leftHandQueryable);

		throw new InvalidOperationException("No known predicate could be generated.");
	}

	protected virtual IQueryable<MovieEntity> GeneratePredicateWithRsn(SortedSet<QueryParameter> parameters, IQueryable<MovieEntity> leftHandQueryable)
	{
		var rsn = parameters.GetValue<Guid>("Rsn");

		IQueryable<MovieEntity> query = (leftHandQueryable ?? GetEmptyQueryPredicate());

		return query.Where
		(
			movie => movie.Rsn == rsn
		);
	}

	/*
	protected virtual IQueryable<MovieEntity> GeneratePredicateWithSuitableForYoungAdults(SortedSet<QueryParameter> parameters, IQueryable<MovieEntity> leftHandQueryable)
	{
		IQueryable<MovieEntity> query = (leftHandQueryable ?? GetEmptyQueryPredicate());

		return query.Where
		(
			movie => movie.Ratings.Contains("G", "PG", "M", "R13")
		);
	}
	*/
}

Note: Here I've left as a comment one possible implementation of a SuitableForYoungAdults query. As this uses properties we've not discussed we'll move beyond it, but as an advanced exercise you could add some appropriate commands, events, handlers and aggregate root logic to write your own implementation.

The GeneratePredicate method matches the name of the query strategy method to a builder method... in this example that is GeneratePredicateWithRsn. The builder method extracts any provided parameters and build a LINQ expression ready to execute. Here we could use any advanced LINQ provider necessary such as the MongoDB LINQ provider available via nuget.

The repository is remarkably simply:

using Cqrs.Repositories;

public class MovieEntityRepository : Repository<MovieQueryStrategy, MovieQueryStrategyBuilder, MovieEntity>
{
	public MovieEntityRepository(IDataStoreFactory dataStoreFactory, MovieQueryStrategyBuilder metricEntityQueryBuilder)
		: base(dataStoreFactory.GetMovieEntityDataStore, metricEntityQueryBuilder)
	{
	}
}

This introduces the IDataStoreFactory and DataStoreFactory:

using cdmdotnet.Logging;
using Cqrs.Configuration;
using Cqrs.DataStores;

public interface IDataStoreFactory
{
	IDataStore<MovieEntity> GetMovieEntityDataStore();
}

public class DataStoreFactory : IDataStoreFactory
{
	protected ILogger Logger { get; private set; }

	protected IConfigurationManager ConfigurationManager { get; private set; }

	public DataStoreFactory(ILogger logger, IConfigurationManager configurationManager)
	{
		Logger = logger;
		ConfigurationManager = configurationManager;
	}

	public IDataStore<MovieEntity> GetMovieEntityDataStore()
	{
		IDataStore<MovieEntity> result = new SqlDataStore<MovieEntity>(ConfigurationManager, Logger);
		return result;
	}
}

Here we finally see where SQL as a technology comes into play, in the form of the SqlDataStore. CQRS.NET comes with plenty of off-the-shelf modules for use with other technologies such as MongoDB, Azure Blob and Azure Table storage, all available via nuget.

Update the Console appliaction

Update the console as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using cdmdotnet.Logging;
using Cqrs.Commands;
using Cqrs.Configuration;
using Cqrs.Repositories.Queries;

static class Program
{
	private static IDictionary<int, MovieEntity> Movies { get; set; }

	private static ICommandPublisher<string> CommandPublisher { get; set; }

	private static IQueryFactory QueryFactory { get; set; }

	public static void Main()
	{
		SampleRuntime<string, CreateMovieCommandHandler>.CustomResolver = (resolver, type) =>
		{
			if (type == typeof(IDataStoreFactory))
				return new DataStoreFactory(resolver.Resolve<ILogger>(), resolver.Resolve<IConfigurationManager>());
			if (type == typeof(MovieEntityRepository))
				return new MovieEntityRepository
				(
					resolver.Resolve<IDataStoreFactory>(),
					new MovieQueryStrategyBuilder(resolver.Resolve<IDataStoreFactory>(), resolver)
				);
			return null;
		};

		using (new SqlSampleRuntime<string, CreateMovieCommandHandler>())
		{
			CommandPublisher = DependencyResolver.Current.Resolve<ICommandPublisher<string>>();
			QueryFactory = DependencyResolver.Current.Resolve<IQueryFactory>();
			string command = "Help";
			while (HandleUserEntry(command))
			{
				command = Console.ReadLine();
			}
		}
	}

	private static bool HandleUserEntry(string text)
	{
		switch (text.ToLowerInvariant())
		{
			case "help":
				Console.WriteLine("Add Movie");
				Console.WriteLine("\tWill allow you to add a new movie.");
				Console.WriteLine("Update Movie Title");
				Console.WriteLine("\tWill allow you to update the title of an existing movie.");
				Console.WriteLine("Get All Movies");
				Console.WriteLine("\tWill get a list of all movies.");
				Console.WriteLine("Quit");
				Console.WriteLine("\tWill exit the running program.");
				Console.WriteLine("Help");
				Console.WriteLine("\tWill display this help menu.");
				break;
			case "add movie":
			case "addmovie":
				Console.WriteLine("Enter the title for a new movie.");
				string title = Console.ReadLine();

				int duration;
				do
				{
					Console.WriteLine("Enter the duration (how long) in minutes, the new movie runs for.");
					string durationValue = Console.ReadLine();
					if (int.TryParse(durationValue, out duration))
						break;
					Console.WriteLine("The entered value of {0} was not a whole number.", durationValue);
				} while (true);

				CommandPublisher.Publish(new CreateMovieCommand(title, DateTime.Now, duration));
				break;
			case "update movie title":
			case "updatemovietitle":
				HandleUserEntry("Get All Movies");
				int movieIndex;
				do
				{
					Console.WriteLine("Enter the number of the movie would you like to update.");
					string value = Console.ReadLine();
					if (int.TryParse(value, out movieIndex))
						break;
					Console.WriteLine("The entered value of {0} was not a whole number.", value);
				} while (true);

				var movieToUpdate = Movies[movieIndex];

				Console.WriteLine("Enter the new title for movie {0}.", movieToUpdate.Title);
				string newTitle = Console.ReadLine();

				CommandPublisher.Publish(new UpdateMovieTitleCommand(movieToUpdate.Rsn, newTitle));
				break;
			case "get all movies":
			case "list all movies":
			case "getallmovies":
			case "listallmovies":
				// Create strategy
				ICollectionResultQuery<MovieQueryStrategy, MovieEntity> query = QueryFactory.CreateNewCollectionResultQuery<MovieQueryStrategy, MovieEntity>();

				// Get resulting data
				var movieEntityRepository = DependencyResolver.Current.Resolve<MovieEntityRepository>();
				ICollectionResultQuery<MovieQueryStrategy, MovieEntity> data = movieEntityRepository.Retrieve(query);

				int index = 1;
				Movies = data.Result.ToDictionary(movie => index++);

				foreach (var movie in Movies)
				{
					Console.WriteLine("{0:N0}) {1}", movie.Key, movie.Value.Title);
					Console.WriteLine("\t{0:N0} minute{1} long.", movie.Value.RunningTimeMinutes, movie.Value.RunningTimeMinutes == 1 ? null : "s");
				}

				Console.WriteLine("A total of {0} movie{1}", Movies.Count, Movies.Count == 1 ? null : "s");
				break;
			case "quit":
			case "exit":
				return false;
		}
		return true;
	}
}

The main change here is the introduction of the CustomResolver method at the beginning of the program... something we won't have to deal with too much longer when we switch to a proper resolver. There is also the introduction of the MovieEntityRepository and QueryFactory properties, as well as their usage. The QueryFactory provides two method, one for single results and one for collections, in our case we're wanting a collection so we are using the CreateNewCollectionResultQuery() method. The repository Retrieve() method is then called to execute our query and return data.

Importantly, there is no longer any sign of EntityFramework... proving we've hidden the complexity of dealing with specialist technology while at the same time allowing us to change what technology we use with minimal effort.

Update the Web application

Update the controller as follows:

using System;
using System.Net;
using System.Web.Mvc;
using Cqrs.Commands;
using Cqrs.Repositories.Queries;

public class MoviesController : Controller
{
	private static ICommandPublisher<string> CommandPublisher { get; set; }

	private static IQueryFactory QueryFactory { get; set; }

	static MoviesController()
	{
		CommandPublisher = Cqrs.Configuration.DependencyResolver.Current.Resolve<ICommandPublisher<string>>();
		QueryFactory = Cqrs.Configuration.DependencyResolver.Current.Resolve<IQueryFactory>();
	}

    public ActionResult Index()
    {
        return View();
    }

	public ActionResult GetAllMovies()
	{
		// Create strategy
		ICollectionResultQuery<MovieQueryStrategy, MovieEntity> query = QueryFactory.CreateNewCollectionResultQuery<MovieQueryStrategy, MovieEntity>();

		// Get resulting data
		var movieEntityRepository = Cqrs.Configuration.DependencyResolver.Current.Resolve<MovieEntityRepository>();
		ICollectionResultQuery<MovieQueryStrategy, MovieEntity> data = movieEntityRepository.Retrieve(query);

		return Json(data.Result, JsonRequestBehavior.AllowGet);
	}

	[HttpPost]
	public ActionResult AddMovie(string title, int duration)
	{
		CommandPublisher.Publish(new CreateMovieCommand(title, DateTime.Now, duration));

		return new HttpStatusCodeResult(HttpStatusCode.Accepted);
	}

	[HttpPost]
	public ActionResult RenameMovie(Guid rsn, string newTitle)
	{
		CommandPublisher.Publish(new UpdateMovieTitleCommand(rsn, newTitle));

		return new HttpStatusCodeResult(HttpStatusCode.Accepted);
	}
}

This has the same improvements as the console application. Finally update the Global.asax.cs file as follows:

using System;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using cdmdotnet.Logging;
using cdmdotnet.StateManagement;
using Cqrs.Authentication;
using Cqrs.Bus;
using Cqrs.Configuration;
using Cqrs.Events;
using Cqrs.WebApi.Events.Handlers;
using Cqrs.WebApi.SignalR.Hubs;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

public class MvcApplication : HttpApplication
{
	private SampleRuntime<string, CreateMovieCommandHandler> SampleRuntime { get; set; }

	protected void Application_Start()
	{
		SampleRuntime<string, CreateMovieCommandHandler>.CustomResolver = (resolver, type) =>
		{
			if (type == typeof(IDataStoreFactory))
				return new DataStoreFactory(resolver.Resolve<ILogger>(), resolver.Resolve<IConfigurationManager>());
			if (type == typeof(MovieEntityRepository))
				return new MovieEntityRepository
				(
					resolver.Resolve<IDataStoreFactory>(),
					new MovieQueryStrategyBuilder(resolver.Resolve<IDataStoreFactory>(), resolver)
				);
			return null;
		};

		SampleRuntime = new SqlSampleRuntime<string, CreateMovieCommandHandler>();
		AreaRegistration.RegisterAllAreas();
		FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
		RouteConfig.RegisterRoutes(RouteTable.Routes);
		BundleConfig.RegisterBundles(BundleTable.Bundles);

		var formatter = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
		formatter.SerializerSettings = new JsonSerializerSettings
		{
			Formatting = Formatting.Indented,
			TypeNameHandling = TypeNameHandling.Objects,
			ContractResolver = new CamelCasePropertyNamesContractResolver()
		};

		var eventHandlerRegistrar = Cqrs.Configuration.DependencyResolver.Current.Resolve<IEventHandlerRegistrar>();
		var proxy = new GlobalEventToHubProxy<string>
		(
			Cqrs.Configuration.DependencyResolver.Current.Resolve<ILogger>(),
			new NotificationHub(Cqrs.Configuration.DependencyResolver.Current.Resolve<ILogger>(), Cqrs.Configuration.DependencyResolver.Current.Resolve<ICorrelationIdHelper>()),
			new AuthenticationTokenHelper<string>(Cqrs.Configuration.DependencyResolver.Current.Resolve<IContextItemCollectionFactory>())
		);
		eventHandlerRegistrar.RegisterGlobalEventHandler<IEvent<string>>(proxy.Handle, false);
	}

	protected void Application_BeginRequest(object sender, EventArgs e)
	{
		new BusRegistrar(Cqrs.Configuration.DependencyResolver.Current)
			.Register(typeof(GlobalEventToHubProxy<>));

		Cqrs.Configuration.DependencyResolver.Current.Resolve<ICorrelationIdHelper>().SetCorrelationId(Guid.NewGuid());
	}

	protected void Application_End(object sender, EventArgs e)
	{
		SampleRuntime.Dispose();
	}
}

This contains the same dependency resolver changes as the console application.

Clone this wiki locally