Asynchronous programming in .NET and C# is omnipresent these days.
To understand the purpose of ValueTask we first have to understand Stack vs Heap memory.
Task<Post[]> LoadPostsAsync()
{
return await _dbContext.Posts.ToArrayAsync();
}
A Task is a class and thus will be allocated on the Heap memory.
But what if we already have the Posts cached in memory and do not have to access the DB again? Here comes ValueTask into play!
A ValueTask is a struct and thus will be allocated on the Stack memory.
async ValueTask<Post[]> LoadPostsAsync()
{
var posts = _memoryCache.Get<Post[]>("Posts");
if (posts is not null)
{
// Synchronously return the posts from memory
return posts;
}
// Asynchronously return the posts from database
return await _dbContext.Posts.ToArrayAsync();
}
That's it! We saved a Task allocation on the Heap memory when the posts are already available in the cache.
Additional performance optimization
To improve the above snippet even further, we could avoid the state machine generation overhead when not needed.
ValueTask<Post[]> LoadPostsAsync()
{
var posts = _memoryCache.Get<Post[]>("Posts");
if (posts is not null)
{
// Synchronously return the posts from memory
return ValueTask.FromResult(posts);
}
// Asynchronously return the posts from database
// no await here to avoid the state machine generation
return Awaited();
async ValueTask<Post[]> Awaited()
{
return await _dbContext.Posts.ToArrayAsync();
}
}
Obviously this only makes sense for very hot paths. Benchmark before doing this 😉