A simple IRepository implementation for MongoDB and NoRM

David’s at the World Cup in South Africa, Cain’s been to Sonar in Barcelona and I’ve been coding! Mostly around OpenId and OAuth, Raven DB, MongoD


Neon Code by maistora Neon Code by maistora

David’s at the World Cup in South Africa, Cain’s been to Sonar in Barcelona and I’ve been coding! Mostly around OpenId and OAuth, Raven DB, MongoDB and NoRM. I reckon I’ve had just as much fun! I absolutely love NoRM, it makes working with Mongo (which is a brilliant document database) into a dream. Andrew Theken and the team are doing a fantastic job with it. And they are extremely responsive – a small change on my fork was integrated in just a few hours, really bringing home the agility of Open Source Software.

So I needed a simple IRepository<T> implementation to make my application unaware that it was working with Mongo – like getting data from down a tunnel where I don’t care what’s at the other end. And, of course, I want to make my life even easier. This is what I came up with.

Unlike with Raven there really isn’t a concept of Unit of Work (or transactions) with Mongo. Documents are updated atomically, though, which takes away half the situations where you would normally need transactions. This means that the Repository pattern is easier to implement – its sessions don’t need to represent a UoW that is tied to the lifetime of the web request. We don’t need to track instances or do change tracking in the session. And NoRM also provides connection pooling so it’s perfectly reasonable to grab a Mongo connection every time you need to do something.

That’s great for atomic methods like Save or Remove that don’t return anything, or that return a single entity (like FindOne). But it’s a problem for methods that return an IEnumerable<T> or IQueryable<T> because by the time the query actually instigates a connection to the database the session is long gone – and it’s not a good idea to “materialize” these into an array or list in order to return data from the method because then all the flexibility of LINQ is lost.

So there seems no alternative but to pass the session out to client code so that the user can control when it is released (after the query has been used). Passing it out as IDisposable is great, though, so that consumers of the repository don’t need to know that the implementation is using Mongo. And other Repository implementations can be slotted in instead.

I wanted to be able to use the repository in two ways. Firstly for atomic operations I wanted to be able to call the relevant method on my repository instance like this (note that an instance of the repository would normally be injected into the constructor rather than calling the IoC container directly):

var repository = ObjectFactory.GetInstance<IRepository<User>>(); User user = repository.FindOne(userId);

And, secondly, for methods that return an enumerable I need to be able to materialize the query before disposing of the session:

var repository = ObjectFactory.GetInstance<IRepository<Role>>(); using (repository.NewSession()) { return repository.AsQueryable().Select(role => role.Name).ToArray(); }

I decided to use NoRM’s HiLo Id generator for my entities so that URLs are more friendly (Mongo’s ObjectId and Guids are not human – although I understand how they can be useful sometimes). NoRM sees that I have a property called Id, and because it is Nullable<int>, NoRM uses the HiLo algorithm for me by default. Objects have a null Id until they are saved by NoRM:

namespace RedBadger.Data { public interface IEntity { int? Id { get; set; } } }

So at the top of the Repository hierarchy is IRepository which simply exposes a method for creating a new session (for use by clients in their using blocks):

namespace RedBadger.Data { using System; public interface IRepository { IDisposable NewSession(); } }

Then, from that, is derived an interface that adds familiar methods for interacting with the data:

namespace RedBadger.Data { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; [ContractClass(typeof(IRepositoryContracts<>))] public interface IRepository<TEntity> : IRepository where TEntity : class, IEntity { IQueryable<TEntity> AsQueryable(); void Drop(); IEnumerable<TEntity> Find(Func<TEntity, bool> selector); TEntity FindOne(int? id); TEntity FindOne(Func<TEntity, bool> selector); void Insert(TEntity instance); void Remove(int? id); void Remove(Func<TEntity, bool> selector); void Save(TEntity instance); } }

And the contracts for this interface are in a supporting class:

namespace RedBadger.Data { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; [ContractClassFor(typeof(IRepository<>))] public abstract class IRepositoryContracts<T> : IRepository<T> where T : class, IEntity { public IDisposable NewSession() { Contract.Ensures(Contract.Result<IDisposable>() != null); return default(IDisposable); } public IQueryable<T> AsQueryable() { return default(IQueryable<T>); } public void Drop() { } public IEnumerable<T> Find(Func<T, bool> selector) { Contract.Requires(selector != null); return default(IEnumerable<T>); } public T FindOne(int? id) { Contract.Requires(id.HasValue); return default(T); } public T FindOne(Func<T, bool> selector) { Contract.Requires(selector != null); return default(T); } public void Insert(T instance) { Contract.Requires(instance != null); Contract.Requires(instance.Id == null); } public void Remove(int? id) { Contract.Requires(id.HasValue); } public void Remove(Func<T, bool> selector) { Contract.Requires(selector != null); } public void Save(T instance) { Contract.Requires(instance != null); } } }

Now to the implementation. Note that all the atomic methods (like Save) create their own session and dispose of it as soon as it’s finished with. This allows the connection to return to the pool for re-use. However, the methods that return an IEnumerable (like Find) simply ensure that the user has set up an external session and then use that. Unfortunately, it’s not really possible to make sure that the user creates and disposes of these sessions in a responsible way.

namespace RedBadger.Data.Mongo { using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Linq; using Norm; public class MongoRepository<TEntity> : IRepository<TEntity> where TEntity : class, IEntity { private readonly MongoConnectionStringBuilder mongoConnectionStringBuilder; private Mongo externalSession; public MongoRepository(MongoConnectionStringBuilder mongoConnectionStringBuilder) { this.mongoConnectionStringBuilder = mongoConnectionStringBuilder; } public IDisposable NewSession() { return this.externalSession = this.CreateSession(); } public IQueryable<TEntity> AsQueryable() { this.EnsureSession(); return this.externalSession.GetCollection<TEntity>().AsQueryable(); } public void Drop() { try { using (Mongo session = this.CreateSession()) { session.Database.DropCollection(typeof(TEntity).Name); } } catch (MongoException e) { if (e.Message != "ns not found") { throw; } } } public IEnumerable<TEntity> Find(Func<TEntity, bool> selector) { this.EnsureSession(); return Find(this.externalSession, selector); } public TEntity FindOne(int? id) { return this.FindOne(x => x.Id == id); } public TEntity FindOne(Func<TEntity, bool> selector) { using (Mongo session = this.CreateSession()) { return Find(session, selector).FirstOrDefault(); } } public void Insert(TEntity instance) { using (Mongo session = this.CreateSession()) { session.GetCollection<TEntity>().Insert(instance); } } public void Remove(int? id) { this.Remove(i => i.Id == id); } public void Remove(Func<TEntity, bool> selector) { using (Mongo session = this.CreateSession()) { session.GetCollection<TEntity>().Delete(this.Find(selector)); } } public void Save(TEntity instance) { using (Mongo session = this.CreateSession()) { session.GetCollection<TEntity>().Save(instance); } } private static IEnumerable<TEntity> Find(Mongo session, Func<TEntity, bool> selector) { Contract.Requires(selector != null); IQueryable<TEntity> queryable = session.GetCollection<TEntity>().AsQueryable(); Contract.Assume(queryable != null); return queryable.Where(selector); } private Mongo CreateSession() { return Mongo.Create(this.mongoConnectionStringBuilder.ToString()); } private void EnsureSession() { if (this.externalSession == null) { throw new InvalidOperationException( "This Repository has no current session. Create one like this: using(repository.NewSession()) { ... }"); } } } }

I love the way that NoRM uses a URL for the connection string which means that I can use a class derived from UrlBuilder to set defaults for me:

namespace RedBadger.Data.Mongo { using System; public class MongoConnectionStringBuilder : UriBuilder { public MongoConnectionStringBuilder() : base("mongodb", "localhost", 27017) { } public string Database { get { return this.Path; } set { this.Path = value; } } } }

Finally, wiring it all up using StructureMap is really easy:

ObjectFactory.Configure( x => { x.For<MongoConnectionStringBuilder>().Singleton().Use( () => new MongoConnectionStringBuilder { Database = "MyTestDB" }); x.For(typeof(IRepository<>)).Use(typeof(MongoRepository<>)); });

Incidentally, with StructureMap, you can also add more specialised repositories to the container. At point of use you don't need to know whether you’re getting a specialised repository or a more generic one, which is really cool:

ObjectFactory.Configure( x => { x.For<MongoConnectionStringBuilder>().Singleton().Use( () => new MongoConnectionStringBuilder { Database = "MyTestDB" }); x.For(typeof(IRepository<>)).Use(typeof(MongoRepository<>)); x.For<IRepository<User>>().Use<UserRepository>(); });

So that’s all straight forward and totally abstracts away the database driver implementation (the specialised UserRepository could easily be an implementation over another data store). Anyway, enjoy.

Similar posts

Are you looking to build a digital capability?