Domain Driven Design with Web API extensions part 14: implementing the ITimetableRepository interface in MongoDb

Introduction

In the previous post we looked at a couple of basic operations with the MongoDb .NET driver. I decided to include that “intermediate” post so that you won’t get overwhelmed with a lot of new code if you’re new to MongoDb.

In this post we’ll to be implement the ITimetableRepository interface in the MongoDb repository layer.

Preparations for the MongoDb TimetableRepository

We already have a project called WebSuiteDemo.Loadtesting.Repository.MongoDb in our DDD demo solution with the MongoDb representations of the domain. We’ll now extend this project with the implementation of the ITimetableRepository interface. However, first we need to put some other important building blocks in place.

Make sure that WebSuiteDemo.Loadtesting.Repository.MongoDb has a project reference to the following:

  • WebSuiteDDD.Infrastructure.Common
  • WebSuiteDDD.SharedKernel
  • WebSuiteDemo.Loadtesting.Domain

Then add a folder called Repositories to the project just like we have a Repositories in the WebSuiteDemo.Loadtesting.Repository.EF project.

We’ll first add a class called ModelConversions into the folder which will include a number of extension methods to convert MongoDb objects to domains and various other methods. Here’s the complete code of ModelConversions.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MongoDB.Bson;
using WebSuiteDemo.Loadtesting.Domain;
using WebSuiteDemo.Loadtesting.Repository.MongoDb.DatabaseObjects;

namespace WebSuiteDemo.Loadtesting.Repository.MongoDb.Repositories
{
	public static class ModelConversions
	{
		public static Loadtest ConvertToDomain(this LoadtestMongoDb loadtestDbModel)
		{
			LoadtestParametersMongoDb ltParamsDb = loadtestDbModel.Parameters;
			LoadtestParameters ltParamsDomain = new LoadtestParameters(ltParamsDb.StartDateUtc, ltParamsDb.UserCount, ltParamsDb.DurationSec);
			return new Loadtest(
				loadtestDbModel.DomainId,
				ltParamsDomain,
				loadtestDbModel.AgentId,
				loadtestDbModel.CustomerId,
				loadtestDbModel.EngineerId,
				loadtestDbModel.LoadtestTypeId,
				loadtestDbModel.ProjectId,
				loadtestDbModel.ScenarioId);
		}

		public static IEnumerable<Loadtest> ConvertToDomains(this IEnumerable<LoadtestMongoDb> loadtestDbModels)
		{
			foreach (LoadtestMongoDb db in loadtestDbModels)
			{
				yield return db.ConvertToDomain();
			}
		}

		public static LoadtestMongoDb PrepareForInsertion(this Loadtest domain)
		{
			LoadtestMongoDb ltDb = new LoadtestMongoDb();
			ltDb.DomainId = domain.Id;
			ltDb.DbObjectId = ObjectId.GenerateNewId();
			ltDb.AgentId = domain.AgentId;
			ltDb.CustomerId = domain.CustomerId;
			ltDb.EngineerId = domain.EngineerId;
			ltDb.LoadtestTypeId = domain.LoadtestTypeId;

			int duration = domain.Parameters.DurationSec;
			DateTime start = domain.Parameters.StartDateUtc;
			ltDb.Parameters = new LoadtestParametersMongoDb()
			{
				DbObjectId = ObjectId.GenerateNewId(),
				DurationSec = duration,
				ExpectedEndDateUtc = start.AddSeconds(duration),
				StartDateUtc = start,
				UserCount = domain.Parameters.UserCount
			};
			ltDb.ProjectId = domain.ProjectId;
			ltDb.ScenarioId = domain.ScenarioId;
			return ltDb;
		}

		public static IEnumerable<LoadtestMongoDb> PrepareAllForInsertion(this IEnumerable<Loadtest> domains)
		{
			foreach (Loadtest domain in domains)
			{
				yield return domain.PrepareForInsertion();
			}
		}

	}
}

Let’s see what happens in each method:

  • ConvertToDomain: converts a LoadtestMongoDb object into its domain counterpart
  • ConvertToDomains: same as above but for collections
  • PrepareForInsertion: creates a LoadtestMongoDb object out of a domain and makes it ready for insertion. This code is almost identical to the how we inserted the LoadtestMongoDb objects in the Seed method previously
  • PrepareAllForInsertion: same as above but for collections

