MongoDB中结构体的序列化和反序列化解决方法

12
在MongoDB中,结构体(valuetype)的序列化和反序列化是不可能的,因为MongoDB会抛出异常:BsonClassMapSerializer.cs 第84行。但我希望能够通常解决这个问题。
背景:我们想创建一个库,叫做PolyglotPersistence.Lib。我的“客户”使用这个库将他的数据结构保存到数据库中,可以是MongoDB Azure CosomosDB,或自己实现的MemoryDB和其他一些解决方案。但是,由于结构问题,MongoDB不能保存所有类型的数据结构。
我在Stackoverflow上找到了一些问题/答案,但这些解决方案都不是通用解决方案。
例如1: How do you serialize value types with MongoDB C# serializer?。这根本不是通用的。当我应用这个解决方案时,我必须为每个结构创建一个Serialize/Deserializer。这可以通过一个通用的StructSerializer<>完成,但是“客户”仍然需要为所有结构注册它。这是不可接受的,因为他们不知道数据将被序列化到哪里(Cosmos/Mongo/Memory等)。

示例2 使用Mongo C#驱动程序序列化不可变值类型 这个解决方案几乎相同。必须通过 "客户端" 注册特殊的序列化器。

示例3 使用MongoDB C#驱动程序反序列化嵌套结构 他们将其更改为类,这对我们来说不是一个好方法。


可能的解决方案1 我们创建一个新规则:当 "客户端" 在其数据结构中使用结构体时,必须继承特殊基类,比如 "IStruct"。然后我们为该类型注册一个序列化器,并解决问题。

但这对我们的客户来说有点不舒服,也不是百分之百可靠的解决方案。

可能的解决方案2 当用户为我们的库(PolyglotPersistence.Lib)添加一个新类型时,我们必须递归地遍历该类型,并检测其中是否存在该结构。当我们找到它时,则需要为该类型注册一个序列化器(如果尚未注册)。

但对于此解决方案,我们需要找到客户数据结构中的所有结构体。

可能的解决方案3 为基础结构体类型注册序列化器。我不知道它是否存在。但这将是最佳解决方案。结构的最终基类 :)


所以问题是:

  1. 所有最初在C#中构建的结构体是否有终极基类或接口?
  2. 如果我有 System.Type,如何百分之百地安全地检测它是否为结构体?

非常感谢所有回答,但请不要将此问题标记为重复,因为已经回答的解决方案不适用于我们的问题。并且,请在标记之前仔细阅读问题。谢谢。

P.S. 感谢所有评论 :)


IsTypeValue对于所有的ValueType都是true,而不仅仅是结构体。 - György Gulyás
IsTypeValue 对于所有的 ValueType 都是 true,不仅仅是结构体。哪些值类型不是结构体?例如 Int32 是一个结构体, 参见 https://referencesource.microsoft.com/#mscorlib/system/int32.cs,35: public struct Int32 : IComparable, IFormattable, IConvertible ... - dbc
是的,这是真的。但对于 int,Mongo 有一个很好的解决方案。原始数据类型并不感兴趣,只关注真正的结构体。 - György Gulyás
数据结构掌握在“客户”手中。 我们没有任何可能性,只能支持结构。 - György Gulyás
显示剩余5条评论
2个回答

10

最终我找到了解决方案,它位于原始解决方案2和3之间。

主要思想是,在“client”数据结构中查找所有的struct,并为其注册特殊的Struct序列化程序。以下是挑战:

查找“client”数据结构中的所有struct类型

必须进行递归查找,即使该结构是隐藏在集合中的类中的一部分,等等...因此我们必须在所有情况下找到它。幸运的是,MongoDB有助于查找所有实例,因为在序列化期间,MongoDB对每个类型进行递归遍历。因此,我们注册了一个序列化提供程序,它可以“检测”所有结构并为其提供特殊的序列化程序。

检测给定类型是否为struct

为了完成这项工作,在StackOverflow上有很多答案,但其中没有一个完美。也许我的解决方案也不完美,但我们将所有想法合并起来。因此,我们检查类型不是基元类型,不是枚举类型,而是值类型,并且不是某些已经在MongoDB中具有序列化程序的默认结构。


以下是代码:

1. 为MongoDB注册序列化程序提供程序:

BsonSerializer.RegisterSerializationProvider( new MongoDB_SerializationProvider() );

2、实现序列化器:

class MongoDB_SerializationProvider : BsonSerializationProviderBase
{
    private static readonly object locker = new object();
    private static Dictionary<Type, MongoDB_StructSerializer> _StructSerializers;
    private static MongoDB_DecimalSerializer _DecimalSerializer;


    static MongoDB_SerializationProvider()
    {
        _StructSerializers = new Dictionary<Type, MongoDB_StructSerializer>();
        _DecimalSerializer = new MongoDB_DecimalSerializer();
    }

    public override IBsonSerializer GetSerializer( Type type, IBsonSerializerRegistry serializerRegistry )
    {
        if ( type == typeof( decimal ) )
        {
            return _DecimalSerializer;
        }
        else if ( Reflection.Info.IsStruct( type ) && type != typeof( ObjectId ) )
        {
            MongoDB_StructSerializer structSerializer = null;

            lock ( locker )
            {
                if ( _StructSerializers.TryGetValue( type, out structSerializer ) == false )
                {
                    structSerializer = new MongoDB_StructSerializer( type );
                    _StructSerializers.Add( type, structSerializer );
                }
            }

            return structSerializer;
        }
        else
        {
            return null;
        }
    }
}

