将对象反序列化为与MongoDB C#驱动程序的接口

本文关键字:驱动程序 接口 MongoDB 对象 反序列化 | 更新日期: 2023-09-27 17:56:14

我正在开发一个使用MongoDB(带有C#驱动程序)和DDD的项目。

我有一个类(聚合),它有一个类型为接口的属性。在另一门课中,我已经实现了这个接口。此类具有另一个属性,该属性的类型是接口,并使用另一个实现的类进行设置。

下面的代码更好地解释了:

// 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.文件格式异常:反序列化时出错 类的合作伙伴属性。用户:错误 在反序列化类的 Phone 属性时发生 .合作伙伴:价值类 .无法反序列化移动设备。---> System.IO.文件格式异常:反序列化时出错 类的移动属性。合作伙伴:价值 .class。电话无法反序列化。---> MongoDB.Bson.BsonSerializationException: Value class .电话无法反序列化。

然后,我在互联网上搜索了如何反序列化类型作为接口,我认为我必须做到这一点:使用强制转换映射属性,使用BsonClassMap.RegisterClassMap或编写自定义 BSON 序列化程序。

我需要知道这两种方法中哪一种更好以及如何实现它。

注意:我需要一个不修改接口的解决方案,因为他们的项目不能包含任何外部引用。

将对象反序列化为与MongoDB C#驱动程序的接口

好吧,在尝试获得此答案时,我发现了很多问题。

首先,MongoDB C#驱动程序在反序列化接口时确实存在一些问题,就像Craig Wilson在这个问题评论中所说的那样,以及问题页面中所述。

这个问题的安全实现,就像我之前说的,实际上可能是自定义的BSON序列化程序或特定的类映射,使用BsonClassMap.RegisterClassMap

因此,我已经实现了类映射,但问题仍然存在。

展望这个问题,我发现异常与驱动程序的另一个问题有关:反序列化structs时的问题

我已经将项目回滚到初始状态(没有类映射或自定义序列化程序)并将结构类型更改为类类型,并且它起作用了。

在 resume 中,此异常错误与结构反序列化有关,与接口反序列化无关。


无论如何,这是一个真正的问题,第二个问题需要更多地被视为一个错误而不是改进,就像第一个问题一样。

您可以在以下链接中找到问题:

  • https://jira.mongodb.org/browse/CSHARP-485
  • https://jira.mongodb.org/browse/CSHARP-94

[BsonSerializer(typeof(ImpliedImplementationInterfaceSerializer<IReviewExpert, ReviewExpert>))] public IReviewExpert Expert { get; set; }

为我工作

我们在 mongo 驱动程序的 1.x 分支上,遗憾的是没有罗伯特贝克建议的ImpliedImplementationInterfaceSerializer,这似乎是一个很好的解决方案。为此,我创建了自己的序列化程序,它允许您为接口成员指定 confcrete 类型。

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的界面。具体类型将存储在文档中。

但是,如果您折射您的类名甚至它们所在的命名空间,这将中断。因此,像这样存储所有内容不是一个好主意。我已经将它用于一次性配置文件。

如果您正在为"真实数据"设计数据模型,这是一个坏主意。您正在将数据与编写数据的代码完全绑定。这使得从以后可能创建的其他处理相同数据的项目读取数据变得不必要地复杂。