Skip to content

Instantly share code, notes, and snippets.

@IanMercer
Created July 22, 2017 04:20
Show Gist options
  • Save IanMercer/afbf69a6dfc01847970de588d65d28de to your computer and use it in GitHub Desktop.
Save IanMercer/afbf69a6dfc01847970de588d65d28de to your computer and use it in GitHub Desktop.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Dynamic;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using ImpromptuInterface;
using log4net;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace MongoData
{
/// <summary>
/// All MongoDynamic objects support this interface because every object needs an _id in MongoDB
/// It also gives us a way to always get back to the dynamic object itself for saving to database
/// </summary>
public interface IId
{
ObjectId _id { get; set; }
MongoDynamic Entity { get; }
}
/// <summary>
/// MongoDynamic is like an ExpandoObject that also understands document Ids and uses Improptu interface
/// to act like any other collection of interfaces ...
/// It can be serialized and deserialized from BSon and thus stored in a MongoDB database.
/// </summary>
/// <remarks>
/// This simple class gives you the ability to define database objects using only .NET interfaces - no classes!
/// Those objects can be dynamically extended to support any interface you want to add to them - polymorphism!
/// When loaded back from the database the object will support all of the interfaces that were ever applied to it.
/// Adding a new field is easy. Removing one works too.
/// All fields must be nullable since they may not be present on earlier instances of an object type.
/// </remarks>
public class MongoDynamic : DynamicObject, IId
{
private static readonly ILog log= LogManager.GetLogger("MongoDynamic");
[BsonId(Order = 1)]
public ObjectId _id { get; set; }
public const string IdField = "_id";
// Dumb name for a property - which is precisely why I chose it! - It's very unlikely it will ever conflict with a real property name
public const string InterfacesField = "int";
// Couple of other common fields I use with my dynamic entities (can't define constants in interfaces)
public const string UriField = "Uri"; // IEntity
public const string NameField = "Name"; // INamedEntity
public const string NameFieldLower = "name"; // Lower-cased version of name field, use this for searches
public const string LocField = "Loc"; // ILocation
public const string TagsField = "Tags"; // ITaggable
public const string TagsKeyField = "Tags.Key"; // ITaggable
public const string TagsValueField = "Tags.Value"; // ITaggable
public const string TagsTypeField = "Tags._t"; // ITaggable
public const string LiteralField = "lit";
/// <summary>
/// Interfaces that have been added to this object
/// </summary>
/// <remarks>
/// We always begin by supporting the _id interface
/// </remarks>
[BsonElement(InterfacesField, Order = 2)]
internal HashSet<string> @int = new HashSet<string>() { typeof(IId).FullName };
[BsonIgnore]
public MongoDynamic Entity { get; private set; }
public MongoDynamic()
{
this._id = ObjectId.GenerateNewId();
this.Entity = this;
}
/// <summary>
/// Clone constructor
/// </summary>
public MongoDynamic(MongoDynamic other)
{
this._id = ObjectId.GenerateNewId();
this.children = other.children.ToDictionary(v => v.Key, v => v.Value); // clone it
this.@int = new HashSet<string>(other.@int);
this.Entity = this;
}
/// <summary>
/// Clone constructor from an object with a given interface so we act like that interface
/// </summary>
public MongoDynamic(Type @interface, object other)
{
Debug.WriteLine(" Creating a clone object for interface " + @interface.Name);
this._id = ObjectId.Empty;
this.children = new Dictionary<string, object>();
var allInterfacesAndDescendants = new[] { @interface }.Concat(@interface.GetInterfaces());
foreach (var mem in allInterfacesAndDescendants)
{
foreach (var prop in mem.GetProperties())
{
if (this.children.ContainsKey(prop.Name)) continue; // already have it
else if (prop.Name == "_id") this._id = (ObjectId)prop.GetValue(other, null);
else if (prop.Name == "Entity") continue; // Self reference isn't copied
else
{
log.Debug(" " + mem.Name + " supports " + prop.Name);
this.children.Add(prop.Name, prop.GetValue(other, null));
}
}
}
// Implements all of the mentioned interfaces and descendants
this.@int = new HashSet<string>(allInterfacesAndDescendants.Select(x => x.FullName));
this.Entity = this;
}
/// <summary>
/// A text version of all interfaces - mostly for debugging purposes, stored in alphabetical order
/// </summary>
[BsonIgnore]
public string InterfacesAsText
{
get { return string.Join(",", this.@int.OrderBy(i => i)); }
}
/// <summary>
/// Add support for an interface to this document if it doesn't already have it
/// </summary>
public T AddLike<T>()
where T : class
{
@int.Add(typeof(T).FullName);
// And also act like any interfaces that interface implements (which will include ones they represent too)
foreach (var @interface in typeof(T).GetInterfaces())
@int.Add(@interface.FullName);
return Impromptu.ActLike<T>(this, this.GetAllInterfaces());
}
/// <summary>
/// Add support for multiple interfaces
/// </summary>
public T AddLike<T>(Type[] otherInterfaces)
where T : class
{
var allInterfaces = otherInterfaces.Concat(new[] { typeof(T) });
var allInterfacesAndDescendants = allInterfaces.Concat(allInterfaces.SelectMany(x => x.GetInterfaces()));
foreach (var @interface in allInterfacesAndDescendants)
@int.Add(@interface.FullName);
return this.ActLike<T>(this.GetAllInterfaces());
}
/// <summary>
/// Cast this object to an interface only if it has previously been created as one of that kind
/// </summary>
public T AsLike<T>()
where T : class
{
if (!this.@int.Contains(typeof(T).FullName)) return null;
else return this.ActLike<T>(this.GetAllInterfaces());
}
// A rather large cache of all interface types loaded into the App Domain
private static List<Type> cacheOfTypes = null;
// A cache of the interface types corresponding to a given 'key' of interface names
private static readonly ConcurrentDictionary<string, Type[]> cacheOfInterfaces = new ConcurrentDictionary<string, Type[]>();
public Type[] GetAllInterfaces()
{
// We always behave like an object with an Id plus any other interfaces we have
var key = string.Join(",", this.@int.OrderBy(i => i));
if (!cacheOfInterfaces.ContainsKey(key))
{
// Normalize Services. and MongoData. because we have a mixture in the datbase
// TODO: Fix mixture
EnsureCacheOfAllTypes();
var interfaces = cacheOfTypes.Where(t => this.@int.Any(i => i.Replace("Services.", "MongoData.") == t.FullName.Replace("Services.", "MongoData.")));
log.Debug("Key: " + key);
if (interfaces.Count() != this.@int.Count)
{
foreach (var interf in this.@int)
{
if (cacheOfTypes.All(ct => ct.FullName.Replace("Services.", "MongoData.") != interf.Replace("Services.", "MongoData.")))
{
log.Error(" Key: " + key + " --> " + interf + " ** MISSING **");
}
}
}
// Could trim the interfaces to remove any that are inherited from others ...
cacheOfInterfaces.TryAdd(key, interfaces.ToArray());
}
return cacheOfInterfaces[key];
}
/// <summary>
/// Load every interface type in every assembly into one giant cache
/// </summary>
private static void EnsureCacheOfAllTypes()
{
if (cacheOfTypes == null)
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
List<Type> listOfTypes = new List<Type>();
foreach (var assembly in assemblies)
{
if (assembly.IsDynamic) continue;
// Only interested in User define interfaces really ...
if (assembly.FullName.StartsWith("Autofac")) continue; // skip it
if (assembly.FullName.StartsWith("Microsoft.")) continue; // skip it
if (assembly.FullName.StartsWith("System.")) continue; // skip it
if (assembly.FullName.StartsWith("App_Web_")) continue; // skip it
if (assembly.FullName.StartsWith("MongoDB.")) continue; // skip it
if (assembly.FullName.StartsWith("Newtonsoft.")) continue; // skip it
if (assembly.FullName.StartsWith("SMDiagnostics.")) continue; // skip it
if (assembly.FullName.StartsWith("PresentationCore")) continue; // skip it
if (assembly.FullName.StartsWith("PresentationFramework")) continue; // skip it
if (assembly.FullName.StartsWith("WindowsBase")) continue; // skip it
if (assembly.FullName.StartsWith("Utility")) continue; // skip it
if (assembly.FullName.StartsWith("log4net")) continue; // skip it
if (assembly.FullName.StartsWith("Google")) continue; // skip it
if (assembly.FullName.StartsWith("nunit")) continue;
if (assembly.FullName.StartsWith("mscorlib")) continue;
if (assembly.FullName.StartsWith("JetBrains")) continue;
if (assembly.FullName.StartsWith("System")) continue;
#if LOG4NET
log.Error("Adding " + assembly.Location);
#endif
try
{
var interfaceTypes = assembly.GetTypes().Where(t => t.IsInterface);
listOfTypes.AddRange(interfaceTypes);
#if LOG4NET
foreach (var intF in interfaceTypes)
log.Debug(" " + intF.Name + " -> " + intF.FullName);
#endif
}
catch (System.Reflection.ReflectionTypeLoadException ex)
{
foreach (var e in ex.LoaderExceptions)
{
#if LOG4NET
log.Error(e.Message);
#endif
}
}
#if LOG4NET
catch (Exception ex)
{
log.Error(ex);
#else
catch (Exception)
{
// ignored
#endif
}
}
cacheOfTypes = listOfTypes;
}
}
/// <summary>
/// Get a mapping from a field name to a type according to the interfaces on this object
/// </summary>
/// <returns></returns>
public Dictionary<string, Type> GetTypeMap()
{
var interfaces = this.GetAllInterfaces();
Dictionary<string, Type> typeMap;
if (typeMapCache.TryGetValue(interfaces, out typeMap))
return typeMap;
typeMap = new Dictionary<string, Type>();
foreach (var mi in interfaces.SelectMany(intf => intf.GetProperties()))
{
typeMap[mi.Name] = mi.PropertyType;
}
typeMapCache.TryAdd(interfaces, typeMap);
return typeMap;
}
static readonly ConcurrentDictionary<Type[], Dictionary<string,Type>> typeMapCache = new ConcurrentDictionary<Type[], Dictionary<string, Type>>();
/// <summary>
/// Becomes a Proxy object that acts like it implements all of the interfaces listed as being supported by this Entity
/// </summary>
/// <remarks>
/// Because the returned object supports ALL of the interfaces that have ever been added to this object
/// you can cast it to any of them. This enables a type of polymorphism.
/// </remarks>
public object ActLikeAllInterfacesPresent()
{
return Impromptu.DynamicActLike(this, this.GetAllInterfaces());
}
[BsonIgnore]
// BsonIgnore because Bson serialization will happen on the dynamic interface this class exposes, not on this dictionary
private readonly Dictionary<string, object> children = new Dictionary<string, object>();
/// <summary>
/// Fetch a property by name
/// </summary>
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
if (binder.Name == "_id") { result = this._id; return true; }
else if (binder.Name == InterfacesField) { result = this.@int; return true; }
else
{
bool found = this.children.TryGetValue(binder.Name, out result);
if (!found)
{
log.Debug("Did not find property " + binder.Name);
// This value was not in the database
// If it's a value type we need a different default value ...
var typeMap = this.GetTypeMap();
Type targetType;
if (typeMap.TryGetValue(binder.Name, out targetType))
{
result = targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
}
else
{
log.Error("Did not find " + binder.Name + " in type map");
foreach (var x in typeMap)
{
log.Debug(" " + x.Key + " -> " + x.Value.Name);
}
result = null;
}
}
return true;
}
}
/// <summary>
/// Set a property (e.g. person1.Name = "Smith")
/// </summary>
public override bool TrySetMember(SetMemberBinder binder, object value)
{
if (binder.Name == "_id") { this._id = (ObjectId)value; return true; } // you shouldn't need to use this
if (binder.Name == InterfacesField) throw new AccessViolationException("You cannot set the interfaces directly, use AddLike() instead");
if (!this.GetTypeMap().ContainsKey(binder.Name)) throw new ArgumentException("Property '" + binder.Name + "' not found. You need to call AddLike to specify the interfaces you want to support.");
children[binder.Name] = value;
return true;
}
public override IEnumerable<string> GetDynamicMemberNames()
{
return new[] { "_id", InterfacesField }.Concat(children.Keys);
}
/// <summary>
/// An indexer for use by serialization code
/// </summary>
internal object this[string key]
{
get
{
if (key == "_id") return this._id;
else if (key == InterfacesField) return this.@int;
else if (key == "Entity") return this.Entity;
else return children[key];
}
set
{
if (key == "_id" && value is BsonObjectId) this._id = ((BsonObjectId)value).Value;
else if (key == "_id") this._id = (ObjectId)value;
else if (key == InterfacesField) this.@int = new HashSet<string>((IEnumerable<string>)value);
else if (key == "Entity") this.Entity = (MongoDynamic)value;
else children[key] = value;
}
}
public override string ToString()
{
return "MongoDynamic("
+ (children.ContainsKey("Name") ? (children["Name"] + " ") : "")
+ this.InterfacesAsText + ")";
}
public string ToStringValues()
{
try
{
return string.Join(System.Environment.NewLine, this.children.Select(c => $" {c.Key} = {c.Value}"));
}
catch (Exception ex)
{
return ex.Message;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment