Function to lookup arbitrary column in DataTable and return value in another arbitrary column - vb.net

I'm trying to write a generic function which can be used to look up an arbitrary value in an arbitrary column in an arbitrary DataTable, and return the corresponding value in another arbitrary column in the same DataTable. I'm not concerned with multiple values or multiple matches; the data is organised such that they don't occur anyway, all I want is for it to return the first match if it exists, or nothing if it doesn't.
I'm basing the code on this very simple example :
Private Function TableLookup(dtb As DataTable, lookupFieldName As String, lookupFieldValue As Integer, returnFieldName As String) As String
Dim result As String
Dim matches = From row In dtb Let lookup = row.Field(Of Integer)(lookupFieldName) Where lookup = lookupFieldValue
If matches.Any Then result = matches.First().row.Field(Of String)(returnFieldName)
Return result
End Function
But obviously that only works if the lookupField is an Integer field and the returnField is a String field. Because the function needs to handle arbitrary columns, those columns could have arbitrary DataTypes? And the value being returned is also arbitrary (could be an Integer, could be a String... etc.)
Obviously I can determine what the DataTypes are for each column easily enough :
Dim lookupFieldType As Type = dtb.Columns("lookupFieldName").DataType
Dim returnFieldType As Type = dtb.Columns("returnFieldName").DataType
But that's still no use as row.Field(Of T) is strongly-typed; I can't use a variable to specify the DataType :
Dim matches = From row In dtb Let lookup = row.Field(Of lookupFieldType)(lookupFieldName) Where lookup = lookupFieldValue
If matches.Any Then result = matches.First().row.Field(Of returnFieldType)(returnFieldName)
Have a feeling I'm going about this in completely the wrong way to begin with but it seems like there should be a straightforward way of looking up arbitrary columns in data tables (otherwise what's the point in having them, right?)
Any suggestions?

If you will know what the types of both columns will be when you call the method, you can make it generic like this:
Private Function TableLookup(Of TKey As IEquatable(Of TKey), TResult)(table As DataTable,
keyColumnName As String,
key As TKey,
resultColumnName As String) As TResult
Dim row = table.AsEnumerable().FirstOrDefault(Function(dr) dr.Field(Of TKey)(keyColumnName).Equals(key))
Return If(row Is Nothing, Nothing, row.Field(Of TResult)(resultColumnName))
End Function
This method might be called like so:
Dim name As String = TableLookup(Of Integer, String)(myDataTable,
"Id",
id,
"Name")
If you won't know what the column types are, you could use something like this:
Private Function TableLookup(table As DataTable,
keyColumnName As String,
key As Object,
resultColumnName As String) As Object
Dim keyType = key.GetType()
If keyType IsNot table.Columns(keyColumnName).DataType Then
Return Nothing
End If
Dim filterExpression As String
If keyType Is GetType(String) Then
filterExpression = $"{keyColumnName} = '{key}'"
ElseIf keyType Is GetType(Date) Then
filterExpression = $"{keyColumnName} = #{key:M/dd/yyyy h:mm:ss tt}#"
Else
filterExpression = $"{keyColumnName} = {key}"
End If
Dim row = table.Select(filterExpression).FirstOrDefault()
Return If(row Is Nothing, Nothing, row(resultColumnName))
End Function

Related

Pass an arbitrary set of paired parameters, where one of the pair items needs to be an arbitrary datatype

