Skip to content

Instantly share code, notes, and snippets.

@amcdnl
Last active August 29, 2015 14:01
Show Gist options
  • Save amcdnl/0544ceba13c52b60ce1a to your computer and use it in GitHub Desktop.
Save amcdnl/0544ceba13c52b60ce1a to your computer and use it in GitHub Desktop.

Mongo Facade

  • Custom ogging
  • Pre/Post triggers for custom validations
  • Nice/easy syntax

Usage

Models

Descend your models from FacadeModel<T> like:

namespace API.Models.Record
{

    [CollectionName("Records")]
    public class Record : FacadeModel<Record>
    {
        public Dictionary<string, dynamic> Values { get; set; }

        public DateTime Created { get; set; }
        
        public DateTime Updated { get; set; }
    }
}

API

namespace API.Controllers
{
    [Authorize]
    [Route("api/record/{appId}/{id?}")]
    public class RecordController : ApiController
    {
        public Record Get(string id)
        {
            return Record.SingleOrDefault(r => r.Id == id);
        }

        public Record Post([FromBody]Record model)
        { 
            // generate an id, prob might remove later
            model.Id = ObjectId.GenerateNewId().ToString();
            model.Save();
            return model;
        }

        public Record Put([FromBody]Record model)
        {
            model.Save();
            model.Updated = DateTime.Now;
            return model;
        }

        public void Delete(string id)
        {
            Record.Remove(Query.And(new[] { Query.EQ("_id", id) }));
        }
    }
}
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Bson.Serialization.IdGenerators;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
using MongoDB.Driver.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using System.Web;
namespace API.Models
{
public enum TriggerTimes {
Pre,
Post
}
public enum TriggerStages {
Save,
Remove
}
public interface IFacadeModel
{
string Id { get; set; }
MongoDB.Driver.WriteConcernResult Remove();
MongoDB.Driver.WriteConcernResult Save();
}
public abstract class FacadeModel<T> : API.Models.IFacadeModel where T : IFacadeModel
{
#region Static Methods
// Class (static) methods. Provided as a convenience for interacting with the Mongo collection using a clean syntax.
public static MongoCollection<T> Collection
{
get
{
return Facade.Collection<T>();
}
}
public static IQueryable<T> Queryable
{
get
{
return Facade.AsQueryable<T>();
}
}
public static MongoCursor<T> FindAll()
{
return Collection.FindAll();
}
public static MongoCursor<T> Find(IMongoQuery query) {
return Collection.Find(query);
}
public static T FindOneById(BsonValue id)
{
return Collection.FindOneById(id);
}
public static T Single()
{
return Queryable.Single<T>();
}
public static T Single(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
return Queryable.Single<T>(predicate);
}
public static T SingleOrDefault(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
return Queryable.SingleOrDefault<T>(predicate);
}
public static T FirstOrDefault()
{
return Queryable.FirstOrDefault<T>();
}
public static T FirstOrDefault(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
return Queryable.FirstOrDefault<T>(predicate);
}
public static IEnumerable<T> Where(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
return Queryable.Where<T>(predicate);
}
public static long Count()
{
return Collection.Count();
}
public static long Count(IMongoQuery query)
{
return Collection.Count(query);
}
public static WriteConcernResult Remove(IMongoQuery query)
{
return Collection.Remove(query);
}
public static WriteConcernResult Remove(string id)
{
return Collection.Remove(Query.EQ("_id", id));
}
public static WriteConcernResult Update(IMongoQuery query, IMongoUpdate update)
{
return Collection.Update(query, update);
}
public static FindAndModifyResult FindAndModify(IMongoQuery query, IMongoSortBy sortBy, IMongoUpdate update, bool returnNew)
{
return Collection.FindAndModify(query, sortBy, update, returnNew);
}
public static IEnumerable<BsonDocument> Group(IMongoQuery conditions, BsonJavaScript keyf, BsonDocument initial, BsonJavaScript reduce, BsonJavaScript finalize)
{
return Collection.Group(conditions, keyf, initial, reduce, finalize);
}
private static IEnumerable<MethodInfo> InstanceMethods
{
get
{
return typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
}
}
private static IDictionary<TAttribute, MethodInfo> FindMethodAttributes<TAttribute> () where TAttribute : class
{
var found = new Dictionary<TAttribute, MethodInfo>();
foreach (var method in InstanceMethods)
{
foreach (var attribute in method.GetCustomAttributes(false))
{
TAttribute a = attribute as TAttribute;
if (a == null) continue;
found.Add(a, method);
}
}
return found;
}
#endregion
#region Instance Methods
// Instance Methods
private void ExecuteTriggers (TriggerTimes time, TriggerStages stage) {
var attributes = FindMethodAttributes<Trigger>();
foreach (var trigger in attributes.Keys)
{
// If the time and stage don't match, skip it.
if (trigger.Time != time) continue;
if (trigger.Stage != stage) continue;
// Found a matching trigger. Execute it.
var method = attributes[trigger];
method.Invoke(this, null);
}
}
public string Validate ()
{
var messages = new StringBuilder();
var attributes = FindMethodAttributes<Trigger>();
foreach (var trigger in attributes.Keys)
{
var method = attributes[trigger];
var message = (string)method.Invoke(this, null);
// If validation passed, we're done.
if (message == null) continue;
// Otherwise, add the validation message to the cumulative messages.
messages.AppendLine(message);
}
if (messages.Length == 0) return null;
else return messages.ToString();
}
public WriteConcernResult Save () {
var messages = this.Validate();
if (messages != null) {
throw new Exception(messages); // TODO this is not really exceptional ... or maybe it is because validation was not checked manually prior to saving?
}
this.ExecuteTriggers(TriggerTimes.Pre, TriggerStages.Save);
var concern = Collection.Save(this);
this.ExecuteTriggers(TriggerTimes.Post, TriggerStages.Save);
return concern;
}
public WriteConcernResult Remove()
{
this.ExecuteTriggers(TriggerTimes.Pre, TriggerStages.Remove);
return Collection.Remove(Query.EQ("_id", this.Id));
this.ExecuteTriggers(TriggerTimes.Post, TriggerStages.Remove);
}
#endregion
[BsonId(IdGenerator = typeof(StringObjectIdGenerator))]
public string Id { get; set; }
}
public abstract class Facade
{
private readonly static ConnectionStringSettings ConnectionString = ConfigurationManager.ConnectionStrings["MongoServerSettings"];
private readonly static string[] connectionStringParts = ConnectionString.ConnectionString.Split('/');
public readonly static string DatabaseName = connectionStringParts[connectionStringParts.Length - 1];
public readonly static MongoClient Client = new MongoClient(ConnectionString.ConnectionString);
public readonly static MongoServer Server = Client.GetServer();
public readonly static MongoDatabase Database = Server.GetDatabase(DatabaseName);
private static IEnumerable<Attribute> Attributes<T> ()
{
return System.Attribute.GetCustomAttributes(typeof(T));
}
public static string CollectionName<T>()
{
foreach (Attribute attribute in Attributes<T>())
{
CollectionName nameAttribute = attribute as CollectionName;
if (nameAttribute == null) continue;
else return nameAttribute.Name;
}
throw new ConfigurationException(String.Format("Generic type \"{0}\" must be annotated with [CollectionName(string)].", typeof(T).Name));
}
public static MongoCollection<T> Collection<T>()
{
string name = Facade.CollectionName<T>();
return Facade.Database.GetCollection<T>(name);
}
public static IQueryable<T> AsQueryable<T>()
{
return Facade.Collection<T>().AsQueryable<T>();
}
}
/// <summary>
/// Attribute for associating a class with a MongoDB collection.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class CollectionName : System.Attribute
{
public readonly string Name;
public CollectionName (string name)
{
this.Name = name;
}
}
public abstract class Trigger : Attribute
{
public readonly TriggerStages Stage;
public readonly TriggerTimes Time;
public Trigger(TriggerStages stage)
{
this.Stage = stage;
}
}
[AttributeUsage(AttributeTargets.Method)]
public class Validate : Attribute
{
}
/// <summary>
/// Attribute for marking methods as pre hooks.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class Pre : Trigger
{
public readonly TriggerTimes Trigger = TriggerTimes.Pre;
public Pre (TriggerStages stage) : base(stage) { }
}
/// <summary>
/// Attribute for marking methods as post hooks.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
public class Post : Trigger
{
public readonly TriggerTimes Trigger = TriggerTimes.Post;
public Post (TriggerStages stage) : base(stage) { }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment