Linq join select all columns and CopyToDataTable - vb.net

TABLE SCHEMA I'm having trouble solving a query in linq to dataset; I have to make a simple left join between two datatable but I do not know in advance the exact number of columns in the datatable B (A, B, C ...?) so I wanted to select all the columns; I found the following link
select-all-columns-after-join-in-linq
select-all-columns-for-all-tables-in-join-linq-join
Dim Query = From A In TableA _
Join B In TableB _
On A("COD") Equals B("COD") _
Select New With {A, B}
Dim TableC As DataTable = Query.CopyToDataTable()
I tried also
Select New With {.DAT = A.ItemArray.Concat(P.ItemArray).ToArray()}).ToList
and many more but i failed to bring the query result to a new datatable; I received type conversion errors or i did not understand how to bring the query result provided in two separate tables into one datatable.

Creating some extension methods and using Reflection will do what you want, though this may not be the ideal way to solve your issue (generally, Reflection never is).
Public Module SomeExt
<System.Runtime.CompilerServices.Extension()>
Public Function GetTypedValue(Of TProp)(ByVal p As PropertyInfo, obj As Object) As TProp
Return DirectCast(p.GetValue(obj), TProp)
End Function
<System.Runtime.CompilerServices.Extension()>
Public Function FlattenToDataTable(Of T)(src As IEnumerable(Of T)) As DataTable
Dim ans = New DataTable()
Dim srcdtpis = GetType(T).GetProperties().Cast(Of PropertyInfo)().ToList()
For Each aDT As DataTable In srcdtpis.Select(Function(pi) pi.GetTypedValue(Of DataRow)(src.First()).Table)
For Each col In aDT.Columns.Cast(Of DataColumn)()
ans.Columns.Add(col.ColumnName, col.DataType)
Next
Next
For Each drs In src
Dim newDR = ans.NewRow
For Each aDR In srcdtpis.Select(Function(pi) pi.GetTypedValue(Of DataRow)(drs))
For Each colname In aDR.Table.Columns.Cast(Of DataColumn)().Select(Function(dc) dc.ColumnName)
newDR(colname) = aDR(colname)
Next
Next
ans.Rows.Add(newDR)
Next
Return ans
End Function
End Module
You would use it as follows:
Dim Query = From A In TableA.AsEnumerable() _
Join B In TableB.AsEnumerable() _
On A("COD") Equals B("COD") _
Select New With {A, B}
Dim TableC As DataTable = Query.FlattenToDataTable()
Note that duplicate column names will be set to the last DataTable's value.

Related

LINQ Select All Columns From 2 DataTables and Return as 1 DataTable VB.NET

Tearing my hair out here unfortunately.
In a nut shell I have a DataTable containing data to be completed and then in a second DataTable I have the results of the actions. (completed, not completed etc.)
I need to return both sets of information into DataGridView together with essentially a LEFT OUTER JOIN.
This is what I've got so far:
Dim Query = From t1 In MasterTbl Group Join t2 In MasterActionTbl On t1.Field(Of String)("FreshAppsID") Equals t2.Field(Of String)("FreshAppsID") Into ps = Group From p In ps.DefaultIfEmpty()
Select t1
Return Query.CopyToDataTable
It fails when I attempt to do:
Select t1, t2
I essentially wish to return all the information from t1 and t2 using a left outer join because there may not be any 'action' records in existence in t2 for all the values in t1.
I looked into DataRelation's however this doesn't allow all the data to be returned into the same DataGridView.
TLDR
Want to select information from two datatables, join them together using a left outer join and return them as a single datatable for use in a datagridview.
Muchas
Using some extensions, you can use a method designed to merge the results in an anonymous object of DataRows into a new DataTable. I wrote one for an answer here but this uses some new techniques I have picked up:
Public Module SomeExt
<System.Runtime.CompilerServices.Extension()>
Public Function GetTypedValue(Of TProp)(ByVal p As PropertyInfo, obj As Object) As TProp
Return DirectCast(p.GetValue(obj), TProp)
End Function
' Create new DataTable from LINQ results on DataTable
' Expect T to be anonymous object of form new { DataRow d1, DataRow d2, ... }
<System.Runtime.CompilerServices.Extension()>
Public Function FlattenToDataTable(Of T)(src As IEnumerable(Of T)) As DataTable
Dim res = New DataTable()
If src.Any Then
Dim firstRow = src.First()
Dim rowType = firstRow.GetType()
Dim memberInfos = rowType.GetProperties.Cast(Of MemberInfo).Concat(rowType.GetFields).ToList
Dim allDC = memberInfos.SelectMany(Function(mi) mi.GetValue(Of DataRow)(firstRow).Table.DataColumns())
For Each dc In allDC
Dim newColumnName = dc.ColumnName
If res.ColumnNames.Contains(newColumnName) Then
Dim suffixNumber = 1
While (res.ColumnNames.Contains($"{newColumnName}.{suffixNumber}"))
suffixNumber += 1
End While
newColumnName = $"{newColumnName}.{suffixNumber}"
End If
res.Columns.Add(New DataColumn(newColumnName, dc.DataType))
Next
For Each objRows In src
res.Rows.Add(memberInfos.SelectMany(Function(mi) mi.GetValue(Of DataRow)(objRows).ItemArray).ToArray())
Next
End If
Return res
End Function
' ***
' *** DataTable Extensions
' ***
<System.Runtime.CompilerServices.Extension()>
Public Function DataColumns(ByVal aTable As DataTable) As IEnumerable(Of DataColumn)
Return aTable.Columns.Cast(Of DataColumn)
End Function
<System.Runtime.CompilerServices.Extension()>
Public Function ColumnNames(ByVal aTable As DataTable) As IEnumerable(Of String)
Return aTable.DataColumns.Select(Function(dc) dc.ColumnName)
End Function
' ***
' *** MemberInfo Extensions
' ***
<System.Runtime.CompilerServices.Extension()>
Public Function GetValue(ByVal 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)} but is of type {member.GetType}")
End If
End Function
<System.Runtime.CompilerServices.Extension()>
Public Function GetValue(Of T)(ByVal member As MemberInfo, srcObject As Object) As T
Return DirectCast(member.GetValue(srcObject), T)
End Function
End Module
With this extension, you can just join your DataTables and then convert the answer:
Dim Query = From t1 In MasterTbl
Group Join t2 In MasterActionTbl On t1.Field(Of String)("FreshAppsID") Equals t2.Field(Of String)("FreshAppsID") Into ps = Group _
From p In ps.DefaultIfEmpty()
Select New With { t1, t2 }
Return Query.FlattenToDataTable

