使用 system.text.json 序列化和反序列化流

Posted

技术标签:

【中文标题】使用 system.text.json 序列化和反序列化流【英文标题】:Serializing and Deserializing stream using system.text.json 【发布时间】:2021-12-30 00:17:11 【问题描述】:

我是使用 System.Text.Json 的新手。我使用的是 BinaryFormatter,由于 Binaryformmater 的安全漏洞,现在需要迁移到 System.Text.Json。我需要将对象序列化到流中并将其存储在磁盘中。然后在调用 get 方法时,它应该获取流并将数据反序列化为对象。我没有发现文档有用 (https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to?pivots=dotnet-6-0)。下面是伪代码,我需要编写 JsonSerializer 类。有人可以帮我吗?

public class Foo

 public void get() 
 
   using (MemoryStream stream = new MemoryStream())
   
     FetchStreamFromDisk() // Fetches stream from disk
     return JsonSerializer.deserialize(stream)
   
 
 public void Put(Object data) 
 
   using (MemoryStream stream = new MemoryStream())
   
     JsonSerializer.serialize(data, stream);
     StoreStreamIntoDisk()// Store the data into the disk/DB. So the stream should not get closed in the JsonSerializer Class
   
 


public static class JsonSerializer

  public void serialize(Object data, out MemoryStream stream) 
  
    // Serialize Data 
  
  public Object deserialize(MemoryStream stream) 
  
    // Deserialize Data 
  

【问题讨论】:

文档很好。问题是BinaryFormatter 与 JSON 或任何类型的文本格式的序列化无关,因此您使用的任何模式或习语现在都无法使用。你实际上是在错误地看待这个问题。 System.Text.Json 的等价物是 XmlSerializer、DataContractSerializer、Json.NET。与 BinaryFormatter 不同,所有序列化程序都适用于整个对象图,而不是单个字段 您想解决的实际问题是什么?将 BinaryFormatter 替换为 binary 序列化?你想如何使用你的序列化代码?序列化器是基于对象的。 XML 和 JSON 都不是二进制序列化的好选择。更好的选择是协议缓冲区,gRPC 的二进制格式。您可以使用.NET's gRPC tooling 或protobuf-net 指定对象的架构并对其进行序列化。 XMLSerializers 比 JsonSerializers 慢,根据多个博客(例如:inspiration.nlogic.ca/en/…)。所以,我想使用 JsonSerializer。在 JsonSerializer 中,有 2 个序列化器,即 System.text.json 和 NewtonsoftJson。但是,我想使用 system.text.json,因为它比另一个更快、更安全。 这并不能解释您的问题和问题。而且 JSON 仍然是一种文本格式,比二进制格式更大更慢 我想从使用 BinaryFormatter 迁移到 System.Text.Json 的 JsonSerializer。 【参考方案1】:

如果您想读写磁盘,没有理由使用MemoryStream。这只是Stream 缓冲区上的byte[] 包装器。 XmlSerializer 和 Json.NET 等序列化程序可以直接写入 StreamTextWriter 派生对象。 System.Text.Json 可以序列化为 Stream 或 Utf8JsonWriter,这是 ASP.NET Core 用来将 JSON 对象直接序列化为 HTTP 响应的高速专用写入器,具有最少的分配和可重用缓冲区。

在JSON serialization documentation 中唯一需要更改的是使用写入流的JsonSerializer.Serialize 或SerializeAsync 重载:

async using var stream=File.Create(somePath);
await JsonSerializer.SerializeAsync(stream,myObject);

您不需要编写自己的类和方法来抽象单个JsonSerializer.SerializeAsync 调用。如果您想创建一个类似存储库的对象来抽象文件存储,这将是有意义的,而不仅仅是 JSON 序列化,即基于配置和某种标识符确定存储位置和路径的类,例如:

interface IMyStorage

    public Task<T> Get(string someId);
    public async Task Store<T>(T value,string someId);


