3

For it to actually become fire and forget as I expect it to. I'll need to call it inside a Task.Run(() => DbCallTestCommand()) instead which seems a bit unnecessary to me?

This is an example code that reproduced the issue in LINQPad:

void Main()
{
    "Main start!".Dump();

    _ = DbCallTestCommand();

    "Main Done!".Dump();
}

//Makes no sense to really execute this as a fire and forget but I included it because Im guessing that EF also uses DbCommand behind the scenes.

async Task DbCallTestEF()
{
    var db = new EventContext(this.connectString);

    var query = db.TBLEVENT.Where(t => t.STARTDATE > DateTime.Now.AddDays(-65));

    var result = await query.ToListAsync(); //Takes about 10 seconds to run in total

    "DbCallTestEF done".Dump();
}

async Task DbCallTestCommand()
{
    using var connection = new OracleConnection(this.connectString);
    await connection.OpenAsync();

    using var command = connection.CreateCommand();
    command.CommandText = "UPDATE_STATISTICS"; //<- long running statistic calculation job that the client shouldn't need to wait for.
    command.CommandType = CommandType.StoredProcedure;

    await command.ExecuteNonQueryAsync();

    "DbCallTestCommand done".Dump();
}

Result:
Main Start!
DbCallTestCommand!
Main Done!

The expected result here (imho) is that the main method should complete BEFORE the discarded method. Because I didn't use an await on DbCallTestCommand() but that is not what happens here.

However if I instead discard a method that simply just awaits a Task.Delay. method. Then it works as expected. Main method completes before discarded method.

See here:

void Main()
{
    "Main start!".Dump();

    _ = TaskDelayTest();

    "Main Done!".Dump();
}

async Task TaskDelayTest()
{
    await Task.Delay(10000);
    
    "TaskDelayTest done!".Dump();
}

Result: (which is the expected result for discarding a task):
Main Start!
Main Done!
TaskDelayTest done!

I'm quite stumped by this and I really think both discards should behave the same (i.e to NOT wait for the methods completion before continuing). So I'm wondering if anyone knows the reason for this and if this is indeed the correct behaviour?

Karmgahl
  • 41
  • 3
  • 2
    Please [don't post images of code](https://meta.stackoverflow.com/questions/285551/why-should-i-not-upload-images-of-code-data-errors-when-asking-a-question). – Xerillio Oct 04 '22 at 13:46
  • Looks like both links are the same. Some methods can only be done asynchronously. Using FirstOrDefault will terminate query faster when only one results is returned instead of waiting until all results are found. – jdweng Oct 04 '22 at 13:46
  • I used images because I wanted to show the result i got as well. But I'll change it! – Karmgahl Oct 05 '22 at 05:30
  • Don't use `await Task.Delay(1);`. Instead, remove the `async` keyword and return `Task.FromResult()` or for methods without a return type, `Task.CompletedTask` – Eric J. Oct 05 '22 at 05:55
  • @EricJ I've changed the code above to try and avoid confusion. But It doesn't really matter if the main method is async or not. The discard still doesn't behave as expected if you try to discard an async method that awaits a dbCommand async method (E.g `await command.ExecuteNonQueryAsync`). – Karmgahl Oct 05 '22 at 06:05

3 Answers3

0

The discard still doesn't behave as expected.

It behaves as it should. Specifically, it doesn't affect program flow at all. You can remove the discard assignment and observe the same behavior.

The Main() function continues to execute while the Task created when you invoke DbCallTestCommand() continues to execute.

which is the expected result for discarding a task

You don't discard the task with that syntax. You tell the compiler that you don't care to track whatever is returned from the function call (task in this case) in a variable. The task is still created and continues to execute until completion.

You can see that without a dependency on your database with the following modification (includes LinqPad's .Dump()):

// The behavior is the same if you change this to
// void Main()
Task Main()
{
    "Main start!".Dump();

    // Same as:
    // DbCallTestCommand();
    _ = DbCallTestCommand();

    "Main Done!".Dump();

    // To force the program to exit immediately, uncomment this:
    // Environment.Exit(0);
    return Task.CompletedTask;
}

async Task DbCallTestCommand()
{
    "DbCallTestCommand() starting".Dump();
    for (int i = 0; i < 3; i++)
    {
        $"DbCallTestCommand() {i}".Dump();
        await Task.Delay(1000);
    }
    
    "DbCallTestCommand() ending".Dump();
}

Output:

Main start!

DbCallTestCommand() starting

DbCallTestCommand() 0

Main Done!

DbCallTestCommand() 1

DbCallTestCommand() 2

DbCallTestCommand() ending

You will also have compiler warning BC42358 informing you of what's going on:

Because this call is not awaited, execution of the current method continues before the call is completed

Eric J.
  • 147,927
  • 63
  • 340
  • 553
  • Yes when DbCallTestCommand is only awaiting a Task.Delay this is what happens and what I expect to happen. But if you inside DbCallTestCommand instead await an oracleCommand.ExecuteNonQueryAsync then the "_ = DbCallTestCommand" all of a sudden behaves just as If I await it. I.e the program waits for _ = DbCallTestCommand to finish before continuing which is only supposed to happen if i wrote `await DbCallTestCommand()` – Karmgahl Oct 05 '22 at 06:19
  • I don't see how that could possibly happen, since the behavior of running that code in a separate task is a .NET concern and unrelated to what happens inside of `DbCallTestCommand()`. – Eric J. Oct 05 '22 at 18:04
  • Yes, it is very strange. But nevertheless it is what is currently happening. Which is why I felt it was finally time to ask a question here on stackoverflow. Because this doesn't make any sense. – Karmgahl Oct 06 '22 at 07:52
0

After seeing a comment on a tweet about the upcoming ODP.NET 7 release. Where they were asking about if this release will finally come with "true async". I started doing some digging.

As it turns out. No async method in OracleCommand is truly async. They're apparently fake. Which I'm guessing is the most likely reason why "fire and forget" using the _ discard operator simply doesn't work with OracleCommand executes.

True async is however something that will be added in another ODP.NET release version, due sometime in 2023. See: https://github.com/oracle/dotnet-db-samples/issues/144 for more info.

Karmgahl
  • 41
  • 3
0

Because Oracle driver is not actually "truly" async. See more here.

If for some reason you need fire-and-forget functionality as a quick fix you can wrap the call into Task.Run.

Also note that your example code is not guaranteed to wait for the completion of the fire-and-forget task at all, so depending on how do you run it the "truly" async version can miss any output from the DbCallTestCommand after first await.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132