John Bustos
John Bustos

Reputation: 19544

EF6 ToListAsync() Cancel Long Query not working

I am working on an Entity Framework WinForms app and am trying to run queries on a thread separate from the UI and allow the user to cancel long queries. I've already asked one question about how to implement this correctly and might still be incorrect there too, but my current question is how do I allow a user to cancel a long-running EF6 query?

I found links along the lines of this, but still can't seem to get this working well... Again, it may be that I programmed the original part wrong (as from my first question), but my question is how do I allow a user to click a cancel button that would stop a long query on the DB?

My (relevant) current code is as follows...

Private cts As New CancellationTokenSource

Private Sub Cancel_Click(sender As Object, e As EventArgs) Handles Cancel.Click
    cts.Cancel()
End Sub

Attempt 1 (add cancellation token in Task):

Private Async Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim y As New List(Of DBData)

    Try
        Var1 = "Var1"

        Dim qry As Task(Of List(Of DBData)) = Task.Run(Function() GetDBData(Var1), cts.Token))

        Try
            y = Await qry
            cts.Token.ThrowIfCancellationRequested()
        Catch ex As OperationCanceledException
            ' ** ONLY REACH HERE AFTER QUERY PROCESSES!! **
        End Try
End Sub

Attempt 2 (add cancellation token in EF ToListAsync() method):

Private Async Function GetDBData(Var1 As String, ct As CancellationToken) As Task(Of List(Of DBData))

    Dim retval As List(Of DBData)

    Using x As New DBContext
        Try
            retval = Await (From rw In x.DBData
                             Where rw.Val1= Val1
                             Select rw).ToListAsync(ct)
            ct.ThrowIfCancellationRequested()

            Return retval

        Catch ex As Exception
            ' ** ONLY REACH HERE AFTER QUERY PROCESSES!! **
            MsgBox(ex.Message)
            Return Nothing
        End Try
    End Using

End Function

I hope my explanation / code posted makes sense in the differences between the 2... Any help would be greatly appreciated! - And even though this is in VB, I'm equally comfortable with VB / C# solutions.

Thanks!!

Upvotes: 4

Views: 3070

Answers (2)

Tom Deloford
Tom Deloford

Reputation: 2165

It is a bug that was never fixed by the EF team sadly.

In fact it is an overall weakness in the whole EF 6 Async implementation.

The dev team have done their very best to abstract away any access to the underlying SqlCommand by using internal class InternalContext etc.

What they have failed to do is to connect the Cancel() action of your CancellationToken to the SqlCommand.Cancel. This is a clear bug and very frustrating.

If your query returns rows then their code works because the task will return after an iteration, but this a very poor effort.

internal static Task<List<T>> ToListAsync<T>(this IDbAsyncEnumerable<T> source, CancellationToken cancellationToken)
    {
      TaskCompletionSource<List<T>> tcs = new TaskCompletionSource<List<T>>();
      List<T> list = new List<T>();
      IDbAsyncEnumerableExtensions.ForEachAsync<T>(source, new Action<T>(list.Add), cancellationToken).ContinueWith((Action<Task>) (t =>
      {
        if (t.IsFaulted)
          tcs.TrySetException((IEnumerable<Exception>) t.Exception.InnerExceptions);
        else if (t.IsCanceled)
          tcs.TrySetCanceled();
        else
          tcs.TrySetResult(list);
      }), TaskContinuationOptions.ExecuteSynchronously);
      return tcs.Task;
    }

This could be fixed by adding logic to the IDbAsyncEnumerable to handle cancellation or creating a new interface IDbCancellableAsyncEnumerable, that is, a class that exposes a Cancel method that internally accesses the executing SqlCommand and calls Cancel.

As they haven't bothered with this I suspect this will not be happening any time soon as it would be probably quite a lot of work.

Note: Even if you try to use ExecuteSqlCommandAsync() you still have no way to kill the SqlCommand.

Upvotes: 7

Servy
Servy

Reputation: 203821

Too add a CancellationToken to an existing task you can simply add a continuation to that task in which the continuation doesn't do anything other than propagate the task's status and add a new CancellationToken.

public static Task<T> WithToken<T>(
    this Task<T> task,
    CancellationToken token)
{
    return task.ContinueWith(async t => await t, token)
        .Unwrap();
}
public static Task WithToken(
    this Task task,
    CancellationToken token)
{
    return task.ContinueWith(async t => await t, token)
        .Unwrap();
}

It's of course worth noting that using a method like this in no way actually cancels the underlying operation performed by the task in question, it merely allows the execution of the program to continue on despite the fact that the program has not yet completed. The program should be designed such that it will still function if that underlying operation is still doing work in the path where the task is cancelled.

Upvotes: 0

Related Questions