We’ll use each of these methods in the ITimetableRepository implementation.

Before we look at the implementation we’ll also add an abstract base class for each MongoDb repository. The abstract base class makes it possible for the deriving classes to get hold of a valid IConnectionStringRepository which in turn is needed to build a MongoDb LoadTestingContext. Insert a class called MongoDbRepository into the Repositories folder:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WebSuiteDDD.Infrastructure.Common.ApplicationSettings;


namespace WebSuiteDemo.Loadtesting.Repository.MongoDb.Repositories
{
	public abstract class MongoDbRepository
	{
		private readonly IConnectionStringRepository _connectionStringRepository;

		public MongoDbRepository(IConnectionStringRepository connectionStringRepository)
		{
			if (connectionStringRepository == null) throw new ArgumentNullException("ConnectionStringRepository");
			_connectionStringRepository = connectionStringRepository;
		}

		public IConnectionStringRepository ConnectionStringRepository
		{
			get
			{
				return _connectionStringRepository;
			}
		}
	}
}

That’s easy to follow I hope.

The MongoDb TimetableRepository

We can now insert a class called TimetableRepository into the Repositories folder. Here’s the complete code with the explanations underneath:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WebSuiteDDD.Infrastructure.Common.ApplicationSettings;
using WebSuiteDemo.Loadtesting.Domain;
using WebSuiteDemo.Loadtesting.Repository.MongoDb.DatabaseObjects;
using MongoDB.Bson;
using MongoDB.Driver;

namespace WebSuiteDemo.Loadtesting.Repository.MongoDb.Repositories
{
	public class TimetableRepository : MongoDbRepository, ITimetableRepository
	{
		public TimetableRepository(IConnectionStringRepository connectionStringRepository)
			: base(connectionStringRepository)
		{ }

		public IList<Loadtest> GetLoadtestsForTimePeriod(DateTime searchStartDateUtc, DateTime searchEndDateUtc)
		{
			LoadTestingContext context = LoadTestingContext.Create(base.ConnectionStringRepository);
			
			var dateQueryBuilder = Builders<LoadtestMongoDb>.Filter;
			var startDateBeforeSearchStartFilter = dateQueryBuilder.Lte<DateTime>(l => l.Parameters.StartDateUtc, searchStartDateUtc);
			var endDateAfterSearchStartFilter = dateQueryBuilder.Gte<DateTime>(l => l.Parameters.ExpectedEndDateUtc, searchStartDateUtc);
			var firstPartialDateQuery = dateQueryBuilder.And(new List<FilterDefinition<LoadtestMongoDb>>(){startDateBeforeSearchStartFilter, endDateAfterSearchStartFilter});

			var startDateBeforeSearchEndFilter = dateQueryBuilder.Lte<DateTime>(l => l.Parameters.StartDateUtc, searchEndDateUtc);
			var endDateAfterSearchEndFilter = dateQueryBuilder.Gte<DateTime>(l => l.Parameters.ExpectedEndDateUtc, searchEndDateUtc);
			var secondPartialDateQuery = dateQueryBuilder.And(new List<FilterDefinition<LoadtestMongoDb>>() { startDateBeforeSearchEndFilter, endDateAfterSearchEndFilter });

			var thirdPartialDateQuery = dateQueryBuilder.And(new List<FilterDefinition<LoadtestMongoDb>>() { startDateBeforeSearchStartFilter, endDateAfterSearchEndFilter });

			var startDateAfterSearchStartFilter = dateQueryBuilder.Gte<DateTime>(l => l.Parameters.StartDateUtc, searchStartDateUtc);
			var endDateBeforeSearchEndFilter = dateQueryBuilder.Lte<DateTime>(l => l.Parameters.ExpectedEndDateUtc, searchEndDateUtc);
			var fourthPartialQuery = dateQueryBuilder.And(new List<FilterDefinition<LoadtestMongoDb>>() { startDateAfterSearchStartFilter, endDateBeforeSearchEndFilter });

			var ultimateQuery = dateQueryBuilder.Or(new List<FilterDefinition<LoadtestMongoDb>>() { firstPartialDateQuery, secondPartialDateQuery, thirdPartialDateQuery, fourthPartialQuery });

			List<LoadtestMongoDb> mongoDbLoadtestsInSearchPeriod = context.Loadtests.Find(ultimateQuery)
				.ToList();
			List<Loadtest> loadtestsInSearchPeriod = mongoDbLoadtestsInSearchPeriod.ConvertToDomains().ToList();
			return loadtestsInSearchPeriod;
		}

