Code Voyeur
RSS
Data Access Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net Rate My Snippet

A NoRM-MongoDB Repository Base Class

Abstract When working with data access frameworks that map .NET types to an underlying persistence stores, generics can be used to provide very simple but powerful infrastructure. NHibernate, for example, may be used to quickly and easily create a base class that provides full CRUD support for any mapped type. By simply inheriting from this base class, a DAO or repository is able to query and modify its data source without the need for any additional code. This article will introduce a similar pattern for working with the NoRM driver for MongoDB. Article NoRM makes persisting POCOs to a MongoDB collection simple. Object graphs are automatically serialized to a BSON document. Using generics and expressions, it is relatively simple to create a base class that handles virtually all data access required by a typical repository (or DAO). Two additional classes provide support for setting connection strings and guaranteeing that each POCO has an ObjectID (the primary key for all MongoDB documents).

.NET's MongoDB drivers have different ways to create connection strings. MongoDB settings is a simple supporting class that provides a common, dependency injection-friendly structure for setting the various attributes of a MongoDB connection string. A more complete version could make assumptions about a default server (localhost) and port (27017).


public class MongoDBSettings {

    public string Server { get; set; }

    public int Port { get; set; }

    public string Username { get; set; }

    public string Password { get; set; }

    public string Database { get; set; }

    public string Query { get; set; }

    public string ConnectionString {

        get {

            string authentication = string.Empty;
            if (Username != null) {
                authentication = string.Concat(Username, ':', Password, '@');
            }
            if (!string.IsNullOrEmpty(Query) && !Query.StartsWith("?")) {
                Query = string.Concat('?', Query);
            }

            return string.Format("mongodb://{0}{1}:{2}/{3}{4}", authentication, Server, Port, Database, Query);

        }

    }
}
The NoRMModelBase class includes an ObjectId property for its subclasses. Since documents stored in MongoDB have a primary key of type ObjectId (and NoRM enforces this rule), this base class is only needed for convenience.
public abstract class NoRMModelBase {
    public ObjectId Id { get; set; }
}
The NoRMRepositoryBase class is responsible for saving and retrieving objects from MongoDB.
public abstract class NoRMRepositoryBase<T> where T : NoRMModelBase {
    ...
}
It is required that all subclasses of NoRMRepositoryBase are typed against a NoRMModelBase subclass. While this restriction is not entirely necessary, it does enforce that all repositories will work with objects that have valid ObjectId (and necessary) properties.

The only piece of NoRMRepositoryBase that is abstract is the _Collection property. Its purpose is to have each repository to explicitly choose the name of its associated document collection. Without providing an explicit collection name, the singular name of the object type is used. Note there is a bug in the current release of NoRM that ignores explicit collection names when used with LINQ queries. The version of NoRM included with this project is a hack using Inflector.NET to force all collections to use proper pluralization.

protected abstract string _Table { get; }
The Create, Save and two Update methods of the repository are fairly straightforward. Each takes an instance of T and calls the corresponding NoRM method. Upserts are not implemented in the NoRMRepositoryBase, though NoRM supports them.
public void Create(T instance) {

    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        mongo.GetCollection<T>(_Collection).Insert(instance);
    }
}

public void Save(T instance) {
    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        mongo.GetCollection<T>(_Collection).Save(instance);
    }
}

public void Update(object spec, object newValues) {
    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        mongo.GetCollection<T>(_Collection).UpdateOne(spec, newValues);
    }
}

public void UpdateAll(object spec, object newValues) {
    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        mongo.GetCollection<T>(_Collection).Update(spec, newValues, true, false);
    }
}
Two FindById methods allow documents to be queried by either the ObjectId string or an actual instance of an ObjectId. The primary FindById method demonstrates how NoRM supports query by anonymous object. Properties of the object are mapped to a BSON document for the query. Some other drivers use Dictionary structures for queries.

public T FindById(ObjectId id) {

    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        return mongo.GetCollection<T>(_Collection).FindOne(new { _id = id });
    }
}

