BsonSerializationException при сериализации словаря <DateTime, T> в BSON
Недавно я перешел в новый MongoDB С# драйвер v2.0 из устарел v1.9.
Теперь, когда я сериализую класс, у которого есть словарь, я иногда запускаю следующий BsonSerializationException
:
MongoDB.Bson.BsonSerializationException: при использовании DictionaryRepresentation. Значения ключа документа должны сериализоваться как строки.
Здесь минимальное воспроизведение:
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
Проблема заключается в том, что новый драйвер сериализует словари как документ по умолчанию.
У драйвера MongoDB С# есть 3 способа сериализации словаря: Document
, ArrayOfArrays
и ArrayOfDocuments
(больше в документах). Когда он сериализуется в качестве документа, ключи словаря являются именами элемента BSON, который имеет некоторые ограничения (например, по мере появления ошибки, они должны быть сериализованы как строки).
В этом случае ключи словаря DateTime
, которые не сериализуются как строки, а как Date
, поэтому нам нужно выбрать другой DictionaryRepresentation
.
Чтобы изменить сериализацию этого конкретного свойства, мы можем использовать атрибут BsonDictionaryOptions
с другим DictionaryRepresentation
:
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)]
public Dictionary<DateTime, int> Dictionary { get; private set; }
Однако мы должны делать это на каждом проблемном элементе индивидуально. Чтобы применить этот DictionaryRepresentation
ко всем соответствующим членам, мы можем реализовать новое соглашение:
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));
}
}
Что мы регистрируем следующим образом:
ConventionRegistry.Register(
"DictionaryRepresentationConvention",
new ConventionPack {new DictionaryRepresentationConvention(DictionaryRepresentation.ArrayOfArrays)},
_ => true);
Ответ 2
Ответ выше велик, но, к сожалению, вызывает исключение StackOverflowException для определенных иерархий рекурсивных объектов - здесь слегка улучшенная и актуальная версия.
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
Если, как и я, вы просто хотели применить это для одного поля в классе, я добился этого следующим образом (благодаря другим ответам):
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);
});
Или как метод расширения:
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;
}
}