The answers recommending the use of ToBase64Transform
are valid, but there is a big catch. Not sure if this should be an answer, but had I known this it would have saved me a lot of time.
The problem I ran into with ToBase64Transform
is that it is hard-coded to read 3 bytes at a time. If each write to the input stream specified in CryptoStream
constructor is something like a websocket or anything that has non trivial overhead or latency, this can be a huge problem.
Bottom line - if you are doing something like this:
using var cryptoStream = new CryptoStream(httpRequestBodyStream, new ToBase64Transform(), CryptoStreamMode.Write);
It may be worthwhile to fork the class ToBase64Transform
to modify the hard-coded 3/4 byte values to something substantially larger so that it incurs fewer writes. In my case, with the default 3/4 value, transmission rate was about 100 KB/s. Changing to 768/1024 (same ratio) worked and transmission rate was about 50-100 MB/s because of way fewer writes.
public class BiggerBlockSizeToBase64Transform : ICryptoTransform
{
// converting to Base64 takes 3 bytes input and generates 4 bytes output
public int InputBlockSize => 768;
public int OutputBlockSize => 1024;
public bool CanTransformMultipleBlocks => false;
public virtual bool CanReuseTransform => true;
public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
// For now, only convert 3 bytes to 4
byte[] tempBytes = ConvertToBase64(inputBuffer, inputOffset, 768);
Buffer.BlockCopy(tempBytes, 0, outputBuffer, outputOffset, tempBytes.Length);
return tempBytes.Length;
}
public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
{
ValidateTransformBlock(inputBuffer, inputOffset, inputCount);
// Convert.ToBase64CharArray already does padding, so all we have to check is that
// the inputCount wasn't 0
if (inputCount == 0)
{
return Array.Empty<byte>();
}
// Again, for now only a block at a time
return ConvertToBase64(inputBuffer, inputOffset, inputCount);
}
private byte[] ConvertToBase64(byte[] inputBuffer, int inputOffset, int inputCount)
{
char[] temp = new char[1024];
Convert.ToBase64CharArray(inputBuffer, inputOffset, inputCount, temp, 0);
byte[] tempBytes = Encoding.ASCII.GetBytes(temp);
if (tempBytes.Length != 1024)
throw new Exception();
return tempBytes;
}
private static void ValidateTransformBlock(byte[] inputBuffer, int inputOffset, int inputCount)
{
if (inputBuffer == null) throw new ArgumentNullException(nameof(inputBuffer));
}
// Must implement IDisposable, but in this case there's nothing to do.
public void Dispose()
{
Clear();
}
public void Clear()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) { }
~BiggerBlockSizeToBase64Transform()
{
// A finalizer is not necessary here, however since we shipped a finalizer that called
// Dispose(false) in desktop v2.0, we need to keep it in case any existing code had subclassed
// this transform and expects to have a base class finalizer call its dispose method.
Dispose(false);
}
}