class JsonStorage:IMyStorage<T>

    readonly string _root;
    
    public JsonStorage(string root)
    
        _root=root;
    

    public async Task<T> Store(string someId)
    
        var path=Path.Combine(_root,someId);

        async using var stream=File.Create(path);

        await JsonSerializer.SerializeAsync(stream,path);
    

    

JSON 问题

也就是说,JSON 仍然是一种文本格式,不适合二进制序列化。它占用更多空间,编写速度较慢,并且缺少架构意味着无法知道 JSON 字符串包含什么。

另一个问题是 JSON 只能有一个根,要么是对象,要么是数组。它不能有多个元素。这意味着您不能简单地将对象附加到文件或读取单个对象。您必须一次读取和写入整个文件。

将多个对象序列化为 JSON 文件的一种方法是将每个对象序列化到单独的一行:

async using var writer=new StreamWriter(path,true); //append text
foreach(var myObject in myList)

    var line=await JsonSerializer.SerializeAsync(myObject);
    await writer.WriteLine(line);

替代品

还有其他广泛使用的格式更适合序列化,使用更少的空间,某种形式的模式以及处理多个模式版本的能力,如 Protocol Buffers、Orc、Parquet、Avro 等。只需使用列式存储,其中一些格式即可提供压缩,而无需使用 GZip 或 Brotli 等压缩算法。

使用其中一种常见格式意味着其他应用程序将能够读取您的文件。您将能够使用可用的工具来读取/编辑您的文件,而无需打开您自己的应用程序。

一种非常常见的二进制格式是 Goole 的 Protocol Buffers,它用于 gRPC 和许多工具中。很多。您可以使用 .NET 自己的 gRPC tooling 或 Protobuf-net 库在 .NET Core 中使用它。

在协议缓冲区中,您可以在架构文件中预先指定文件的架构。使用 Protobuf-net,您真正需要的只是为您的类添加适当的属性:

[ProtoContract]
class Person 
    [ProtoMember(1)]
    public int Id get;set;
    [ProtoMember(2)]
    public string Name get;set;
    [ProtoMember(3)]
    public Address Address get;set;

[ProtoContract]
class Address 
    [ProtoMember(1)]
    public string Line1 get;set;
    [ProtoMember(2)]
    public string Line2 get;set;

序列化对象与使用 `JsonSerializer:

几乎相同
async using var file = File.Create("person.bin");
Serializer.Serialize(file, person);

多条消息 协议缓冲区允许在一个流中存储多个对象/消息,但无法检测一个开始和另一个结束的位置。解决这个问题的最简单方法是在消息本身之前写入消息的大小。这在Streaming Multiple Messages中有解释:

如果您想将多条消息写入单个文件或流,则由您来跟踪一条消息的结束位置和下一条消息的开始位置。协议缓冲区有线格式不是自定界的,因此协议缓冲区解析器无法自行确定消息的结束位置。解决此问题的最简单方法是在编写消息本身之前写入每条消息的大小。当您读回消息时,您会读取大小,然后将字节读入单独的缓冲区,然后从该缓冲区进行解析。

要使用 Protobuf-net 执行此操作,请使用 SerializeWithLengthPrefix 方法:

Serializer.SerializeWithLengthPrefix(stream, myObject, PrefixStyle.Base128);

从流中读取下一条消息:

var myObject = Serializer.DeserializeWithLengthPrefix<MyObject>(stream, PrefixStyle.Base128);

【讨论】:

以上是关于使用 system.text.json 序列化和反序列化流的主要内容,如果未能解决你的问题,请参考以下文章

.Net Core 5.0 Json序列化和反序列化 | System.Text.Json 的json序列化和反序列化

(de) 使用 System.Text.Json 序列化流

如何让 System.Text.Json 将对象反序列化为其原始类型?

json.net 到 System.text.json 对 .net 5 中嵌套类的期望

使用 System.Text.Json 时,如何处理 Dictionary 中 Key 为自定义类型的问题

WebApi