I have a function to which I want to pass an arbitrary number of paired parameters (i.e. a String variable and a second arbitrary type (could be a String, Integer etc.) - hence am declaring the second half of the pair as an Object. There could be one or more pairs of this nature.
The most obvious structure I could think of for this was therefore a Tuple(Of String, Object)
Here is the function :
Private Function TableLookup(
table As DataTable,
ByVal columnNamesAndKeys As List(Of Tuple(Of String, Object)),
resultColumnName As String) As Object
Dim filterExpression As String = ""
For i = 0 To columnNamesAndKeys.Count
Dim lookupColumn As String = columnNamesAndKeys(i).Item1
Dim lookupKey As Object = columnNamesAndKeys(i).Item2
Dim keyType = lookupKey.GetType()
If keyType IsNot table.Columns(lookupColumn).DataType Then Return Nothing
If keyType Is GetType(String) Then
filterExpression += IIf(Len(filterExpression) > 0, " AND ", "") + $"{lookupColumn} = '{lookupKey}'"
ElseIf keyType Is GetType(Date) Then
filterExpression += IIf(Len(filterExpression) > 0, " AND ", "") + $"{lookupColumn} = #{lookupKey:M/dd/yyyy h:mm:ss tt}#"
Else
filterExpression += IIf(Len(filterExpression) > 0, " AND ", "") + $"{lookupColumn} = {lookupKey}"
End If
Next
Dim row = table.Select(filterExpression).FirstOrDefault()
Return If(row Is Nothing, Nothing, row(resultColumnName))
End Function
Called thus (for a single pair) :
Dim someKey As Integer
Dim someValue = TableLookup(
dtbSomeTable,
New List(Of Tuple(Of String, Object))
From {("SomeKey", DirectCast(someKey, Object)).ToTuple},
"SomeOtherColumn")
And thus (for multiple pairs) :
Dim someKey As Integer
Dim someOtherKey As String
Dim someValue = TableLookup(
dtbSomeTable,
New List(Of Tuple(Of String, Object))
From {("SomeKey", DirectCast(someKey, Object)).ToTuple,
("SomeOtherKey", DirectCast(someOtherKey, Object)).ToTuple},
"SomeOtherColumn")
So - this works - but it feels awfully clunky calling it each time, having to create an ad-hoc list of Tuples, then declare each Tuple and DirectCast each key as an Object to obey the strongly-typed requirement.
The whole point of the function was to provide an easy one-liner throughout the code to quickly look up columns with potentially multiple, arbitrary, criteria but all of these manipulations within the call makes it less intelligible to anyone unfortunate enough to ever have to maintain this...
Is there a smarter / cleaner way to pass an arbitrary set of paired parameters, where one of the pair items needs to be an arbitrary Type?
You can achieve this by using a Parameter Array (ParamArray ) in conjunction with value tuples. This allows you to call the method easily with any number of parameters and no explicit list or array instantiation and neither a New keyword for the values nor any casts. Note that we must swap columnNamesAndKeys and resultColumnName, since the ParamArray used to pass columnNamesAndKeys must be the last parameter of the method.
Usage example:
Dim result = TableLookup(
dtbSomeTable,
"ResultColumn",
("id", 12), ("name", "Joe"), ("date", #2022/10/28#), ("someKey", someValue))
The adapted function:
Private Function TableLookup(
table As DataTable,
resultColumnName As String,
ParamArray columnNamesAndKeys() As (key As String, value As Object)
) As Object
Dim filterExpression As String = ""
For i = 0 To columnNamesAndKeys.Length - 1
Dim lookupColumn As String = columnNamesAndKeys(i).key
Dim lookupKey As Object = columnNamesAndKeys(i).value
Dim keyType = lookupKey.GetType()
If keyType IsNot table.Columns(lookupColumn).DataType Then Return Nothing
If filterExpression.Length > 0 Then
filterExpression += " AND "
End If
If keyType Is GetType(String) Then
filterExpression += $"{lookupColumn} = '{DirectCast(lookupKey, String).Replace("'", "''")}'"
ElseIf keyType Is GetType(Date) Then
filterExpression += $"{lookupColumn} = #{lookupKey:yyyy/MM/dd h:mm:ss tt}#"
Else
filterExpression += $"{lookupColumn} = {lookupKey}"
End If
Next
Dim row = table.Select(filterExpression).FirstOrDefault()
Return If(row Is Nothing, Nothing, row(resultColumnName))
End Function
I also did some refactorings.
Since I used a named tuple, you can access the fields by name instead of just Item1 or `Item2'.
Conditionally adding the " AND " part can be done once before the lengthy If Then Else ...
I replace any single quotes within a string value by two single quotes. E.g., a string like "John's Pub" becomes 'John''s Pub' in SQL notation.
For i = 0 To columnNamesAndKeys.Count is wrong. The index goes from 0 to Count - 1. And since we have an array now, we must use Length.

How to make a generic database record to object conversion function?

I am using the following function to retrieve records from a database and convert the records to a collection of strongly typed objects.
Private Function GetPlantSettingsFiltered(parameters As Dictionary(Of String, Object), queryCondition As String) As PlantSettings
Dim query As String
query = " SELECT * FROM Plant_Settings " _
+ queryCondition
Dim settings As New PlantSettings
Dim table As DataTable = GetQueryResults(parameters, query, GetConnectionString("WeighScaleDB"))
If table Is Nothing Then
Return settings
End If
For Each row As DataRow In table.Rows
settings.Add(New PlantSetting With {
.Setting_ID = ConvertByteArrayToString(TryCast(row("Setting_ID"), Byte())),
.Plant_ID = ConvertByteArrayToString(TryCast(row("Plant_ID"), Byte())),
.Value = row("Setting_Value").ToString(),
.Comments = row("Setting_Comments").ToString()
})
Next
Return settings
End Function
I would like to create a generic version of this function that would work for any of my objects without me creating this function for each object.
For example, if the caller could specify the type, then some other details, the function would return a collection of that type.
Private Function GetObjects(Of T)(parameters As Dictionary(Of String, Object), query As String) As WSAEntityCollection(Of T)
Dim objectCollection As New WSAEntityCollection(Of T)
Dim table As DataTable = GetQueryResults(parameters, query, GetConnectionString("WeighScaleDB"))
If table Is Nothing Then
Return objectCollection
End If
For Each row As DataRow In table.Rows
' Here is my problem
objectCollection.Add(New T With {})
Next
Return objectCollection
End Function
My current problem with this new function is that I do not know how to dynamically match the column names with the parameters of the generic object. Any ideas on how this could be done?

InvalidCastException when trying to Sum datatable rows with LINQ

Hi i am trying to sum all my datatable values to one row. but i retrieve a InvalidCastException:
Failed to convert an object of
typeWhereSelectEnumerableIterator2[System.Linq.IGrouping2[System.Object,System.Data.DataRow],VB$AnonymousType_0`4[System.Object,System.Double,System.Decimal,System.Decimal]]
to type System.Data.DataTable.
SQL Datatypes:
NAME_AGE string
LON money
sal_tjformon money
sal_sjuklon money
Private Function GroupByName(dataTable As DataTable) As DataTable
Dim result = dataTable.AsEnumerable().GroupBy(
Function(row) row.Item("NAME_AGE")).Select(Function(group) New With {
.Grp = group.Key,
.LON = group.Sum(Function(r) Decimal.Parse(r.Item("LON"))),
.sal_tjformon = group.Sum(Function(r) Decimal.Parse(r.Item("sal_tjformon"))),
.sal_sjuklon = group.Sum(Function(r) Decimal.Parse(r.Item("sal_sjuklon")))
})
Return result
The LINQ statement returns an IEnumerable(Of <anonymous_type>). There are two problems with this. First of all, your function returns a DataTable, which your object definitely is not. Secondly of all, you can't return an anonymous type from a function call.
If you want to return the select result, you have to create an explicit type (a class) and return the IEnumerable(Of MyType), like in the code below. I strongly advice to set an explicit type to the Grp property (like String?).
Class GroupNameAgeResult
Public Property Grp As Object
Public Property LON As Decimal
Public Property sal_tjformon As Decimal
Public Property sal_sjuklon As Decimal
End Class
Private Function GroupByName(dataTable As DataTable) As IEnumerable(Of GroupNameAgeResult)
Dim result = dataTable.AsEnumerable().GroupBy(Function(row) row.Item("NAME_AGE")) _
.Select(Function(grp) New GroupNameAgeResult() With
{.Grp = grp.Key,
.LON = grp.Sum(Function(r) Decimal.Parse(r.Item("LON").ToString)),
.sal_tjformon = grp.Sum(Function(r) Decimal.Parse(r.Item("sal_tjformon").ToString)),
.sal_sjuklon = grp.Sum(Function(r) Decimal.Parse(r.Item("sal_sjuklon").ToString))})
Return result
End Function
If you want to return a DataTable, you can define this, loop over the groups and add a row. You can return afterwards the result. See example code below.
Private Function GroupByName(dataTable As DataTable) As DataTable
Dim result As New DataTable()
result.Columns.Add("Grp", GetType(Object))
result.Columns.Add("LON", GetType(Decimal))
result.Columns.Add("sal_tjformon", GetType(Decimal))
result.Columns.Add("sal_sjuklon", GetType(Decimal))
For Each grp In dataTable.AsEnumerable().GroupBy(Function(row) row.Item("NAME_AGE"))
Dim row As DataRow = result.NewRow()
row.Item("Grp") = grp.Key
row.Item("LON") = grp.Sum(Function(r) Decimal.Parse(r.Item("LON").ToString))
row.Item("sal_tjformon") = grp.Sum(Function(r) Decimal.Parse(r.Item("sal_tjformon").ToString))
row.Item("sal_sjuklon") = grp.Sum(Function(r) Decimal.Parse(r.Item("sal_sjuklon").ToString))
result.Rows.Add(row)
Next
Return result
End Function
Last but not least. I strongly advice you to turn on "Option strict" (you can set this in the project properties -> Compile). You'll notice many more (possible) errors with your code (even the small function from this question).

SQL Data Reader - How to handle Null column values elegantly

I'm using an SQLDataReader to retrieve values from a database which may be null. I've worked out how to handle Null string values but can't get the same trick to work with integers or booleans:
Using cmd As DbCommand = store.GetStoredProcCommand("RetrievePOCO")
store.AddInParameter(cmd, "ID", DbType.Int32, ID)
Using reader As IDataReader = store.ExecuteReader(cmd)
If reader.Read() = True Then
Dim newPOCO As New POCO()
With newPOCO
'If the source column is null TryCast will return nothing without throwing an error
.StatusXML = TryCast(reader.GetString(reader.GetOrdinal("StatusXML")), String)
'How can a null integer or boolean be set elegantly?
.AppType = TryCast(reader.GetInt32(reader.GetOrdinal("AppType")), System.Nullable(Of Integer))
.Archived = TryCast(reader.GetBoolean(reader.GetOrdinal("Archived")), Boolean)
So how can a null integer or boolean be set elegantly? I've seen suggestions in C# but they don't translate correctly to VB giving a 'TryCast operand must be reference type, but integer? is a value type' compiler errors.
I use the following function in this scenario:
Public Shared Function NoNull(ByVal checkValue As Object, ByVal returnIfNull As Object) As Object
If checkValue Is DBNull.Value Then
Return returnIfNull
Else
Return checkValue
End If
End Function
Your code would look something like this:
With newPOCO
.StatusXML = NoNull(reader("StatusXML"), "")
.AppType = NoNull(reader("AppType"), -1)
.Archived = NoNull(reader("Archived"), False)
End With
Note that this function requires you to pass the value which should be used if the value is DbNUll as the second Parameter.
You can take advantage of the IsDBNull method of the SqlDataReader and use the VB.NET ternary operator to assign a default value to your poco object
.StatusXML = If(reader.IsDBNull(reader.GetOrdinal("StatusXML")), _
"",reader.GetString(reader.GetOrdinal("StatusXML")))
It is just one line, not very elegant because you need to call two times the GetOrdinal method.
Public Function NotNull(Of T)(ByVal Value As T, ByVal DefaultValue As T) As T
If Value Is Nothing OrElse IsDBNull(Value) Then
Return DefaultValue
Else
Return Value
End If
End Function

VB.Net I'm trying to write an extension for a generic linq search, however I'm not sure how to return more than one result 0.o

I'm a bit new to vb.net and used to working in perl, this is what I'd like to do.
I wanted something similar to DBIX::Class::Resultset's search (from cpan) in my vb.net project, so that I can give my function a hash containing keys and values to search on a table.
Currently it returns a single matching result of type T where I want it to return all results as a data.linq.table(of T)
How should I alter my expression.lambda so that I can say table.Select(Predicate) to get a set of results? After that I think it should be as simple as saying results.intersect(result) instead of Return test.
Any help will be very much appreciated.
Thanks in advance
-Paul
<System.Runtime.CompilerServices.Extension()> _
Public Function Search(Of T As Class)(ByVal context As DataContext, _
ByVal parameters As Hashtable) As T
Dim table = context.GetTable(Of T)()
Dim results As Data.Linq.Table(Of T)
For Each Parameter As DictionaryEntry In parameters
Dim column As Object = Parameter.Key
Dim value As String = Parameter.Value
Dim param = Expression.Parameter(GetType(T), column)
Dim Predicate = Expression.Lambda(Of Func(Of T, Boolean)) _
(Expression.[Call](Expression.Convert(Expression.Property(param, column), _
GetType(String)), GetType(String).GetMethod("Contains"), _
Expression.Constant(value)), New ParameterExpression() {param})
Dim test = table.First(Predicate)
Return test
' result.intersect(result)
Next
'Return results
End Function
This works assuming you want an "AND" conjunction between predicates
For instance:
Dim h = New System.Collections.Hashtable
h.Add("FieldA", "01 5149")
h.Add("FieldB", "WESTERN")
Dim t = (New DBDataContext).Search(Of DBrecord)(h)
Debug.Print(t.Count.ToString)
Would return those records where fieldA matched AND fieldb matched.
If you wanted OR, DiceGuy's right, use UNION.
Here's the search...
Note, I used STARTSWITH instead of contains because it's alot faster for large sets
You can always change it back.
<System.Runtime.CompilerServices.Extension()> _
Public Function Search(Of T As Class)(ByVal context As DataContext, _
ByVal parameters As Hashtable) As IQueryable(Of T)
Dim table = context.GetTable(Of T)()
Dim results As IQueryable(Of T) = Nothing
For Each Parameter As DictionaryEntry In parameters
Dim column = DirectCast(Parameter.Key, String)
Dim value As String = DirectCast(Parameter.Value, String)
Dim param = Expression.Parameter(GetType(T), column)
Dim Predicate = Expression.Lambda(Of Func(Of T, Boolean)) _
(Expression.[Call](Expression.Convert(Expression.Property(param, column), _
GetType(String)), GetType(String).GetMethod("StartsWith", New Type() {GetType(String)}), _
Expression.Constant(value)), New ParameterExpression() {param})
Dim r = table.Where(Predicate)
If results Is Nothing Then
results = r
Else
results = results.Intersect(r)
End If
Next
Return results
End Function
Well, for starters let's change the return type to Data.Linq.Table(Of T).
Then instead of table.First(Predicate), try table.Where(Predicate)
Finally 'Intersect' will only give you results that contain all your parameters. If that's what you want, then fantastic! If not, then try 'Union' instead.
Let me know where that gets you and we can work from there.