Reputation: 19544
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
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
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