Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Workaround for Serialize and Deserialize struct in MongoDB

In MongoDB the struct (valuetype) serialization and Deserialization is not possible, because MongoDB throws an Exception: BsonClassMapSerializer.cs line: 84.

But I want to solve this problem generally.

Background

We want to create a library lets call it PolyglotPersistence.Lib. My "clients" use this library to save his datastuctures into Database, which can be MongoDB Azure CosomosDB, or own implemented MemoryDB and some other solution.

But the MongoDB cannot save all kind of data structure, because of the struct problem.


I found some question/answer already in Stackoverflow but these solution are not general solution.

Example 1 How do you serialize value types with MongoDB C# serializer? This is not general at all, When I apply this solution, I have to create a Serialize/Deserializer for every struct. That is ok, it can be done with a generic StructSerializer<>, but still the "clients" have to register it for all struct. Which is not acceptable, because they are don't know where the data will be serialized (Cosmos/Mongo/Memory/etc...).

Example 2 Serializing Immutable Value types with Mongo C# Driver It is nearly the same solution. Must register a special Serializer by "client".

Example 3 Deserialize a nested struct with MongoDB C# driver They change to class, which is not a good way for us.


Possible Solution 1 We create a new rule: When the "client" uses a struct in his data-structure he must be inhertit from a special base class, lets say "IStruct". And we register a serializer for this type, and the problem solved.

But it is littebit uncomfortable for our clients, and not a bulletproof solution.

Possible Solution 2 When the user add a new type for our library (PolyglotPersistence.Lib) then we have to walk-trough on this type recursively, and detect is there the structure in it. When we found it, then we have to register a serializer for this type, when it is not registered yet.

But for this solution we have to find all the struct in clients datastructure.

Possible Solution 3 Register a Serializer for basetype of struct. I don't know is it exists or not. But this will be best solution. The ultimate base class for struct :)


So the questions are:

  1. Is there ultimate base class or interface for all struct originally built in c#?
  2. If I have System.Type how can I detect it is a struct, with hundred percent safe?

Thank you all answer, and please do not mark this question to duplicate, because the already answered solution are not fit to our problem. And Please, please read the question before mark it. Thank you

P.S. All comment will be appreciated :)

like image 601
György Gulyás Avatar asked Dec 01 '18 07:12

György Gulyás


1 Answers

Finally I found the solution, It is between in original solution 2 and 3.

The main idea is, to find all the struct in "client" data structure, and register the Special Struct Serializer for it. The challenges are following:

Find all the struct type in "client" data structure

It must be find recursively, even the structure is part of the collection which is hidden in the class which is covered in collections, etc.... So we had to find it in all cases. Luckily the MongoDB Helps to find all the instance, because during serialization the MongoDB makes a recursive walk-trough on each types. So we register a serialization provider which is "detect" all structure and give a special serializer for it.

Detect the given type is struct or not

To do this job, there was a lot of answer on StackOverflow, needer of them was perfect. Maybe my solution is also not perfect, but we made a union of all ideas. So we check the type is not primitive, it is not enum, but it is value-type, and not a some default struct, which has already a serializer in MongoDB.


The Codes are following:

1, Register a serializer provider for MongoDB:

BsonSerializer.RegisterSerializationProvider( new MongoDB_SerializationProvider() );

2, Implement a serializer:

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;
        }
    }
}

The decimal part is an another interesting theme, but it is not part of the current question. One thing we must be careful: The MongoDB ObjectId is also a struct, and we do not want to register a serializer for ObjectId-s of course. There is function in the code, which do a little magic: Reflection.Info.IsStruct( type ) Here is the code of it:

    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, Implement the Serializer

It is little bit longer code, but I hope it is still understandable:

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;
        }
    }
}

The magic function : Reflection.Serialize.GetAllSerializableMembers is contains some really interesting stuff, what is serializable member and what not.

    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;
    }

Summary

This solutions tested well (about 15-20 different test cases), and works well. I think MongoDB community also able to implement the struct serialization. They sad it can not be done, because the struct are valutypes, so that is why values are copied not the reference, So when one function changes the value inside, the original not changed. But! All the serialization code inside the MongoDB Uses 'object' and structs are also objects. And nowhere in the driver code, there is no member changes. Only in deserialize, which is overwritten in our code.

So The MongoDB community can do it, if they want it! :)

P.S. Than you to read the long post, here is a Potato

like image 73
György Gulyás Avatar answered Oct 18 '22 20:10

György Gulyás