Saturday, 9 May 2015

Using the Model-Store pattern in Inversion (part 3)

This post is the third in a series about using a Model-Store pattern in the Inversion behavioural micro-framework for .Net (see: https://github.com/guy-murphy/inversion-dev).

The first part can be found here.
The second part can be found here.

Introduction


I finished the last post by describing the base Store for dealing with a MongoDB database. Now I will go through the specialised Store that uses it to perform operations with the User class I introduced in part 1.

Reminder - you can find the source code for this article at https://github.com/fractos/inversion-data.

Forced specialisation of Models


The first thing I need to cover is that I had to rewrite a whole bunch of stuff in this article after trying really hard to make the examples work with the original Model. This is because of the annoying behaviour in MongoDB of it requiring a property which it deserialises the database object ID into. It's not the only database that enforces this kind of thing (I'm looking at you, DynamoDB), and it's also an unwelcome reminder that sometimes we really are at the mercy of the storage gods.

I've also tried hard to make the examples work with simple serialisation and deserialisation to and from the Model's Builder object, but it just doesn't work. The automatic deserialisation forces you to change the original Model and none of my attempts at wrapping the Model in a way that serves as an adaptor seem to work. Maybe there is a way to wrap it, but the extra plumbing just doesn't feel worth the hassle.

As it is, the User class needs an ID, as the operations performed by the specialised Store become over-complicated without one. I've listed the new source of User.cs, below. Note that the ID property is set to a unique value in the User.Builder parameterless constructor, which means that any newly created User object will have a unique ID already assigned.

In order for the ID to be used correctly with the automatic deserialisation in MongoDB's driver, the type of the ID would have to be changed (to ObjectId) and the property itself marked with a Bson-specific attribute (BsonId). However, I'm not willing to do that as it invalidates the whole idea of having a Model class that is agnostic to the storage technology being used.

So I'm going to show you another way.



Another way around - BsonDocuments


As I mentioned in part two, BsonDocuments are objects that represent a piece of BSON data, which is a binary-serialised version of a JSON document peculiar to MongoDB. The driver is very happy dealing with these constructs as they provide a basic representation of the document. This representation is used internally in the database for binary serialisation. I use the BsonDocument method in all my projects that back onto MongoDB.

Properties are accessible by their name, and they can be any one of the primitive types that MongoDB supports which includes strings, dates, numbers, objects (i.e. another document) and arrays.

So, a way of getting around the forced specialisation of a Model type is to provide custom serialisation/deserialisation to and from a BsonDocument as presented and consumed by the MongoDB driver.

To do this, I use extension methods as a tidy solution for compartmentalisation of the conversion methods.

Using extensions to group conversion methods - MongoDBUserEx.cs


This is the static class that sits in the same namespace as the specialised store MongoDBUserStore. Inside are two extension methods - ConvertUserToBson and ConvertBsonToUser. They pretty much do what they say on the tin. Here's the code.



The ConvertUserToBson method extends an instance of the User class and returns a prepared BsonDocument containing a document representation of the User object's data. The document properties are accessed by name, hence the string indexer on doc. The reserved property name "_id" is used to set the document ID which is set from the User object's ID property.

The "username" and "password" document properties are simply set with the respective string values from the User object.

The "metadata" property, however, is set with a different BsonDocument which is made from the contents of the User object's Metadata property. The BsonDocument constructor can take a IDictionary of values and is quite happy to accept the raw Metadata dictionary which is exposed as IDictionary.

The ConvertBsonToUser method extends an instance of the BsonDocument class and returns a populated User object. Again, the document properties are accessed by name and are used to set the values of a User.Builder object. For the ID, the string value of the underlying ObjectId type is used.

The "metadata" property is retrieved as a BsonDocument, and then its Names property is used as the basis of a ToDictionary call which creates the keys and values of the Builder's Metadata object.

The act of returning the User.Builder object implicitly converts it into an immutable User object.

Interface for the User Store - IUserStore.cs


Here is the interface that I will be implementing for the specialised User Store.




The interface is defined as implementing the IStore interface, so certain methods will be expected to be implemented. However, these should be satisfied by the ancestor of our specialised Store - StoreBase.cs.

