Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Force any class to be serialized as a BSON class map and dictionary implementations to serialize keys as regular BSON elements #221

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/MongoDB.Bson/MongoDB.Bson.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@
<Compile Include="Serialization\CreatorMapDelegateCompiler.cs" />
<Compile Include="Serialization\ExpressionVisitor.cs" />
<Compile Include="Serialization\BsonSerializerRegistry.cs" />
<Compile Include="Serialization\ForceAsBsonClassMapSerializationProvider.cs" />
<Compile Include="Serialization\IBsonDictionarySerializer.cs" />
<Compile Include="Serialization\IBsonPolymorphicSerializer.cs" />
<Compile Include="Serialization\IBsonSerializerRegistry.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
namespace MongoDB.Bson.Serialization
{
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Represents a BSON serialization provider which defines that some serializable types should be treated
/// as BSON class maps.
/// </summary>
/// <remarks>
/// 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, <see cref="System.Collections.Generic.IList{T}"/>)
/// 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.
/// </remarks>
/// <example>
/// For example, given the following class:
///
/// <code language="c#">
/// public interface ISomeInterface { }
/// public class SomeImpl : ISomeInterface { }
/// </code>
///
/// This provider can be configured both to force any <codeInline>SomeImpl</codeInline> to be treated as
/// BSON class map and also any implementation of <codeInline>ISomeInterface</codeInline> can be configured as a
/// forced type to let any implementation be serialized as a BSON class map:
///
/// <code language="c#">
/// ForceAsBsonClassMapSerializationProvider provider = new ForceAsBsonClassMapSerializationProvider(typeof(SomeImpl));
///
/// // or
///
/// ForceAsBsonClassMapSerializationProvider provider = new ForceAsBsonClassMapSerializationProvider(typeof(ISomeInterface));
///
/// // or even both
///
/// ForceAsBsonClassMapSerializationProvider provider = new ForceAsBsonClassMapSerializationProvider(typeof(SomeImpl), typeof(ISomeInterface));
/// </code>
/// </example>
public sealed class ForceAsBsonClassMapSerializationProvider : BsonSerializationProviderBase
{
private readonly HashSet<Type> _forcedTypes;

/// <summary>
/// Constructor to give forced types as a type array.
/// </summary>
/// <param name="forcedTypes">The whole types to be forced as BSON class maps</param>
public ForceAsBsonClassMapSerializationProvider(params Type[] forcedTypes)
: this((IEnumerable<Type>)forcedTypes)
{
}

/// <summary>
/// Constructor to give forced types as a sequence of types.
/// </summary>
/// <param name="forcedTypes">The whole types to be forced as BSON class maps</param>
public ForceAsBsonClassMapSerializationProvider(IEnumerable<Type> 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<Type>(forcedTypes);
}

/// <summary>
/// Gets a set of types to be forced as BSON class maps during their serialization.
/// </summary>
public HashSet<Type> ForcedTypes { get { return _forcedTypes; } }

/// <inheritdoc/>
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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ public TClass DeserializeClass(BsonDeserializationContext context)
}
}

var docDictionaryImpl = document as IDictionary<string, object>;
var discriminatorConvention = _classMap.GetDiscriminatorConvention();
var allMemberMaps = _classMap.AllMemberMaps;
var extraElementsMemberMapIndex = _classMap.ExtraElementsMemberMapIndex;
Expand Down Expand Up @@ -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<TKey, TValue>, document to serialize could
// contain extra elements as document properties...
docDictionaryImpl.Add
(
elementName,
BsonTypeMapper.MapToDotNetValue(BsonValueSerializer.Instance.Deserialize(context))
);
}
else
{
if (elementName == discriminatorConvention.ElementName)
Expand All @@ -202,6 +213,7 @@ public TClass DeserializeClass(BsonDeserializationContext context)
if (extraElementsMemberMapIndex >= 0)
{
var extraElementsMemberMap = _classMap.ExtraElementsMemberMap;

if (document != null)
{
DeserializeExtraElementMember(context, document, elementName, extraElementsMemberMap);
Expand Down Expand Up @@ -564,6 +576,7 @@ private void SerializeClass(BsonSerializationContext context, BsonSerializationA
var bsonWriter = context.Writer;

var remainingMemberMaps = _classMap.AllMemberMaps.ToList();
HashSet<string> classElementNames = new HashSet<string>(remainingMemberMaps.Select(map => map.MemberName));

bsonWriter.WriteStartDocument();

Expand Down Expand Up @@ -591,14 +604,39 @@ private void SerializeClass(BsonSerializationContext context, BsonSerializationA
SerializeMember(context, document, memberMap);
}


// It might happen that a class implements IDictionary<string, object>, so
// the keys can be also extra elements.
SerializeDictionary(context, document as IDictionary<string, object>, classElementNames);

bsonWriter.WriteEndDocument();
}

private void SerializeDictionary(BsonSerializationContext context, IDictionary<string, object> extraElements, HashSet<string> 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<string, object>;

if (extraElements != null)
{
if (extraElementsMemberMap.MemberType == typeof(BsonDocument))
Expand All @@ -612,14 +650,7 @@ private void SerializeExtraElements(BsonSerializationContext context, object obj
}
else
{
var dictionary = (IDictionary<string, object>)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<string, object>)extraElements);
}
}
}
Expand Down