Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
289 views
in Technique[技术] by (71.8m points)

c# - how come BinaryFormatter can serialize an Action<> but Json.net cannot

Trying to serialize/deserialize an Action<>.

Try #1 naive by me

JsonConvert.SerializeObject(myAction);
...
JsonConvert.Deserialize<Action>(json);

Deserialize fails saying it cannot serialize Action.

Try #2

JsonConvert.DeserializeObject<Action>(ctx.SerializedJob, new JsonSerializerSettings {ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor });

Same(ish) failure.

Try # 3 Then I found http://mikehadlow.blogspot.com/2011/04/serializing-continuations.html

This uses BinaryFormatter. I dropped this in (base64 encoding the binary to a string). Worked perfectly first time.

Try #4

I then found

https://github.com/DevrexLabs/Modules.JsonNetFormatter

Which is an IFormatter module for json.net. Wired that in, same failure - cannot deserialize.

So how come BinaryFormatter can do it but Json.net cannot?

EDIT:

The general reply is - "thats the most stupid thing to want to do". Let me show what I am trying to do

MyJobSystem.AddJob(ctx=>
{
   // code to do
   // ......
}, DateTime.UtcNow + TimeSpan.FromDays(2));

Ie - execute this lambda in 2 days time.

This works fine for me. Using BinaryFormatter. I was curious about why one serializing infrastructure could do it but the other could not. They both seem to have the same rules about what can and cannot be processed

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The reason that BinaryFormatter is (sometimes) able to round-trip an Action<T> is that such delegates are marked as [Serializable] and implement ISerializable.

However, just because the delegate itself is marked as serializable doesn't mean that its members can be serialized successfully. In testing, I was able to serialize the following delegate:

Action<int> a1 = (a) => Console.WriteLine(a);

But attempting to serialize the following threw a SerializationException:

int i = 0;
Action<int> a2 = (a) => i = i + a;

The captured variable i apparently is placed in a non-serializable compiler-generated class thereby preventing binary serialization of the delegate from succeeding.

On the other hand, Json.NET is unable to round-trip an Action<T> despite supporting ISerializable because it does not provide support for serialization proxies configured via SerializationInfo.SetType(Type). We can confirm that Action<T> is using this mechanism with the following code:

var iSerializable = a1 as ISerializable;
if (iSerializable != null)
{
    var info = new SerializationInfo(a1.GetType(), new FormatterConverter());
    var initialFullTypeName = info.FullTypeName;
    iSerializable.GetObjectData(info, new StreamingContext(StreamingContextStates.All));
    Console.WriteLine("Initial FullTypeName = "{0}", final FullTypeName = "{1}".", initialFullTypeName, info.FullTypeName);
    var enumerator = info.GetEnumerator();
    while (enumerator.MoveNext())
    {
        Console.WriteLine("   Name = {0}, objectType = {1}, value = {2}.", enumerator.Name, enumerator.ObjectType, enumerator.Value);
    }
}

When run, it outputs:

Initial FullTypeName = "System.Action`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]", final FullTypeName = "System.DelegateSerializationHolder".
   Name = Delegate, objectType = System.DelegateSerializationHolder+DelegateEntry, value = System.DelegateSerializationHolder+DelegateEntry.
   Name = method0, objectType = System.Reflection.RuntimeMethodInfo, value = Void <Test>b__0(Int32).

Notice that FullTypeName has changed to System.DelegateSerializationHolder? That's the proxy, and it's not supported by Json.NET.

This begs the question, just what is written out when a delegate is serialized? To determine this we can configure Json.NET to serialize Action<T> similarly to how BinaryFormatter would by setting

If I serialize a1 using these settings:

var settings = new JsonSerializerSettings
{
    TypeNameHandling = TypeNameHandling.All,
    ContractResolver = new DefaultContractResolver
    {
        IgnoreSerializableInterface = false,
        IgnoreSerializableAttribute = false,
    },
    Formatting = Formatting.Indented,
};
var json = JsonConvert.SerializeObject(a1, settings);
Console.WriteLine(json);

Then the following JSON is generated:

