3

I'm trying to serialize this type of object using protobuf-net:

[ProtoContract]
public class RedisDataObject
{
    [ProtoMember(1)]
    public string DataHash;
    [ProtoMember(2, DynamicType = true)]
    public Dictionary<ContextActions, List<Tuple<string, List<object>>>> Value;
}

[Serializable]
public enum ContextActions
{
    Insert,
    Update,
    Delete
}

I'm using List<object> because I'm storing there different class instances of other classes I have in my code.

But I'm getting this error message:

Unable to resolve a suitable Add method for System.Collections.Generic.Dictionary...

This is clearly because of the dictionary, but I couldn't find a solution how to resolve this issue.

dbc
  • 104,963
  • 20
  • 228
  • 340
Liran Friedman
  • 4,027
  • 13
  • 53
  • 96

2 Answers2

5

Your basic problem is that DynamicType = true applies to that specific property only, and serializes type information for the value of that specific property only. It doesn't apply recursively to any of the properties contained by that object. However, your object value is nested deep within several levels of container:

public Dictionary<ContextActions, List<Tuple<string, List<object>>>> Value;

What you need to do is to serialize type information for each object inside this dictionary of tuples of lists. You can do this by introducing a surrogate value type:

[ProtoContract]
public struct DynamicTypeSurrogate<T>
{
    [ProtoMember(1, DynamicType = true)]
    public T Value { get; set; }
}

public static class DynamicTypeSurrogateExtensions
{
    public static List<DynamicTypeSurrogate<T>> ToSurrogateList<T>(this IList<T> list)
    {
        if (list == null)
            return null;
        return list.Select(i => new DynamicTypeSurrogate<T> { Value = i }).ToList();
    }

    public static List<T> FromSurrogateList<T>(this IList<DynamicTypeSurrogate<T>> list)
    {
        if (list == null)
            return null;
        return list.Select(i => i.Value).ToList();
    }
}

And then modifying your RedisDataObject to serialize a surrogate dictionary as follows:

[ProtoContract]
public class RedisDataObject
{
    [ProtoMember(1)]
    public string DataHash;

    [ProtoIgnore]
    public Dictionary<ContextActions, List<Tuple<string, List<object>>>> Value;

    [ProtoMember(2)]
    private Dictionary<ContextActions, List<Tuple<string, List<DynamicTypeSurrogate<object>>>>> SurrogateValue
    {
        get
        {
            if (Value == null)
                return null;
            var dictionary = Value.ToDictionary(
                p => p.Key,
                p => (p.Value == null ? null : p.Value.Select(i => Tuple.Create(i.Item1, i.Item2.ToSurrogateList())).ToList()));
            return dictionary;
        }
        set
        {
            if (value == null)
                Value = null;
            else
            {
                Value = value.ToDictionary(
                    p => p.Key,
                    p => (p.Value == null ? null : p.Value.Select(i => Tuple.Create(i.Item1, i.Item2.FromSurrogateList())).ToList()));
            }
        }
    }
}

Note also the restrictions on DynamicType mentioned here:

DynamicType - stores additional Type information with the type (by default it includes the AssemblyQualifiedName, although this can be controlled by the user). This makes it possible to serialize weak models, i.e. where object is used for property members, however currently this is limited to contract types (not primitives), and does not work for types with inheritance (these limitations may be removed at a later time). Like with AsReference, this uses a very different layout format

While the documentation above exists at the former project site and has not been moved to the current site, the restriction on non-contract types definitely still exists as of version 2.0.0.668. (I tested that adding an int value to the List<object> fails; I have not checked whether the restriction on inheritance still exists.)

dbc
  • 104,963
  • 20
  • 228
  • 340
  • I get an `InvalidOperationException` saying _Dynamic type is not a contract-type: _, object lists containing doubles, decimals, dates and strings. – andrei.ciprian Aug 30 '16 at 08:41
  • @andrei.ciprian - that sounds like the issue described in [this question](https://stackoverflow.com/questions/17656133). – dbc Aug 30 '16 at 08:44
  • I like your approach, but it's not working, for any type. The question you suggested I imagine would, [as this one would](http://stackoverflow.com/a/38914841/2239678) – andrei.ciprian Aug 30 '16 at 09:02
  • @andrei.ciprian - it works when each object is a *class instance of other classes* as mentioned in the original question -- i.e. some POCO that maps to a data contract type. I tested with a couple of example `[ProtoContract]` classes. But maybe I could combine this with the answer from [here](https://stackoverflow.com/questions/17656133) and remove the restriction on non-contract types. – dbc Aug 30 '16 at 09:22
  • @andrei.ciprian, You need to add the attribute `[ProtoContract]` to your class. take a look [here](http://www.codeproject.com/Articles/642677/Protobuf-net-the-unofficial-manual) to see how to set it up. Basically, you will need to either add the attribute [ProtoMember(N)] to the fields you want to use or just use the `ImplicitFields` annotaion like this: `[ProtoContract(ImplicitFields = ImplicitFields.AllFields)]` to incluse all fields in your class. This annotation `ImplicitFields` has more options, so you can explore and use as you need – Liran Friedman Aug 30 '16 at 11:41
  • @dbc - Works great, thank you very much for this great solution and explenation. – Liran Friedman Aug 30 '16 at 11:43
1

With help from dbc, and all links mentioned in his answer and comments

[ProtoContract]
[ProtoInclude(1, typeof(ObjectWrapper<int>))]
[ProtoInclude(2, typeof(ObjectWrapper<decimal>))]
[ProtoInclude(3, typeof(ObjectWrapper<DateTime>))]
[ProtoInclude(4, typeof(ObjectWrapper<string>))]
[ProtoInclude(5, typeof(ObjectWrapper<double>))]
[ProtoInclude(6, typeof(ObjectWrapper<long>))] 
[ProtoInclude(8, typeof(ObjectWrapper<Custom>))]
[ProtoInclude(9, typeof(ObjectWrapper<CustomType[]>))]
public abstract class ObjectWrapper
{
  protected ObjectWrapper() { }
  abstract public object ObjectValue { get; set; }

  public static ObjectWrapper Create(object o) {
    Type objectType = o.GetType();
    Type genericType = typeof(ObjectWrapper<>);
    Type specializedType = genericType.MakeGenericType(objectType);
    return (ObjectWrapper)Activator.CreateInstance(specializedType, new object[] { o });
  }
}

Downside is you have to register all types you are using in your list of objects. Every time a new type that is not included in the ProtoInclude series surfaces out, you would get an InvalidOperationException with message Unexpected sub-type: ObjectWrapper`1[[NewType]].

[ProtoContract]
public class RedisDataObjectWrapper {
  [ProtoMember(1)] public string DataHash;
  [ProtoIgnore] public Dictionary<ContextActions, List<Tuple<string, List<object>>>> Value;

[ProtoMember(2)] 
private Dictionary<ContextActions, List<Tuple<string, List<ObjectWrapper>>>> AdaptedValue {
  get {
    if (Value == null) return null;
    var dictionary = Value.ToDictionary(
        p => p.Key,
        p => (p.Value == null ? null : p.Value.Select(i => Tuple.Create(i.Item1, i.Item2.Select(x=>ObjectWrapper.Create(x)).ToList() )).ToList()));
    return dictionary;
  }
  set {
    if (value == null) Value = null;
    else {
      Value = value.ToDictionary(
          p => p.Key,
          p => (p.Value == null ? null : p.Value.Select(i => Tuple.Create(i.Item1, i.Item2.Select(x=>x.ObjectValue).ToList() )).ToList()));
    } } } }
Community
  • 1
  • 1
andrei.ciprian
  • 2,895
  • 1
  • 19
  • 29
  • This solutions worked great for me! For all those who are using this solution you'll need to create an ObjectWrapper class. Here is the implementation I used. [ProtoContract] public class ObjectWrapper : ObjectWrapper { public ObjectWrapper() :base(){ } public ObjectWrapper(object val) :this() { ObjectValue = val; } public override object ObjectValue { get => Value; set => Value = (T)value; } [ProtoMember(1)] public T Value { get; set; } } – John C Jan 21 '21 at 17:28