问题
I've recently moved to the new MongoDB C# driver v2.0 from the deprecated v1.9.
Now, when I serialize a class that has a dictionary I sometimes run into the following BsonSerializationException
:
MongoDB.Bson.BsonSerializationException: When using DictionaryRepresentation.Document key values must serialize as strings.
Here's a minimal reproduce:
class Hamster
{
public ObjectId Id { get; private set; }
public Dictionary<DateTime,int> Dictionary { get; private set; }
public Hamster()
{
Id = ObjectId.GenerateNewId();
Dictionary = new Dictionary<DateTime, int>();
Dictionary[DateTime.UtcNow] = 0;
}
}
static void Main()
{
Console.WriteLine(new Hamster().ToJson());
}
回答1:
The problem is that the new driver serializes dictionaries as a document by default.
The MongoDB C# driver has 3 ways to serialize a dictionary: Document
, ArrayOfArrays
& ArrayOfDocuments
(more on that in the docs). When it serializes as a document the dictionary keys are the names of the BSON element which has some limitations (for example, as the error suggests, they must be serialized as strings).
In this case, the dictionary's keys are DateTime
s which aren't serialized as strings, but as Date
s so we need to choose another DictionaryRepresentation
.
To change the serialization of this specific property we can use the BsonDictionaryOptions
attribute with a different DictionaryRepresentation
:
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)]
public Dictionary<DateTime, int> Dictionary { get; private set; }
However, we need to do that on every problematic member individually. To apply this DictionaryRepresentation
to all the relevant members we can implement a a new convention:
class DictionaryRepresentationConvention : ConventionBase, IMemberMapConvention
{
private readonly DictionaryRepresentation _dictionaryRepresentation;
public DictionaryRepresentationConvention(DictionaryRepresentation dictionaryRepresentation)
{
_dictionaryRepresentation = dictionaryRepresentation;
}
public void Apply(BsonMemberMap memberMap)
{
memberMap.SetSerializer(ConfigureSerializer(memberMap.GetSerializer()));
}
private IBsonSerializer ConfigureSerializer(IBsonSerializer serializer)
{
var dictionaryRepresentationConfigurable = serializer as IDictionaryRepresentationConfigurable;
if (dictionaryRepresentationConfigurable != null)
{
serializer = dictionaryRepresentationConfigurable.WithDictionaryRepresentation(_dictionaryRepresentation);
}
var childSerializerConfigurable = serializer as IChildSerializerConfigurable;
return childSerializerConfigurable == null
? serializer
: childSerializerConfigurable.WithChildSerializer(ConfigureSerializer(childSerializerConfigurable.ChildSerializer));
}
}
Which we register as follows:
ConventionRegistry.Register(
"DictionaryRepresentationConvention",
new ConventionPack {new DictionaryRepresentationConvention(DictionaryRepresentation.ArrayOfArrays)},
_ => true);
回答2:
The answer above is great, but unfortunately triggers a StackOverflowException on certain recursive object hierarchies - here's a slightly improved and up to date version.
public class DictionaryRepresentationConvention : ConventionBase, IMemberMapConvention
{
private readonly DictionaryRepresentation _dictionaryRepresentation;
public DictionaryRepresentationConvention(DictionaryRepresentation dictionaryRepresentation = DictionaryRepresentation.ArrayOfDocuments)
{
// see http://mongodb.github.io/mongo-csharp-driver/2.2/reference/bson/mapping/#dictionary-serialization-options
_dictionaryRepresentation = dictionaryRepresentation;
}
public void Apply(BsonMemberMap memberMap)
{
memberMap.SetSerializer(ConfigureSerializer(memberMap.GetSerializer(),Array.Empty<IBsonSerializer>()));
}
private IBsonSerializer ConfigureSerializer(IBsonSerializer serializer, IBsonSerializer[] stack)
{
if (serializer is IDictionaryRepresentationConfigurable dictionaryRepresentationConfigurable)
{
serializer = dictionaryRepresentationConfigurable.WithDictionaryRepresentation(_dictionaryRepresentation);
}
if (serializer is IChildSerializerConfigurable childSerializerConfigurable)
{
if (!stack.Contains(childSerializerConfigurable.ChildSerializer))
{
var newStack = stack.Union(new[] { serializer }).ToArray();
var childConfigured = ConfigureSerializer(childSerializerConfigurable.ChildSerializer, newStack);
return childSerializerConfigurable.WithChildSerializer(childConfigured);
}
}
return serializer;
}
回答3:
If like me you just wanted to apply this for a single field in a class, I achieved that like this (thanks to the other answers):
BsonClassMap.RegisterClassMap<TestClass>(cm =>
{
cm.AutoMap();
var memberMap = cm.GetMemberMap(x => x.DictionaryField);
var serializer = memberMap.GetSerializer();
if (serializer is IDictionaryRepresentationConfigurable dictionaryRepresentationSerializer)
serializer = dictionaryRepresentationSerializer.WithDictionaryRepresentation(DictionaryRepresentation.ArrayOfDocuments);
memberMap.SetSerializer(serializer);
});
Or as an extention method:
BsonClassMap.RegisterClassMap<TestClass>(cm =>
{
cm.AutoMap();
cm.SetDictionaryRepresentation(x => x.DictionaryField, DictionaryRepresentation.ArrayOfDocuments);
});
public static class MapHelpers
{
public static BsonClassMap<T> SetDictionaryRepresentation<T, TMember>(this BsonClassMap<T> classMap, Expression<Func<T,TMember>> memberLambda, DictionaryRepresentation representation)
{
var memberMap = classMap.GetMemberMap(memberLambda);
var serializer = memberMap.GetSerializer();
if (serializer is IDictionaryRepresentationConfigurable dictionaryRepresentationSerializer)
serializer = dictionaryRepresentationSerializer.WithDictionaryRepresentation(representation);
memberMap.SetSerializer(serializer);
return classMap;
}
}
来源:https://stackoverflow.com/questions/28111846/bsonserializationexception-when-serializing-a-dictionarydatetime-t-to-bson