如何使用System.Text.Json序列化到给定的TextWriter实例



我知道如何序列化为Stream,但是否可以序列化为TextWriter

问题是TextWriter没有公开Stream对象。

似乎没有这样的选择,但也许我错过了什么。

编辑1

请允许我澄清一下。我知道TextWriter不公开Stream对象,因为有些TextWriter实现没有底层流。这就是这个问题的要点——我们可以使用System.Text.Json序列化为TextWriter吗?

从.NET 6开始,没有内置的方法可以使用System.Text.Json直接序列化到TextWriter。确认:

  1. 中,尝试新的System.Text.Json API,MSFT的Immo Landwerth写道,

    我们需要一组新的JSON API,这些API通过使用Span对性能进行了高度调整,并且可以直接处理UTF-8,而无需转换为UTF-16字符串实例。

    即不需要序列化到某个中间TextWriter是一项设计要求。

  2. JsonSerializer.Serialize()JsonSerializer.SerializeAsync()的覆盖不占用TextWriter

  3. dotnet/runtime中的搜索"textwriter path:src/librarys/System.Text.Json/src/System/Text/Json"当前没有返回任何结果。

    为了进行比较,在同一路径中搜索Stream会返回16个代码结果。

  4. 每当.NET需要使用UTF8以外的某种编码串行化到Stream时,它们都会通过Encoding.CreateTranscodingStream()而不是通过StreamWriter进行;请参阅此搜索以获取示例。

因此,使用System.Text.Json序列化到TextWriter的最简单方法是序列化到CCD20,然后将其写入TextWriter

如果由于某种原因,这会导致性能异常糟糕(例如,因为中间字符串足够大,可以放在大型对象堆上,并导致内存碎片或垃圾收集速度减慢),则可以将TextWriter封装在一些合成的Stream中,该CCD_23取入字节,将每对解释为Unicode字符,并将它们写入TextWriter。然后,Stream可以依次被接受UTF8的代码转换流包裹,并传递给JsonSerializer,如以下扩展方法所示:

