Created
July 22, 2017 04:20
-
-
Save IanMercer/afbf69a6dfc01847970de588d65d28de to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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