		public void AddOrUpdateLoadtests(AddOrUpdateLoadtestsValidationResult addOrUpdateLoadtestsValidationResult)
		{
			LoadTestingContext context = LoadTestingContext.Create(base.ConnectionStringRepository);
			if (addOrUpdateLoadtestsValidationResult.ValidationComplete)
			{
				if (addOrUpdateLoadtestsValidationResult.ToBeInserted.Any())
				{
					IEnumerable<LoadtestMongoDb> toBeInserted = addOrUpdateLoadtestsValidationResult.ToBeInserted.PrepareAllForInsertion();
					context.Loadtests.InsertMany(toBeInserted);
				}

				if (addOrUpdateLoadtestsValidationResult.ToBeUpdated.Any())
				{
					foreach (Loadtest toBeUpdated in addOrUpdateLoadtestsValidationResult.ToBeUpdated)
					{
						Guid existingLoadtestId = toBeUpdated.Id;
						var loadtestInDbQuery = context.Loadtests.Find<LoadtestMongoDb>(lt => lt.DomainId == existingLoadtestId);
						LoadtestMongoDb loadtestInDb = loadtestInDbQuery.SingleOrDefault();
						loadtestInDb.AgentId = toBeUpdated.AgentId;
						loadtestInDb.CustomerId = toBeUpdated.CustomerId;
						loadtestInDb.EngineerId = toBeUpdated.EngineerId;
						loadtestInDb.LoadtestTypeId = toBeUpdated.LoadtestTypeId;
						LoadtestParameters ltDomainParameters = toBeUpdated.Parameters;
						loadtestInDb.Parameters.DurationSec = ltDomainParameters.DurationSec;
						loadtestInDb.Parameters.ExpectedEndDateUtc = ltDomainParameters.StartDateUtc.AddSeconds(ltDomainParameters.DurationSec);
						loadtestInDb.Parameters.StartDateUtc = ltDomainParameters.StartDateUtc;
						loadtestInDb.Parameters.UserCount = ltDomainParameters.UserCount;
						loadtestInDb.ProjectId = toBeUpdated.ProjectId;
						loadtestInDb.ScenarioId = toBeUpdated.ScenarioId;
						context.Loadtests.FindOneAndReplace<LoadtestMongoDb>(lt => lt.DbObjectId == loadtestInDb.DbObjectId, loadtestInDb);
					}
				}
			}
			else
			{
				throw new InvalidOperationException("Validation is not complete. You have to call the AddOrUpdateLoadtests method of the Timetable class first.");
			}
		}

		public void DeleteById(Guid guid)
		{
			LoadTestingContext context = LoadTestingContext.Create(base.ConnectionStringRepository);
			context.Loadtests.FindOneAndDelete<LoadtestMongoDb>(lt => lt.DomainId == guid);
		}
	}
}

That’s quite a lot of code. The method implementations actually perform the same actions as their EF equivalent but the techniques are of course suited for MongoDb. Let’s see how the functions are built up:

  • GetLoadtestsForTimePeriod: this query is quite complex and the way the complete query is constructed is also pretty involved at first sight. Recall that the EF implementation of ITimetableRepository.GetLoadtestsForTimePeriod had a where clause with 4 AND subconditions, each divided by OR. Each of these 4 AND conditions is represented by the “partial” queries: firstPartialDateQuery, secondPartialDateQuery, thirdPartialDateQuery and fourthPartialQuery. Each of these partial queries is constructed using the FilterDefinition technique we saw in the previous post. The “And” extension method requires a collection of FilterDefinition objects and they will be translated into MongoDb And operations by the driver. So we have 4 of those subconditions, all based on the start and expected end dates of the load tests. Finally we have a variable called ultimateQuery which chains the 4 individual AND conditions into a series of OR’s. We then feed the ultimate query into the Find method, convert the LoadtestMongoDb objects into Loadtest ones an return the collection.
  • AddOrUpdateLoadtests: this is also very similar to what we have in the EF implementation. The loadtest objects targeted for insertion are “dressed up” a little and converted into LoadtestMongoDb objects. They are then inserted into the DB by the InsertMany method. The update – actually replace – operations are a bit more involved. We first retrieve the LoadtestMongoDb object by its domain ID field so that the ObjectIds are preserved. We then modify its parameters according to the replacement values and finally call the FindOneAndReplace method in order to execute the replacement
  • DeleteById: this is trivially simple. The FindOneAndDelete does exactly as its name suggests. It finds a LoadtestMongoDb by its DomainId field and deletes the document.

