C# How to Serialize/Deserialize an IEnumerator from a Yield function (to Json?)

Viewed 22

I'm trying to serialize/deserialize an IEnumerator generated from a function using yield. I would like to serialize the IEnumerator at any iteration, I don't want to force it to generate all of it values.

I know that the yield keyword generate a class behind the scene, and that why I'm using it, to avoid manually writing iterator, and also to make the code cleaner.

My goal is to make a small game engine similar to Nick Gravelyn - The magic of yield, where each GameElement generate an iterator about his behavior, allowing the programmer to easily control timings (because the iterator allow to interrupt and continue a script). I want to try to add a multiplayer layer on top of that, that why I need to serialize/deserialize an IEnumerator.

Forcing the IEnumerator to generate all of his values also force the game to update, this should be avoided.

My first tentative was somethings like :

using System;
using System.Collections.Generic;
using System.Text.Json;

namespace SerializeTest
{
    class Program
    {
        public static IEnumerator<int> CountTo(int end)
        {
            for(int i = 1; i <= end; i++) 
            {
                Console.WriteLine("i = " + i);
                yield return i;
            }
        }

        static void Main(string[] args)
        {
            IEnumerator<int> e = CountTo(5);
            string json = JsonSerializer.Serialize(e);
            Console.WriteLine(json);
            var f = JsonSerializer.Deserialize<IEnumerator<int>> (json); // crash because interface

            while (f.MoveNext());
        }
    }
}

But deserializing an interface (IEnumerator<int>) is illegal, we need to know the Type of the object behind IEnumerator<int>.

Since the type is generated by the compiler, I try to use reflection over the Deserialize method in order to call it with the correct type generated by the function CountTo() (SerializeTest.Program+<CountTo>d__0 if you are curious)

So my second tentative was somethings like :

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;

namespace SerializeTest
{
    class Program
    {
        public static IEnumerator<int> CountTo(int end)
        {
            for(int i = 1; i <= end; i++) 
            {
                Console.WriteLine("i = " + i);
                yield return i;
            }
        }

        public static T Deserialize<T>(string json) => JsonSerializer.Deserialize<T>(json); // still crash

        static void Main(string[] args)
        {
            IEnumerator<int> e = CountTo(5);

            string json = JsonSerializer.Serialize(e);
            Console.WriteLine(json);

            Type eType = e.GetType();
            MethodInfo method = typeof(Program).GetMethod("Deserialize");
            MethodInfo genericMethod = method.MakeGenericMethod(eType);

            object fObj = genericMethod.Invoke(null, new object[] { json });
            var f = (IEnumerator<int>)fObj;
            while (f.MoveNext()) ;
        }
    }
}

but it still crash when calling JsonSerializer.Deserialize() even if the type is now correct and not an interface.

(System.InvalidOperationException : 'Each parameter in constructor 'Void .ctor(Int32)' on type 'SerializeTest.Program+<CountTo>d__0' must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object. The match can be case-insensitive.' )

The format don't have any importance (json, xml...) as long as it is possible to send it to another computer and deserialize it back.

I naively hope that there is a way to Serialize/Deserialize an IEnumerator because a lot of cool stuff can be done with it, although I have some serious doubts about such a things to be possible.

Thank for reading so far.

1 Answers

I naively hope that there is a way to Serialize/Deserialize an IEnumerator because a lot of cool stuff can be done with it, although I have some serious doubts about such a things to be possible.

Sending code (and enumerator state machine generated by the compiler which you are trying to send is basically a code) is far more complex then just serializing it to json/xml/etc. For example Apache Ignite supports sending code to another nodes including assembly peer loading. One of the starting points for investigation can be here.

As for your attempt, as you should have already seen that serialized version of the state machine contains only one one property - Current: {"Current":0} while the generated class contains some other state data looking something like this:

private sealed class <<<Main>$>g__CountTo|0_0>d : IEnumerator<int>, IEnumerator, IDisposable
{
    private int <>1__state;

    private int <>2__current;

    public int end;

    private int <i>5__1;

    int IEnumerator<int>.Current
    {
        [DebuggerHidden]
        get
        {
            return <>2__current;
        }
    }
    // ... rest of the generated code
}

While you can look into writing your own custom converter that will use reflection to actually serialize/deserialize the internal state data (that is possibly not that hard), it can be quite brittle (class names can change, the generated code can change, the code generator code can change, and we have not even started with multiversion client support), in the end those fields a private for a reason, so I suggest looking into some set of messages the multiplayer clients will send to each other and you can turn them into iterators, classes, method calls, etc.

Related