diff --git a/src/MongoDB.Bson/MongoDB.Bson.csproj b/src/MongoDB.Bson/MongoDB.Bson.csproj index c7aa01084e0..a23d6d6ccf3 100644 --- a/src/MongoDB.Bson/MongoDB.Bson.csproj +++ b/src/MongoDB.Bson/MongoDB.Bson.csproj @@ -187,6 +187,7 @@ + diff --git a/src/MongoDB.Bson/Serialization/ForceAsBsonClassMapSerializationProvider.cs b/src/MongoDB.Bson/Serialization/ForceAsBsonClassMapSerializationProvider.cs new file mode 100644 index 00000000000..d7abc634ad9 --- /dev/null +++ b/src/MongoDB.Bson/Serialization/ForceAsBsonClassMapSerializationProvider.cs @@ -0,0 +1,89 @@ +namespace MongoDB.Bson.Serialization +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// Represents a BSON serialization provider which defines that some serializable types should be treated + /// as BSON class maps. + /// + /// + /// Argumented types to be forced as BSON class maps can be either concrete or also base class and interface ones. + /// + /// This serialization provider is useful when a class may implement a collection interface (for example, ) + /// because the domain requires the class to act as a collection, but in terms of serialization, it must be serialized as a regular + /// POCO class. + /// + /// + /// For example, given the following class: + /// + /// + /// public interface ISomeInterface { } + /// public class SomeImpl : ISomeInterface { } + /// + /// + /// This provider can be configured both to force any SomeImpl to be treated as + /// BSON class map and also any implementation of ISomeInterface can be configured as a + /// forced type to let any implementation be serialized as a BSON class map: + /// + /// + /// ForceAsBsonClassMapSerializationProvider provider = new ForceAsBsonClassMapSerializationProvider(typeof(SomeImpl)); + /// + /// // or + /// + /// ForceAsBsonClassMapSerializationProvider provider = new ForceAsBsonClassMapSerializationProvider(typeof(ISomeInterface)); + /// + /// // or even both + /// + /// ForceAsBsonClassMapSerializationProvider provider = new ForceAsBsonClassMapSerializationProvider(typeof(SomeImpl), typeof(ISomeInterface)); + /// + /// + public sealed class ForceAsBsonClassMapSerializationProvider : BsonSerializationProviderBase + { + private readonly HashSet _forcedTypes; + + /// + /// Constructor to give forced types as a type array. + /// + /// The whole types to be forced as BSON class maps + public ForceAsBsonClassMapSerializationProvider(params Type[] forcedTypes) + : this((IEnumerable)forcedTypes) + { + } + + /// + /// Constructor to give forced types as a sequence of types. + /// + /// The whole types to be forced as BSON class maps + public ForceAsBsonClassMapSerializationProvider(IEnumerable forcedTypes) + { + if (forcedTypes == null || forcedTypes.Count() == 0) + throw new ArgumentException("Cannot configure a forced BSON class map serialization provider which contains no types to be forced as BSON class maps", "forcedTypes"); + if (!forcedTypes.All(type => type.IsClass || type.IsInterface)) + throw new ArgumentException("Forced types must be classes or interfaces"); + + _forcedTypes = new HashSet(forcedTypes); + } + + /// + /// Gets a set of types to be forced as BSON class maps during their serialization. + /// + public HashSet ForcedTypes { get { return _forcedTypes; } } + + /// + public override IBsonSerializer GetSerializer(Type type, IBsonSerializerRegistry serializerRegistry) + { + // Forcing can happen either if type to be serialized is within forced type set, or if one of forced types + // is implemented or inherited by the given type. + if (ForcedTypes.Contains(type) || ForcedTypes.Any(forcedType => forcedType.IsAssignableFrom(type))) + { + BsonClassMapSerializationProvider bsonClassMapProvider = new BsonClassMapSerializationProvider(); + + return bsonClassMapProvider.GetSerializer(type); + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/MongoDB.Bson/Serialization/Serializers/BsonClassMapSerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/BsonClassMapSerializer.cs index 5914baa67d8..559b10d5050 100644 --- a/src/MongoDB.Bson/Serialization/Serializers/BsonClassMapSerializer.cs +++ b/src/MongoDB.Bson/Serialization/Serializers/BsonClassMapSerializer.cs @@ -142,6 +142,7 @@ public TClass DeserializeClass(BsonDeserializationContext context) } } + var docDictionaryImpl = document as IDictionary; var discriminatorConvention = _classMap.GetDiscriminatorConvention(); var allMemberMaps = _classMap.AllMemberMaps; var extraElementsMemberMapIndex = _classMap.ExtraElementsMemberMapIndex; @@ -191,6 +192,16 @@ public TClass DeserializeClass(BsonDeserializationContext context) } memberMapBitArray[memberMapIndex >> 5] |= 1U << (memberMapIndex & 31); } + else if(docDictionaryImpl != null) + { + // If the document itself implements IDictionary, document to serialize could + // contain extra elements as document properties... + docDictionaryImpl.Add + ( + elementName, + BsonTypeMapper.MapToDotNetValue(BsonValueSerializer.Instance.Deserialize(context)) + ); + } else { if (elementName == discriminatorConvention.ElementName) @@ -202,6 +213,7 @@ public TClass DeserializeClass(BsonDeserializationContext context) if (extraElementsMemberMapIndex >= 0) { var extraElementsMemberMap = _classMap.ExtraElementsMemberMap; + if (document != null) { DeserializeExtraElementMember(context, document, elementName, extraElementsMemberMap); @@ -564,6 +576,7 @@ private void SerializeClass(BsonSerializationContext context, BsonSerializationA var bsonWriter = context.Writer; var remainingMemberMaps = _classMap.AllMemberMaps.ToList(); + HashSet classElementNames = new HashSet(remainingMemberMaps.Select(map => map.MemberName)); bsonWriter.WriteStartDocument(); @@ -591,14 +604,39 @@ private void SerializeClass(BsonSerializationContext context, BsonSerializationA SerializeMember(context, document, memberMap); } + + // It might happen that a class implements IDictionary, so + // the keys can be also extra elements. + SerializeDictionary(context, document as IDictionary, classElementNames); + bsonWriter.WriteEndDocument(); } + private void SerializeDictionary(BsonSerializationContext context, IDictionary extraElements, HashSet classElementNames = null) + { + if (extraElements != null && extraElements.Count > 0) + { + foreach (var key in classElementNames == null ? + extraElements.Keys : extraElements.Keys.Where(key => !classElementNames.Contains(key))) + + { + context.Writer.WriteName(key); + var value = extraElements[key]; + var bsonValue = BsonTypeMapper.MapToBsonValue(value); + BsonValueSerializer.Instance.Serialize(context, bsonValue); + } + } + } + private void SerializeExtraElements(BsonSerializationContext context, object obj, BsonMemberMap extraElementsMemberMap) { var bsonWriter = context.Writer; var extraElements = extraElementsMemberMap.Getter(obj); + + if (extraElements == null) + extraElements = obj as IDictionary; + if (extraElements != null) { if (extraElementsMemberMap.MemberType == typeof(BsonDocument)) @@ -612,14 +650,7 @@ private void SerializeExtraElements(BsonSerializationContext context, object obj } else { - var dictionary = (IDictionary)extraElements; - foreach (var key in dictionary.Keys) - { - bsonWriter.WriteName(key); - var value = dictionary[key]; - var bsonValue = BsonTypeMapper.MapToBsonValue(value); - BsonValueSerializer.Instance.Serialize(context, bsonValue); - } + SerializeDictionary(context, (IDictionary)extraElements); } } }