VB Running grouped LINQ into DataTable

I'm trying to run a LINQ to datatable, that normally works for me. This time I'm trying something new to me, Grouping and taking the first row from each group. questionGroups is of type IEnumerable(Of Object). When I open the query in the debugger I see a collection of DataRows, each with an ItemArray of the values I expect in the columns. How do I run this query to a DataTable, or select the two columns I want and run those into a Dictionary?
Public member 'ToTable' on type 'WhereSelectEnumerableIterator(Of
VB$AnonymousType_0(Of Object,IEnumerable(Of Object)),Object)' not
found.
Dim answerGroup As String = "QuestionSortKey"
Dim answerNo As String = "AnswerNo"
Dim surveyDefinitionNo As String = "Pk_SurveyDefinitionNo"
Dim query = _
From rows In surveyAnswerKeys.Rows _
Where rows(answerNo) IsNot Nothing _
Order By Guid.NewGuid() _
Group By questionSortKey = rows(answerGroup) _
Into questionGroups = Group _
Select questionGroups.First()
Dim randomAnswerNos As DataTable = query.ToTable
Edit: This question is an offshoot of this one: VB LINQ - Take one random row from each group

Datatables left join linq

I'm having a hard time joining 2 datatables and have the joined datatable
as result.
First datatable (labels) holds data including a printerid.
Second datatable (printers) holds printer references (id > unc)
I would like to have as endresult (joined) a datatable with all data
from the first datatable with the field (unc) of the second datatable.
This is what i have been trying: (mind that fixed paths are for convenience...)
Sub Main()
Dim ds1 As new DataSet
ds1.ReadXml("C:\Program Files (x86)\[COMPANY]\ASW2XML\BICOLOR_a07bfc62-501e-4444-9b6e-3b9d3550e1a4.xml")
Dim ds2 As New DataSet
Dim li As string()
li = IO.File.ReadAllLines("C:\Program Files (x86)\[COMPANY]\ASW2XML\printers.dat")
Dim printers As New DataTable("Printers")
printers.Columns.Add("REPRT2")
printers.Columns.Add("REPRT3")
For Each s In li.ToList
Dim dr As DataRow = printers.NewRow
dr.Item(0) = s.Split("=")(0)
dr.Item(1) = s.Split("=")(1)
printers.Rows.Add(dr)
Next
printers.AcceptChanges
Dim labels As DataTable = ds1.Tables(0)
Dim joined As new DataTable("data")
'Dim lnq = From label In labels.AsEnumerable Join printer In printers.AsEnumerable On label("REPRT") Equals printer("REPRT2") Select printer
'Dim lnq = From l In labels Group Join p In printers On l Equals p("REPRT2") Into Group From p In Group Select label = l, ppath = If(p Is Nothing, "(Nothing)", p("REPRT3"))
Dim lnq = labels.AsEnumerable().Where(Function(o)printers.Select("REPRT2 =" & o.Item("REPRT").ToString).Length = 0)
joined = lnq.CopyToDataTable
End Sub
Thx for your help and inspiration!
grtz -S-
Have you tried http://msdn.microsoft.com/en-us/library/system.linq.enumerable.join.aspx
Make your datatables as an extension of IEnumerable (like list as you did) and a join in linq would make it work easily.
then send the joined tables to any destination you want.
I thought a comment would be harder to understand so I moved it in a post.
I wrote a bit of strucure that shoud help you understand how Join works:
structure Label
Public printerId as long
Public driver as Strring
end structure
structure Printer
Public unc as string
end structure
If you set Labels and Printers to DataTable (instead of the structures below), you shoud have something like :
function DoJoin() as datatable
'You might remove as datatable in query declaration
dim query as datatable = Labels.Join(Printers, Function(aLabel) aLabel, _
function(aPrinter) aPrinter.unc, _
function(aLabel, aPrinter) New With
{ .printerID = aLabel.printerId, .driver = aLabel.Driver, _
.unc = aPrinter.unc
})
return query
end function
However I wote it this morning in notepad, so you might have to adjust this. I just want to add that you must have the same type of container in order to use join (eg : example in Join at msdn they used 2 Lists.), that are compatible with Linq obects (datatable I do not know).
I decided to do it the "hard" way and loop through all rows in parent table.
This goes very fast for a minor amount of records, I don't know if running this on
a larger amount of records speed will greatly decrease over using a Linq solution...
This is the code that I'm using atm:
Sub Main()
Dim ds As new DataSet
ds.ReadXml("C:\Program Files (x86)\[COMPANY]\ASW2XML\BICOLOR_a07bfc62-501e-4444-9b6e-3b9d3550e1a4.xml")
Dim labels As DataTable = ds.Tables(0)
Dim printers As DataTable = GetPrinters
labels.Columns.Add("REPRT2").SetOrdinal(labels.Columns.IndexOf("REPRT")+1) 'insert new column after key column
labels.CaseSensitive=False
labels.AcceptChanges
For Each dr As DataRow In labels.Rows
Dim p As String = String.Empty
Try
p = printers.Select("ID='" & dr("REPRT") & "'")(0).Item("PATH").ToString
Catch ex As Exception
End Try
dr("REPRT2") = p
Next
labels.AcceptChanges
End Sub
Function GetPrinters As DataTable
Dim printers As New DataTable("Printers")
Dim li As string()
li = IO.File.ReadAllLines("C:\Program Files (x86)\[COMPANY]\ASW2XML\printers.dat")
printers.Columns.Add("ID")
printers.Columns.Add("PATH")
For Each s In li.ToList
Dim dr As DataRow = printers.NewRow
dr.Item(0) = s.Split("=")(0)
dr.Item(1) = s.Split("=")(1)
printers.Rows.Add(dr)
Next
printers.AcceptChanges
Return printers
End Function

