BinaryFormatter为何可以序列化Action<>但是Json.net不能 [英] how come BinaryFormatter can serialize an Action<> but Json.net cannot

查看:78
本文介绍了BinaryFormatter为何可以序列化Action<>但是Json.net不能的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

尝试对动作进行序列化/反序列化.

尝试我的#1天真

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

反序列化无法说它不能序列化操作.

尝试#2

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

相同(失败)失败.

尝试#3 然后我发现 http://mikehadlow.blogspot.com/2011/04/serializing -continuations.html

这使用BinaryFormatter.我将其放入(base64将二进制编码编码为字符串).第一次工作完美.

尝试#4

然后我发现了

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

这是json.net的IFormatter模块.将其连接到同一故障-无法反序列化.

那么BinaryFormatter怎么能做到而Json.net无法做到呢?

通常的答复是-那是要做的最愚蠢的事情".让我展示我要做什么

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

即-在2天的时间内执行此Lambda.

这对我来说很好.使用BinaryFormatter.我很好奇为什么一个序列化基础结构可以做到而另一个却不能做到.他们俩似乎对可以处理和不能处理的内容具有相同的规则

解决方案

BinaryFormatter(有时)能够往返于Action<T>的原因是,此类委托被标记为 [Serializable] 并实施 ISerializable .

但是,仅因为委托本身被标记为可序列化并不意味着其成员可以成功序列化.在测试中,我能够序列化以下委托:

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

但是尝试序列化以下内容抛出了SerializationException:

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

捕获的变量i显然放置在不可序列化的编译器生成的类中,从而阻止了委托的二进制序列化成功.

另一方面,尽管Action<T> ="nofollow noreferrer">支持ISerializable ,因为它不支持通过

运行时,它输出:

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).

是否注意到FullTypeName已更改为 System.DelegateSerializationHolder ?那是代理,Json.NET不支持.

这引起了一个问题,当委托被序列化时,会写出什么内容?要确定这一点,我们可以通过设置

如果我使用以下设置序列化a1:

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);

然后生成以下JSON:

{
  "$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
  }
}

不包括替代品FullTypeName,但其他所有物品都包括在内.正如您所看到的,它实际上并没有存储委托的IL指令.它存储要调用的方法的完整签名,包括此答案<Test>b__0 .您只需打印a1.Method.Name即可看到隐藏的方法名称.

顺便说一句,要确认Json.NET确实保存了与BinaryFormatter相同的成员数据,您可以将a1序列化为二进制并打印任何嵌入式ASCII字符串,如下所示:

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

使用扩展方法:

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();
        }
    }
}

这样做将导致以下字符串:

????"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)

断言永不触发,表明编译器生成的方法名称<Test>b__0确实也存在于二进制文件中.

现在,这是最恐怖的部分.如果我修改c#源代码以在a1之前创建另一个Action<T>,如下所示:

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

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

然后重新构建并重新运行, a1.Method.Name更改为<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
  }
}

现在,如果我反序列化从早期版本保存的a1的二进制数据,它将以a0的形式返回!因此,在代码库中的某个地方添加另一个委托,或者以明显无害的方式重构代码,可能会导致先前序列化的委托数据损坏并失败,甚至在反序列化为新版本的软件时可能执行错误的方法.此外,除了将所有更改从代码中还原出来并且再也不要进行此类更改之外,这不太可能解决.

总而言之,我们发现序列化的委托人信息对于代码库中看似无关的更改非常脆弱.我强烈建议不要使用BinaryFormatter或Json.NET通过序列化来持久化委托.相反,请考虑维护一个由命名委托或命令对象组成的表,并对名称进行序列化.

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

解决方案

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 or command objects, and serializing the names.

这篇关于BinaryFormatter为何可以序列化Action&lt;&gt;但是Json.net不能的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