Search code examples
c#wcfserializationdatacontractserializer

Cannot serialize member ... of type System.Collections.Generic.Dictionary`2 because it implements IDictionary


I am trying to pass a class with a Dictionary property over WCF and it is failing for one method but works for another. When the class is returned inside a List, it works. But when the class is returned inside a DataTable, the client just says the connection was disconnected and no error shows up.

Here is the class causing issues:

[DataContract]
public class DetailLog
{
    [DataMember]
    public string Name
    {
        get;
        set;
    }

    [DataMember]
    public string SubAction
    {
        get;
        set;
    }

    [DataMember]
    public Dictionary<string, string> Fields
    {
        get;
        set;
    }

    [DataMember]
    public string UserName
    {
        get;
        set;
    }
}

I started out creating a method that works without issue:

public List<DetailLog> GetDetailLog(List<int> IDs, List<int> actionTypeIds, List<int> userIds, DateTime beginDate, DateTime endDate)

Then we needed to create some very dynamic reports so I used a DataTable which we have used previously for other dynamic reports.

But I needed to pass along the DetailLog class so I created a DataTable column of that type:

public DataTable GetCustomDetailReport(int CustomReportID, List<CustomReportFilter> reportFilters)
{
DataTable data = new DataTable();
...
data.Columns.Add("DetailLog", typeof(DetailLog));
...
}

This method would would exit fine on the WCF host side but the client side would error about the connection being lost. I tried adding ServiceKnownType for the OperationContract in the interface but it did not fix it:

[OperationContract]
[ServiceKnownType(typeof(DetailLog))]
DataTable GetCustomUserAuditReport(int CustomReportID, List<CustomReportFilter> reportFilters);

I cannot really debug the serialization when the method returns the DataTable so I added this code to the end of the GetCustomDetailReport() to catch the error.

DataContractSerializer ser = new DataContractSerializer(typeof(DataTable), new List<Type> { typeof(DetailLog) });
ser.WriteObject(Stream.Null, data);

When I did, I saw an exception

Cannot serialize member ... of type System.Collections.Generic.Dictionary`2 because it implements IDictionary.

Solution

  • Your issue is as follows:

    1. Dictionary<TKey, TValue> is supported by the data contract serializer used in WCF, as explained in the docs. This is why your DetailLog class can be sent over the wire successfully by WCF as a root object.

    2. This serializer also supports IXmlSerializable to allow types to manually serialize themselves to XML.

    3. DataTable implements IXmlSerializable.

    4. Internally, DataTable serializes non-primitive entries using XmlSerializer -- a completely different serializer that uses a completely different code base.

    5. XmlSerializer does not support dictionaries. Thus your DetailLog cannot be sent over the wire by WCF when nested in a DataTable.

    6. As an unrelated problem, you also need to set the data table name, serialization will throw an exception if you do not:

          data.TableName = "CustomDetailReport"; // For instance
      

    To work around the issue with dictionaries, you need to make your DetailLog class serializable by both serializers. The question How to serialize/deserialize to Dictionary<int, string> from custom XML not using XElement? gives a variety of ways to serialize its dictionary property, including using a proxy array property:

    [XmlType("KeyValue"), XmlRoot("KeyValue")]
    public class SerializableKeyValuePair<TKey, TValue>
    {
        public TKey Key { get; set; }
        public TValue Value { get; set; }
    }
    
    public static class SerializableKeyValuePairExtensions
    {
        public static SerializableKeyValuePair<TKey, TValue> ToSerializablePair<TKey, TValue>(this KeyValuePair<TKey, TValue> pair)
        {
            return new SerializableKeyValuePair<TKey, TValue> { Key = pair.Key, Value = pair.Value };
        }
    }
    
    [DataContract]
    public class DetailLog
    {
        [DataMember]
        public string Name { get; set; }
    
        [DataMember]
        public string SubAction { get; set; }
    
        [DataMember]
        [XmlIgnore]
        public Dictionary<string, string> Fields { get; set; }
    
        [IgnoreDataMember]
        [XmlArray("Fields")]
        [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
        public SerializableKeyValuePair<string, string>[] FieldsArray
        {
            get
            {
                return Fields == null ? null : Fields.Select(p => p.ToSerializablePair()).ToArray();
            }
            set
            {
                Fields = value == null ? null : value.ToDictionary(p => p.Key, p => p.Value);
            }
        }
    
        [DataMember]
        public string UserName { get; set; }
    }
    

    WCF should now be able to send your DataTable over the wire successfully.