public T FindById(string id) {
    return FindById(new ObjectId(id));
}
Two FindOne methods allow documents to be retrieved either by an expression or an object spec (as described above for FindById).
public T FindOne(object spec) {

    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        return mongo.GetCollection<T>(_Collection).FindOne(spec);
    }
}

public T FindOne(Func<T, bool> func) {

    using (MongoQueryProvider provider = new MongoQueryProvider(Mongo.Create(Settings.ConnectionString))) {
        MongoQuery<T> query = new MongoQuery<T>(provider, _Collection);
        return query.Where(func).FirstOrDefault();
    }
}
Two Find methods are similar to the two FindOne methods, but return an enumerable instance of T instead of a single instance of T.
public IEnumerable<T> Find(object spec) {

    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        return mongo.GetCollection<T>(_Collection).Find(spec);
    }
}

public IEnumerable<T> Find(Func<T, bool> func) {

    using (MongoQueryProvider provider = new MongoQueryProvider(Mongo.Create(Settings.ConnectionString))) {
        MongoQuery<T> query = new MongoQuery<T>(provider, _Collection);
        return query.Where(func);
    }
}
The FindAll method simply returns all an IEnumerator for all documents in a collection.
public IEnumerable<T> FindAll() {
    using (MongoQueryProvider provider = new MongoQueryProvider(Mongo.Create(Settings.ConnectionString))) {
        MongoQuery<T> query = new MongoQuery<T>(provider, _Collection);
        return query;
    }
}
Four remove methods provide different ways for removing documents from a collection.
public void Remove(object spec) {
    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        mongo.GetCollection<T>(_Collection).Delete(spec);
    }
}

public void Remove(T instance) {
    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        mongo.GetCollection<T>(_Collection).Delete(instance);
    }
}

public void Remove(string id) {
    Remove(new ObjectId(id));
}

public void Remove(ObjectId id) {
    using (Mongo mongo = Mongo.Create(Settings.ConnectionString)) {
        mongo.GetCollection<T>(_Collection).Delete(new { _id = id });
    }
}
That is the extent of the repository infrastructure. As an example of the repository base in action, consider the standard blog/posts relationship (intentionally simplified). Each class need only extend NoRMModelBase, which again is not entirely necessary but is however a convenience.
public class Blog : NoRMModelBase {

    public string Name { get; set; }

    public string Author { get; set; }

}

public class Post : NoRMModelBase {

    public string Title { get; set; }

    public string Content { get; set; }

    public ObjectId BlogId { get; set; }

}
Given these two domain objects, the associated repositories look as follows:
public class BlogRepository : NoRMRepositoryBase<Blog> {

    protected override string _Collection {
        get { return "Blogs"; }
    }
}

public class PostRepository : NoRMRepositoryBase<Post> {
       
    protected override string _Collection {
        get { return "Posts"; }
    }
}
The generic approach of the NoRMRepositoryBase allows for each repository to inherit complex data access behavior with no code required beyond that which is shown above. The sample project for this article uses the NoRM repository base with standard ASP.NET MVC CRUD templates to create very simple admin pages. A partial BlogsController is shown below:
public class BlogsController : AdminBaseController
{

    private BlogRepository _repository = null;

    public BlogsController() {
        _repository = new BlogRepository() { Settings = Settings };
    }

     public ActionResult Index()
    {
        return View(_repository.FindAll());
    }
    
    ...
    
    [HttpPost]
    public ActionResult Create([Bind]Blog blog)
    {
        try
        {
            // TODO: Add insert logic here
            _repository.Create(blog);
            return RedirectToAction("Index");
        }
        catch
        {
            return View();
        }
    }
    
    ...
        
    public ActionResult Edit(string id)
    {
        return View(_repository.FindById(id));
    }

    }
}

Download Sample Project

References

A generic NHibernate repository base class
Article Posted: Friday, May 28, 2010

Comments

Posted by Jailen on 7/4/2011 6:50:12 AM
Thanks for sharing. What a pealsure to read!
Posted by Alamat on 9/9/2012 8:19:35 PM
This piece was cognet, well-written, and pithy.

Leave a Comment

Shout it

Contact Code Voyeur about this article.