Ref structs in async methods

Today, I had the chance to review a pull request of a colleague at work, containing code along these lines:

private async Task<CustomType> DoStuff(byte[] src)
{
var data = Task.WaitSafe(() =>
{
var utf8Reader = new Utf8JsonReader(src);
return Task.FromResult(JsonSerializer.Deserialize<CustomType>(ref utf8Reader));
});
// Some proper tasks to await here, just use some imagination :)
await Task.CompletedTask;
return data;
}

(Note that Task.WaitSafe is a custom method we use, containing some Task.Run(…).Result, as well as exception handling.You get the point 😉)

I thought that this looks kind of weird. Given that this part of the JsonSerializer API is synchronous, why wouldn’t you just run it as such?

So I went ahead and tried something I thought was quite obvious before proposing a solution:

private async Task<CustomType> DoStuff(byte[] src)
{
var utf8Reader = new Utf8JsonReader(src);
var data = JsonSerializer.Deserialize<CustomType>(ref utf8Reader);
// Some proper tasks to await here, just use some imagination :)
await Task.CompletedTask;
return data;
}

Well, here is what Visual Studio has to say about this:

I thought, huh, I do remember this. in, out and ref are not allowed in an async context. So here is my proposed solution with a local function:

private async Task<CustomType> DoStuff(byte[] src)
{
CustomType Deserialize(byte[] source)
{
var utf8Reader = new Utf8JsonReader(source);
return JsonSerializer.Deserialize<CustomType>(ref utf8Reader);
}
var data = Deserialize(src);
// Some proper tasks to await here, just use some imagination :)
await Task.CompletedTask;
return data;
}

Cool, this works. However, on my way back to the parking lot after work, I couldn’t find peace thinking about the why.

So I spent my evening reading up and experimenting around a bit with sharplab, until I figured it out.

The problem lies in the Utf8JsonReader type, which is a ref struct. The problem with ref structs can be found here: MSDN Article.

Essentially, a ref struct is not allowed to be a field of a class. Why does this matter in an async context? Because the compiler converts an async method to a class, and all local variables are turned into a field, essentially. Here’s a short example:

static async Task Main(string[] args)
{
var myVariable = 5;
await Task.CompletedTask;
Console.WriteLine(myVariable);
}

Turns into:

And there we have it. Given that the compiler turns local variables into fields, and we just learned that ref structs cannot be fields, this is not possible.

Feel free to check it out yourself over at Sharplab

25 year old full stack software developer from Germany - Enthusiatic about C#, .Net and Cloud