Last active
July 22, 2017 04:13
-
-
Save IanMercer/1749fc024dfc728ec1096dcc83737753 to your computer and use it in GitHub Desktop.
This file contains hidden or 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; | |
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