Using() block hell in C#
So the other day I was working on transforming streams for an encryption layer for work. I ended up having some code that looked similar to this:
public async Task SendAsync(byte[] data, Delegate next)
{
using (var stream = ...)
{
using (var encryptStream = ...)
{
using (var encodeStream = ...)
{
using (var reader = ...)
{
await next(...);
}
}
}
}
}
public async Task RecieveAsync(byte[] data, Delegate next)
{
using (var stream = ...)
{
using (var decodeStream = ...)
{
using (var decryptStream = ...)
{
using (var reader = ...)
{
await next(...);
}
}
}
}
}
Which doesn't look very great! There's a nice solution relying on next-line context statements showcased over at stackoverflow but my colleagues and I agreed that "hack" doesn't necessarily get us what we want. There's a reason most people don't do:
if (this)
if (andThat)
if (andKitchenSink)
{
// Do stuff
}
It's just as ugly in a non-aesthetic way.
Ok, what about just using try/finally?
public async Task SendAsync(byte[] data, Delegate next)
{
IDisposable stream = null, encryptedStream = null, encodeStream = null;
StreamReader reader = null;
try {
stream = ...;
encryptStream = ...;
encodeStream = ...;
reader = ...;
await next(...);
}
finally
{
stream?.Dispose();
encryptedStream?.Dispose();
encodedStream?.Dispose();
reader?.Dispose();
}
}
public async Task RecieveAsync(byte[] data, Delegate next)
{
var stream = null, decodeStream = null, decryptStream = null;
StreamReader reader = null;
try {
stream = ...;
decodeStream = ...;
decryptStream = ...;
reader = ...;
await next(...);
}
finally
{
stream?.Dispose();
decodeStream?.Dispose();
decryptStream?.Dispose();
reader?.Dispose();
}
}
But...that's equally bad to look at. So I threw together a builder pattern that makes the above look something like this:
public async Task SendAsync(byte[] data, Delegate next)
{
With.Using(() => new ...)
.Using(memoryStream => ...)
.Using(cryptoStream => ...)
.Using(base64Stream => ...)
.Then(async streamReader => await next(...));
}
public async Task RecieveAsync(byte[] data, Delegate next)
{
With.Using(() => new ...)
.Using(memoryStream => ...)
.Using(base64Stream => ...)
.Using(cryptoStream => ...)
.Then(async streamReader => await next(...));
}
I think it looks pretty clean? Obviously this isn't something you just throw into a JobCorpCo code base but it's a fun exercise in what you can do with code. If you're interested, here are the helper classes.