Display all contents of a "table" created in LINQ in VB.NET

Working from a previous question I asked (which was answered very well).. Ive come across another snag... In the following code
Public Sub Main()
Dim EntireFile As String
Dim oRead As System.IO.StreamReader
oRead = File.OpenText("testschedule.txt")
EntireFile = oRead.ReadToEnd
Dim table As New List(Of List(Of String))
' Process the file
For Each line As String In EntireFile.Split(Environment.NewLine)
Dim row As New List(Of String)
For Each value In line.Split(",")
row.Add(value)
Next
table.Add(row)
Next
' Display all contents of 5th column in the "table" using LINQ
Dim v = From c In table Where c(5) = ""
For Each x As List(Of String) In v
Console.WriteLine(x(0)) ' printing the 1st column only
Next
Console.WriteLine("Value of (2, 3): " + table(1)(2))
End Sub
`
The area where it says Dim v = From c In table Where c(5) = "" the blank quotations will only accept a specific number that its looking for in that column.
For Example:
Dim v = From c In table Where c(5) = "7" Will only show me any 7's in that column. Normally there will be many different values and I want it to print everything in that column, I just cant figure out the command to have it display everything in the selected column
Once again Many MANY Thanks!
If you want to show all rows (to be precise: items in the IEnumerable), just remove the Where condition
Dim v = From c In table
Just a note: table is not a very good name for your list, it leads the thought to SQL. This is just Linq2Objects and you don't query tables you query plain objects with a syntax very similar to Linq2SQL that in turn is heavily inspired by SQL.

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.