我正在开发一个使用 MongoDB(带有 C# 驱动程序)和 DDD 的项目。
我有一个类(aggregate),它有一个类型是接口的属性。在另一个类中,我实现了这个接口。这个类还有另一个属性,该属性的类型是一个接口,并使用另一个实现的类进行设置。
下面的代码解释得更好:
// Interfaces
public interface IUser {
Guid Id { get; set;}
IPartner Partner{ get; set; }
}
public interface IPartner {
IPhone Mobile { get; set; }
}
public interface IPhone {
string number { get; set; }
}
// Implemented Classes
public class User: IUser {
[BsonId(IdGenerator = typeof(GuidGenerator))]
public Guid Id { get; set; }
[BsonIgnoreIfNull]
public IPartner Partner { get; set; }
}
public struct Partner : IPartner {
public IPhone Mobile { get; set; }
}
public struct Phone : IPhone {
public string Number { get; set; }
}
嗯,当我调用
MongoCollection<User>.Insert()
方法时,它抛出两个异常:
System.IO.FileFormatException:反序列化时发生错误 .User 类的 Partner 属性:错误 反序列化类的 Phone 属性时发生 .合作伙伴:价值类 .Mobile无法反序列化。 ---> System.IO.FileFormatException:反序列化时发生错误 .Partner 类的 Mobile 属性:值 class .Phone 无法反序列化。 ---> MongoDB.Bson.BsonSerializationException:值类 .电话无法反序列化。
然后,我在互联网上搜索如何将类型反序列化为接口,我想我必须采取一些方法来做到这一点:使用强制转换映射属性,使用
BsonClassMap.RegisterClassMap
或编写自定义 BSON 序列化器。
我需要知道这两种方式哪种更好以及如何实现。
注意:我需要一个不修改接口的解决方案,因为他们的项目不能包含任何外部引用。
好吧,当我试图得到这个答案时,我发现了很多问题。
首先,MongoDB C# 驱动程序在反序列化接口时确实存在一些问题,正如 Craig Wilson 在本问题评论中所说,以及问题页面中所述。
这个问题的安全实现,就像我之前说过的,实际上可能是一个自定义的 BSON 序列化器或特定的类映射,使用
BsonClassMap.RegisterClassMap
。
所以,我已经实现了类映射,但问题仍然存在。
展望问题,我发现该异常与驱动程序的另一个问题有关:反序列化时的问题
structs
。
我已将项目回滚到初始状态(没有类映射或自定义序列化器)并将结构类型更改为类类型,并且它起作用了。
恢复一下,这个异常错误与结构反序列化有关,与接口反序列化无关。
无论如何,这是一个真正的问题,第二个问题需要更多地被视为错误而不是改进,就像第一个问题一样。
您可以通过以下链接找到问题:
[BsonSerializer(typeof(ImpliedImplementationInterfaceSerializer<IReviewExpert, ReviewExpert>))]
public IReviewExpert Expert { get; set; }
对我有用
我们处于 mongo 驱动程序的 1.x 分支上,遗憾的是没有 Robert Baker 建议的
ImpliedImplementationInterfaceSerializer
,这似乎是一个很好的解决方案。为此,我创建了自己的序列化器,它允许您为接口成员指定具体类型。
public class ConcreteTypeSerializer<TInterface, TImplementation> : BsonBaseSerializer where TImplementation : TInterface
{
private readonly Lazy<IBsonSerializer> _lazyImplementationSerializer;
public ConcreteTypeSerializer()
{
var serializer = BsonSerializer.LookupSerializer(typeof(TImplementation));
_lazyImplementationSerializer = new Lazy<IBsonSerializer>(() => serializer);
}
public override object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options)
{
if (bsonReader.GetCurrentBsonType() == BsonType.Null)
{
bsonReader.ReadNull();
return default(TInterface);
}
else
{
return _lazyImplementationSerializer.Value.Deserialize(bsonReader, nominalType, typeof(TImplementation), options);
}
}
public override void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options)
{
if (value == null)
{
bsonWriter.WriteNull();
}
else
{
var actualType = value.GetType();
if (actualType == typeof(TImplementation))
{
_lazyImplementationSerializer.Value.Serialize(bsonWriter, nominalType, (TImplementation)value, options);
}
else
{
var serializer = BsonSerializer.LookupSerializer(actualType);
serializer.Serialize(bsonWriter, nominalType, value, options);
}
}
}
}
使用方法如下:
[BsonSerializer(typeof(ConcreteTypeSerializer<IMyInterface,MyClass>))]
public IMyInterface MyProperty {get; set;}
关于代码的一些注释 - 它真正所做的就是延迟加载适当的具体类型的序列化器,然后使用适当的具体类型而不是接口传递所有序列化/反序列化调用。
它还检查该类型是否确实是预期的类型,如果不是,则仅查找该类型的默认序列化器。
注册一个具体的序列化器对我来说不起作用,因为我想要存储的对象包含一个具有 6 个不同实现的接口列表。使用 CosmosDB,通过一些 Newtonsoft 设置可以很好地存储。它将实现的类型序列化到对象中,并将其称为
$type
。所以,我想我也会做同样的事情来让它在 MongoDB 中工作。
public class BsonTypeSerializer<T> : IBsonSerializer<T>
{
public Type ValueType { get => typeof(T); }
public T Deserialize(
BsonDeserializationContext context,
BsonDeserializationArgs args)
{
var document = BsonSerializer.Deserialize<BsonDocument>(context.Reader);
var typeStr = document.GetValue("$type").AsString;
var type = Type.GetType(typeStr);
var result = (T) BsonSerializer.Deserialize(document, type);
return result;
}
public void Serialize(
BsonSerializationContext context,
BsonSerializationArgs args,
T value)
{
var typeStr = value.GetType().FullName;
BsonDocument document = value.ToBsonDocument();
document.Add(new BsonElement("$type", BsonValue.Create(typeStr)));
BsonSerializer.Serialize(context.Writer, typeof(BsonDocument), document);
}
public void Serialize(
BsonSerializationContext context,
BsonSerializationArgs args,
object value)
=> Serialize(context, args, (T) value);
object IBsonSerializer.Deserialize(
BsonDeserializationContext context,
BsonDeserializationArgs args)
=> Deserialize(context, args);
}
注册时您可以将其指定为
T
。具体类型将存储在 Document 中。
但是,如果您折射类名甚至它们所在的命名空间,这会破坏。因此,像这样存储所有内容并不是一个好主意。我已将其用作一次性配置文件。
如果您正在为“真实数据”设计数据模型,这是一个坏主意。您将数据与编写它的代码完全绑定在一起。这使得从您稍后可能创建的处理相同数据的其他项目中读取数据变得不必要的复杂。
除了@Robert Baker的回答之外,如果有人遇到像我这样的情况,接口会有多个实现。我有一个解决方法。
BsonClassMap.RegisterClassMap<AuthorUpdatedDomainEvent>(cm =>
{
cm.AutoMap();
cm.SetIgnoreExtraElements(true);
cm.SetDiscriminatorIsRequired(true);
cm.SetDiscriminator(nameof(AuthorUpdatedDomainEvent));
});
BsonClassMap.RegisterClassMap<PublisherUpdatedDomainEvent>(cm =>
{
cm.AutoMap();
cm.SetIgnoreExtraElements(true);
cm.SetDiscriminatorIsRequired(true);
cm.SetDiscriminator(nameof(PublisherUpdatedDomainEvent));
});
BsonClassMap.RegisterClassMap<OutboxMessage>(cm => {
cm.AutoMap();
cm.MapMember(m => m.DomainEvent)
.SetSerializer(new ImpliedImplementationInterfaceSerializer<IDomainEvent, IDomainEvent>());
});
这里的“AuthorUpdatedDomainEvent”类和“PublisherUpdatedDomainEvent”类是“IDomainEvent”的实现;