Wednesday, July 16, 2008

Yield Returns and Remoting

I ran across a weird error message the other day:



System.Runtime.Serialization.SerializationException: Test method YieldSerializtionTests.Test.TestRemoting threw exception: System.Runtime.Serialization.SerializationException: Type 'YieldSerialization.RemoteObject+d__0' in Assembly 'YieldSerialization, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' is not marked as serializable.



Well I looked at looked at type RemoteObject and it was marked as serializable and inherited from MarshalByRef. So that seamed fine. So what the heck is the CLR's problem? I'll let you look at the code and see if you can spot the problem.

The server:


public class Server : IDisposable
{
    private IChannel channel;
    public Server()
    {
        this.channel = new TcpChannel(Common.Port);
        ChannelServices.RegisterChannel(this.channel,false);
        RemotingConfiguration.RegisterWellKnownServiceType(typeof(RemoteObject),
            Common.ObjectName, WellKnownObjectMode.Singleton);
    }
    public void Dispose()
    {
        ChannelServices.UnregisterChannel(this.channel);
    }
}

The Client:


namespace YieldSerialization
{
    public class Client
    {
        private RemoteObject myObject;
        public Client()
        {
            this.myObject = (RemoteObject)Activator.GetObject(typeof(RemoteObject),
                string.Format("tcp://localhost:{0}/{1}",Common.Port, Common.ObjectName));
        }
 
        public int Works
        {
            get
            {
                return this.myObject.WorkingList.Count();
            }
        }
 
        public int DoesNotWork
        {
            get
            {
                return this.myObject.NonWorkingList.Count();
            }
        }
    }
}

The test:


[TestMethod()]
public void TestRemoting()
{
    using (Server s = new Server())
    {
        Client c = new Client();
        Console.WriteLine(c.Works);
        Console.WriteLine(c.DoesNotWork);
    }
}

The (outline of the) Remote Object:


[Serializable]
public class RemoteObject : MarshalByRefObject
{
    public IEnumerable<int> NonWorkingList
 
    public IEnumerable<int> WorkingList
}


One thing to note is the the call to WorkingList works fine, where as the call to NonWorkingList causes problems, but they have the same signature, how can that be? Well lets take a look at the full remote object:


[Serializable]
public class RemoteObject : MarshalByRefObject
{
    public IEnumerable<int> NonWorkingList
    {
        get
        {
            for (int i = 0; i < 10; i++)
            {
                yield return i;
            }
        }
    }
 
    public IEnumerable<int> WorkingList
    {
        get
        {
            List<int> results = new List<int>();
            for (int i = 0; i < 10; i++)
            {
                results.Add(i);
            }
            return results;
        }
    }
}


As you may have groked from the title of the article yield returns and remoting don't get along. Using the yield return generates a separate hidden class that isn't marked as serializable. The trick is that you have to provide a custom enumerator. Seams a little bogus to me, if the compiler is generating a fake class from a serializable class it should do everything if can to generate a serializable class for you.

The worst part of this is that it is only caught at runtime. If making the hidden class serializable is not feasible then the next best thing would be to error or warn at compile time. Maybe a warning when using yield returns in serializable classes?

0 comments: