Our application uses a custom DataAccessLayer class almost exclusively, and within that we do use Data Access Application Block (currently version 2). We are getting the infamous "GetOrdinal" error sporadically. We are not using out-of-method connections. We are using DAAB version 2. Below is a typical example of our DAL methods:
Public Function MyDALMethod(ByVal Param1 As Integer, ByVal Param2 As Integer) As System.Data.IDataReader
Dim db As Database = DatabaseFactory.CreateDatabase()
Dim sqlCommand As String = "usp_MyProcedure"
Dim dbCommand As DbCommand = db.GetStoredProcCommand(sqlCommand)
db.AddInParameter(dbCommand, "Param1", DbType.Int32, MyParam1)
db.AddInParameter(dbCommand, "Param2", DbType.Int32, MyParam2)
Return db.ExecuteReader(dbCommand)
End Function
In our code we just instantiate a DAL var and call the desired method. After using the DataReader, the referencing code will close, dispose, and set the reader to nothing. However, nothing is done with the reference to the DAL. I've wondered if this is part of our problem. A typical method would use our DAL like this:
Dim DAL as New DataAccessLayer
Dim dbReader as system.data.idatareader
dbreader = DAL.MyDALMethod(parm1, parm2)
While dbreader.read
i = dbreader.items("column1")
j = dbreader.items("column2")
End While
dbreader.close()
dbreader.dispose()
dbreader = nothing
My main question is should these DAL references be disposed somehow? It's a custom class written in VB.NET, so it doesn't implement IDisposable so I'm not sure if there's anything to be done or not, but we do have errors and issues (like the GetOrdinal problem) which seem to be load-related, and I'm wondering if this is part of the problem.
If the DAL holds at least one member variable that is Disposable (implements IDisposable), then it too should implement IDisposable. This would then indicate to DAL's client that Dispose() should be called. The DAL's Dispose() would then call the member variable(s) Dispose().
That is the general guidance for implementing IDisposable.
BTW - there is no need to dbreader = nothing - it achieves nothing. See Eric Lippert's post
Related
I'm having trouble with selecting only part of a collection and passing it by reference.
So I have a custom class EntityCollection which is , who guessed, a collection of entities. I have to send these entities over HTTPSOAP to a webservice.
Sadly my collection is really big, let's say 10000000 entities, which throws me an HTTP error telling me that my request contains too much data.
The method I am sending it to takes a Reference of the collection so it can further complete the missing information that is autogenerated upon creation of an entity.
My initial solution:
For i As Integer = 0 To ecCreate.Count - 1 Step batchsize
Dim batch As EntityCollection = ecCreate.ToList().GetRange(i, Math.Min(batchsize, ecCreate.Count - i)).ToEntityCollection()
Q.Log.Write(SysEnums.LogLevelEnum.LogInformation, "SYNC KLA", "Creating " & String.Join(", ", batch.Select(Of String)(Function(e) e("nr_inca")).ToArray()))
Client.CreateMultiple(batch)
Next
ecCreate being an EntityCollection.
What I forgot was that using ToList() and ToEntityCollection() (which I wrote) it creates a new instance...
At least ToEntityCollection() does, idk about LINQ's ToList()...
<Extension()>
Public Function ToEntityCollection(ByVal source As IEnumerable(Of Entity)) As EntityCollection
Dim ec As New EntityCollection()
'ec.EntityTypeName = source.FirstOrDefault.EntityTypeName
For Each Entity In source
ec.Add(Entity)
Next
Return ec
End Function
Now, I don't imagine my problem would be solved if I change ByVal to ByRef in ToEntityCollection(), does it?
So how would I actually pass just a part of the collection byref to that function?
Thanks
EDIT after comments:
#Tim Schmelter it is for a nightly sync operation, having multiple selects on the database is more time intensive then storing the full dataset.
#Craig Are you saying that if i just leave it as an IEnumerable it will actually work? After all i call ToArray() in the createmultiple batch anyway so that wouldn't be too much of a problem to leave out...
#NetMage you're right i forgot to put in a key part of the code, here it is:
Public Class EntityCollection
Implements IList(Of Entity)
'...
Public Sub Add(item As Entity) Implements ICollection(Of Entity).Add
If IsNothing(EntityTypeName) Then
EntityTypeName = item.EntityTypeName
End If
If EntityTypeName IsNot Nothing AndAlso item.EntityTypeName IsNot Nothing AndAlso item.EntityTypeName <> EntityTypeName Then
Throw New Exception("EntityCollection can only be of one type!")
End If
Me.intList.Add(item)
End Sub
I Think that also explains the List thing... (BTW vb or c# don't matter i can do both :p)
BUT: You got me thinking properly:
Public Sub CreateMultiple(ByRef EntityCollection As EntityCollection)
'... do stuff to EC
Try
Dim ar = EntityCollection.ToArray()
Binding.CreateMultiple(ar) 'is also byref(webservice code)
EntityCollection.Collection = ar 'reset property, see below
Catch ex As SoapException
Raise(GetCurrentMethod(), ex)
End Try
End Sub
And the evil part( at least i think it is) :
Friend Property Collection As Object
Get
Return Me.intList
End Get
Set(value As Object)
Me.Clear()
For Each e As Object In value
Me.Add(New Entity(e))
Next
End Set
End Property
Now, i would still think this would work, since in my test if i don't use Linq or ToEntityCollection the byref stuff works perfectly fine. It is just when i do the batch thing, then it doesn't... I was guessing it could maybe have to do with me storing it in a local variable?
Thanks already for your time!
Anton
The problem was that i was replacing the references of Entity in my local batch, instead of in my big collection... I solved it by replacing the part of the collection that i sent as a batch with the batch itself, since ToList() and ToEntityCollection both create a new object with the same reference values...
Thanks for putting me in the correct direction guys!
I'm new to working with background workers, and I'm trying to run the following code. But I recieve a run-time error on the m._Value_CreatedDate = m._MyCMD.ExecuteScalar() line. The error is:
Additional information: ExecuteScalar: CommandText property has not
been initialized
Try
Dim m As MyParameters = DirectCast(e.Argument, MyParameters)
m._Con.Open()
m._QueryStr = "SELECT TOP 1 CONVERT(varchar(10),aCreated,103) FROM Account WHERE aMember_ID = " & m._Selected_MemberID & ""
m._MyCMD.CommandType = CommandType.Text
m._Value_CreatedDate = m._MyCMD.ExecuteScalar()
Catch ex As Exception
m._Value_CreatedDate = "N/A"
End Try
Here's the Parameter's I'm using:
Class MyParameters
Friend _QueryStr As String
Friend _Value_CreatedDate As Object
Friend _AccountID As Object
Friend _Selected_MemberID As String = Committee_Database.GridView1.GetFocusedRowCellValue("Unique ID").ToString
Friend _Con As New SqlConnection('Connection string ommitted)
Friend _MyCMD As New SqlCommand(_QueryStr, _Con)
End Class
Please forgive me if I'm doing something extremely wrong, I'm self taught and experimenting with the backgroundworker. It's worth noting that _QueryStr will change multiple times as the background worker runs multiple queries against the same database and (as I understand it) stores each of the returned values from the queries into variables - _Value_CreatedDate is the variable I am using in this code. I've included an example of how I'm recycling the _QueryStr variable below and storing the returned value into a diffent Variable each time.
Try
m._QueryStr = "SELECT TOP 1 aAccount_ID FROM Account WHERE aUser_Name='" & _Selected_AccountID & "'"
m._MyCMD.CommandType = CommandType.Text
m._AccountID = m._MyCMD.ExecuteScalar()
Catch ex As Exception
End Try
Am I doing something wrong?
In the implementation of your class MyParameters you initialize the SqlCommand directly with the declaration using the value of the variable _QueryStr. At that point in time the variable _QueryStr is not yet initialized, so it is an empty string.
After the initialization of an instance of MyParameters, you change the value of _QueryStr (many times according to you) but these changes are not automatically passed to the CommandText of your SqlCommand. It still contains the empty initial value.
You could fix this problem building appropriate getter and setter for a new QueryStr property. When someone tries to set the property the code below change the value of the internal field _QueryStr (now private) and reinitializes the CommandText property of your SqlCommand.
Class MyParameters
Private _QueryStr As String
Public Property QueryStr() As String
Get
Return _QueryStr
End Get
Set(ByVal value As String)
_QueryStr = value
_MyCMD.CommandText = _QueryStr
End Set
End Property
..... other properties
Friend _MyCMD As New SqlCommand(_QueryStr, _Con)
End Class
Now when you write
Try
m.QueryStr = "SELECT ...."
....
the new command text is correctly assigned to your command.
As a side note: I suggest to use plain ADO.NET objects (or learn how to use an ORM tool). Do not try to encapsulate them in custom classes unless you have a very deep understanding on how these ADO.NET objects works. You gain nothing and expose your code to many problems. For example your code is very easy to exploit with Sql Injection because the MyParameters class has no provision to use a parameterized query. Your code has no method to close and dispose the SqlConnection embedded in your class thus leading to resource leaks.
In my experience the application layer should reference the business layer and the business layer should reference the data layer. I want to make a change to an app so that the application layer references the data layer directly as shown below:
Imports System.Data.SqlClient
Public Class ApplicationLayerClass
Public Function ProcessAllPersons()
Dim data As New DataLayerClass
Dim list As List(Of Person) = data.getAllPersons()
For Each person In list
'Call this function from the application client. Do some complex work on the person here.
Next
End Function
End Class
Public Class DataLayerClass
Public Function getAllPersons() As List(Of Person)
Dim list As List(Of Person) = New List(Of Person)
Dim p As New Person
Dim objCommand As New SqlCommand
Dim strConString As String = "Data Source=IANSCOMPUTER;Initial Catalog=Test;Integrated Security=True"
Dim objCon As New SqlConnection
objCon.ConnectionString = strConString
objCon.Open()
objCommand.Connection = objCon
objCommand.CommandText = "select * from person "
Dim objDR As SqlDataReader = objCommand.ExecuteReader
If objDR.HasRows Then
Using objCon
Do While objDR.Read
p.Name = objDR("Name")
p.age = objDR("Age")
list.Add(p)
Loop
End Using
End If
Return list
End Function
End Class
Public Class Person
Public Name As String
Public age As String
End Class
Alternatively I could create a class in the business layer that uses the adaptor pattern (http://en.wikipedia.org/wiki/Adapter_pattern) i.e. a function called BusinessLayerClass.ProcessAllPersons, which is called by ApplicationLayerClass.ProcessAllPersons and calls DataLayerClass.getAllPersons. Which option is more appropriate? I suppose it depends to some extent on the problem domain.
If you have a valid reason for calling the data layer directly, then do it. If you add a pass-through function in the business layer, then all you've done is added more code for no apparent benefit.
Now, if your business layer is exposed via an interface, IBusinessLayer for example, then adding a ProcessAllPersons() function to it and having it pass the call directly to the data layer makes more sense and consistency. This is what I would recommend.
the business layer should reference the data layer
This is one approach but arguably not the best if you're aiming at maximum decoupling between your modules and being able to change your data access layer easily without touching your business layer (this is called Persistence Ignorance).
Have a look at Onion Architecture and/or Hexagonal Architecture. These architectural styles emphasize on your business layer being at the core of the system and independent from any other layer.
Concretely, how it works is you define abstractions for data access objects in your business core, which will be implemented in peripheral modules and used by the application layer.
http://jeffreypalermo.com/blog/the-onion-architecture-part-1/
http://alistair.cockburn.us/Hexagonal+architecture
http://blog.kamil.dworakowski.name/2010/08/from-layers-to-hexagon-architecture.html
If you have no intention of changing your database provider and you have the ADO.net skills that a business layer would protect developers from, then you don't need a business layer.
However, where are you going to put your business logic?
Is this the right way of declaration dbreaders when mulitple users access the same page?
public dbReader as system.Data.IDataReader at class level or
Dim dbReader as System.Data.IDataReader in each function inside a class.
What would be the best practice to make the dbReader thread safe in VB.Net?
Does declaring them as static makes it thread safe?
Thanks in Advance,
If you'd like each thread to modify the variable without 'fear' another thread will change it somewhere along the line, it is best you adorn the variable with the ThreadStatic attribute.
The ThreadStatic attribute creates a different instance of the variable for each thread that's created so you're confident there won't be any race conditions.
Example (from MSDN)
Imports System
<ThreadStatic> Shared value As Integer
I would recommend you using reentrant functions when possible which are thread safe by definition instead of using class fields:
Function GetIds() As IEnumerable(Of Integer)
Dim result = New List(Of Integer)()
Using conn = New SqlConnection("SomeConnectionString")
Using cmd = conn.CreateCommand()
conn.Open()
cmd.CommandText = "SELECT id FROM foo"
Using reader = cmd.ExecuteReader()
While reader.Read()
result.Add(reader.GetInt32(0))
End While
End Using
End Using
End Using
Return result
End Function
If you are Diming the variable in a function, no other thread can access that variable making it thread-safe by definition.
However, if you are declaring it at a class level, you might want to use SyncLock which will prevent other threads from accessing it if it is currently being used by an other one.
Example:
Public Sub AccessVariable()
SyncLock Me.dbReader
'Work With dbReader
End SyncLock
End Sub
My present contract engagement is at a large E-Commerce company. Their code base which has origins going back to .Net 1.0 has caught me by surprise to contain many issues that raise the level of smell beyond the last crap I took.
That notwithstanding and trying to diffuse my level of distraction from it, I go along merrily trying to add in features to either fix other problems or extend more crap. Where I touch the DAL/BLL the time it will take to fix the aforementioned will be done. However I wanted to get a vote of confidence from the experts to get some assurance of not wasting the clients time or worse having my credibility voted down by touching "stuff that works". Of course unit testing would solve or at least soften this worry. Perhaps this should also be added to the wtf.com?
Public Function GetSizeInfoBySite(ByVal siteID As String) As IList
Dim strSQL As String = "YES INLINE SQL!! :)"
Dim ci As CrapInfo
Dim alAnArrayList As ArrayList
Dim cn As New SqlConnection(ConfigurationSettings.AppSettings("ConnectionString"))
Dim cmd As New SqlCommand(strSQL, cn)
cmd.Parameters.Add(New SqlParameter("#MySiteID", SqlDbType.NVarChar, 2)).Value = siteID
cn.Open()
Dim rs As SqlDataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection)
While rs.Read()
ci = New CategoryInfo(rs("someID"), rs("someName"))
If IsNothing(alAnArrayList) Then
alAnArrayList = New ArrayList
End If
alAnArrayList.Add(ci)
End While
rs.Close()
Return CType(alAnArrayList, IList)
End Function
Does anyone see problems with this aside from the inline SQL which makes my gut churn? At the least wouldn't you ordinarily wrap the above in a try/catch/finally which most of us knows has been around since .Net v1.0? Even better would'nt it be wise to fix with Using statements? Does the SQLDataReader close really encapsulate the connection close automagically?
Nothing wrong with inline sql if the user input is properly parameterized, and this looks like it is.
Other than that, yes you do need to close the connections. On a busy web site you could hit your limit and that would cause all kinds of weirdness.
I also noticed it's still using an arraylist. Since they've moved on from .Net 1.0 it's time to update those to generic List<T>'s (and avoid the call to CType- you should be able to DirectCast() that instead).
Definitely get some using statements around the Connection and Reader objects. If there is an exception, they won't be closed until the Garbage Collector gets around to them.
I tend not to call .Close() when there are using statements. Even if the SqlDataReader closes the connection on dispose (check the doco), putting a using around the Connection can't hurt and sticks to the pattern .
If you do that the try/finally is only needed if you need to do exception handling right there. I tend to leave exception handling at the higher levels (wrap each UI entry point, Library entry points, extra info in exception) as the stacktrace is usually enough to debug the errors.
Not that it matters much, but if you are re-factoring, move the collection initialization outside the loop. On second thoughts the code returns null if there are no records.
At least SqlParameters are used! Get rid of anything that concatenates user input with SQL if you find it (SQL Injection attack) no matter how well "Cleaned" it is.
The connection will be closed when the reader is closed because it's using the CloseConnection command behavior.
Dim rs As SqlDataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection)
According to MSDN (http://msdn.microsoft.com/en-us/library/aa326246(VS.71).aspx)
If the SqlDataReader is created with CommandBehavior set to CloseConnection, closing the SqlDataReader closes the connection automatically.
In reply to some of the great points indicated by Joel and Robert I refactored the method as follows which ran flawless.
Public Function GetSomeInfoByBusObject(ByVal SomeID As String) As IList
Dim strSQL As String = "InLine SQL"
Dim ci As BusObject
Dim list As New GenList(Of BusObject)
Dim cn As New SqlConnection(
ConfigurationSettings.AppSettings("ConnectionString"))
Using cn
Dim cmd As New SqlCommand(strSQL, cn)
Using cmd
cmd.Parameters.Add(New SqlParameter
("#SomeID", SqlDbType.NVarChar, 2)).Value = strSiteID
cn.Open()
Dim result As SqlDataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection)
While result.Read()
ci = New BusObject(rs("id), result("description"))
list.Add(DirectCast(ci, BusObject))
End While
result.Close()
End Using
Return list
End Using
End Function
Created a nice little helper class to wrap the generic details up
Public Class GenList(Of T)
Inherits CollectionBase
Public Function Add(ByVal value As T) As Integer
Return List.Add(value)
End Function
Public Sub Remove(ByVal value As T)
List.Remove(value)
End Sub
Public ReadOnly Property Item(ByVal index As Integer) As T
Get
Return CType(List.Item(index), T)
End Get
End Property
End Class
If you were using c# I would wrap the datareader creation in a using statement but I don't think vb has those?
Public Function GetSizeInfoBySite(ByVal siteID As String) As IList(Of CategoryInfo)
Dim strSQL As String = "YES INLINE SQL!! :)"
'reference the 2.0 System.Configuration, and add a connection string section to web.config
' <connectionStrings>
' <add name="somename" connectionString="someconnectionstring" />
' </connectionStrings >
Using cn As New SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings("somename").ConnectionString
Using cmd As New SqlCommand(strSQL, cn)
cmd.Parameters.Add(New SqlParameter("#MySiteID", SqlDbType.NVarChar, 2)).Value = siteID
cn.Open()
Using reader As IDataReader = cmd.ExecuteReader()
Dim records As IList(Of CategoryInfo) = New List(Of CategoryInfo)
'get ordinal col indexes
Dim ordinal_SomeId As Integer = reader.GetOrdinal("someID")
Dim ordinal_SomeName As Integer = reader.GetOrdinal("someName")
While reader.Read()
Dim ci As CategoryInfo = New CategoryInfo(reader.GetInt32(ordinal_SomeId), reader.GetString(ordinal_SomeName))
records.Add(ci)
End While
Return records
End Using
End Using
End Using
End Function
You could try something like the above, the using statements will handle connection closing and object disposal. This is available when the class implements IDisposable. Also build and return your IList of CategoryInfo.