BinaryFormatter 怎么可以序列化一个 Action<>但 Json.net 不

时间:2023-04-25
本文介绍了BinaryFormatter 怎么可以序列化一个 Action<>但 Json.net 不能的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着跟版网的小编来一起学习吧!

问题描述

尝试序列化/反序列化一个动作<>.

Trying to serialize/deserialize an Action<>.

尝试 #1 天真的我

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

反序列化失败,说它不能序列化动作.

Deserialize fails saying it cannot serialize Action.

尝试#2

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

同样的(ish)失败.

Same(ish) failure.

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

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

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

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

尝试#4

然后我找到了

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

这是 json.net 的 IFormatter 模块.连线,同样的失败 - 无法反序列化.

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

那么为什么 BinaryFormatter 可以做到,而 Json.net 却不能呢?

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

一般的回答是 - 这是最愚蠢的事情".让我展示一下我想要做什么

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

即 - 在 2 天内执行此 lambda.

Ie - execute this lambda in 2 days time.

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

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

推荐答案

BinaryFormatter (有时)能够往返 Action< 的原因T> 是这样的代表被标记为 [Serializable] 并实现 ISerializable.

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

但尝试序列化以下内容会引发 SerializationException:

But attempting to serialize the following threw a SerializationException:

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

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

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

另一方面,Json.NET 无法往返 Action<T> 尽管 支持 ISerializable,因为它不支持通过 SerializationInfo.SetType(Type).我们可以通过以下代码确认 Action<T> 正在使用这种机制:

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

运行时输出:

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 不支持.

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

这就引出了一个问题,当委托被序列化时,到底写出什么?为了确定这一点,我们可以配置 Json.NET 来序列化 Action,类似于BinaryFormatter 如何设置

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

如果我使用这些设置序列化 a1:

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

然后生成以下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
  }
}

替换 FullTypeName 不包括在内,但其他所有内容都包括在内.正如你所看到的,它实际上并没有存储委托的 IL 指令.它存储要调用的方法的完整签名,包括 <Test>b__0a/17709299">这个答案.您只需打印 a1.Method.Name 就可以自己查看隐藏的方法名称.

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.

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

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

使用扩展方法:

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程序,版本= 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 确实也存在于二进制文件中.

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

现在,可怕的部分来了.如果我修改我的 c# 源代码以在 a1 之前创建另一个 Action<T>,如下所示:

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

然后重新构建并重新运行,a1.Method.Name变为<Test>b__1:

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
  }
}

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

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.

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

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.

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

上一篇:避免默认构造函数和公共属性设置器 下一篇:在 Json.net 中使用自定义 JsonConverter 和 TypeNameHandling

相关文章