public static partial class JsonSerializerExtensions
{
public static void Serialize<TValue>(TextWriter textWriter, TValue value, JsonSerializerOptions? options = default)
{
if (textWriter == null)
throw new ArgumentNullException(nameof(textWriter));
using (var stream = textWriter.AsWrappedWriteOnlyStream(Encoding.UTF8, true))
{
JsonSerializer.Serialize(stream, value, options);
}
}

public static async Task SerializeAsync<TValue>(TextWriter textWriter, TValue value, JsonSerializerOptions? options = default, CancellationToken cancellationToken = default)
{
if (textWriter == null)
throw new ArgumentNullException(nameof(textWriter));
await using (var stream = textWriter.AsWrappedWriteOnlyStream(Encoding.UTF8, true))
{
await JsonSerializer.SerializeAsync(stream, value, options);
}
}
}
public static partial class TextExtensions
{
public static Encoding PlatformCompatibleUnicode { get; } = BitConverter.IsLittleEndian ? Encoding.Unicode : Encoding.BigEndianUnicode;
public static bool IsPlatformCompatibleUnicode(this Encoding encoding) => BitConverter.IsLittleEndian ? encoding.CodePage == 1200 : encoding.CodePage == 1201;
public static Stream AsWrappedWriteOnlyStream(this TextWriter textWriter, Encoding outerEncoding, bool leaveOpen = false)
{
if (textWriter == null || outerEncoding == null)
throw new ArgumentNullException();
Encoding innerEncoding;
if (textWriter is StringWriter)
innerEncoding = PlatformCompatibleUnicode;
else
innerEncoding = textWriter.Encoding ?? throw new ArgumentException(string.Format("No encoding for {0}", textWriter));

return outerEncoding.IsPlatformCompatibleUnicode()
? new TextWriterStream(textWriter, leaveOpen)
: Encoding.CreateTranscodingStream(new TextWriterStream(textWriter, leaveOpen), innerEncoding, outerEncoding, false);
}
}
sealed class TextWriterStream : Stream
{
// By sealing UnicodeTextWriterStream we avoid a lot of the complexity of MemoryStream.
TextWriter textWriter;
bool leaveOpen;
Nullable<byte> lastByte = null;
public TextWriterStream(TextWriter textWriter, bool leaveOpen) => (this.textWriter, this.leaveOpen) = (textWriter ?? throw new ArgumentNullException(), leaveOpen);
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;

public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
public override void SetLength(long value) => throw new NotSupportedException();
public override int Read(Span<byte> buffer) => throw new NotSupportedException();
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException();
public override int ReadByte() => throw new NotSupportedException();
bool TryPopLastHalfChar(byte b, out char ch)
{
if (lastByte != null)
{
Span<byte> tempBuffer = stackalloc byte [2];
tempBuffer[0] = lastByte.Value; tempBuffer[1] = b;
ch = MemoryMarshal.Cast<byte, char>(tempBuffer)[0];
lastByte = null;
return true;
}
ch = default;
return false;
}
void PushLastHalfChar(byte b) 
{
if (lastByte != null)
throw new InvalidOperationException("Last half character is already saved.");
this.lastByte = b;
}
void EnsureOpen()
{
if (textWriter == null)
throw new ObjectDisposedException(GetType().Name);
}

static void Flush(TextWriter textWriter, Nullable<byte> lastByte)
{
if (lastByte != null)
throw new InvalidOperationException(string.Format("Attempt to flush writer with pending byte {0}", (int)lastByte));
textWriter?.Flush();
}

static Task FlushAsync(TextWriter textWriter, Nullable<byte> lastByte, CancellationToken cancellationToken)
{
if (lastByte != null)
throw new InvalidOperationException("Attempt to flush writer with pending byte");
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
return textWriter.FlushAsync(); // No overload takes a cancellation token?
}

public override void Flush() 
{
EnsureOpen();
Flush(textWriter, lastByte);
}

public override Task FlushAsync(CancellationToken cancellationToken)
{
EnsureOpen();
return FlushAsync(textWriter, lastByte, cancellationToken);
}
public override void Write(byte[] buffer, int offset, int count)
{
ValidateBufferArgs(buffer, offset, count);
Write(buffer.AsSpan(offset, count));
}
public override void Write(ReadOnlySpan<byte> buffer)
{
EnsureOpen();
if (buffer.Length < 1)
return;
if (TryPopLastHalfChar(buffer[0], out var ch))
{
textWriter.Write(ch);
buffer = buffer.Slice(1);
}
if (buffer.Length % 2 != 0)
{
PushLastHalfChar(buffer[buffer.Length - 1]);
buffer = buffer.Slice(0, buffer.Length - 1);
}
if (buffer.Length > 0)
textWriter.Write(MemoryMarshal.Cast<byte, char>(buffer));
}
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
ValidateBufferArgs(buffer, offset, count);
return WriteAsync(buffer.AsMemory(offset, count)).AsTask();
}
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
if (cancellationToken.IsCancellationRequested)
return ValueTask.FromCanceled(cancellationToken);
try
{
return WriteAsyncCore(buffer, cancellationToken);
}
catch (OperationCanceledException oce)
{
return new ValueTask(Task.FromCanceled(oce.CancellationToken));
}
catch (Exception exception)
{
return ValueTask.FromException(exception);
}
}
async ValueTask WriteAsyncCore(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
EnsureOpen();
if (buffer.Length < 1)
return;
if (TryPopLastHalfChar(buffer.Span[0], out var ch))
{
await textWriter.WriteAsync(ch);
buffer = buffer.Slice(1);
}
if (buffer.Length % 2 != 0)
{
PushLastHalfChar(buffer.Span[buffer.Length - 1]);
buffer = buffer.Slice(0, buffer.Length - 1);
}
if (buffer.Length > 0)
await textWriter.WriteAsync(Utils.Cast<byte, char>(buffer), cancellationToken);
}
protected override void Dispose(bool disposing)
{
try 
{
if (disposing) 
{
var textWriter = Interlocked.Exchange(ref this.textWriter!, null);
if (textWriter != null)
{
Flush(textWriter, lastByte);
if (!leaveOpen)
textWriter.Dispose();
}
}
}
finally 
{
base.Dispose(disposing);
}
}   
public override async ValueTask DisposeAsync()
{
var textWriter = Interlocked.Exchange(ref this.textWriter!, null);
if (textWriter != null)
{
await FlushAsync(textWriter, lastByte, CancellationToken.None);
if (!leaveOpen)
await textWriter.DisposeAsync();
}
await base.DisposeAsync();
}
static void ValidateBufferArgs(byte[] buffer, int offset, int count)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (offset < 0 || count < 0)
throw new ArgumentOutOfRangeException();
if (count > buffer.Length - offset)
throw new ArgumentException();
}
}
public static class Utils
{
// Adapted for read only memory from this answer https://stackoverflow.com/a/54512940/3744182
// By https://stackoverflow.com/users/23354/marc-gravell
// To https://stackoverflow.com/questions/54511330/how-can-i-cast-memoryt-to-another
public static ReadOnlyMemory<TTo> Cast<TFrom, TTo>(ReadOnlyMemory<TFrom> from)
where TFrom : unmanaged
where TTo : unmanaged
{
// avoid the extra allocation/indirection, at the cost of a gen-0 box
if (typeof(TFrom) == typeof(TTo)) return (ReadOnlyMemory<TTo>)(object)from;
return new CastMemoryManager<TFrom, TTo>(MemoryMarshal.AsMemory(from)).Memory;
}
private sealed class CastMemoryManager<TFrom, TTo> : MemoryManager<TTo>
where TFrom : unmanaged
where TTo : unmanaged
{
private readonly Memory<TFrom> _from;
public CastMemoryManager(Memory<TFrom> from) => _from = from;
public override Span<TTo> GetSpan() => MemoryMarshal.Cast<TFrom, TTo>(_from.Span);
protected override void Dispose(bool disposing) { }
public override MemoryHandle Pin(int elementIndex = 0) => throw new NotSupportedException();
public override void Unpin() => throw new NotSupportedException();
}
}

现在你可以做一些类似的事情:

JsonSerializerExtensions.Serialize(textWriter, someObject);

但老实说,我不确定所有这些努力是否值得。

注:

  • 未在big-endian平台上进行测试(这种情况很少见)。

  • 可能需要更多的异步测试。

在这里演示小提琴。