问题
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
回答1:
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
- DefaultContractResolver.IgnoreSerializableAttribute = false
- DefaultContractResolver.IgnoreSerializableInterface = false
- JsonSerializerSettings.TypeNameHandling = TypeNameHandling.All
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 or command objects, and serializing the names.
来源:https://stackoverflow.com/questions/49138328/how-come-binaryformatter-can-serialize-an-action-but-json-net-cannot