Task.ContinueWith implicit type conversion gotcha

I was recently writing an e-mail sending service using MailKit. I thought I was done with it, but when it came to testing, it turned out that the emails rarely get sent. The reason for that was this exception:

System.NotSupportedException:  The ReadAsync method cannot be called when another read operation is pending.
   at MailKit.Net.Smtp.SmtpClient.FlushCommandQueueAsync(MimeMessage message, MailboxAddress sender, IList`1 recipients, Boolean doAsync, CancellationToken cancellationToken)
   at MailKit.Net.Smtp.SmtpClient.SendAsync(FormatOptions options, MimeMessage message, MailboxAddress sender, IList`1 recipients, Boolean doAsync, CancellationToken cancellationToken, ITransferProgress progress)

This is what my code looked like:

var client = new SmtpClient();
var connectTask = client.ConnectAsync(_smtpConfig.Host, _smtpConfig.Port, _smtpConfig.Security);
if (!string.IsNullOrEmpty(_smtpConfig.Username) && !string.IsNullOrEmpty(_smtpConfig.Password))
{
    connectTask = connectTask.ContinueWith(_ => client.AuthenticateAsync(_smtpConfig.Username, _smtpConfig.Password), TaskContinuationOptions.OnlyOnRanToCompletion);
}

var message = new MimeMessage();
...
        
await connectTask;
await client.SendAsync(message);
await client.DisconnectAsync(true);

If you can spot the error right away, kudos to you! But I couldn't. So I put a breakpoint and started debugging the problem.

The first time the debugger hit the breakpoint and let me go through each instruction one by one, the mail was sent successfully. So I played with it, toggling the breakpoint between on and off.

Every time I took my time analysing each line, the mail was sent, and most of the time I let the code run by itself - the exception appeared. That, of course, pointed me towards my usage of Tasks and, in particular, the ContinueWith method - which I'll admit I do not use often.

There's pretty comprehensive documentation about chaining Tasks at Chaining tasks using continuation tasks (it's in the .NET Fundamentals category 😅). Particularly the Continuations that return Task types section caught my eye because, well, my continuation function did return a Task.

Then it all clicked. So the connectTask.ContinueWith(...) call actually returned Task<Task>, and it's that inner Task I should have called await on to make sure that the client.AuthenticateAsync(...) call is completed.

BUT since I decided to reuse the existing connectTask variable, which was of Task type, the await call did not wait for the inner task to complete. Implicit conversion from Task<Task> to Task caused that I no longer had control over the continuation task.

Maybe if I didn't reuse the connectTask variable, I would've noticed the funny Task<Task> type, maybe not - but the implicit conversion certainly did not make things easier.

The solution was simple - adding .Unwrap() after connectTask.ContinueWith(...) caused the connectTask to point to the inner Task of the Task<Task> returned by ContinueWith method (or rather an UnwrapPromise which acts like a proxy between the tasks).

var client = new SmtpClient();
var connectTask = client.ConnectAsync(_smtpConfig.Host, _smtpConfig.Port, _smtpConfig.Security);
if (!string.IsNullOrEmpty(_smtpConfig.Username) && !string.IsNullOrEmpty(_smtpConfig.Password))
{
	connectTask = connectTask
                	.ContinueWith(
                    	_ => client.AuthenticateAsync(_serverConfiguration.Username, _serverConfiguration.Password),
                    	TaskContinuationOptions.OnlyOnRanToCompletion)
                	.Unwrap(); // <-- the important bit
}

var message = new MimeMessage();
...
        
await connectTask;
await client.SendAsync(message);
await client.DisconnectAsync(true);

I hope I explained this well enough. I produced this little demo in LINQPad to get the point across without all the MailKit noise:

async Task Main()
{
	var stack = new Stack<int>();
	stack.Dump("Before starting first Task");
	var task = Method1(stack);
	task = task.ContinueWith(_ => Method2(stack));
	stack.Dump("After starting the first task and chaining the second one");
	await task;
	stack.Dump("After awaiting the 'task' variable");
}

private async Task Method1(Stack<int> stack)
{
	stack.Push(1);
	await Task.Delay(1000);
}

private async Task Method2(Stack<int> stack)
{
	stack.Push(2);
	await Task.Delay(1000);
	stack.Push(3);
}

Here's the output it produces:

And here's the output after adding .Unwrap() at the end of line #6 in the above snippet:

There are two more resources I recommend if something is not clear. Another MSDN article: How to: Unwrap a Nested Task, and this StackOverflow answer (particularly the second snippet, which uses the Unwrap() method).

Cover photo by Karine Avetisyan on Unsplash