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.