overview
PipeWriter manages buffers for writing on the caller’s behalf. It implements IBufferWriter<byte> to get access to buffers to perform
writes without extra buffer copies:
async Task WriteHelloAsync(PipeWriter writer, CancellationToken cancellationToken = default)
{
    // Request a buffer of at least 5 bytes from the PipeWriter.
    Memory<byte> memory = writer.GetMemory(5);
    // Write directly into the buffer.
    int written = Encoding.ASCII.GetBytes("Hello".AsSpan(), memory.Span);
    // Tell the writer how many bytes were written.
    writer.Advance(written);
    // Send the bytes to the underlying device.
    await writer.FlushAsync(cancellationToken);
}
Using PipeWriter.WriteAsync instead of the buffers provided by PipeWriter is easier. WriteAsync calls GetSpan and Advance as needed
and finishes by calling FlushAsync:
async Task WriteHelloAsync(PipeWriter writer, CancellationToken cancellationToken = default)
{
    byte[] helloBytes = Encoding.ASCII.GetBytes("Hello");
    // Write helloBytes to the writer; there's no need to call Advance here (Write does that).
    await writer.WriteAsync(helloBytes, cancellationToken);
}
cancellation with pipewriter
PipeWriter.FlushAsync accepts a CancellationToken. However, PipeWriter.CancelPendingFlush cancels the flush operation without raising
an exception. It causes the current or next call to FlushAsync or WriteAsync to return a FlushResult with IsCanceled set to true.
common problems with pipewriter
See this page for more information.