DataContractSerializer and Dictionary<string,object> fails when reading
Asked Answered
L

3

7

I'm using DataContractSerializer to serialize an object that contains a Dictionary<string,object> member, which is marked with [DataMember()]. The idea is to have a flexible bag of object attributes, and I don't know what those attributes could be.

This works great when I'm putting int, double and string objects into the dictionary, but when I put a List<string> in it fails to deserialize the object with:

System.InvalidOperationException: Node type Element is not supported in this operation.

The entire dictionary is serialized to XML, and it looks pretty reasonable:

<Attributes xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
    <d2p1:KeyValueOfstringanyType>
        <d2p1:Key>name</d2p1:Key>
        <d2p1:Value xmlns:d4p1="http://www.w3.org/2001/XMLSchema" i:type="d4p1:string">Test object</d2p1:Value>
    </d2p1:KeyValueOfstringanyType>
    <d2p1:KeyValueOfstringanyType>
        <d2p1:Key>x</d2p1:Key>
        <d2p1:Value xmlns:d4p1="http://www.w3.org/2001/XMLSchema" i:type="d4p1:double">0.5</d2p1:Value>
    </d2p1:KeyValueOfstringanyType>
    <d2p1:KeyValueOfstringanyType>
        <d2p1:Key>y</d2p1:Key>
        <d2p1:Value xmlns:d4p1="http://www.w3.org/2001/XMLSchema" i:type="d4p1:double">1.25</d2p1:Value>
    </d2p1:KeyValueOfstringanyType>
    <d2p1:KeyValueOfstringanyType>
        <d2p1:Key>age</d2p1:Key>
        <d2p1:Value xmlns:d4p1="http://www.w3.org/2001/XMLSchema" i:type="d4p1:int">4</d2p1:Value>
    </d2p1:KeyValueOfstringanyType>
    <d2p1:KeyValueOfstringanyType>
        <d2p1:Key>list-of-strings</d2p1:Key>
        <d2p1:Value>
            <d2p1:string>one string</d2p1:string>
            <d2p1:string>two string</d2p1:string>
            <d2p1:string>last string</d2p1:string>
        </d2p1:Value>
    </d2p1:KeyValueOfstringanyType>
</Attributes>

Note the list-of-strings at the end there. It's got all the values but nothing indicating that it's a List<string> or anything.

What's the correct way of handling this situation?

Larcenous answered 1/10, 2011 at 14:5 Comment(0)
B
9

Try using the KnownTypeAttribute so that DataContractSerializer knows about the List<string> type. Unfortunately, that seems to go against your idea of not having to know about the types before hand.

I'm basing this on the following code, which uses DataContractSerializer to serialize a Dictionary<string, object> containing List<string>:

Dictionary<string,object> dictionary = new Dictionary<string, object>();

dictionary.Add("k1", new List<string> { "L1", "L2", "L3" });

List<Type> knownTypes = new List<Type> { typeof(List<string>) };
DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string,object>), knownTypes);
MemoryStream stream = new MemoryStream();

serializer.WriteObject(stream, dictionary);

StreamReader reader = new StreamReader(stream);

stream.Position = 0;
string xml = reader.ReadToEnd();

If you knownTypes is not provided to the DataContractSerializer, it throws an exception.

SerializationException: Type 'System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]' with data contract name 'ArrayOfstring:http://schemas.microsoft.com/2003/10/Serialization/Arrays' is not expected. Consider using a DataContractResolver or add any types not known statically to the list of known types - for example, by using the KnownTypeAttribute attribute or by adding them to the list of known types passed to DataContractSerializer.

Bicameral answered 1/10, 2011 at 14:37 Comment(0)
I
0

WCF has no way of knowing that what you have is a List<string> there - notice that all the other <Value> elements have a "type hint" (the i:type attribute). If you want to deserialize it, it needs to have the marking, and you also need to tell WCF that List<string> is a "known type" - see below. For more information on known types (and why they're needed) there are many good resources in the web.

public class StackOverflow_7620718
{
    public static void Test()
    {
        Dictionary<string, object> dict = new Dictionary<string, object>
        {
            { "name", "Test object" },
            { "x", 0.5 },
            { "y", 1.25 },
            { "age", 4 },
            { "list-of-strings", new List<string> { "one string", "two string", "last string" } }
        };
        MemoryStream ms = new MemoryStream();
        XmlWriter w = XmlWriter.Create(ms, new XmlWriterSettings
        {
            Indent = true,
            Encoding = new UTF8Encoding(false),
            IndentChars = "  ",
            OmitXmlDeclaration = true,
        });
        DataContractSerializer dcs = new DataContractSerializer(dict.GetType(), new Type[] { typeof(List<string>) });
        dcs.WriteObject(w, dict);
        w.Flush();
        Console.WriteLine(Encoding.UTF8.GetString(ms.ToArray()));
        ms.Position = 0;
        Console.WriteLine("Now deserializing it:");
        Dictionary<string, object> dict2 = (Dictionary<string, object>)dcs.ReadObject(ms);
        foreach (var key in dict2.Keys)
        {
            Console.WriteLine("{0}: {1}", key, dict2[key].GetType().Name);
        }
    }
}
Instrumentation answered 1/10, 2011 at 14:39 Comment(0)
S
0

I had a similiar idea number of times, and I implemented it with additional field holding assembly qualified names of all Dictionary items. It populates list on every item addition or rewrite, then uses it on serialization and uses XmlReader to extract type information, build list of types and deserialize object.

Code:

[DataContract]
public class Message
{
    [DataMember] private List<string> Types = new List<string>();
    [DataMember] private Dictionary<string, object> Data = new Dictionary<string, object>();

    public object this[string id]
    {
        get => Data.TryGetValue(id, out var o) ? o : null;
        set {
            Data[id] = value;
            if (!Types.Contains(value.GetType().AssemblyQualifiedName))
                Types.Add(value.GetType().AssemblyQualifiedName);
        }
    }

    public byte[] Serialize()
    {
        var dcs = new DataContractSerializer(typeof(Message), Types.Select(Type.GetType));
        using (var ms = new MemoryStream()) {
            dcs.WriteObject(ms, this);
            return ms.ToArray();
        }
    }

    public static Message Deserialize(byte[] input)
    {
        var types = new List<string>();
        using (var xr = XmlReader.Create(new StringReader(Encoding.UTF8.GetString(input)))) {
            if (xr.ReadToFollowing(nameof(Types))) {
                xr.ReadStartElement();
                while (xr.NodeType != XmlNodeType.EndElement) {
                    var res = xr.ReadElementContentAsString();
                    if (!string.IsNullOrWhiteSpace(res))
                        types.Add(res);
                }
            }
        }
        var dcs = new DataContractSerializer(typeof(Message), types.Select(Type.GetType));
        using (var ms = new MemoryStream(input))
            if (dcs.ReadObject(ms) is Message msg)
                return msg;
        return null;
    }
}
Sauers answered 5/3, 2019 at 11:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.