Preliminary tests

If you want then you can test the above implementation from MongoDbDatabaseTester. Make sure that the MongoDb server is running.

Here’s an example of calling GetLoadtestsForTimePeriod with a wide time interval from the tester console application:

ITimetableRepository repo = new TimetableRepository(new WebConfigConnectionStringRepository());
IList<Loadtest> loadtestsInPeriod = repo.GetLoadtestsForTimePeriod(DateTime.UtcNow.AddYears(-1), DateTime.UtcNow.AddYears(1));

Testing the insert and update operations is a bit clumsy as you’ll need to get hold of the GUIDs of the constituent objects, e.g. the agent, the engineer etc. Here’s an example of building a new Loadtest object:

List<Loadtest> toBeUpdated = new List<Loadtest>();
Loadtest ltNewOne = new Loadtest
	(Guid.NewGuid(),
	new LoadtestParameters(DateTime.UtcNow.AddDays(1), 100, 90),
	Guid.Parse("52d4e276-193d-4ff3-a40e-c45381969d24"),
	Guid.Parse("cb5c3463-d1cb-4c1c-b667-f1c6a065edd1"),
	Guid.Parse("c1cf1179-98a7-4c58-bcaa-a0e7c697293f"),
	Guid.Parse("612cf872-3967-41e7-a30d-28e26df66dcc"),
	Guid.Parse("c3d79996-7045-4bce-a6cd-fcf398717ae5"),
	Guid.Parse("4d27ad00-14d8-4c1c-98b9-64556e957daf"));

It is added to a collection:

List<Loadtest> toBeInserted = new List<Loadtest>();
toBeInserted.Add(ltNewOne);

Here’s an example of an update, i.e. where we use the GUID of an existing load test:

Loadtest ltUpdOne = new Loadtest
			(Guid.Parse("71b29573-8f67-49ab-8ee0-f8dd0bbceefd"),
			new LoadtestParameters(DateTime.UtcNow.AddDays(14), 50, 300),
			Guid.Parse("52d4e276-193d-4ff3-a40e-c45381969d24"),
			Guid.Parse("5b16880e-b0dd-4d66-bff9-f79eba6490ec"),
			Guid.Parse("40ccb6aa-c9a6-466d-be02-c73122d6edca"),
			Guid.Parse("612cf872-3967-41e7-a30d-28e26df66dcc"),
			Guid.Parse("e2caa1f0-2ee9-4e8f-86a0-51de8aba4eca"),
			Guid.Parse("4d27ad00-14d8-4c1c-98b9-64556e957daf"));

…which is then added to another collection:

List<Loadtest> toBeUpdated = new List<Loadtest>();
toBeUpdated.Add(ltUpdOne);

We can then construct a valid AddOrUpdateLoadtestsValidationResult object:

AddOrUpdateLoadtestsValidationResult validationRes = new AddOrUpdateLoadtestsValidationResult(toBeInserted, toBeUpdated, 
	new List<Loadtest>(), "Validation summary");

…and call AddOrUpdateLoadtests with it:

ITimetableRepository repo = new TimetableRepository(new WebConfigConnectionStringRepository());
repo.AddOrUpdateLoadtests(validationRes);

It’s a good idea to set breakpoints within the functions and follow the code execution step by step.

In the next post we’ll implement the ITimetableViewModelRepository interface.

View the list of posts on Architecture and Patterns here.

Advertisement

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Elliot Balynn's Blog

A directory of wonderful thoughts

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

WEB APPLICATION DEVELOPMENT TUTORIALS WITH OPEN-SOURCE PROJECTS

Once Upon a Camayoc

Bite-size insight on Cyber Security for the not too technical.

%d bloggers like this: