Using ValueTask to create methods that can work as sync or async

Last Updated: January 25, 2021 | Created: January 23, 2021

In this article I delve into C#’s ValueTask struct, which provides a subset of the Task class features, and use it’s features to solve a problem of building libraries that need both sync and async version of the library’s methods. Along the way I learnt something about ValueTask and how it works with sync code.

NOTE: Thanks to Stephen Toub, who works on the Microsoft NET platform and wrote the article “Understanding the Whys, Whats, and Whens of ValueTask”, for confirming this is a valid approach and is used inside Microsoft’s code. His feedback, plus amoerie’s comment, helped me to improve the code to return the correct stack trace.

TL;DR – summary

  • Many of my libraries provide a sync and async version of each method. This can cause me to have to duplicate code, one for the sync call and one for the async call, with just a few different calls, e.g. SaveChanges and SaveChangesAsync
  • This article tells you how the ValueTask (and ValueTask <TResult>) works when it returns without running an async method, and what its properties mean. I also have some unit tests to check this.
  • Using this information, I found a way to use C#’s ValueTask to build a single method work as sync or async method, which is selected by a book a parameter. This removes a lot of duplicate code.
  • I have built some extension methods that will check that the returned ValueTask a) didn’t use an async call, and b) if an exception was thrown in the method (which won’t bubble up) it then throws it so that it does bubble up.

Setting the scene – why I needed methods to work sync or async

I have built quite a few libraries, NuGet says I have 15 packages, and most are designed to work with EF Core (a few are for EF6.x). Five of these have both sync and async versions of the methods to allow the developer to use it whatever way they want to. This means I have to build some methods twice: one for sync and one for async, and of course that leads to duplication of code.

Normally I can minimise the duplication by building internal methods that return IQueryable<T>, but when I developed the EfCore.GenericEventRunner library I wasn’t querying the database but running sync or async code provided by the developer. The internal methods normally have lots of code with one or two methods that could be sync or async, e.g. SaveChanges and SaveChangesAsync.

Ideally, I wanted internal methods that I could call that sync or async, where a parameter told it whether to call sync or async, e.g.

  • SYNC:   var result = MyMethod(useAsync: false)
  • ASYNC: var result = await MyMethod(useAsync: true)

I found the amazingly good article by Stephen Toub called “Understanding the Whys, Whats, and Whens of ValueTask” which explained about ValueTask <TResult> and synchronous completion, and this got me thinking – can I use ValueTask to make a method that could work sync and async? And I could! Read on to see how I did this.

What happens when a ValueTask has synchronous completion?

The ValueTask (and ValueTask <TResult>) code is complex and linked to the Task class, and the documentation is rather short on explaining what an “failed operation”. But from lots of unit tests and inspecting the internal data I worked out what happens with a sync return.

The ValueTask (and ValueTask <TResult>) have four bool properties. They are:

  • IsCompleted: This is true if the ValueTask is completed. So, if I captured the ValueTask, and this was true, then it had finished with means I don’t have to await it.
  • IsCompletedSuccessfully: This is true if no error happened. In a sync return it means no exception has been thrown.
  • IsFaulted: This is true if there was an error, and for a sync return that means an exception.
  • IsCancelled: This is true the CancellationToken cancelled the async method. This is not used in a sync return.

From this information I decided I could check that a method had synchronously if the IsCompleted property is true.

The next problem was what to do when a method using ValueTask throws an exception. The exception isn’t bubbled up but is held inside the ValueTask so I needed to extract that exception to throw it. I bit more unit testing and inspecting the ValueTask internals showed me how to extract the exception and throw it. Information provided by Stephen Toub showed a better way to throw the exception with the correct stacktrace.

NOTE: You can see the unit tests I did to detect what ValueTask and ValueTask <TResult> here.

So I could my var valueTask =MyMethod(useAsync: false) method and inspect the valueTask returned to check it didn’t call any async methods inside it, and calls GetResult, which will throw an exception if there is one. The code below does this for a ValueTask (this ValueTaskSyncCheckers class also contains a similar method for ValueTask<TResult>).

This code comes from from Microsoft code which this approach is used (look at this code and search for “useAsync: false”). Stephen Toub told me The valueTask.GetAwaiter().GetResult(); is the best way to end an ValueTask, even for the version that doesn’t return a result.. That’s because:

  • If there was an exception, then that call will throw the exception inside the method with the correct stacktrace.
  • Stephen Toub said that it should call GetResult even in the version with no result as if your method is used in a pooled resource, that call it typically used to tell the pooled resource is no longer used.

The listing below shows the two versions of the CheckSyncValueTaskWorked methods – the first is for ValueTask and the second for ValueTask<TResult>.

public static void CheckSyncValueTaskWorked(
    this ValueTask valueTask)
{
    if (!valueTask.IsCompleted)
        throw new InvalidOperationException(
            "Expected a sync task, but got an async task");
    valueTask.GetAwaiter().GetResult();
}

public static TResult CheckSyncValueTaskWorkedAndReturnResult
    <TResult>(this ValueTask<TResult> valueTask)
{
    if (!valueTask.IsCompleted)
        throw new InvalidOperationException(
             "Expected a sync task, but got an async task");
    return valueTask.GetAwaiter().GetResult();
}

NOTE: You can access these extension methods via this link.

How I used this feature in my libraries

I first used this in my EfCore.GenericEventRunner library, but those examples are complex, so I show a simple example in my EfCore.SoftDeleteServices, which has a very simple example. Here is a method that uses the useAsync property – see the highlighted lines at the end of the code.

public static async ValueTask<TEntity> LoadEntityViaPrimaryKeys<TEntity>(this DbContext conte
    Dictionary<Type, Expression<Func<object, bool>>> otherFilters, 
    bool useAsync,
    params object[] keyValues)
    where TEntity : class
{
    // Lots of checks/exceptions left out 

    var entityType = context.Model.FindEntityType(typeof(TEntity));
    var keyProps = context.Model.FindEntityType(typeof(TEntity))
        .FindPrimaryKey().Properties
        .Select(x => x.PropertyInfo).ToList();

    var filterOutInvalidEntities = otherFilters
          .FormOtherFiltersOnly<TEntity>();
    var query = filterOutInvalidEntities == null
        ? context.Set<TEntity>().IgnoreQueryFilters()
        : context.Set<TEntity>().IgnoreQueryFilters()
            .Where(filterOutInvalidEntities);

    return useAsync
        ? await query.SingleOrDefaultAsync(
              CreateFilter<TEntity>(keyProps, keyValues))
        : query.SingleOrDefault(
              CreateFilter<TEntity>(keyProps, keyValues));
}

The following two versions – notice the sync takes the ValueTask and then calls the CheckSyncValueTaskWorked  method, while the async uses the normal async/await approach.

SYNC VERSION

var entity= _context.LoadEntityViaPrimaryKeys<TEntity>(
    _config.OtherFilters, false, keyValues)
    .CheckSyncValueTaskWorkedAndReturnResult();
if (entity == null)
{
    //… rest of code left out

ASYNC VERSION

var entity = await _context.LoadEntityViaPrimaryKeys<TEntity>(
     _config.OtherFilters, true, keyValues);
if (entity == null) 
{
    //… rest of code left out

NOTE: I generally create the sync version of a library first, as its much easier to debug because async exception stacktraces are hard to read and the debug data can be harder to read. Once I have the sync version working, with its unit tests, then I build the async side of the library.

Conclusion

So, I used this sync/async approach in my EfCore.GenericEventRunner library, where the code is very complex, and it really made the job much easier. I then used the same approach in EfCore.SoftDeleteServices library – again there was a complex class called CascadeWalker, that “walks” the dependant navigational properties. Both of this approach stopped a significant duplication of code.

You might not be building a library, but you have learnt how the ValueTask does when it returns a sync result to an async call. The ValueType is there to make the sync return faster, and especially memory usage. Also, you now have another approach if you have a similar sync/async need.

NOTE:  ValueTask has a number of limitations so I only use ValueType in my internal parts of my libraries and provide a Task version to the user of my libraries.  

In case you missed it do read the excellent article “Understanding the Whys, Whats, and Whens of ValueTask” which explained ValueTask. And thanks again to Stephen Toub and amoerie’s comment for improving the solution.

Happy coding.

0 0 vote
Article Rating
Subscribe
Notify of
guest
4 Comments
Newest
Oldest Most Voted
Inline Feedbacks
View all comments
Tom Winter
Tom Winter
5 months ago

Microsoft documents this method here: https://docs.microsoft.com/en-us/archive/msdn-magazine/2015/july/async-programming-brownfield-async-development#the-flag-argument-hack

They use, and I’ve done this before with, Task. I’m not sure why ValueTask would be necessary. I don’t believe it is.

amoerie
6 months ago

Hi Jon, thanks for the nice article! Can I suggest a possible improvement?

Instead of throwing the exception via “throw task.Exception;”, it could be better to use ExceptionDispatchInfo.Capture(task.Exception).Throw();

This would cause the call stack to point to the original exception, instead of all errors pointing to your code and losing a lot of context.