Converting an iteration to Async - asp.net

I am returning a large number of DataTable rows, iterating them and pushing the values of each row to a webservice, which then returns a response code (string). Should any errors occur in the webservice, the whole process is stopped and an error shown:
Protected Sub DoStuff(sender As Object, e As EventArgs) Handles lnk_orderCards.Click
Dim dt as DataTable = GetDataBaseData()
For each dr as DataRow in dt.Rows()
Dim f as String = dr.Item("firstname").ToString()
Dim m as String = dr.Item("middleName").ToString()
Dim s as String = dr.Item("surname").ToString()
Dim err as String = String.Empty
Dim result as String = XYZService.DoIt(f, m, s)
Select Case result
Case "ok"
' OK - allow For Loop Next '
Case "e"
err = "Some error"
Case "e2"
err = "Another error"
End Select
If Not String.IsNullOrWhiteSpace(err) Then
ShowError(err)
Exit Sub
End If
Next
XYZService.Complete()
ltl_status.Text = "Success!"
End Sub
I assumed that the above would be a good candidate for an Async approach, especially if the datatable has 1000 rows, as each webservice request could be sent in parallel. However, from MSDN I can't find enough examples on how best to implement Async.
Can anyone recommend a more thorough approach? I have read on MSDN about Task.WaitAll and Task.Factory.StartNew but the examples aren't straightforward. If Task.WaitAll was utilised like this, how do you stop the flow if one (or more) tasks fail?
It's important that all tasks much return success before XYZService.Complete() is called.
Final code based on Stephen's input
Protected Async Sub DoStuff(sender As Object, e As EventArgs) Handles lnk_orderCards.Click
Dim cts As New CancellationTokenSource
Dim dt As DataTable = GetDataBaseData()
Dim rows As IEnumerable(Of Task(Of String)) = (From dr As DataRow In dt.Rows Select DoServiceCall(cts, dr))
Dim results() As String = Await Task.WhenAll(rows)
Dim errors As List(Of String) = (From s As String In results Where s <> String.Empty).ToList()
If errors.Count > 0 Then
ShowError(String.Join("<br/>", errors))
Exit Sub
Else
Console.WriteLine("Success")
End If
End Sub
Protected Async Function DoServiceCall(t As CancellationTokenSource, dr As DataRow) As Task(Of String)
If t.IsCancellationRequested Then
t.Token.ThrowIfCancellationRequested()
End If
Dim f As String = dr.Item("firstname").ToString()
Dim m As String = dr.Item("middleName").ToString()
Dim s As String = dr.Item("surname").ToString()
Dim returnResult As XYZService.ServiceReturnResult = Await XYZService.DoItAsync(f, s, s)
Select Case returnResult.return
Case "ok"
' OK - allow For Loop Next '
Case Else
t.Cancel(False)
Throw New Exception("Web service error: " & returnResult.return)
End Select
Return returnResult.return
End Function

The simplest solution is to use Task.WhenAll. Specifically, you can project (LINQ's Select) each item into a Task (of T) and then do an Await Task.WhenAll.
However, that simple approach will execute all requests concurrently, and will not stop other requests if one of them failed. That's a bit more complicated; I recommend using a CancellationTokenSource to represent the "emergency stop", with each request receiving the CancellationToken and cancelling the source if it fails.
Do not use Task.Run or its even-worse cousin StartNew. Since your operation is asynchronous by nature (an I/O-bound network request), you want asynchronous concurrency (Task.WhenAll), not parallel concurrency (StartNew).

Related

Access VB property based on name as string - Fastest Option

I'm developing an ASP.NET MVC web app in VB and I am required to output a set of data to a table format, and to allow the user to configure the order and presence of columns from an available set. The data set is stored as a list of the object type representing the row model.
Currently, I implement this using CallByName. Iterating over an ordered list of property names and outputting the value from the instance of the row model. However, based on testing this seems to be a major bottleneck in the process.
I've seen a recommendation to store delegates to get the property, against the string representation of the property's name. So, I can presumably do something like this:
Public Delegate Function GetColumn(ByRef RowObj As RowModel) As String
Dim GetPropOne As GetColumn = Function(ByRef RowObj As RowModel) RowObj.Prop1.ToString()
Dim accessors As New Hashtable()
accessors.Add("Prop1", GetPropOne)
Then, loop through and do something like this:
Dim acc As GetColumn = accessors(ColumnName)
Dim val As String = acc.Invoke(currentRow)
It looks faster, but it also looks like more maintenance. If this is indeed faster, is there a way I can dynamically build something like this? I'm thinking:
Public Delegate Function GetObjectProperty(Instance As Object) As Object
For Each prop In GetType(RowModel).GetProperties()
Dim acc As GetObjectProperty = AddressOf prop.GetValue
columns.Add(prop.Name, acc)
Next
Dim getColVal As GetObjectProperty = columns(ColumnName)
Dim val As String = getColVal.Invoke(currentRow).ToString()
Open to suggestions for different approaches.
I do a similar thing to turn a SOAP response into a Data Table
Public Function ObjectToDataSource(objName) As DataSet
Dim CollName = ""
Dim ds As New DataSet()
For Each m As System.Reflection.PropertyInfo In objName.GetType().GetProperties()
If m.CanRead Then
If InStr(m.PropertyType.ToString, "[]") <> 0 Then
CollName = m.Name
Exit For
End If
End If
Next
Dim CollObj
CollObj = CallByName(objName, CollName, CallType.Get)
If CollObj.length = 0 Then
Call EndTask("No Supply Chains to display", "Royal Mail failed to return Supply Chain information for these credentials", 3)
Else
Dim dt_NewTable As New DataTable(CollName)
ds.Tables.Add(dt_NewTable)
Dim ColumnCount = 0
For Each p As System.Reflection.PropertyInfo In CollObj(0).GetType().GetProperties()
If p.CanRead Then
If p.Name <> "ExtensionData" Then
dt_NewTable.Columns.Add(p.Name, p.PropertyType)
ColumnCount = ColumnCount + 1
End If
End If
Next
Dim rowcount = CollObj.Length - 1
For r = 0 To rowcount
Dim rowdata(ColumnCount - 1) As Object
For c = 0 To ColumnCount - 1
rowdata(c) = CallByName(CollObj(r), dt_NewTable.Columns.Item(c).ToString, CallType.Get)
Next
dt_NewTable.Rows.Add(rowdata)
rowdata = Nothing
Next
End If
Return ds
End Function
This is specific to my needs in terms of getting CollName and not requiring ExtensionData
If ColumnName is the same name as one of the RowModel's properties I don't see why you need the long workaround with delegates...
An extension method which gets only the property you want right now is both faster and consumes less memory.
Imports System.Runtime.CompilerServices
Public Module Extensions
<Extension()> _
Public Function GetProperty(ByVal Instance As Object, ByVal PropertyName As String, Optional ByVal Arguments As Object() = Nothing) As Object
Return Instance.GetType().GetProperty(PropertyName).GetValue(Instance, Arguments)
End Function
End Module
Example usage:
currentRow.GetProperty("Prop1")
'or:
currentRow.GetProperty(ColumnName)

Stackoverflow exception - Recursive Treeview ASP.Net

I am trying to build a treeview recursively using VB.Net this is my code so far
.... Code to get the data table
For Each row As DataRow In dt.Rows
Dim oName As String = Nothing
Dim pId As String = Nothing
Dim cId As String = Nothing
Dim cmts As String = Nothing
Dim lvl As String = Nothing
oName = row(0)
pId = row(4)
cId = row(5)
If Not String.IsNullOrEmpty(row(3).ToString()) Then
cmts = row(3)
End If
lvl = row(2)
list.Add(New MyObject() With { _
.ObjectName = oName,
.ParentId = pId,
.ChildId = cId,
.Comments = cmts,
.level = lvl
})
Next
BindTree(list, Nothing)
End Sub
Private Sub BindTree(list As IEnumerable(Of MyObject), parentNode As TreeNode)
Dim nodes = list.Where(Function(x) If(parentNode Is Nothing, x.ParentId = "[Transform].[(Root)]", x.ParentId = parentNode.Value))
For Each node As MyObject In nodes
Dim newNode As New TreeNode(node.ObjectName, node.ParentId.ToString())
If parentNode Is Nothing Then
TreeView1.Nodes.Add(newNode)
Else
parentNode.ChildNodes.Add(newNode)
End If
BindTree(list, newNode)
Next
End Sub
also the new class
Public Class MyObject
Public ObjectName As String
Public ParentId As String
Public ChildId As String
Public Comments As String
Public level As Integer
End Class
The issue I am having is that when this goes so far through the recursion I get a System.StackOverFlowException. When looking at the exception snapshot every thing says "unable to evaluate expression" The error is coming from this line
Dim nodes = list.Where(Function(x) If(parentNode Is Nothing, x.ParentId = "[Transform].[(Root)]", x.ParentId = parentNode.Value)) but Ive no idea why or how to resolve it.
Any help would be very much appreciated.
Thanks
Simon
A StackOverflowException isn't thrown by a particular line of code, that's just the first line of code in the method being called which is overflowing the stack. And there isn't any more information that can really be given, because no further code can be executed. (Because the stack is full.)
Your method is recursive:
Private Sub BindTree(list As IEnumerable(Of MyObject), parentNode As TreeNode)
' ...
BindTree(list, newNode)
End Sub
Which is ok, except that you're not modifying the list variable anywhere. So every time you call the method, you call it with the same list. Therefore it will continue to perform the same logic, recursively, indefinitely, with no terminating condition.
Generally, you should think of a recursive method with the following structure (in VB-ish pseudo-code):
Method(ByVal something As SomeType)
' Check if the recursion should end
If SomeTerminatingCondition Then
Return
End If
' Perform the logic for this step of the recursion
DoSomething()
' Recurse again
Method(somethingModified)
End Method
It's not entirely clear to me from the code how list should be modified. But presumably when you call BindTree(list, newNode) at that point list should be some subset of the original list.

How to select GUID column by another GUID column in VB.net?My Select query does not work

I am new to programming and get the chance to work and maintain in another developers project.
The project is built with ASP.Net Vb.Net and SQl Server.
I am trying to select the primary key ID (which is actually a GUID) from a table.
SQID = Core.DB.GetString("SELECT id FROM SQC WHERE sid = " & sid)
In the Table SQC the primary key is id which is guid and the sid is also guid which is primary key to another table.
my previous developer developed the code to select string variable GetString function where GetString is
Shared Function GetString(ByVal selectQueryText As String, ByVal ParamArray params As SqlParameter()) As String
Dim dt As DataTable = Nothing
Try
dt = GetData(selectQueryText, CommandType.Text, params)
If dt.Rows.Count = 0 Then
Return ""
Else
If TypeOf dt.Rows(0)(0) Is DBNull Then
Return ""
Else
Return CStr(dt.Rows(0)(0))
End If
End If
Finally
If dt IsNot Nothing Then dt.Dispose()
End Try
End Function
When I debug the code my process enters into GetString Function and from Get String it goes to GetData function
Shared Function GetData(ByVal selectCommandText As String, ByVal selectCommandType As CommandType, ByVal ParamArray params As SqlParameter()) As DataTable
Dim conn As SqlConnection = Nothing
Try
conn = GetOpenSqlConnection()
Return GetData(conn, selectCommandText, selectCommandType, params)
Finally
If conn IsNot Nothing Then conn.Dispose()
End Try
End Function
Shared Function GetData(ByVal conn As SqlConnection, ByVal selectCommandText As String, ByVal selectCommandType As CommandType, ByVal ParamArray params As SqlParameter()) As DataTable
If conn Is Nothing Then Return GetData(selectCommandText, selectCommandType, params)
Dim sa As SqlDataAdapter = Nothing
Try
sa = New SqlDataAdapter(selectCommandText, conn)
sa.SelectCommand.CommandType = selectCommandType
Dim dt As New DataTable
Try
For Each param As SqlParameter In params
sa.SelectCommand.Parameters.Add(param)
Next
sa.Fill(dt)
Return dt
Catch ex As Exception
dt.Dispose()
Throw ex
End Try
Finally
If sa IsNot Nothing Then sa.Dispose()
End Try
End Function
In the Try Catch area of exeption handling the code breaks and throws the exception error. It saying Incorrect syntax near 'a379'. which is first the part of sid (GUID). I mean the sid value is 9417A379-6371-432F-9DA5-BCFC46DD95A1
I am not sure how to handle this. I want to select the id from from SQC table and store it in a variable.
I am looking for your advice and suggestion. As I am new in the programming world please also point me my mistakes.
Thanks
It looks like your issue could be fixed like so:
SQID = Core.DB.GetString("SELECT id FROM SQC WHERE sid = '" & sid & "'")
But you should be aware that this style of code is open to SQL injection and you may want to look at ways of parameterising your queries (i.e. don't take what's in this project as good practice).

Trying to spawn a new thread in ASP.NET; no errors, no thread

I am trying to spawn a new thread from an ASP page written in VB.NET, on .NET 3.5.
The page allows a user to upload files and then processes the files into the database.
As the processing can take a while, I want to upload them, spawn the processing onto a new thread, and then when it completes send the user a notification through our database-driven notification module in the system.
I've tried a couple different examples I've found online, but they don't seem to work. There is no error thrown, but it never seems to actually launch the processing code on a new thread either.
Here is my submittal code in the web page:
Protected Sub btnSubmit_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btnSubmit.Click
Try
If Not (file1.HasFile Or file2.HasFile Or file3.HasFile Or file4.HasFile Or file5.HasFile) Then
AddErrorMessage("Please specify at least one file.")
Else
Dim l = New List(Of InventoryUploads.File)
If file1.HasFile Then l.Add(New InventoryUploads.File With {.Name = file1.FileName, .Content = file1.FileContent})
If file2.HasFile Then l.Add(New InventoryUploads.File With {.Name = file2.FileName, .Content = file2.FileContent})
If file3.HasFile Then l.Add(New InventoryUploads.File With {.Name = file3.FileName, .Content = file3.FileContent})
If file4.HasFile Then l.Add(New InventoryUploads.File With {.Name = file4.FileName, .Content = file4.FileContent})
If file5.HasFile Then l.Add(New InventoryUploads.File With {.Name = file5.FileName, .Content = file5.FileContent})
InventoryUploads.ProcessFiles(l, Session.UserIdent, chkRcvOverwrite.Checked)
NewRow("Your files have been queued and are being processed. You will be sent a notification when they are completed.")
End If
Catch ex As Exception
LogEx(ex)
NewRow("There was an error queueing your files." & BR & ex.Message)
End Try
End Sub
From a UI point of view, this all works, the page posts the files, and it shows the user the message about "your files have been queued".
The call to InventoryUploads.ProcessFiles is an encapsulation function in front of the threading code, which is all contained in the InventoryUploads module (following):
Imports System.Threading
Imports System.Threading.Tasks
Public Module InventoryUploads
Public Structure File
Private pName As String
Private pContent As IO.Stream
Private pProcessed As Boolean
Public Property Name As String
Get
Return pName
End Get
Set(value As String)
pName = value
End Set
End Property
Public Property Content As IO.Stream
Get
Return pContent
End Get
Set(value As IO.Stream)
pContent = value
End Set
End Property
Public Property Processed As Boolean
Get
Return pProcessed
End Get
Set(value As Boolean)
pProcessed = value
End Set
End Property
End Structure
Public Sub ProcessFiles(files As List(Of File), userident As String, receivingsOverwrite As Boolean)
Try
Dim params = Array.CreateInstance(GetType(Object), 3)
params(0) = files
params(1) = userident
params(2) = receivingsOverwrite
'// Threading Method 1
Dim pts = New ParameterizedThreadStart(AddressOf ThreadedProcessFiles)
Dim thd = New Thread(pts)
thd.IsBackground = True
thd.Start(params)
'// Threading Method 2
'ThreadPool.QueueUserWorkItem(AddressOf ThreadedProcessFiles, params)
'// Threading Method 3
Dim f As Func(Of Integer) = Function() ThreadedProcessFiles(params)
Task.Factory.StartNew(f)
Catch ex As Exception
IO.File.WriteAllText("C:\idwherenet.log", ex.Message)
Throw
End Try
End Sub
''' <summary>
'''
''' </summary>
''' <param name="params">exepcts params to contain 3 objects,
''' 1) type List(Of File),
''' 2) string userident to notify on finish
''' 3) boolean whether receivings should overwrite</param>
''' <remarks></remarks>
Private Function ThreadedProcessFiles(params() As Object) As Boolean
IO.File.WriteAllText("C:\mylog.log", "hello ThreadedProcessFiles?")
'Log("threadedprocessfiles: got here")
Dim files As List(Of File), uident As String = "", rcvovr As Boolean
Try
files = params(0)
uident = CStr(params(1))
rcvovr = CBool(params(2))
If files.Count = 0 Then
SendNotification(NotificationTypes.SYS, uident, "No files provided for inventory upload; nothing processed.")
Exit Sub
End If
For Each f In files
f.Processed = False
If f.Content.Length = 0 Then Continue For
'// process the file here....
f.Processed = True
Next
SendNotification(NotificationTypes.SYS, uident, "Inventory upload processing completed.")
Return True
Catch ex As Exception
'LogEx(ex)
'Log(ex.Message, ex.ToString)
If Not uident.IsEmpty Then SendNotification(NotificationTypes.SYS, uident, "Inventory upload processing encountered an error. " & ex.Message, ex.ToString)
Return False
End Try
End Sub
End Module
In the ProcessFiles sub, you can see the three different methods I have tried to spawn the thread, including a NuGet backport of the Task Parallel Library... neither works. I have confirmed that is is making it into the ProcessFiles sub, and the code to spawn the thread is not throwing any errors... it just never actually calls the ThreadedProcessFiles function.
Any ideas?
UPDATE: Oh, actually, the NuGet package DOES seem to be working, FINALLY! It just doesn't write out to the file (context/security/impersonation permissions I guess). Now I can finally move on!
If you're using .NET 4 or 4.5, the Task Parallel Library makes this sort of thing super easy:
Task.Factory.StartNew(Function() ThreadedProcessFiles(params));
(I think that's the right syntax for VB)
If you go this route, I would change the params to just send the three parameters separately.

.NET Framework Data Provider for Oracle multiple open connection

I have the below mentioned code in a seperate class file for establishing connection and carry out DB transactions. I have an issue where multiple connections being opened which sometime exceed the connection pool. When I stepped through the code I found that there are codes which call ConnectDB() in a loop without calling DisconnectDB(). But I expected that the condition OraConn.State = ConnectionState.Closed should handle the situation. Somehow the condition is always satisfied hence openning another set of connection. Can you suggest where am I going wrong and also what best practice can be adopted here?
Public Class Connection
Dim Str_conn As String = "Data Source=...; User=...; password=...; Min Pool Size=10; Max Pool Size=500;"
Public OraConn As OracleConnection
Dim cmd As OracleCommand
Dim dr As OracleDataReader
Dim data_adapt As OracleDataAdapter
Dim dt As DataTable
Dim ds As DataSet
Public Sub ConnectDB()
OraConn = New OracleConnection(Str_conn)
If OraConn.State = ConnectionState.Closed Then
OraConn.Open()
End If
End Sub
Public Sub DisconnectDB()
If OraConn.State = ConnectionState.Open Then
OraConn.Close()
End If
End Sub
Public Function get_dataset(ByVal query As String, ByRef ds As DataSet) As DataSet
data_adapt = New OracleDataAdapter(query, OraConn)
data_adapt.Fill(ds)
Return ds
End Function
Public Function get_datareader(ByVal query As String) As OracleDataReader
cmd = New OracleCommand(query, OraConn)
dr = cmd.ExecuteReader()
Return dr
End Function
Public Sub UpdateDB(ByVal query As String)
cmd = New OracleCommand(query, OraConn)
cmd.ExecuteNonQuery()
cmd.Dispose()
End Sub
The class is refered in other classes or directly in the aspx.vb pages like this.
Public Function InsertData(ByVal var1 As String, ByVal var2 As String) As Integer
conn.ConnectDB()
Dim qryInsert As String
qryInsert = " INSERT INTO TABLE VALUES ('" & var1 & "', "
qryInsert = qryInsert & var2 & "')"
Try
conn.UpdateDB(qryInsert)
Catch ex As OracleException
If ex.Code = 1 Then
updData(var1, var2)
ElseIf ex.Code = 2091 Then
msgprompt("Duplicate Unique Key!", "Warning")
End If
Finally
conn.DisconnectDB()
End Try
Return count
End Function
The connection is again opened in function updData(). While I understand that it has to be closed correctly but keeping tab on every developer is not possible. Hence I want to control it directly from the connection class by using the same connection but the condition If OraConn.State = ConnectionState.Closed is not helping.
UPDATE
I have put the code in UpdateDB under a Using block and removed call to ConnectDB and DisconnectDB from function like InsertData(...). It seems that the issue has been resolved. But I would like to know in case of exception will the connection remain open? and also OraConn is a public variable defined outside Using block so will it be disposed of by the GC?
Public Sub UpdateDB(ByVal query As String)
Using OraConn = New OracleConnection(Str_conn)
cmd = New OracleCommand(query, OraConn)
Try
OraConn.Open()
cmd.ExecuteNonQuery()
Catch ex As Exception
Throw
Finally
cmd.Dispose()
End Try
End Using
End Sub
You must close all the connections as soon as you are done with it, no matter what.
Suggestion:
The best practice for closing the connection is to do it in finally block. So that even if there is any error, catch it (log it if required) in catch block, and then connection will get close in finally block.
UPDATE
You can put one private static counter in your Connection class. When ever ConnectDB() is called you increment this counter and decrement it in every DisconnectDB(). Now in ConnectDB() you check the value of counter, if it exceeds a minimum threshold you throw error, by doing this way; you can come to know idle connection present in your code and refactor it. On production keep this threshold value high or ignore it in code.

Resources