十进制部分是另一个有趣的主题,但它不是当前问题的一部分。有一件事情我们必须小心:MongoDB的ObjectId也是一个结构体,当然我们不想为ObjectId注册序列化程序。代码中有一个函数,它做了一些小魔术:Reflection.Info.IsStruct( type ) 这是它的代码:

    public static bool IsStruct( Type type )
    {
        if ( IsPrimitiveType( type ) == true )
            return false;

        if ( type.IsValueType == false )
            return false;

        return true;
    }

    static public bool IsPrimitiveType( Type type )
    {
        if ( type.GetTypeInfo().IsPrimitive == true )
            return true;

        if ( type.GetTypeInfo().IsEnum == true )
            return true;

        if ( type == typeof( decimal ) )
            return true;

        if ( type == typeof( string ) )
            return true;

        if ( type == typeof( DateTime ) )
            return true;

        if ( type == typeof( DateTimeOffset ) )
            return true;

        if ( type == typeof( TimeSpan ) )
            return true;

        if ( type == typeof( Guid ) )
            return true;

        return false;
    }

3、实现序列化器

这段代码有点长,但我希望它仍然易于理解:

public class MongoDB_StructSerializer : IBsonSerializer
{
    public Type ValueType { get; }

    public MongoDB_StructSerializer( Type valueType )
    {
        ValueType = valueType;
    }

    public void Serialize( BsonSerializationContext context, BsonSerializationArgs args, object value )
    {
        if ( value == null )
        {
            context.Writer.WriteNull();
        }
        else
        {
            List<MemberInfo> members = Reflection.Serialize.GetAllSerializableMembers( ValueType );

            context.Writer.WriteStartDocument();
            foreach( MemberInfo member in members )
            {
                context.Writer.WriteName( member.Name );
                BsonSerializer.Serialize( context.Writer, Reflection.Info.GetMemberType( member ), Reflection.Info.GetMemberValue( member, value ), null, args );
            }
            context.Writer.WriteEndDocument();
        }
    }

    public object Deserialize( BsonDeserializationContext context, BsonDeserializationArgs args )
    {
        BsonType bsonType = context.Reader.GetCurrentBsonType();
        if ( bsonType == BsonType.Null )
        {
            context.Reader.ReadNull();
            return null;
        }
        else
        {
            object obj = Activator.CreateInstance( ValueType );

            context.Reader.ReadStartDocument();

            while ( context.Reader.ReadBsonType() != BsonType.EndOfDocument )
            {
                string name = context.Reader.ReadName();

                FieldInfo field = ValueType.GetField( name );
                if ( field != null )
                {
                    object value = BsonSerializer.Deserialize( context.Reader, field.FieldType );
                    field.SetValue( obj, value );
                }

                PropertyInfo prop = ValueType.GetProperty( name );
                if ( prop != null )
                {
                    object value = BsonSerializer.Deserialize( context.Reader, prop.PropertyType );
                    prop.SetValue( obj, value, null );
                }
            }

            context.Reader.ReadEndDocument();

            return obj;
        }
    }
}

这个神奇的函数:Reflection.Serialize.GetAllSerializableMembers 包含一些非常有趣的东西,涉及可序列化成员和不可序列化成员。

    public static List<MemberInfo> GetSerializableMembers( Type type, BindingFlags bindingFlags )
    {
        List<MemberInfo> list = new List<MemberInfo>();

        FieldInfo[] fields = type.GetFields( bindingFlags );
        foreach ( FieldInfo field in fields )
        {
            if ( IsFieldSerializable( type, field ) == false )
                continue;

            list.Add( field );
        }

        PropertyInfo[] properties = type.GetProperties( bindingFlags );
        foreach ( PropertyInfo property in properties )
        {
            if ( IsPropertySerializable( type, property ) == false )
                continue;

            list.Add( property );
        }

        return list;
    }

    public static bool IsFieldSerializable( Type type, FieldInfo field )
    {
        if ( field.IsInitOnly == true )
            return false;

        if ( field.IsLiteral == true )
            return false;

        if ( field.IsDefined( typeof( CompilerGeneratedAttribute ), false ) == true )
            return false;

        if ( field.IsDefined( typeof( IgnoreAttribute ), false ) == true )
            return false;

        return true;
    }

    public static bool IsPropertySerializable( Type type, PropertyInfo property )
    {
        if ( property.CanRead == false )
            return false;

        if ( property.CanWrite == false )
            return false;

        if ( property.GetIndexParameters().Length != 0 )
            return false;

        if ( property.GetMethod.IsVirtual && property.GetMethod.GetBaseDefinition().DeclaringType != type )
            return false;

        if ( property.IsDefined( typeof( IgnoreAttribute ), false ) == true )
            return false;

        return true;
    }
这个解决方案经过了多达15-20个不同测试用例的测试,并且表现良好。我认为MongoDB社区也能够实现结构体序列化。他们说不能这样做,因为结构体是值类型,所以复制的是值而不是引用。因此,当一个函数在内部更改了值时,原始值不会改变。但是,在MongoDB的所有序列化代码中都使用了'object',而结构体也是对象。并且在驱动程序代码中没有成员更改。只有在我们的代码中重写的反序列化中才有更改。

因此,如果他们想要,MongoDB社区也可以做到! :)

P.S. 感谢您阅读这篇长篇文章,这里有一个土豆



0
补充György Gulyás的答案: 如果您的模型具有可空类型,请在public static bool IsStruct( Type type )方法中添加以下内容,作为该方法的第一行:

if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) type = Nullable.GetUnderlyingType(type);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接