12

I'm using .NET Core 2.0. I've got the following function that calls IDbCommand.ExecuteReader

public async Task<IEnumerable<Widget>> ReadAllAsync(
    System.Data.IDbConnection databaseConnection,
    System.Data.IDbTransaction databaseTransaction)
{
    var commandText = "SELECT WidgetId, Name FROM Widget";

    // _databaseCommandFactory.Create returns an IDbCommand
    var command = this._databaseCommandFactory.Create(databaseConnection, databaseTransaction, commandText);

    using (var dataReader = command.ExecuteReader())
    {
        // iterate through the data reader converting a collection of Widgets (`IEnumerable<Widget>`)
    }
}

I get a warning

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

I was thinking about converting the command.ExecuteReader() statement to await Task.Run(() => command.ExecuteReader()) as advised in the warning. But I'm not sure this is the correct approach, I believe Task.Run(...) is for doing CPU based work. This is primarily IO work.

So my questions are

  1. Is Task.Run(...) the correct approach?
  2. If not, is there another solution?
  3. Or should I just ignore the warning for now and wait until ExecuteReaderAsync gets added to the IDbCommand interface? (is there a plan for this?)
Kevin Brydon
  • 12,524
  • 8
  • 46
  • 76
  • 3
    `IDbCommand` is unlikely to get `ExecuteReaderAsync` any time soon -- it could break existing implementations that have no support for async/await. `DbCommand`, however, does have it, and all framework implementations of `IDbCommand` inherit from it, so it's no great risk to cast it. – Jeroen Mostert Aug 23 '17 at 10:28
  • I second what JeroenMostert said. You could always check for DbCommand and if viable, cast to it so you have access to the async member. – Nkosi Aug 23 '17 at 10:41
  • You could also consider using [StackExchange.Dapper](https://github.com/StackExchange/Dapper) – Nkosi Aug 23 '17 at 11:32
  • @Nkosi I'm currently seeing what I can do with the framework as-is i.e. not introducing third party components unless absolutely necessary. It's going well so far. I will have a look at Dapper though. – Kevin Brydon Aug 23 '17 at 11:59
  • You shall consider using Dapper Micro ORM, as it makes working with Ado.net much simpler and has all the Async options available. Needn't even worry about opening / closing the connection. – Mrinal Kamboj Aug 24 '17 at 17:15

1 Answers1

9

The await keyword is what allows the method to run asynchronously. The async keyword enables the use of the await keyword within the method and assists in managing the return.

Until await is called the method will run synchronously.

So all of this runs synchronously. It will not return anything or move through the method until it has completed.

public async Task<IEnumerable<Widget>> ReadAllAsync(
    System.Data.IDbConnection databaseConnection,
    System.Data.IDbTransaction databaseTransaction)
{
    var commandText = "SELECT WidgetId, Name FROM Widget";

    // _databaseCommandFactory.Create returns an IDbCommand
    var command = this._databaseCommandFactory.Create(databaseConnection, databaseTransaction, commandText);

    using (var dataReader = command.ExecuteReader())
    {
        // iterate through the data reader converting a collection of Widgets (`IEnumerable<Widget>`)
    }
}

By casting to DbCommand, which most IDbCommand derived implementations already do, then casting to DbCommand and adding await would work e.g.

var dbCommand = (DbCommand) command;
using (var dataReader = await dbCommand.ExecuteReaderAsync())
{
    while (await dataReader.ReadAsync()) 
    {
        // iterate through the data reader converting a collection of Widgets (`IEnumerable<Widget>`)
    }
}

or creating a separate Task

public async Task MyAsyncMethod()
{
  // Do your stuff that takes a long time
}

public async Task CallMyAsyncMethod()
{
  // We can await Tasks, regardless of where they come from.
  await MyAsyncMethod();

}

This way - the program will continue while awaiting the return from this method, rather than locking up the UI and all else.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • `(DbCommand) command` has the courtesy of failing immediately with an `InvalidCastException`. Getting the object with `as`, then using it, just opens you up to a mysterious `NullReferenceException`. If you think you know what the type is, and you're not prepared to handle errors, just hard cast. – Jeroen Mostert Aug 23 '17 at 11:13
  • @JeroenMostert I was inspecting Dapper and they do the same thing within their extension methods. They don't expose confusing implementation concerns like what I was suggesting in my answer. Nice catch. – Nkosi Aug 23 '17 at 11:25
  • 2
    Thanks @YvetteColomb. I've decided to cast to DbCommand for now. I'm not entirely sure how creating a seperate task would help. Would that not just move the problem to a different method and I'd still get all the warnings? – Kevin Brydon Aug 23 '17 at 11:57
  • 2
    Looks like casting to DbCommand has caused a few problems. I'm getting a "There is already an open DataReader associated with this Command which must be closed first." exception when I try to get data for a second time (refresh a page). My connection and transaction are wrapped in usings so not sure whats happening. – Kevin Brydon Aug 23 '17 at 12:19
  • 2
    @KevinBrydon try wrapping the command as well. Also take a look at this answer re that last error https://stackoverflow.com/a/21131596/5233410 – Nkosi Aug 23 '17 at 12:21
  • Hi @YvetteColomb. Thanks for your help. I'm still getting issues with the "...Command must be closed first" and "This connection does not support MultipleActiveResultSets". I've got everything I can think of in `using` blocks so I'll have to investigate my code a bit deeper. I don't want to enable MultipleActiveResultSets until I know that's the correct solution. – Kevin Brydon Aug 23 '17 at 15:13
  • @Nkosi I'm not sure I want to enable MultipleActiveResultSets until I really understand what that means. Need to do more reading. And I'm pretty sure the root issue lies in my design/implementation. – Kevin Brydon Aug 23 '17 at 15:16
  • 2
    @YvetteColomb False alarm. My code was at fault. Had a method that should have been returning a Task but was returning void. Inside that method was a call to one of my asyncronous repositories. Lesson learned! – Kevin Brydon Aug 23 '17 at 16:06
  • Multiple active result set is mechanism through which multiple result sets can be fetched from the database in a single round trip, like in Sql server management studio we can fire multiple select statements and their results are listed one after another. This is a `DataReader` feature accessed using `NextResult` call, check if there's an explicit call to fetch the multiple result sets – Mrinal Kamboj Aug 24 '17 at 17:26