The first part can be found here.
Introduction
In this post, I'm going to talk about the IStore interface and how it can be implemented to be a lightweight, efficient data store that uses immutable models.
The code for this post is not contained in the standard Inversion framework repository. Instead, I have created a new repository that contains the Inversion.Data namespace here: https://github.com/fractos/inversion-data
Stores
A Store provides data access for a specific type of storage medium. This could be a database, a filesystem, or anything that you can perform basic CRUD operations upon.
It is generally tied to a particular type of Model, or a limited set of Models that could be described as being within a Bounded Context. This is in order to avoid duplication of access to the Model objects in storage. If a set of Models exist within a Bounded Context then they are not accessed by the rest of a system and so therefore access duplication should not occur. In other situations it is probably best to define a Store that handles as few Model types as possible so that the granularity of configuration could be on a per-Model basis.
In the example from part one of this series, a Store that is implemented for User objects would also cover UserMetadata objects since the UserMetadata class could be a private class within the User class's domain.
Tying a Store to a Model type has connotations. It is advised that the most suitable way of modelling with this pattern is to use simple objects that do not maintain a web of active references to other types of object and which do not have the expectation of these references to be kept up to date automatically by the actions of a DAL. This won't suit everyone's architectural styles.
It is generally tied to a particular type of Model, or a limited set of Models that could be described as being within a Bounded Context. This is in order to avoid duplication of access to the Model objects in storage. If a set of Models exist within a Bounded Context then they are not accessed by the rest of a system and so therefore access duplication should not occur. In other situations it is probably best to define a Store that handles as few Model types as possible so that the granularity of configuration could be on a per-Model basis.
In the example from part one of this series, a Store that is implemented for User objects would also cover UserMetadata objects since the UserMetadata class could be a private class within the User class's domain.
Tying a Store to a Model type has connotations. It is advised that the most suitable way of modelling with this pattern is to use simple objects that do not maintain a web of active references to other types of object and which do not have the expectation of these references to be kept up to date automatically by the actions of a DAL. This won't suit everyone's architectural styles.
The Model should only represent the data. The Store should only deal with the translation of that data in and out of a storage medium.
A Store is NOT a Repository
The IStore Interface
Stores are a resource
For this reason a Store implements the IDisposable interface. A Store may not have anything to dispose at the end of its use, but this enforces that there is the chance to perform actions at that point.
Stores have state
For this reason, a Store implements methods that adjust its state and any data access methods should check the current state when they are called, raising an exception if the Store is in an unsuitable state.
The Store state is represented by a simple enumeration - StoreState:
A Store which has never been started will have a state equal to StoreState.Unstarted. When started explicitly, a Store will have a state equal to StoreState.Started. When stopped explicitly, a Store will have a state equal to StoreState.Stopped.
The IStore interface defines two methods for explicitly changing the state of the Store - Start and Stop:
void Start();
void Stop();
The IStore interface defines a public get property HasStarted, which should report whether the Store has had Start called upon it and has not yet had Stop called upon it:
bool HasStarted { get; }
The IStore interface also defines another public get property HasStopped, which should report whether the Store has had Stop called upon it:
bool HasStopped { get; }
The StoreBase abstract base class
This abstract class provides a base upon which to build other implementations of a Store, as we will see below.
The private property _state keeps track of whether the Store has ever been started, or is currently started or stopped. The initial value is StoreState.Unstarted. Only methods in the base class can adjust the _state property since it is private. Overriding methods for Start (and Stop) must call their base implementations as part of their instructions.
The public property HasStarted can report whether the Store is in a state where it can perform operations, i.e. whether the _state property is equal to StoreState.Started.
The public property HasStopped can report whether the Store is in a stopped state, i.e. whether the _state property is equal to StoreState.Stopped.
The public method Start is used to explicitly start any resources that the Store implementation needs. If the Store is already started, then an exception is raised.
The public method Stop is used to explicitly stop any resources that the Store implementation needs. This should be called by a descendant implementation of Dispose, and since both Stop or Dispose could be called explicitly (by a call from user code) or implicitly (when leaving a using statement block), then the Stop method does not try to raise an exception if the Store is in an incompatible state. Instead, it changes the state to Stopped if the Store is currently started. Generally, it is useful to keep a flag that records whether Dispose has been called already, and if the flag is set then no action is taken. This protects against the situation of a call to Stop being followed by an implicit call to Dispose. However, this may not be necessary if a Store has nothing to do during its disposal phase.
It is the descendant implementation's responsibility to have the correct behaviour within its overridden instructions for Stop and Dispose, and this is especially important for database connection types which must have their connection objects disposed (e.g. SqlClient). For this reason, Dispose is marked as abstract so that the descendant is forced to provide some form of implementation.
I have also added a simple method for asserting whether the Store has been started called AssertIsStarted. This will be put to a lot of use in our specialised Store.
StoreStartedException is a simple wrapper for ApplicationException. It is used to emit an exception from the Store when trying to start it if it is already in a started state:
Similarly there is a StoreStoppedException which should be emitted from a data access method when the Store is found to be in a stopped state.
Also, StoreProcessException - an instance or descendant of which should be emitted from a Store method if something goes wrong during processing a call, although that's not a hard and fast rule, just a useful convention.
I've omitted the source code for these as they are exactly the same as StoreStartedException apart from the name, but you can find it in the GitHub repository for Inversion.Data. The differentiation between them is just an exercise in taxonomy.
Now we are ready to see how a Store can be implemented for a particular storage technology, and then show how it can be used.
Implementing a Store for MongoDB
You probably already know this, but MongoDB by 10Gen is a cross-platform, "NoSQL" style document database which is used by many projects and applications across the globe. I use it extensively for most of my side-projects as a primary storage mechanism. My experience with it has led me to appreciate a horses-for-courses approach to databases - MongoDB can be a very quick leg-up to a working system and in production it is very stable and well-behaved, clustering notwithstanding. If you're looking to get into document databases then it's a really good starting place.
It's pretty straightforward to set up on Windows or Linux. Check out its home site https://www.mongodb.org/ and also the very good admin UI Windows application MongoVue at http://www.mongovue.com/
A word about namespaces
This convention is in place because we (Guy Murphy and I) have found that it aids a composition-by-augmentation approach; if you include an assembly with a particular technology stack, then it will appear within a well known namespace. No special knowledge of namespaces that are tied to a technology is required and, frankly, it just feels neater.
The base MongoDB implementation - MongoDBStore.cs
To use MongoDB, I start by adding the NuGet package "MongoDB.Driver". That package loads a couple of dependencies - MongoDB.Bson and MongoDB.Core.
The code for the MongoDBStore class is as follows:
This gives us some very basic functionality that can be used to build our specialised Store for a particular Model type.
The constructor for MongoDBStore requires the passing of both a connection string argument and a database name. I've made the base store centric around the named database in MongoDB because this allows basic functionality with collections that would be common across Stores that might want to access more than one collection within a database. Frankly, it's as good a place as any to put this level of organisation. If you feel that you want to change that around then go for it. The connection string and database name are stored in read-only properties which are private as they are unlikely to be of use to anything outside of the base implementation. Connection strings and database names are things that shouldn't leak outside of the Store's abstraction as they are a configuration concern.
Start has been overridden, taking the opportunity to create a MongoClient instance from the connection string and then use that client to gain an IMongoDatabase reference to the named database. The base Start method is called in order to correctly set the state of the Store. The result is that we now have an active MongoClient instance and a stored reference to the required database that is ready to go.
Stop has not been overridden as there is no action to take when stopping.
Similarly, Dispose has no actual work to do so it simply calls Stop on the base Store.
This basic Store has its index property overridden which allows access by name to a MongoDB collection within a database. The get-accessor performs a quick state check of the Store to determine if it has been started (and thus, whether it is safe to use the Database property), and then returns an IMongoCollection generic of type BsonDocument (objects that represent a piece of BSON data, which is a binary-serialised version of JSON peculiar to MongoDB).
Some OO-purists might want to mark this base Store as abstract, such that it cannot itself be instantiated and must be inherited from before it can perform any work. However, this is a fully working Store providing a basic mechanism for retrieving documents from a named collection, therefore I have not marked it as abstract as it could be put to immediate use as-is.
Next time ...
Update:
Had to change name of Store to StoreBase to avoid a namespace conflict. Gists updated to reflect this.
No comments:
Post a Comment