InvalidCastException when trying to Sum datatable rows with LINQ - vb.net

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).

Related

Function to lookup arbitrary column in DataTable and return value in another arbitrary column

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

How to get IEnumerable(Of DataRow) From DataTable query

All the examples I have found of complex grouping of DataTable results that use linq query commands look to have no problems getting an IEnumerable(Of DataRow) object as the result.
However I seem to only get a AnonymousType Enumerator return that I cannot cast to DataTable.
I have workarounds, but would prefer to convert the results to a DataTable, as it looks possible and I may be doing something wrong.
It's a simple table with Many ClientID and ClientName columns and other columns with login timestamps.
Dim dtMatrix As DataTable = New DataTable()
... (populate DataTable)
Dim qClients = From row In dtMatrix
Group row By client = New With {Key .ClientID = row("ClientID"), Key .ClientName = row("ClientName")} Into Group
Select New With {Key .ClientID = client.ClientID, Key .ClientName = client.ClientName}
This returns the generic Enumerator result, however
Dim qClients As IEnumerable(Of DataRow) = From row In dtMatrix
Group row By client = New With {Key .ClientID = row("ClientID"), Key .ClientName = row("ClientName")} Into Group
Select New With {Key .ClientID = client.ClientID, Key .ClientName = client.ClientName}
Throws an exception
Unable to cast object of type... to type
'System.Collections.Generic.IEnumerable`1[System.Data.DataRow]'.
I will be happy to paste the whole error message if it will add more clarity.
My base assumption is that the DataTable should allow the cast to occur inherently as it is the object being queried. However this does not seem to be the case. Have I constructed my query incorrectly? (Framework 4.6.2)
You can use OfType on the Rows property of the DataTable:
Dim dtMatrix As DataTable = New DataTable()
'' Populate code goes here...
Dim dtRows As IEnumerable(Of DataRow) = dtMatrix.Rows.OfType(Of DataRow)()
The Rows property returns a DataRowCollection, that implements (through inheritance) the IEnumerable interface but not the IEnumerable(Of T) interface, that's why you can't use most of linq over it directly.
The following extension uses Reflection to create a new DataTable and create DataColumns in it that match the properties and fields of the type passed in. In general, if you are creating anonymous types in LINQ, you can't just convert to a DataRow which must be tied to a DataTable which must already have matching columns. I went ahead and wrote a second extension to DataTable that adds an IEnumerable<T> with matching field/property names to it.
Public Module Ext
<Extension()>
Public Function GetValue(member As MemberInfo, srcObject As Object) As Object
If TypeOf member Is FieldInfo Then
Return DirectCast(member, FieldInfo).GetValue(srcObject)
ElseIf TypeOf member Is PropertyInfo Then
Return DirectCast(member, PropertyInfo).GetValue(srcObject)
Else
Throw New ArgumentException("MemberInfo must be of type FieldInfo or PropertyInfo", Nameof(member))
End If
End Function
<Extension()>
Public Function GetMemberType(member As MemberInfo) As Type
If TypeOf member Is FieldInfo Then
Return DirectCast(member, FieldInfo).FieldType
ElseIf TypeOf member Is PropertyInfo Then
Return DirectCast(member, PropertyInfo).PropertyType
ElseIf TypeOf member Is EventInfo Then
Return DirectCast(member, EventInfo).EventHandlerType
Else
Throw New ArgumentException("MemberInfo must be of type FieldInfo, PropertyInfo or EventInfo", Nameof(member))
End If
End Function
<Extension()>
Public Function ToDataTable(Of T)(rows As IEnumerable(Of T)) As DataTable
Dim dt = New DataTable
If (rows.Any()) Then
Dim rowType = rows.First().GetType()
Dim memberInfos = rowType.GetProperties.Cast(Of MemberInfo)().Concat(rowType.GetFields).ToArray()
For Each info In memberInfos
dt.Columns.Add(New DataColumn(info.Name, info.GetMemberType()))
Next
For Each r In rows
dt.Rows.Add(memberInfos.Select(Function (i) i.GetValue(r)).ToArray())
Next
End If
Return dt
End Function
<Extension()>
Public Function AddObjects(Of T)(dt As DataTable, rows As IEnumerable(Of T))
If (rows.Any()) Then
Dim rowType = rows.First().GetType()
Dim memberInfos = rowType.GetProperties().Cast(Of MemberInfo)().Concat(rowType.GetFields()).ToArray()
For Each r In rows
Dim newRow = dt.NewRow()
For Each memberInfo In memberInfos
newRow(memberInfo.Name) = memberInfo.GetValue(r)
Next
dt.Rows.Add(newRow)
Next
End If
Return dt
End Function
End Module
Note that I write in C# and translated this from my C# extension. It is untested but compiles.
Using the extension, you should be able to get a DataTable from your qClients by:
Dim dtClients = qClients.ToDataTable()

