3

Using VS2013, EF6.1.1, MVC5, .net 4.5.

I have just started looking into async/await for the first time and I am not sure if this is doing it properly. It appears to work, but could it be simpler? I seem to be sticking async/await in a lot of places for one method call down multiple layers.

All code has been simplified for brevity.

In my MVC controller action method I have:

public async Task<ActionResult> TestAction1()
{
    var testResponse = await _testService.TestMethod1(1);

    if (testResponse != null)
    {
        _unitOfWork.CommitAsync();
        return View(testResponse);
    }

    return RedirectToAction("TestAction2");
}

My service class is as follows:

public class TestService : ITestService
{
    public async Task<TestObject> TestMethod1()
    {
        var testObject1 = await privateMethod1Async();
        if (testObject1 != null) return testObject1;

        testObject1 = await privateMethod2Async();

        return testObject1;
    }

    private async Task<TestObject> privateMethod1Async()
    {
        return await _testRepository.FirstOrDefaultAsync();
    }

    private async Task<TestObject> privateMethod2Async()
    {
        return await _testRepository.FirstOrDefaultAsync();
    }
}

And my repository method:

public async Task<TEntity> FirstOrDefaultAsync()
{
    return await _entitySet.FirstOrDefaultAsync();
}

Basically, I have a controller method, that calls a service method. The service method is to call the database layer twice, in an async fashion. But I feel that I am changing every single method and layer to deal with async and I wasn't sure if what I have here is correct.

Secondly, in the controller method I am unsure how to call the unit of work commit method asynchronously. The line that has "_unitOfWork.CommitAsync();". I cannot stick an "await" before it as it is a void method.

Any thoughts?


EDIT 1

Here is the full version of the repository method call to EF:

public async Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> 
                                               predicate, params 
                                               Expression<Func<TEntity, object>>[]
                                               includeProperties)
{
    IQueryable<TEntity> query = EntitySet;
    if (includeProperties != null && includeProperties.Any())
    {
        query = IncludeProperties(query, includeProperties);
    }
    return await query.FirstOrDefaultAsync(predicate);
}
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
eyeballpaul
  • 1,725
  • 2
  • 25
  • 39
  • *The line that has "_unitOfWork.CommitAsync();". I cannot stick an "await" before it as it is a void method.* `public async Task CommitAsync() { ... }`. – ta.speot.is Jan 06 '15 at 11:50
  • Ah ok, I suppose I can just do nothing with the return of that. So there's no way to use await with a void that I'm missing? – eyeballpaul Jan 06 '15 at 11:52
  • *I suppose I can just do nothing with the return of that.* No, you can `await` it. – ta.speot.is Jan 06 '15 at 11:54
  • ok thanks. What about the rest of it? I seem to be adding the same async/await stuff to every methodf and layer, is that right? – eyeballpaul Jan 06 '15 at 11:56

2 Answers2

8

I see a reoccurring pattern in your code:

private async Task<TestObject> privateMethod2Async()
{
    return await _testRepository.FirstOrDefaultAsync();
}

When you have a single-liner which simply queries your DB for a Task<T>, you can avoid the state-machine allocation which await causes, and simply return the hot task to the caller (as he'll probably await on it anyway higher up the call-chain):

private Task<TestObject> privateMethod2Async()
{
    return _testRepository.FirstOrDefaultAsync();
}

Note that async-await will make you go "async all the way", that's the nature of async. Make sure that you group tasks that can run concurrently using Task.WhenAll if possible, for example (not sure this might be the best example):

public async Task<TestObject> TestMethod1()
{
    var testObject1 = await privateMethod1Async();
    if (testObject1 != null) return testObject1;

    testObject1 = await privateMethod2Async();

    return testObject1;
}

Might be able to turn into:

return Task.WhenAny(privateMethod1Async(), privateMethod2Async());

given that one of the methods is applicable as a return type.

Edit:

What is the general rule rule of thumb for adding the async/await? is it for a method that does other processing in it? Which, as you point out, this method doesn't?

You would want to use await when you'd want to be doing more processing on the returned Task. If all you want is to return the actual Task, there is no need to await it, you can simply return the hot task. That's the general rule i use. Also note that exception handling is different when you use return await vs simply return.

You can read more on that in At the end of an async method, should I return or await? and Any difference between "await Task.Run(); return;" and "return Task.Run()"?

Community
  • 1
  • 1
Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • Is `async` necessary in your last example? – ta.speot.is Jan 06 '15 at 12:21
  • My code was simplified to make it easier to read. However, you do hit a point. What is the generule rule of thumb for adding the sync/await? is it for a method that does other processing in it? Which, as you point out, this method doesn't? – eyeballpaul Jan 06 '15 at 12:23
  • @YuvalItzchakov I have updated my question with an edit at the bottom to show the full version of that method. Would you still say it doesnt need the await-async? Also, those 2 method calls need to be done one after the other. One only runs if the first is null. – eyeballpaul Jan 06 '15 at 13:03
  • @eyeballpaul Regarding the second part, that's why i said it might not be the best example, but i just wanted to convey the point of what could be done. Regarding your edit, yes, you could definitely `return query.FirstOrDefaultAsync(predicate);` and remove the `async` modifier. – Yuval Itzchakov Jan 06 '15 at 13:08
  • Thank you for your help. I have simplified the lower levels to only return "Task", and leave the async-await to the service levels that do more actual work. – eyeballpaul Jan 06 '15 at 13:45
3

async-await does have a tendency to crawl upwards in your code base and it's perfectly fine. You should let it rise up as long as it's possible (in a UI event handler it's possible to rise all the way to the top because of the combination of async-void and the UI SynchronizationContext)

When it isn't possible anymore (the root of a console application for example) you can simply Wait the returned task:

var task = RunAsync();
task.Wait();

Or even better, you can use Stephen Cleary's AsyncContext:

AsyncContext.Run(RunAsync);
i3arnon
  • 113,022
  • 33
  • 324
  • 344