I am beginning to learn about Async / Task functions for running cancellable SQL queries in VB.NET.
I have the following code in a class library which runs two tasks and does not have any exception handling, as I would like to handle this in the application that calls the class library. I have the following code in the class library.
Public Async Function DirectoryList(ct As CancellationToken) As Task(Of List(Of Directory))
ct.ThrowIfCancellationRequested()
Dim ds As DataSet
Dim dirs As List(Of Directory)
Dim ctUnregister As CancellationTokenRegistration
ds = Await Task.Run(Function()
Using newConnection = New SqlConnection(Me.InitializationData.ConnectionString)
Using sqlAdapter = New SqlDataAdapter("DirectoryList", newConnection)
ctUnregister = ct.Register(Sub() sqlAdapter.SelectCommand.Cancel())
With sqlAdapter
.SelectCommand.CommandType = CommandType.StoredProcedure
.SelectCommand.CommandTimeout = Me.InitializationData.CommandTimeout
End With
Dim newDataSet As DataSet = New DataSet()
sqlAdapter.Fill(newDataSet)
Return newDataSet
End Using
End Using
' Remove the registration we set earlier so the cancellation token can be used normally.
ctUnregister.Dispose()
End Function, ct)
dirs = Await Task.Run(Function()
Dim dirsResult As New List(Of Directory)
Dim tbl As DataTable = ds.Tables(0)
For Each row As DataRow In tbl.Select()
' process the data
ct.ThrowIfCancellationRequested()
Next
Return dirsResult
End Function, ct)
Return dirs
End Function
I then call it as follows:
Try
dirs = Await databaseLibrary.DirectoryList(cts.Token)
MsgBox("Query complete!")
Catch ex As System.Data.SqlClient.SqlException
MsgBox("Cancelled (SQL)")
Catch ex2 As OperationCanceledException
MsgBox("Cancelled")
Catch ex3 As Exception
MsgBox("Cancelled")
End Try
Functionally it seems to work as expected - I can cancel the requests and exceptions are thrown as expected.
However I would like to handle the exceptions so I can gracefully cancel the tasks, but even when running in Debug mode within the IDE, the app still breaks and the exception (e.g. SqlException) is shown within the IDE. If I step on, eventually the Catch logic runs.
When the application is run outside of the IDE, the exception handling works as expected.
This seems different to the normal behaviour when running in the debugger- normally the exception handling runs but only breaks if the exception is unhandled.
Why is this behaviour different, presumably because of the Async functions?
when running in Debug mode within the IDE, the app still breaks and the exception (e.g. SqlException) is shown within the IDE.
Why is this behaviour different, presumably because of the Async functions?
Exceptions thrown by asynchronous functions are not caught directly by the catch. What actually happens is that the exception is caught by a compiler-generated state machine, and that exception is placed on the returned Task. Later, when that task is awaited, the exception is re-raised, and then can be caught by the catch.
However, this "indirection" in exception handling means that the IDE does kind of freak out when the exception is initially thrown. As far as it can tell, there is no catch in your code that is going to catch it, so that's why it breaks and displays the exception. It doesn't know that the compiler-generated code will catch it. You can tell the IDE not to worry about uncaught exceptions.
Related
I have an async function that makes a call to an API, but sometimes there is bad data and I want an exception to be thrown to stop other subsequent procedures from having to run. The Async procedure looks like this:
public async Function getInfo(url as string) as task(of string)
Dim htpRes As HttpResponseMessage = Await url.GetAsync().ConfigureAwait(False)
Dim result = htpRes.Content.ReadAsStringAsync.Result
If result = "" Then
Throw New Exception("API Failed")
Else
Return result
End If
End Function
That function is called by a procedure that looks like this:
sub hitAllAPIs(apiList As List(Of String))
For each i In apiList
Try
Dim info As String = getInfo(i)
doOtherStuffWithInfo(info)
Catch ex As Exception
logError
End Try
Next
End sub
The desired behavior is for the forloop in 'hitAllAPIs' to keep running even if an exception is thrown within 'getInfo'. Instead, what happens is that the exception gets hit and stops the code from running, whether I'm in Debug mode or Release mode. If I'm not there to babysit it and hit 'continue' then the forloop will just stop and the program won't run anymore. Once I hit 'continue', btw, the 'Catch' will work and the error will be logged.
The issue is that I need this to all happen automatically and that's not happening. I can't just eliminate the exception and check the function for a null value, since this is a very simplified version of my code and the function is actually called all over the place. I know that I can change my exception settings to simply skip over all exceptions like this, but this is happening even in Release mode to code that has been deployed. I can't imagine that my debugging exceptions should have an effect on code deployed in Release mode. In any case, I'm hoping someone can help me understand why this exception isn't being automatically handled by the try block.
Thanks!
It seems result = "" is an expected result not an exception. Using Try/Catch is rather heavy handed. Exception handling is for unexpected results. Get rid of the Throw in the Function and add and If in the For Each.
Public Async Function getInfo(url As String) As Task(Of String)
Dim htpRes As HttpResponseMessage = Await url.GetAsync().ConfigureAwait(False)
Dim result = htpRes.Content.ReadAsStringAsync.Result
Return result
End Function
Sub hitAllAPIs(apiList As List(Of String))
For Each i In apiList
Dim info As String = getInfo(i)
If info = "" Then
'Add an overload of logError that accepts a string
logError("API failed")
Else
doOtherStuffWithInfo(info)
End If
Next
End Sub
I have a webservice GetCard which calls a function CreateRaces. Inside the CreateRaces I am saving the new objects. This works fine without async. However, when I call the async function for task1 CommitAsync the method executes and returns a response without hitting the next break point. I have tried to work around with via Task.Run, ConfigureWait(true), and with
With HttpContext.Current
bulk.Setup etc...
End With
but nothing seems to work.
The bulk.Setup - CommitAsync calls a third party dll (SqlBulkTools) which returns Task.
It looks like it works on a different thread.
Any ideas how to configure it so I the function executes and I can hit the next break point which is sw.Stop()?
<HttpGet>
<AllowAnonymous>
Public Async Function GetCard(cardurl As String) As HttpResponseMessage
Try
Dim oRace As New RaceCard(cardurl)
oRace.DoWork()
Dim oRC As New RaceController
await oRC.CreateRaces(oRace.Races)
Return Request.CreateResponse(HttpStatusCode.OK, True.ToJson)
Catch ex As Exception
Return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex)
End Try
End Function
CreateRaces function
Public Async Function CreateRaces(ByVal t As List(Of Race)) As Threading.Tasks.Task(Of Boolean)
Try
'Proceed only if list has items
If t.Count > 0 Then
Dim bulk As New BulkOperations()
Dim sw As New Stopwatch()
Dim oCon As New SqlClient.SqlConnection(DotNetNuke.Common.Utilities.Config.GetConnectionString)
sw.Start()
Dim task1 = Await bulk.Setup(Of Race).ForCollection(t).WithTable("ArbiRace").AddAllColumns().BulkInsertOrUpdate().SetIdentityColumn(Function(i) i.RaceID).MatchTargetOn(Function(m) m.RaceName).CommitAsync(oCon).ConfigureAwait(True)
sw.Stop()
Dim swt = sw.ElapsedMilliseconds
End If
Return True
Catch ex As Exception
Dim s As String
s = ex.ToString
Return False
End Try
End Function
Asnyc calls can be handled in 2 ways:
Deliberately halting thread execution until one, several, or all tasks are completed.
Leaving the task to run while the current thread goes to do something else.
Based on your question, you are expecting #1, but your code currently does #2. Await Documentation:
An Await expression or statement does not block the thread on which it is executing. Instead, it causes the compiler to sign up the rest of the async method, after the Await expression, as a continuation on the awaited task. Control then returns to the caller of the async method. When the task completes, it invokes its continuation, and execution of the async method resumes where it left off.
In your case, once CreateRaces() reaches the Dim task1 line (which will eventually store the result, not the task), control returns to GetCard(). However, the CreateRaces() call here also uses an Await, so control attempts to go back up the call stack again. Except, there is nowhere left to go up, because you are at the original endpoint. This is why you seem to get a response too soon.
Since you are waiting for the task to finish to get the elapsed time, you want to wait for the tasks to finish before returning a response. This can be accomplished with task.Result, task.Wait(), or the shared method Task.WaitAll().
Example:
<HttpGet>
<AllowAnonymous>
Public Function GetCard(ByVal cardUrl As String) As HttpResponseMessage
Try
Dim oRace As New RaceCard(cardurl)
oRace.DoWork()
Dim oRC As New RaceController
Dim createRacesTask As Threading.Tasks.Task(Of Boolean) = oRC.CreateRaces(oRace.Races)
'Do other tasks here while we wait
Threading.Tasks.Task.WaitAll(createRacesTask, ...)
Return Request.CreateResponse(HttpStatusCode.OK, True.ToJson)
Catch ex As Exception
Return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, ex)
End Try
End Function
I am planning to implement TPL in my existing asp.net application to improve the performance.
I am implementing them in the selective places (like calls to write data to one table and some kind of irrelevant and non sync operations ), and we are processing all the unhandled exception in the application_error event in the global.asax file.
But when I get an error the TPL its not throwing that error to the application_error event.
When I tried with Task.WaitAll(t1) then its throwing to app_error event, but its taking a while to process the written code.
But when I try with the following method (to take some time only when fault is occurred) then also its not throwing the exception to the main thread.
Task.Factory.StartNew(Sub(mainContext)
HttpContext.Current = mainContext
LookUpRepository.AddItems(CurrentContext.LoggedInUser.UserID, ClientID)
End Sub, HttpContext.Current).ContinueWith(Sub(tplException)
For Each ex In tplException.Exception.InnerExceptions
Throw ex
Next
End Sub, TaskContinuationOptions.OnlyOnFaulted Or TaskContinuationOptions.ExecuteSynchronously)
I tried with ConcurrentQueue to catch the exceotion and sometimes it halts the applcation and the control is not proceeding further line of code.
So how can I attach the the TPL exceotion to the main thread which is hosting the asp.net application.
There is an observable collection ObservableCollection that is available in .Net. If there is an error in the thread that we opened and if we add them in this ObservableCollection , then it will raise an event, From that event we can handle this error
Task.Factory.StartNew(Sub(mainContext)
'Using sw As StreamWriter = File.AppendText("c:/logs/t7.txt")
' sw.WriteLine("thread=" + Thread.CurrentThread.ManagedThreadId.ToString())
'End Using
HttpContext.Current = mainContext
'Do some operation
End Sub, HttpContext.Current).ContinueWith(Sub(tplException, subContext)
HttpContext.Current = subContext
If IsNothing(tplException.Exception) Then Exit Sub
For Each ex In tplException.Exception.InnerExceptions
MyAppState.Instance.ObservableException.Add(ex)
Next
End Sub, HttpContext.Current)
Collection needs to declared
Public WithEvents ObservableException As ObservableCollection(Of Exception)
Event that will get raised by .Net framework to handle the exception
Private Sub CollectionChangedHandler(sender As Object, e As NotifyCollectionChangedEventArgs) Handles ObservableException.CollectionChanged
Dim ex = DirectCast(e.NewItems(0), Exception)
ExceptionLog.SaveFile(ExceptionLog.ReadExceptionInformationFromException(ex))
End Sub
The following method is called from within a loop:
Public Sub ExampleAbort()
Using trans As New TransactionScope(TransactionScopeOption.Required, New TransactionOptions With {.IsolationLevel = IsolationLevel.ReadUncommitted, .Timeout = New TimeSpan(0, 10, 0)})
Try
DoSomeTransationalStuffHere()
trans.Complete()
Catch ex As Exception
ShowError(ex.Message)
Throw
End Try
End Using
End Sub
This will lead to the DTS aborting the transaction after the first item. If I place the 'Using' statement within the Try/Catch block, everything works. Can someone please explain this behavior? I was under the assumption that Using-blocks ALWAYS Dispose'd no matter how you return from a method.
Best regards,
Goran
Here's the code example :
Try
Throw New FirstException()
Finally
Throw New SecondException()
End Try
I figured out it only throws SecondException out and FirstException just vanishes.
I thought FirstException would be inside InnerException property of SecondException but it appears it is not.
I'm not blocked on anything as I don't really need the FirstException to show up, I'm just rather intrigued about this behaviour.
Is there a way to know SecondException did get thrown first when
catching it all at upper level ?
If the first exception really is overriden by the second, what is the
reason ?
Does it happen in every other language ? Is it logical ?
I guess the primary explanation for why this works this way is that you are never catching your first exception and passing it along the chain. If you have a situation like the above where you may be throwing several exceptions on the way back to the original caller then you have to either catch them as they are thrown (and include them as an inner exception when creating the next one) :
Dim ex1 As Exception = Nothing
Try
Throw New Exception("first exception")
Catch ex As Exception
ex1 = ex
Finally
Throw New Exception("second exception", ex1)
End Try
Or, probably better - just don't throw until you have all of the exceptions figured out:
Dim ex1 As Exception = Nothing
Try
ex1 = New Exception("first exception")
Finally
Throw New Exception("second exception", ex1)
End Try
Throwing and catching exceptions is expensive, so it's probably best to not throw until you're ready to return and just log along the way.
One of the limitations of exception handling in .net is that there is no nice way for code in a Finally block to know what exception, if any, caused the code in the Try block to exit, nor is there any normal way for code in a finally block which does have such information to make it available to code which might throw an exception.
In vb.net, it's possible to kludge things in a manner that works pretty well, even though it looks a bit ugly.
Module ExceptionDemo
Function CopySecondArgToFirstAndReturnFalse(Of T)(ByRef dest As T, src As T) As Boolean
dest = src
Return False
End Function
Function AnnotateExceptionAndReturnFalse(ex As Exception, TryBlockException As Exception) As Boolean
If ex Is Nothing Then Return False ' Should never occur
If TryBlockException Is Nothing Then Return False ' No annotation is required
ex.Data("TryBlockException") = TryBlockException
Return False
End Function
Sub ExceptionTest(MainAction As Action, CleanupAction As Action)
Dim TryBlockException As Exception = Nothing
Try
MainAction()
Catch ex As Exception When CopySecondArgToFirstAndReturnFalse(TryBlockException, ex)
' This block never executes, but above grabs a ref to any exception that occurs
Finally
Try
CleanupAction()
Catch ex As Exception When AnnotateExceptionAndReturnFalse(ex, TryBlockException)
' This block never executes, but above performs necessary annotations
End Try
End Try
End Sub
Sub ExceptionTest2(Message As String, MainAction As Action, CleanupAction As Action)
Debug.Print("Exception test: {0}", Message)
Try
ExceptionTest(MainAction, CleanupAction)
Catch ex As Exception
Dim TryBlockException As Exception = Nothing
Debug.Print("Exception occurred:{0}", ex.ToString)
If ex.Data.Contains("TryBlockException") Then TryBlockException = TryCast(ex.Data("TryBlockException"), Exception)
If TryBlockException IsNot Nothing Then Debug.Print("TryBlockException was:{0}", TryBlockException.ToString)
End Try
Debug.Print("End test: {0}", Message)
End Sub
Sub ExceptionDemo()
Dim SuccessfulAction As Action = Sub()
Debug.Print("Successful action")
End Sub
Dim SuccessfulCleanup As Action = Sub()
Debug.Print("Cleanup is successful")
End Sub
Dim ThrowingAction As Action = Sub()
Debug.Print("Throwing in action")
Throw New InvalidOperationException("Can't make two plus two equal seven")
End Sub
Dim ThrowingCleanup As Action = Sub()
Debug.Print("Throwing in cleanup")
Throw New ArgumentException("That's not an argument--that's just contradiction")
End Sub
ExceptionTest2("Non-exception case", SuccessfulAction, SuccessfulCleanup)
ExceptionTest2("Exception in main; none in cleanup", ThrowingAction, SuccessfulCleanup)
ExceptionTest2("Exception in cleanup only", SuccessfulAction, ThrowingCleanup)
ExceptionTest2("Exception in main and cleanup", ThrowingAction, ThrowingCleanup)
End Sub
End Module
The module above starts with a couple helper modules which should probably be in their own "Exception helpers" module. The ExceptionTest method shows the pattern for code which might throw an exception in both the Try and Finally block. The ExceptionTest2 method calls ExceptionTest and reports what exception if any comes back from it. ExceptionDemo calls ExceptionTest2 in such a way as to cause exceptions in different combinations of the Try and Finally blocks.
As shown, if an exception occurs during cleanup, that exception will be returned to the caller, with the original exception being an item in its Data dictionary. An alternative pattern would be to catch the exception that occurs on cleanup and include it in the data of the original exception (which would be left uncaught). My general inclination is that it's probably better in many cases to propagate the exception that occurs during cleanup, since any code which was planning to deal with the original exception will probably expect that cleanup succeeded; if such an expectation cannot be met, the exception that escapes should probably not be the one the caller was expecting. Note also that the latter approach would require a slightly different method of adding information to the original exception, since an exception which is thrown in a nested Try block might need to hold information about multiple exceptions that were thrown in nested Finally blocks.