GetType of Values in DataTable

I've been working on a project which has a Function of reading Data from a DataTable and returning it as a list. I made specific statements over some rows to get the data I want.
Little problem is, my entity "Number" is declared As an Integer and in the DataTable I do have some String values which leads the function to crash. Do any of you guys has an Idea about how I can check which Data Type the row "Number" has and if it's not an Integer to just leave it out?
My function looks like this:
Public Function LiefereAlleRechte(ByVal dt As DataTable) As ICollection(Of Recht) Implements IBenutzerInfoServiceUtil.LiefereAlleRechte
If dt Is Nothing Then
Throw New ArgumentNullException("DataTable can't be empty", "dt")
End If
Dim query = From dr As DataRow In dt.DefaultView.ToTable(True, "Name", "Number")
Dim coll As ICollection(Of Recht) = New List(Of Recht)
For Each row In query
Dim recht As Recht = New Recht()
With recht
.Name = row.Item("Name")
.Nummer = row.Item("Number")
coll.Add(recht)
End With
Next
Return coll
End Function
Thanks for any help!

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?

Sorting a DataTable using LINQ, when sort-by-columns may vary

I need to sort DataTables, however the sort-by-columns vary.
Scenario #1, DataTable1 should be sorted by "Column1".
Scenario #2, DataTable2 should be sorted by "Column1, Column2".
Below is my first attempt at creating a helper function for this purpose. This works ok.
Private Sub SortDataTable(ByRef dataTable As DataTable, ByVal sortColumnNames As List(Of String))
'Validation (not shown here)
Dim sortOrder = String.Join(", ", sortColumnNames)
dataTable.DefaultView.Sort = sortOrder
dataTable = dataTable.DefaultView.Table
End Sub
I tried implementing this in LINQ, however, I don't know how to pass multiple sort-by-columns to the lambda function. Work-in-progress code shown below.
Private Sub SortDataTable(ByRef dataTable As DataTable, ByVal sortColumnNames As List(Of String))
'Validation (not shown here)
dataTable.AsEnumerable().OrderBy(Function (row) row(sortColumnNames(0))).ThenBy(...)
End Sub
How should I pass multiple sort-by-columns to the OrderBy/ThenBy extension methods?
Something like that:
Private Function SortDataTable(table As DataTable, ParamArray columns As String()) As DataTable
If columns.Length = 0 Then
Return table
End If
firstColumn = columns.First()
Dim result = table.AsEnumerable().OrderBy(Function(r) r(firstColumn))
For Each columnName As var In columns.Skip(1)
result = result.ThenBy(Function(r) r(columnName))
Next
Return result.AsDataView().ToTable()
End Function
Converted from this C# code ( I've written this in C# and then used http://www.developerfusion.com/tools/convert/csharp-to-vb/ ):
DataTable SortDataTable(DataTable table, params string[] columns)
{
if (columns.Length == 0)
{
return table;
}
firstColumn = columns.First();
var result = table.AsEnumerable().OrderBy(r => r[firstColumn]);
foreach (var columnName in columns.Skip(1))
{
result = result.ThenBy(r => r[columnName]);
}
return result.AsDataView().ToTable();
}
PS: didn't test that. But that's very simple, so should be no problems.