为什么BinaryFormatter可以序列化一个Action <>但是Json.net不能

pm1*_*100 0 c# json.net

尝试序列化/反序列化Action <>.

尝试#1天真的我

JsonConvert.SerializeObject(myAction);
...
JsonConvert.Deserialize<Action>(json);
Run Code Online (Sandbox Code Playgroud)

反序列化失败,表示无法序列化Action.

试试#2

JsonConvert.DeserializeObject<Action>(ctx.SerializedJob, new JsonSerializerSettings {ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor });
Run Code Online (Sandbox Code Playgroud)

相同(ish)失败.

尝试#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));
Run Code Online (Sandbox Code Playgroud)

即 - 在2天内执行此lambda.

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

dbc*_*dbc 7

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

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

Action<int> a1 = (a) => Console.WriteLine(a);
Run Code Online (Sandbox Code Playgroud)

但试图序列化以下投掷SerializationException:

int i = 0;
Action<int> a2 = (a) => i = i + a;
Run Code Online (Sandbox Code Playgroud)

捕获的变量i显然放在一个不可序列化的编译器生成的类中,从而防止代理的二进制序列化成功.

在另一方面,Json.NET无法往返的Action<T>,尽管支持ISerializable,因为它不提供对通过配置的序列化代理的支持SerializationInfo.SetType(Type).我们可以Action<T>通过以下代码确认正在使用此机制:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

运行时,输出:

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).
Run Code Online (Sandbox Code Playgroud)

请注意,FullTypeName已更改为System.DelegateSerializationHolder?这是代理,Json.NET不支持它.

这就引出了一个问题,就是当一个委托被序列化时会写出什么? 为了确定这一点,我们可以将Json.NET配置为Action<T>BinaryFormatter设置方式类似

如果我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);
Run Code Online (Sandbox Code Playgroud)

然后生成以下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
  }
}
Run Code Online (Sandbox Code Playgroud)

更换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
Run Code Online (Sandbox Code Playgroud)

使用扩展方法:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这样做会产生以下字符串:

????"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)
Run Code Online (Sandbox Code Playgroud)

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

现在,这是可怕的部分.如果我修改我的C#源代码创建另一个Action<T>之前a1,就像这样:

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

Action<int> a1 = (a) => Console.WriteLine(a);
Run Code Online (Sandbox Code Playgroud)

然后重新构建并重新运行,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
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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