19

I have a class Team that holds a generic list:

[DataContract(Name = "TeamDTO", IsReference = true)]
public class Team
{
    [DataMember]
    private IList<Person> members = new List<Person>();

    public Team()
    {
        Init();
    }

    private void Init()
    {
        members = new List<Person>();
    }

    [System.Runtime.Serialization.OnDeserializing]
    protected void OnDeserializing(StreamingContext ctx)
    {
        Log("OnDeserializing of Team called");
        Init();
        if (members != null) Log(members.ToString());
    }

    [System.Runtime.Serialization.OnSerializing]
    private void OnSerializing(StreamingContext ctx)
    {
        Log("OnSerializing of Team called");
        if (members != null) Log(members.ToString());
    }

    [System.Runtime.Serialization.OnDeserialized]
    protected void OnDeserialized(StreamingContext ctx)
    {
        Log("OnDeserialized of Team called");
        if (members != null) Log(members.ToString());
    }

    [System.Runtime.Serialization.OnSerialized]
    private void OnSerialized(StreamingContext ctx)
    {
        Log("OnSerialized of Team called");
        Log(members.ToString());
    }

When I use this class in a WCF service, I get following log output

OnSerializing of Team called    
System.Collections.Generic.List 1[XXX.Person]

OnSerialized of Team called    
System.Collections.Generic.List 1[XXX.Person]

OnDeserializing of Team called    
System.Collections.Generic.List 1[XXX.Person]

OnDeserialized of Team called    
XXX.Person[]

After the deserialization members is an Array and no longer a generic list although the field type is IList<> (?!) When I try to send this object back over the WCF service I get the log output

OnSerializing of Team called
XXX.Person[]

After this my unit test crashes with a System.ExecutionEngineException, which means the WCF service is not able to serialize the array. (maybe because it expected a IList<>)

So, my question is: Does anybody know why the type of my IList<> is an array after deserializing and why I can't serialize my Team object any longer after that?

Thanks

Fabiano
  • 5,124
  • 6
  • 42
  • 69

4 Answers4

21

You've run into one of the DataContractSerializer gotchas.

Fix: Change your private member declaration to:

[DataMember]
private List<Person> members = new List<Person>();

OR change the property to:

[DataMember()]
public IList<Person> Feedback {
    get { return m_Feedback; }
    set {
        if ((value != null)) {
            m_Feedback = new List<Person>(value);

        } else {
            m_Feedback = new List<Person>();
        }
    }
}

And it will work. The Microsoft Connect bug is here

This problem occurs when you deserialize an object with an IList<T> DataMember and then try to serialize the same instance again.

If you want to see something cool:

using System;
using System.Collections.Generic;

class TestArrayAncestry
{
    static void Main(string[] args)
    {
        int[] values = new[] { 1, 2, 3 };        
        Console.WriteLine("int[] is IList<int>: {0}", values is IList<int>);
    }
}

It will print int[] is IList<int>: True.

I suspect this is possibly the reason you see it come back as an array after deserialization, but it is quite non-intuitive.

If you call the Add() method on the IList<int> of the array though, it throws NotSupportedException.

One of those .NET quirks.

Leon Breedt
  • 1,196
  • 8
  • 13
  • 3
    Thanks. That's really hard to find out what an "I" can affect. In the meanwhile I found an other solution especially when you require an IList<> instead of List<> (e.g. my OR-Mapper can only handle IList<> but not List<>) : instead of marking the "members" field with [DataMember] mark the property and use this code for the setter: if (value != null) members = new List(value); else members = new List(); With this the deserialized array is passed to the setter which creates a new List from it. – Fabiano Apr 06 '10 at 08:59
  • The latter technique is what I always do as well. That member field is not something that the serializer should be touching. – IDisposable Apr 13 '12 at 19:55
  • I second the property approach! I'm upset that I did not think of it myself. – codechurn Nov 06 '12 at 21:16
2

I got this error while transporting an IList read from a database via LINQ. The WCF was hosted in IIS 7 on a Windows Server 2008 x64.

The app pool crashed with no warnings.

[ServiceBehavior]
public class Webservice : IWebservice
{

    public IList<object> GetObjects()
    {
        return Database.Instance.GetObjects();
    }
}

Its not exactly the same problem but may have the same cause.

The resolution for was to install MS hotfix KB973110 http://support.microsoft.com/kb/971030/en-us

Jacob
  • 3,598
  • 4
  • 35
  • 56
Fedearne
  • 7,049
  • 4
  • 27
  • 31
1

Taken straight from my blog. i hope it will be helpful:

I recently ran into an issue where we were consuming a WCF service and using a custom model binder in our ASP.net MVC app. Everything worked fine excerpt when we were serializing our ILists. IList gets serialized to arrays by default. I ended up converting our the arrays back to ILists using Reflection and calling the following method in the custom model binder. Here is how method looks like:

public object ConvertArraysToIList(object returnObj)    
{

if (returnObj != null)
{
    var allProps = returnObj.GetType().GetProperties().Where(p => p.PropertyType.IsPublic 
        && p.PropertyType.IsGenericType 
        && p.PropertyType.Name==typeof(IList<>).Name).ToList();

    foreach (var prop in allProps)
    {
        var value = prop.GetValue(returnObj,null);
        //set the current property to a new instance of the IList<>
        var arr=(System.Array)value; 
        Type listType=null;

        if(arr!=null)
        {
            listType= arr.GetType().GetElementType();
        }

        //create an empty list of the specific type
        var listInstance = typeof(List<>)
          .MakeGenericType(listType)
          .GetConstructor(Type.EmptyTypes)
          .Invoke(null);

        foreach (var currentValue in arr)
        {
            listInstance.GetType().GetMethod("Add").Invoke(listInstance, new[] { currentValue });
        }

        prop.SetValue(returnObj, listInstance, null);
    }
}

return returnObj;
}
Fabiano
  • 5,124
  • 6
  • 42
  • 69
1

It sounds like your WCF service reference is creating a proxy class rather than using the existing type. Proxy classes can only use simple arrays and not any .NET specific types like the generic List.

To avoid this proxy class conversion, in the Add Service Reference screen, click the Advanced button, and then make sure "Reuse types in referenced assemblies" is checked. This will ensure that the existing class (with the generic List) is used when serializing and deserializing the object.

jeremcc
  • 8,633
  • 11
  • 45
  • 55
  • I don't use a service reference here. I have a reference to my WCF project and create the host and the channel programmatically. So, I reuse the types by myself – Fabiano Apr 01 '10 at 23:36
  • I see. Well in that case, I have no idea. ;-) – jeremcc Apr 02 '10 at 16:01