Skip to content

Instantly share code, notes, and snippets.

@IanMercer
Last active July 22, 2017 04:13
Show Gist options
  • Save IanMercer/1749fc024dfc728ec1096dcc83737753 to your computer and use it in GitHub Desktop.
Save IanMercer/1749fc024dfc728ec1096dcc83737753 to your computer and use it in GitHub Desktop.
using System;
using System.Collections;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.IdGenerators;
using System.Collections.Generic;
using log4net;
namespace MongoData
{
public class MongoDynamicBsonSerializer : SerializerBase<MongoDynamic>, IBsonIdProvider //, IBsonDocumentSerializer//, IBsonArraySerializer
{
private static readonly ILog log= LogManager.GetLogger("DynMongoSer");
public static MongoDynamicBsonSerializer Instance { get; } = new MongoDynamicBsonSerializer();
private static readonly IBsonSerializer<string> stringSerializer = new StringSerializer();
public override MongoDynamic Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
log.Debug($"Deserializing ... {args.NominalType}");
var bsonReader = context.Reader;
Type nominalType = args.NominalType;
var bsonType = bsonReader.GetCurrentBsonType();
if (bsonType == BsonType.Null)
{
bsonReader.ReadNull();
return null;
}
else if (bsonType == BsonType.Document)
{
MongoDynamic md = new MongoDynamic();
bsonReader.ReadStartDocument();
// Scan document first to find interfaces and id fields
Dictionary<string, Type> typeMap = ScanToLoadTypeMapFromInterfaces(context, bsonReader, md, nominalType);
while (bsonReader.ReadBsonType() != BsonType.EndOfDocument)
{
var name = bsonReader.ReadName();
log.Debug($"Reading field {name}");
if (name == "_id")
{
md[name] = bsonReader.ReadObjectId();
log.Debug($" {name} = {md[name]}");
}
else if (name == MongoDynamic.InterfacesField)
{
// Read it and ignore it, we already have it
bsonReader.SkipValue();
log.Debug(" Skipping int field");
}
else if (name == "_t")
{
log.Debug(" Skipping _t field, don't need that for interfaces");
bsonReader.SkipValue();
}
else if (name == "Entity")
{
log.Debug(" Skipping Entity field");
// Read it and ignore it, this was a broken earlier attempt at serialization
bsonReader.SkipValue();
}
else if (bsonReader.CurrentBsonType == BsonType.Null)
{
bsonReader.ReadNull();
md[name] = null;
log.Debug($" {name} = null");
}
else
{
if (typeMap == null)
{
throw new FormatException("No interfaces defined for this dynamic object - can't deserialize [" + md["_id"] + "]");
}
// lookup the type for this element according to the interfaces
Type elementType;
if (typeMap.TryGetValue(name, out elementType))
{
if (elementType.IsArray)
{
var subElementType = elementType.GetElementType();
if (subElementType.IsInterface)
{
bsonReader.ReadStartArray();
var listType = typeof (List<>).MakeGenericType(subElementType);
var elements = (IList) Activator.CreateInstance(listType);
while (bsonReader.ReadBsonType() != BsonType.EndOfDocument)
{
var subargs = new BsonDeserializationArgs {NominalType = subElementType};
MongoDynamic o = this.Deserialize(context, subargs);
elements.Add(o.ActLikeAllInterfacesPresent());
}
bsonReader.ReadEndArray();
var array = Array.CreateInstance(subElementType, elements.Count);
for (int i = 0; i < array.Length; i++)
{
try
{
array.SetValue(elements[i], i);
}
catch (InvalidCastException)
{
log.Error(
$"Cannot cast {elements[i].GetType().Name} to {subElementType}");
}
}
md[name] = array;
log.Debug($" {name} = {md[name]}");
}
else
{
md[name] = BsonSerializer.LookupSerializer(elementType).Deserialize(context, args);
log.Debug($" {name} = {md[name]}");
}
}
else if (elementType.IsInterface)
{
//log.Debug("Recursing on an interface " + elementType.Name);
var subargs = new BsonDeserializationArgs {NominalType = elementType};
var value = this.Deserialize(context, subargs);
md[name] = value.ActLikeAllInterfacesPresent();
log.Debug($" {name} = {md[name]}");
}
else
{
// Not an interface type, call the normal deserializer
try
{
IBsonSerializer serializer = BsonSerializer.LookupSerializer(elementType);
var value = serializer.Deserialize(context);
md[name] = value;
}
catch (FormatException fex)
{
log.Error($"Unrecoverable error reading a {elementType}", fex);
log.Error(md.ToStringValues());
throw;
}
catch (Exception ex)
{
log.Error($"Could not Deserialize a {elementType}", ex);
log.Error(md.ToStringValues());
}
log.Debug($" {name} = {md[name]}");
}
}
else
{
log.Debug("Trying to deserialize field " + name + " which was not found in the type map");
log.Debug("TypeMap contains " + string.Join(", ", typeMap.Select(x => x.Key + "=" + x.Value)));
try
{
// This is a value that is no longer in the interface, maybe a column you removed
// not really much we can do with it ... but we need to read it and move on
var value = BsonSerializer.Deserialize(bsonReader, typeof (object));
md[name] = value;
log.Debug($" {name} = {md[name]}");
}
catch (Exception ex)
{
ex.Data.Add("Explanation",
"As with all databases, removing elements from the schema is going to cause problems");
throw;
}
}
}
}
bsonReader.ReadEndDocument();
log.Debug($"--------");
return md;
}
else
{
var message = $"Can't deserialize a {nominalType.FullName} from BsonType {bsonType}.";
throw new FormatException(message);
}
}
static Dictionary<string, Type> ScanToLoadTypeMapFromInterfaces(BsonDeserializationContext context, IBsonReader bsonReader,
MongoDynamic md, Type nominalType)
{
Dictionary<string, Type> typeMap = null;
var bookMark = bsonReader.GetBookmark();
if (bsonReader.FindElement(MongoDynamic.InterfacesField))
{
bsonReader.ReadStartArray();
var innerList = new List<string>();
while (bsonReader.ReadBsonType() != BsonType.EndOfDocument)
{
innerList.Add(stringSerializer.Deserialize(context));
}
bsonReader.ReadEndArray();
md[MongoDynamic.InterfacesField] = innerList;
typeMap = md.GetTypeMap();
log.Debug($" Interfaces = {md.InterfacesAsText}");
log.Debug($" Properties = {string.Join(", ", typeMap.Keys)}");
}
else
{
log.Debug($"No 'int' field on this dynamic object - switching to use nominal type {nominalType}");
var interfaces = nominalType.GetInterfaces();
List<string> interfaceNames = new List<string>();
if (nominalType.IsInterface) interfaceNames.Add(nominalType.FullName);
interfaceNames.AddRange(interfaces.Select(i => i.FullName));
// What about concrete types? Well, they are deserialized by normal MongoDB deserializer
md[MongoDynamic.InterfacesField] = interfaceNames;
typeMap = md.GetTypeMap();
}
bsonReader.ReturnToBookmark(bookMark);
return typeMap;
}
public bool GetDocumentId(object document, out object id, out Type idNominalType, out IIdGenerator idGenerator)
{
log.Debug("GetDocumentId");
MongoDynamic x = (MongoDynamic)document;
id = x._id;
idNominalType = typeof(ObjectId);
idGenerator = new ObjectIdGenerator();
return true;
}
public void SetDocumentId(object document, object id)
{
MongoDynamic x = (MongoDynamic)document;
x._id = (ObjectId)id;
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args,
MongoDynamic value)
{
var bsonWriter = context.Writer;
log.Debug("Serialize " + value);
if (value == null)
{
bsonWriter.WriteNull();
return;
}
// ------------------- NEW WAY --- WE KNOW IT'S A MONGODYNAMIC, NO NEED TO GO ALL IDynamicMetaObjectProvider ON IT
MongoDynamic mongoDynamic = value as MongoDynamic;
bsonWriter.WriteStartDocument();
// And now write the fields according to the interface map
var typeMap = mongoDynamic.GetTypeMap();
var metaObject = ((IDynamicMetaObjectProvider)value).GetMetaObject(Expression.Constant(value));
var memberNames = metaObject.GetDynamicMemberNames().ToList();
if (memberNames.Count == 0)
{
bsonWriter.WriteNull();
return;
}
foreach (var memberName in memberNames)
{
log.Debug("Examining " + memberName);
// Get the value
object memberValue;
Type memberType;
if (memberName == "_id")
{
memberValue = mongoDynamic._id;
memberType = typeof(ObjectId);
}
else if (memberName == "int")
{
memberValue = mongoDynamic.@int;
memberType = memberValue.GetType(); // A Hashset<string>
}
else
{
memberValue = mongoDynamic[memberName]; /// Could also use ... Impromptu.InvokeGet(value, memberName);
// Lookup the intended type for that field because it
// may be different from the typeof(memberValue) which
// is likely a Proxy type. If we can't find it, go ahead
// and use the type of the object - for some reason it's not in the interfaces
if (!typeMap.TryGetValue(memberName, out memberType))
memberType = memberValue?.GetType();
}
bsonWriter.WriteName(memberName);
WriteValue(context, args, memberValue, bsonWriter, memberType);
if (memberName == "Name" && memberType == typeof(string))
{
string cleanedName = ITagExtensions.RemoveDiacritics(((string)memberValue).ToLowerInvariant());
// Normalize the name field, lower case, replace special characters (TODO)
bsonWriter.WriteName("name");
WriteValue(context, args, cleanedName, bsonWriter, memberType);
}
}
bsonWriter.WriteEndDocument();
return;
}
private void WriteValue(BsonSerializationContext context, BsonSerializationArgs args, object memberValue,
IBsonWriter bsonWriter, Type memberType)
{
if (memberValue == null)
bsonWriter.WriteNull();
else
{
if (memberType.IsArray)
{
context.Writer.WriteStartArray();
memberType = memberType.GetElementType();
foreach (var item in memberValue as IEnumerable)
{
// It could be a normal type here, or it could be an interface
var iid = item as IId;
var type = item.GetType();
if (iid != null)
{
var mdInner = iid.Entity;
this.Serialize(context, mdInner);
}
else
{
var serializer = BsonSerializer.LookupSerializer(memberType);
serializer.Serialize(context, item);
}
}
context.Writer.WriteEndArray();
}
else if (memberType.IsInterface)
{
// log.Debug("Serializing " + memberName + " : " + memberValue.GetType().Name + " a " + memberType.Name + " from a " + args.NominalType.Name + " by using another MongoDynamic!");
// Make it into a MongoDynamic object so we can read it back using only its interface
MongoDynamic expanded = new MongoDynamic(memberType, memberValue);
// Recursively call ourself to serialize this embedded interface which will again embed another interfaces field
// to identify the interface type to load back in
Serialize(context, args, expanded);
}
else
{
// log.Debug("Serializing " + memberName + " : " + memberValue.GetType() + " a " + memberType.Name + " from a " + args.NominalType.Name + " using normal lookup");
var serializer = BsonSerializer.LookupSerializer(memberType);
serializer.Serialize(context, memberValue);
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment