Domain Driven Design with Web API extensions part 11: the MongoDb database objects
December 14, 2015 Leave a comment
Introduction
In the previous post we mainly discussed the advantages and limitations of coding against MongoDb using .NET. We also discussed the MongoDb context a little bit and started building the MongoDb version of the repository. We said that there’s not much automation available in the .NET MongoDb driver compared to what you get in EF. However, that’s not necessarily a bad thing since you’re not tied to some “secret” and “magic” underlying mechanism that does a lot of work in the background. Instead you’re free to implement the objects, the rules, the conversions etc. as you wish. It usually means more code, but you get absolute freedom for your repository implementation in return.
In this post we’ll first add a new element to the common infrastructure layer. Then we’ll add the MongoDb database representation of our domain objects.
The connection string repository
Do you recall how we abstracted away the application settings repository?
public interface IConfigurationRepository { T GetConfigurationValue<T>(string key); T GetConfigurationValue<T>(string key, T defaultValue); }
The same arguments apply for retrieving connection strings. Connection strings can be stored in the configuration file, in the database, etc. So we’ll hide those details behind an abstraction.
Add the following to the ApplicationSettings folder of the WebSuiteDDD.Infrastructure.Common project:
public interface IConnectionStringRepository { string ReadConnectionString(string connectionStringName); }
Also, insert the web config implementation of the connection repository in the same folder:
public class WebConfigConnectionStringRepository : IConnectionStringRepository { public string ReadConnectionString(string connectionStringName) { return System.Configuration.ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString; } }
We’ll inject the above abstract dependency into the MongoDb repositories which we’ll implement later on.
The database objects
As mentioned before we will write the database representations of the domain objects ourselves. There’s no mechanism built into the MongoDb .NET driver that could generate the entities. However, we’re in full control over the mappings between the pure domain and database objects.
We inserted a C# class library called WebSuiteDemo.Loadtesting.Repository.MongoDb in the previous post. Add a folder called DatabaseObjects into that project. We’ll add the database objects here.
I couldn’t really decide what to call the database objects. I didn’t want to give them the same names as the domain objects to avoid name clashes and having to use the fully qualified names like WebSuiteDemo.Loadtesting.Repository.MongoDb.DatabaseObjects.Agent and WebSuiteDemo.Loadtesting.Domain.Agent. So I decided to take the domain object name and append “MongoDb” to it. Feel free to go for different names if you wish.
Before we insert the actual database objects let’s add two base classes into the DatabaseObjects folder. First off the ultimate base class for all MongoDb objects that will contain the ObjectId:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; namespace WebSuiteDemo.Loadtesting.Repository.MongoDb.DatabaseObjects { [BsonIgnoreExtraElements] public abstract class MongoDbObjectBase { [BsonId] public ObjectId DbObjectId { get; set; } } }
The BsonIgnoreExtraElements attribute will make sure that all extra properties added to the database object in code will be ignored for older versions of the object already saved in the database. With that attribute you can safely add properties to the object without worrying about property binding problems. Without this attribute the MongoDb driver throws an exception in case there’s any property in a MongoDb document for which it doesn’t find a matching property in the object in code.
Next we’ll also need a base class MongoDbDomainBase.cs for all objects with a Guid. This will be the base class for all our entities that have a unique ID:
public abstract class MongoDbDomainBase : MongoDbObjectBase { [BsonRepresentation(BsonType.String)] public Guid DomainId { get; set; } }
We won’t need the above base class for the value objects like Description. MongoDbObjectBase will suffice for those.
Note that we’ll store the GUIDs as string in MongoDb. I don’t like the way MongoDb stores GUIDs so this is a good workaround. You can find more on that topic in this StackOverflow thread.
The database objects are otherwise almost identical to their Loadtest domain counterparts. I’ll just show them here in alphabetical order without any further explanation. You can save all of them in the DatabaseObjects folder:
public class AgentMongoDb : MongoDbDomainBase { public LocationMongoDb Location { get; set; } } public class CustomerMongoDb : MongoDbDomainBase { public string Name { get; set; } } public class DescriptionMongoDb : MongoDbObjectBase { public string ShortDescription { get; set; } public string LongDescription { get; set; } } public class EngineerMongoDb : MongoDbDomainBase { public string Name { get; set; } } public class LoadtestMongoDb : MongoDbDomainBase { [BsonRepresentation(BsonType.String)] public Guid AgentId { get; set; } [BsonRepresentation(BsonType.String)] public Guid CustomerId { get; set; } [BsonRepresentation(BsonType.String)] public Guid? EngineerId { get; set; } [BsonRepresentation(BsonType.String)] public Guid LoadtestTypeId { get; set; } [BsonRepresentation(BsonType.String)] public Guid ProjectId { get; set; } [BsonRepresentation(BsonType.String)] public Guid ScenarioId { get; set; } public LoadtestParametersMongoDb Parameters { get; set; } } public class LoadtestParametersMongoDb : MongoDbObjectBase { public DateTime StartDateUtc { get; set; } public int UserCount { get; set; } public int DurationSec { get; set; } public DateTime ExpectedEndDateUtc { get; set; } } public class LoadtestTypeMongoDb : MongoDbDomainBase { public DescriptionMongoDb Description { get; set; } } public class LocationMongoDb : MongoDbObjectBase { public string City { get; set; } public string Country { get; set; } } public class ProjectMongoDb : MongoDbDomainBase { public DescriptionMongoDb Description { get; set; } } public class ScenarioMongoDb : MongoDbDomainBase { public string UriOne { get; set; } public string UriTwo { get; set; } public string UriThree { get; set; } }
You’ll notice an extra property in LoadtestParametersMongoDb compared to the LoadtestParameters value object in the Domain layer: ExpectedEndDateUtc. It’s there for querying purposes. We’ll see later how it is used to retrieve the load tests whose execution times overlap the query start and end dates. Otherwise ExpectedEndDateUtc will be equal to StartDateUtc + DurationSec.
We can now extend the MongoDB LoadTestingContext class with the appropriate properties to get hold of the database object collections. These are the MongoDb equivalent of the DbSet of T properties of the EF object context, e.g.:
public DbSet<Agent> Agents { get; set; }
Here’s the updated version of WebSuiteDemo.Loadtesting.Repository.MongoDb.LoadTestingContext:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using MongoDB.Driver; using WebSuiteDDD.Infrastructure.Common.ApplicationSettings; using WebSuiteDemo.Loadtesting.Repository.MongoDb.DatabaseObjects; namespace WebSuiteDemo.Loadtesting.Repository.MongoDb { public class LoadTestingContext { private IMongoClient Client { get; set; } private IMongoDatabase Database { get; set; } private const string _databaseName = "Loadtests"; private static LoadTestingContext _loadTestingContext; private LoadTestingContext() { } public static LoadTestingContext Create(IConnectionStringRepository connectionStringRepository) { if (_loadTestingContext == null) { _loadTestingContext = new LoadTestingContext(); string connectionString = connectionStringRepository.ReadConnectionString("MongoDbWebSuiteContext"); _loadTestingContext.Client = new MongoClient(connectionString); _loadTestingContext.Database = _loadTestingContext.Client.GetDatabase(_databaseName); } return _loadTestingContext; } public IMongoCollection<AgentMongoDb> Agents { get { return Database.GetCollection<AgentMongoDb>("Agents"); } } public IMongoCollection<CustomerMongoDb> Customers { get { return Database.GetCollection<CustomerMongoDb>("Customers"); } } public IMongoCollection<EngineerMongoDb> Engineers { get { return Database.GetCollection<EngineerMongoDb>("Engineers"); } } public IMongoCollection<LoadtestMongoDb> Loadtests { get { return Database.GetCollection<LoadtestMongoDb>("Loadtests"); } } public IMongoCollection<LoadtestTypeMongoDb> LoadtestTypes { get { return Database.GetCollection<LoadtestTypeMongoDb>("LoadtestTypes"); } } public IMongoCollection<ProjectMongoDb> Projects { get { return Database.GetCollection<ProjectMongoDb>("Projects"); } } public IMongoCollection<ScenarioMongoDb> Scenarios { get { return Database.GetCollection<ScenarioMongoDb>("Scenarios"); } } } }
We’ve only added a number of convenience properties that all get a collection with the domain names in plural, e.g. “Projects”.
In the next post we’ll start interacting with the load testing database from the MongoDbDatabaseTester console application.
View the list of posts on Architecture and Patterns here.