Custom type serialization: LINQ support

47 views
Skip to first unread message

Vyacheslav Stroy

unread,
Aug 26, 2016, 6:05:36 PM8/26/16
to mongodb-user
I am trying to implement custom type serialization for some generic class container. The main idea is to serialize class as Int64 which is actually mapped to object unique ID.

//interface for objects with Id
interface IPersistentObject {
    long Id { get; }
}

//reference container
public class Ref<T> : IPersistentObject, IEquatable<long> where T : IPersistentObject, new()
{
    public long Id { get; set; }

    private T _Object;

    public T Get()
    {
        return _Object;
    }

    public void Set(T value)
    {
        _Object = value;
        Id = value == null ? 0L : value.Id;
    }

    //... Equals() and IEquatable implemetation here
}

//serializer implementation
public class RefSerializer<T> : SerializerBase<Ref<T>> where T : IPersistentObject, new()
{        
    public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Ref<T> value)
    {
        long v = value;
        context.Writer.WriteInt64(v);
    }
    public override Ref<T> Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
    {
        var id = context.Reader.ReadInt64();
        return new Ref<T> {Id = id};
    }
}

Here is the usage example: 

//both Sample and SampleWithReference types are mapped to separate collections
public class Sample : IPersistentObject
{   
    public long Id { get; set; }
}

public class SampleWithReference : IPersistentObject
{   
    public long Id { get; set; }
    public Ref<Sample> Parent { get; set; }
}

//somewhere in the code
var r = new SampleWithReference();
r.Parent.Set(new Sample());

//and save it all to DB 

Saved SampleWithReference object DB representation looks like that:

{
  "_id": 94329387,
  "Parent": 574832483
}

And finally the problem:

//the following can't be compiled

from r in DB.Query<SampleWithReference>()
join s in DB.Query<Sample>() on r.Parent equals s.Id
select s;

//this works but filter points to Parent.Id which doesn't exist
//in document because Parent object is being serialized to plain int64

from r in DB.Query<SampleWithReference>()
join s in DB.Query<Sample>() on r.Parent.Id equals s.Id
select s;

What I've already tried:
  • IBsonDocumentSerializer implementation on RefSerializer<T>, but it only allows me to specify custom element name, full path rewrite is impossible.
  • Implicit types conversion in query, like (long)r.Parent equals s.Id. Driver failed to execute it, it accepts only field selector expression.
LINQ translators implementation is sealed or internal and I can't hook into the expression parsing process from another assembly.

Any help will be much appreciated.

Craig Wilson

unread,
Aug 26, 2016, 7:45:58 PM8/26/16
to mongodb-user
Yeah, this is going to be really difficult to support. We'll have to find some kind of extension point to expose that you can use. Please file a feature request for something like this. Until then, I'd maybe do something more like this:

public class SampleWithReference : IPersistentObject
{   
         private long? parentId;
         private Sample parent;

    public long Id { get; set; }
    public Sample Parent
    {
        get { return parent; }
        set 
        { 
           parent = value; 
           parentId = parent?.Id;
        }
    }
}

Then, you'll want to manually map this class to exclude the Parent property and include the parentId field.

Vyacheslav Stroy

unread,
Aug 27, 2016, 7:10:24 PM8/27/16
to mongodb-user
Thank you for your answer, Craig.

I thought about the same solution, but unfortunately it is not applicable to my case. I'm trying to create an ActiveRecord-like framework. Having two properties adds additional complexity and, the worst of it, end-users still can use Parent property instead of ParentId in LINQ queries (and of course they will receive an error). 

So far I came with another solution. I derived my Ref<T> from simple RefId class.

public class Ref<T> : RefId where T : IPersistentObejct, new()

public class RefId : IEquatable<long>, IEqualityComparer<long>
{
    protected long Id;
    public override bool Equals(object obj)
    {
        if (obj == null) return false;
        return Id == (long)obj;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }

    public override string ToString()
    {
        return Id.ToString();
    }

    public bool Equals(long other)
    {
        return Id.Equals(other);
    }

    public bool Equals(long x, long y)
    {
        return x.Equals(y);
    }

    public int GetHashCode(long obj)
    {
        return Id.GetHashCode();
    }

    public static implicit operator RefId(long id)
    {
        return new RefId() { Id = id };
    }

    public static implicit operator long(RefId idref)
    {
        return idref.Id;
    }
}

And defined property with this type in base entity object, from which my test entities (Sample and SampleWithReference)derive.

public RefId Id { get; internal set; }

Then added custom serializer for my Id representation with the same logic as in RefSerializer<T>.

And that's it. 

Join equals operator is satisfied because Ref<T> can be downcasted to RefId. Query is being translated correctly despite the fact that it threats Ref<T> and RefId as objects, not the primitive Int64. Therefore the driver executes aggregation command successfully and gives me correct $lookup results. So I'm happy with it.

We'll have to find some kind of extension point to expose that you can use.
 
It will be great. Your BSON serialization conventions mechanism and custom serialization support allows to build really complex solutions. But LINQ is completely sealed, so some scenarios can't be implemented.
I'll try to formalise my thoughts on the that topic and create a feature request.

And by the way, thanks a lot for your LINQ provider for MongoDB. I'm using it since the very first implementations, when it was the separate project. Great work!
 
P.S. I was waiting for moderation of my question long enough to think that it was buried somewhere in the depths of the forum, so I've duplicated my question here: https://jira.mongodb.org/browse/CSHARP-1757
Sorry for that.
Reply all
Reply to author
Forward
0 new messages