There is a Get method that accepts a username to search by, returning an immutable User object. Then there is a GetAll method that should return all the items in the database as an IEnumerable of User objects. I reckon the interface could probably do with a Get method that takes a user's ID value as a parameter, but feel free to do that yourselves (one of them will have to be renamed as they both accept a lone string parameter).

Then the Put method, taking an individual User object which will be entered into the database. I'm assuming that Put also serves as an Update operation in this implementation.

Finally there is a Delete operation that accepts a User object as its target to remove from the database.

Specialised Store for our User model - MongoDBUserStore.cs


Now we have the interface and those conversion tools available, we can proceed with the specialised Store for the User model.

I should point out that this Store flagrantly subverts the new, asynchronous capabilities of the MongoDB driver. This is because not all storage technologies will support async/await style calls and one of the main goals of this pattern is to allow any storage technology to be used behind the same interface. Having some implementations use async/await while others do not is therefore a really bad idea because it forms a leaky abstraction - you need to know from outside the component whether it can be called asynchronously. Further, if an async/await call to an IDisposable object is incorrectly executed, then resources could end up being disposed before an asynchronous call has completed. So, I believe that if you are dealing with async/await calls then you should define an explicit interface for them to make it clear to the developer calling the interface that they will be dealing with the side-effects themselves.

    public class MongoDBUserStore : MongoDBStore, IUserStore

The class signature shows that we are inheriting from the basic MongoDBStore class, and we will be exhibiting the behaviour specified in the IUserStore interface.

Constructor and properties


        

The constructor takes the information needed by the base Store - connStr and dbName, and passes them into the base class constructor. It also takes a collectionName argument which is stored in a private, read-only property. It is assumed that this specialised Store for the User object will only be dealing with a single MongoDB Collection.

Start


        

Knowing the name of the collection up front allows us to perform this operation during the Start method, after having called the base implementation to change the Store state. This step retrieves an IMongoCollection object with which we can perform queries and commands upon the named collection.

Get


        

Now we are starting to implement the IUserStore interface with the first method - Get. The first thing to do is to assert whether the Store is in the correct state to perform operations, i.e. whether Start has been called. The AssertIsStarted method is called which you may remember is implemented on the grandparent Store class.

The query itself is performed by calling the Find method of the IMongoCollection we initialised in the Start method. It is passed a BsonDocument that contains the information to evaluate appropriate documents in the collection. In this case, we set the "username" property in the document to the value that we are looking for. Only the first result is needed (or expected), and so the FirstAsync method is called on the result of the Find operation. Because we are wrapping asynchronous behaviour in this implementation, we will wait for the result of the FirstAsync method by accessing it's Result property. Finally, the Result property (which will be of type BsonDocument to match the generic type of the IMongoCollection) has the extension method ConvertBsonToUser called upon it, that will instantiate an immutable User object that contains the values from the document.

That's a lot of behaviour wrapped up in one line. Fluent interfaces like this which libraries like System.Linq allow are fantastic to use (when you know what you are doing), but unwinding the meaning of them by way of explanation can be a time-consuming business.

GetAll


        

The next part of the IUserStore interface to be implemented is GetAll. This simply returns the entire contents of the collection. However, to do that we need to make a query that will return all items, then turn that list of results into an IEnumerable of the correct User objects. More fluency in the calling methods for this gives us those results.

Special note should be taken that the query used is represented by an empty BsonDocument passed to the FindAsync method - no matching criteria means that it matches against all the items in the collection. The result of the Find operation must be transformed into a list and that is performed by the ToListAsync method. Again, we must wait for the result and this gives us a List which then gives us access to the Select method that we can use to call the ConvertBsonToUser method upon each item finally resulting in an IEnumerable that is returned back to the caller.

Put (single User object)


        

We use the MongoDB driver's Replace method to effectively perform an Upsert operation on the database, creating or replacing an existing entry which has the same ID.


Delete


        

And finally the Delete method, giving us all the CRUD operations (provided you don't mind if the Create and Update are represented by the Put method). Again, we use the BsonDocument method of passing the matching criteria into the driver.

Next time ...


In the next part of the series, I'll show you how to use the Store in practice.

No comments:

Post a Comment