Domain Driven Design with Web API extensions part 13: query examples with the MongoDb driver
December 21, 2015 1 Comment
Introduction
In the previous post we successfully seeded our MongoDb load testing data store. We saw that the Seed() method wasn’t all that different from its EntityFramework equivalent.
In this post we’ll look at a range of examples of using the MongoDb driver. We’ll primarily consider CRUD operations. Originally I wanted to simply present the implementation of the domain repository interfaces. However, I thought it may be too overwhelming for MongoDb novices to be presented all the new object types and query functions. The purpose of this “intermediate” post is therefore to provide a soft start in MongoDb queries.
We’ll be working in the MongoDbDatabaseTester project of the DDD demo solution in this post. Make sure you start up the MongoDb server with the “mongod” command in a command prompt.
Queries in MongoDb
You need to know that the MongoDb driver has undergone a lot of changes, updates and additions over the years of its existence. The way to write queries has also evolved a lot: old techniques have been kept and new ones have been added. Therefore there are multiple ways to express the same type of query. I’ll try to present the most object oriented solutions which don’t require us to work with BSON documents back and forth.
SELECT * FROM
Let’s start with probably the easiest scenario: retrieve all objects without any filter. We’ll retrieve all Engineer objects from the DB – to be exact, all EngineerMongoDb objects. The Find() extension method applicable to an IMongoCollection – which is the MongoDb approximation of DbSet in EF – accepts a LINQ filter expression that returns a boolean. There’s no empty Find() method which returns all element. The trick is to insert a filter that simply returns true as follows:
private static void TestSelectAllNoFilter() { LoadTestingContext context = LoadTestingContext.Create(new WebConfigConnectionStringRepository()); Task<List<EngineerMongoDb>> dbEngineersTask = context.Engineers.Find(x => true).ToListAsync(); Task.WaitAll(dbEngineersTask); List<EngineerMongoDb> dbEngineers = dbEngineersTask.Result; foreach (EngineerMongoDb eng in dbEngineers) { Debug.WriteLine(eng.Name); } }
This prints…
John
Mary
Fred
…in the debug window. ToListAsync() is an awaitable method which returns a Task so we’ll simply wait for it to finish.
SELECT * FROM WHERE
The above example does technically contain a WHERE clause. However, it always returns true so it’s not much of a real filter.
Let’s retrieve all agents that are located in Germany:
private static void TestSelectWithWhereClause() { LoadTestingContext context = LoadTestingContext.Create(new WebConfigConnectionStringRepository()); Task<List<AgentMongoDb>> germanAgentsTask = context.Agents.Find(a => a.Location.Country == "Germany").ToListAsync(); Task.WaitAll(germanAgentsTask); List<AgentMongoDb> germanAgents = germanAgentsTask.Result; foreach (AgentMongoDb agent in germanAgents) { Debug.WriteLine(agent.Location.City); } }
This finds a single agent in the city of Frankfurt.
If you’d like to find a single result, which is mostly applicable when searching with a unique ID, then here’s an option:
Guid agentDomainGuid = Guid.Parse("7c9eb839-2c1c-4ae9-8140-170917c975e1"); Task<AgentMongoDb> singleAgentByIdTask = context.Agents.Find(a => a.DomainId == agentDomainGuid).SingleOrDefaultAsync(); Task.WaitAll(singleAgentByIdTask); AgentMongoDb singleAgentById = singleAgentByIdTask.Result; Debug.WriteLine(singleAgentById.Location.City);
The ID of the Frankfurt agent in my case is the one you see above. You’ll probably guess that the SingleOrDefaultAsync() extension method is the MongoDb asynchronous equivalent of FirstOrDefault() in LINQ.
LINQ-style queries are not the only way to add a filter to a query. Another overload of the Find method accepts a FilterDefinition object. Filter definitions can be built using the fluent interface of the Builders class. Here’s an example which produces the same result as the one above where we extract the German agents:
Task<List<AgentMongoDb>> germanAgentsWithBuilderTask = context.Agents .Find(Builders<AgentMongoDb>.Filter.Eq<string>(a => a.Location.Country, "Germany")).ToListAsync(); Task.WaitAll(germanAgentsWithBuilderTask); List<AgentMongoDb> germanAgentsWithBuilder = germanAgentsWithBuilderTask.Result;
At first the query builder may look strange but it’s worth exploring because it’s really powerful for complex queries. Filter.Eq means the equality comparison of course. The Filter definition builder includes a lot of other conditions like Gte, Lt, In and much more. You can probably guess what they stand for. We’ll use this type query technique in the next post where we implement the ITimetableRepository interface.
INSERT INTO
Insertion is also a very straightforward operation. Here comes an example of inserting a single document. We saw in the previous post how to insert many object at once with the InsertManyAsync() extension method. It has an InsertOneAsync() counterpart for single objects:
LoadTestingContext context = LoadTestingContext.Create(new WebConfigConnectionStringRepository()); CustomerMongoDb newCustomer = new CustomerMongoDb() { DomainId = Guid.NewGuid(), DbObjectId = ObjectId.GenerateNewId(), Name = "Brand new customer" }; Task insertionTask = context.Customers.InsertOneAsync(newCustomer); Task.WaitAll(insertionTask);
UPDATE
Update operations are a relatively large topic in MongoDb. An update can mean literally updating an existing document or replacing it with a new one. You’ll find extension methods on IMongoCollection that correspond to these two techniques. Here’s an example with the Update operation:
LoadTestingContext context = LoadTestingContext.Create(new WebConfigConnectionStringRepository()); Guid existingAgentGuid = Guid.Parse("7c9eb839-2c1c-4ae9-8140-170917c975e1"); UpdateDefinition<AgentMongoDb> agentUpdateDefinition = Builders<AgentMongoDb>.Update.Set<string>(a => a.Location.City, "Munich"); Task<AgentMongoDb> updateTask = context.Agents.FindOneAndUpdateAsync(a => a.DomainId == existingAgentGuid, agentUpdateDefinition); Task.WaitAll(updateTask);
The above code shows how to find an object with a domain ID and set one of its fields, namely the city. We’ve just moved the Frankfurt agent to Munich. We can also see another example of the Builders definition builder. The Update method also includes a range of extensions, just like Filter we saw above. “Set” is a convenient way to update the value of a single property.
Let’s also see an example of the Replace method. It requires us to first find the ObjectId of the existing object as it cannot be altered even when it’s replaced:
LoadTestingContext context = LoadTestingContext.Create(new WebConfigConnectionStringRepository()); Guid existingProjectId = Guid.Parse("1d9fd06f-66d6-41f6-847d-f80dc48d4ede"); Task<ProjectMongoDb> projectTask = context.Projects.Find(p => p.DomainId == existingProjectId).SingleOrDefaultAsync(); Task.WaitAll(projectTask); ProjectMongoDb project = projectTask.Result; project.Description.LongDescription = "This is an updated long description"; project.Description.ShortDescription = "This is an updated short description"; Task<ProjectMongoDb> replacementTask = context.Projects.FindOneAndReplaceAsync(p => p.DbObjectId == project.DbObjectId, project);
DELETE
Deletions work the same way as above using the above methods:
- DeleteOneAsync
- DeleteManyAsync
- FindOneAndDeleteAsync
I won’t show any example here. We’ll see one when we implement the ITimetableRepository interface, it’s just as painless as the InsertOneAsync method.
In the next post we’ll implement the ITimetableRepository interface in the MongoDb repository.
View the list of posts on Architecture and Patterns here.
Pingback: Domain Driven Design with Web API extensions part 13: query examples with the MongoDb driver | Dinesh Ram Kali.