{
  "$type": "System.Action`1[[System.Int32, mscorlib]], mscorlib",
  "Delegate": {
    "$type": "System.DelegateSerializationHolder+DelegateEntry, mscorlib",
    "type": "System.Action`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]",
    "assembly": "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
    "target": null,
    "targetTypeAssembly": "Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
    "targetTypeName": "Question49138328.TestClass",
    "methodName": "<Test>b__0",
    "delegateEntry": null
  },
  "method0": {
    "$type": "System.Reflection.RuntimeMethodInfo, mscorlib",
    "Name": "<Test>b__0",
    "AssemblyName": "Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
    "ClassName": "Question49138328.TestClass",
    "Signature": "Void <Test>b__0(Int32)",
    "MemberType": 8,
    "GenericArguments": null
  }
}

The replacement FullTypeName is not included but everything else is. And as you can see, it's not actually storing the IL instructions of the delegate; it's storing the full signature of the method(s) to call, including the hidden, compiler-generated method name <Test>b__0 mentioned in this answer. You can see the hidden method name yourself just by printing a1.Method.Name.

Incidentally, to confirm that Json.NET is really saving the same member data as BinaryFormatter, you can serialize a1 to binary and print any embedded ASCII strings as follows:

var binary = BinaryFormatterHelper.ToBinary(a1);
var s = Regex.Replace(Encoding.ASCII.GetString(binary), @"[^u0020-u007E]", string.Empty);
Console.WriteLine(s);
Assert.IsTrue(s.Contains(a1.Method.Name)); // Always passes

Using the extension method:

public static partial class BinaryFormatterHelper
{
    public static byte[] ToBinary<T>(T obj)
    {
        using (var stream = new MemoryStream())
        {
            new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter().Serialize(stream, obj);
            return stream.ToArray();
        }
    }
}

Doing so results in the following string:

????"System.DelegateSerializationHolderDelegatemethod00System.DelegateSerializationHolder+DelegateEntry/System.Reflection.MemberInfoSerializationHolder0System.DelegateSerializationHolder+DelegateEntrytypeassemblytargettargetTypeAssemblytargetTypeNamemethodNamedelegateEntry0System.DelegateSerializationHolder+DelegateEntrylSystem.Action`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]Kmscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=nullQuestion49138328.TestClass<Test>b__0/System.Reflection.MemberInfoSerializationHolderNameAssemblyNameClassNameSignatureMemberTypeGenericArgumentsSystem.Type[]Void <Test>b__0(Int32)

And the assert never fires, indicating that the compiler-generated method name <Test>b__0 is indeed present in the binary also.

Now, here's the scary part. If I modify my c# source code to create another Action<T> before a1, like so:

// I inserted this before a1 and then recompiled: 
Action<int> a0 = (a) => Debug.WriteLine(a);

Action<int> a1 = (a) => Console.WriteLine(a);

Then re-build and re-run, a1.Method.Name changes to <Test>b__1:

{
  "$type": "System.Action`1[[System.Int32, mscorlib]], mscorlib",
  "Delegate": {
    "$type": "System.DelegateSerializationHolder+DelegateEntry, mscorlib",
    "type": "System.Action`1[[System.Int32, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]",
    "assembly": "mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
    "target": null,
    "targetTypeAssembly": "Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
    "targetTypeName": "Question49138328.TestClass",
    "methodName": "<Test>b__1",
    "delegateEntry": null
  },
  "method0": {
    "$type": "System.Reflection.RuntimeMethodInfo, mscorlib",
    "Name": "<Test>b__1",
    "AssemblyName": "Tile, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
    "ClassName": "Question49138328.TestClass",
    "Signature": "Void <Test>b__1(Int32)",
    "MemberType": 8,
    "GenericArguments": null
  }
}

Now if I deserialize binary data for a1 saved from the earlier version, it comes back as a0! Thus, adding another delegate somewhere in your code base, or otherwise refactoring your code in an apparently harmless way, may cause previously serialized delegate data to be corrupt and fail or even possibly execute the wrong method when deserialized into the new version of your software. Further, this is unlikely to be fixable other than by reverting all changes out of your code and never making such changes again.

To sum up, we have found that serialized delegate information is incredibly fragile to seemingly-unrelated changes in one's code base. I would strongly recommend against persisting delegates through serialization with either BinaryFormatter or Json.NET. Instead, consider maintaining a table of named delegates and serializing the names, or following the command pattern and serialize command objects.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...