Perform an aggregation on a DataTable with Linq in vb.net - vb.net

Given a datatable, I wish to output an IEnumerable type (dictionary(of T) would be perfect, otherwise a datatable would be acceptable) that provides an aggregation of the data within the datatable.
In SQL I would write the query as such:
select groupByColumn, sum(someNumber) from myTable group by groupByColumn
In VB.NET the closest I have got to this (achieved using information here) is:
' dt is a datatable containing two columns, referred to by index in p below
Dim q = From p In dt Group p By p(0) Into Sum(p(1)) Select p(0), SumOfNumber = Sum
However I receive error: "Range variable name can be inferred only from a simple or qualified name with no arguments." on the p(0) element.
Therefore my question is as follows:
How can I resolve this error?
How do I process the result (q) as an IEnumerable type?
Life isn't made any easier because I'm using Linq in unfamiliar vb.net. Many examples are in C#, but I haven't really come across anything suitable even in C#.

Resolved: I had to alias the columns returned.
Dim q = From p In dt
Group p By transactionTypeName = p(0) _
Into totalForType = Sum(Convert.ToDouble(p(1))) _
Select transactionTypeName, totalForType
Also note that I had to do a conversion on the value that I was summing because being a dynamically returned datatable didn't have the column type specified.
Hint found here because vb.net oh so helpfully gives us a different error message to C#'s "Invalid anonymous type member declarator. Anonymous type members must be declared with a member assignment, simple name or member access."
For completeness, results are processed as below:
For Each item In q ' no need for defining item as a type (in C# we'd use the var keyword)
If item.totalForType = magicNumber Then
'do something
Else
'do something else
End If
Next

Related

LINQ Order DataTable based on string date

I have a dataTable with column "date" which has dates in string format.
I can't sort it with LINQ.
Table example
ID date
1 20.02.2022
2 15.05.2021
3 03.07.2019
This is the LINQ expression I have so far, but it always says String was not recognized as a valid DateTime
(From x In dt.AsEnumerable()
Order By Convert.ToDateTime(x("date").ToString)
Select x).CopyToDataTable
I've also tried this, but with the same result
(From x In dt.AsEnumerable()
Order By DateTime.ParseExact(x("date").ToString,"dd/MM/yyyy",System.Globalization.CultureInfo.InvariantCulture)
Select x).CopyToDataTable
What am I missing ?
Make your life easy; use a strongly typed datatable. It can still be used like a normal datatable but it's less of a pig to work with
add a new DataSet type of file to your project
call it eg MyProjectNameHereDataSet
right click the design surface, choose Add DataTable, give it a nice name like Concerts
right click the DataTable and Add Column
call it ID, make it an int
add another column, call it something more interesting than Date - ConcertDate perhaps. Avoid naming columns words that are keywords or types
make it a DateTime type
This visual designer effectively just wrote you a class with two properties, int Id and DateTime ConcertDate, and it made a datatable capable of holding them.
When you're loading your data into your table, do the parsing then (you'll have to now it's strongly typed). It is more efficient to do so than doing it every time you query
Dim dt = new ConcertsDataTable()
'in a loop? Some api call? Wherever the data comes from
dt.AddConcertsRow(theId, DateTime.ParseExact(theDate, "dd.MM.yyyy", CultureInfo.InvariantCulture)
Now you can LINQ it more nicely; no need for AsEnumerabke, or digging column names out by string and casting them etc
dt.OrderBy(Function(r) r.ConcertDate)
Nice:
Dim totalAttendancePast = 0
For Each ro in dt
If ro.ConcertDate < DateTime.Now Then totalAttendancePast += ro.Attendance
Next ro
Nasty:
For Each ro as DataRow in dt.Rows
If DateTime.ParseExact(ro("ConcertDate"), "dd.MM.yyyy") < DateTime.Now Then totalAttendancePast += DirectCast(ro("Attendance"), Integer)
Next ro
Nicer:
dt.
Where(Function(r) r.ConcertDate < DateTime.Now).
Sum(Function(r) r.Attendance)
Nastier:
dt.AsEnumerable().
Where(Function(r) DateTime.ParseExact(ro("ConcertDate"), "dd.MM.yyyy") < DateTime.Now).
Sum(Function(r) r.Field(Of Integer)(Attendance))
You can use this strongly typed datatable anywhere you would use a normal datatable - you can still access it by string column names etc if you desperately wanted to:
someDatagridview.DataSource = dt
someDataAdapter.Fill(dt)
MessageBox.Show(dt.Rows(0)("Id").ToString())
But it's very helpful to have intellisense be able to guide you and stay in strongly typed land:
MessageBox.Show(dt.First().Id.ToString())
As pointed out in the comments, I was using "dd/MM/yyyy" when my data comes in as "20.02.2021".
I used "dd.MM.yyyy" and it works.

MDX Query returns value in report but not in Visual Basic code

This is for an application which dynamically sets data for and renders reports.
I have an MDX query for a report which relies on a parameter. The query is:
SELECT NULL ON COLUMNS, strtomember(#DateYear) ON ROWS FROM [MYDATACUBE]
When running this in the report query designer, it returns a value properly. However, when running this in visual basic code, it returns nothing. Here is the important part of my code:
Dim cn = New AdomdConnection(adomdparamconnectionstrings(countparamsadomd))
Dim da As AdomdDataAdapter = New AdomdDataAdapter()
Dim cmd = New AdomdCommand(paramcommands(countparamsadomd), cn)
Dim tbl = New DataTable
If (adomdparams) Then 'If there are parameters, adds them to the query
For l As Integer = 0 To (countparamsadomd - 1)
If (adomdparamconnectionstrings(l) = "NODATASET") Then
Dim p As New AdomdParameter(paramvaradomd(l), paramadomd(l))
cmd.Parameters.Add(p)
Else
Dim p As New AdomdParameter(paramvaradomd(l), adomdqueryvalues(l))
cmd.Parameters.Add(p)
End If
Next
End If
da.SelectCommand = cmd
cn.Open()
da.Fill(tbl)
cn.Close()
I know the connection string works because all the other datasets use the same one. I know the command is right using break points. I know the parameter's value is right also using break points. I know the code overall works because it works with every dataset I've tested it with except this one. Using break points, everything seems to work as it does for other datasets, but then it just doesn't return any values.
The table resulting from this does have a properly named column ([Date].[Year].[Year].[MEMBER_CAPTION]) but has no rows. The returned value should be a single row with the year in it.
I have done something similar in C#.
If you have access to the dimension and hierarchy name, then you can have a generic query like this:
WITH
MEMBER [Measures].[Member Caption] AS StrToMember(#Hierarchy).HIERARCHY.CURRENTMEMBER.MEMBER_CAPTION
SELECT
[Measures].[Member Caption] ON COLUMNS,
StrToMember(#DateYear) ON ROWS
FROM
[MYDATACUBE]
Note that you'll have to add the parameter, #Hierarchy, to the command before executing the query. There is a little trick in the value expression for the calculated member. When passing a hierarchy expression to StrToMember(), it returns the default member from that hierarchy. From the default member, you can use the HIERARCHY function to work backwards to get the hierarchy. From the hierarchy, you can get the CURRENTMEMBER and its MEMBER_CAPTION property.
If you want a query specific for the hierarchy in your example, you can use:
WITH
MEMBER [Measures].[Member Caption] AS [Date].[Year].CURRENTMEMBER.MEMBER_CAPTION
SELECT
[Measures].[Member Caption] ON COLUMNS,
StrToMember(#DateYear) ON ROWS
FROM
[MYDATACUBE]

Retrieving the Query used for a OleDBCommand

I'm currently using the following VB code to make a query against an Access Database, I would like to know is it possible to obtain what the SELECT statement that is being run and send that output to the console.
Dim QuestionConnectionQuery = New OleDb.OleDbCommand("SELECT Questions.QuestionID FROM Questions WHERE Questions.QuestionDifficulty=[X] AND ( Questions.LastDateRevealed Is Null OR Questions.LastDateRevealed < DateAdd('d',-2,Date() ) AND Questions.LastUsedKey NOT LIKE ""[Y]"" );", QuestionConnection)
QuestionConnectionQuery.Parameters.AddWithValue("X", questionDifficulty.ToString)
QuestionConnectionQuery.Parameters.AddWithValue("Y", strDatabaseKey)
Right now when I try to use: Console.WriteLine("Query: " & QuestionConnectionQuery.ToString)
I only get this:
Loop Question #1
Query: System.Data.OleDb.OleDbCommand
The short version comes down to this:
QuestionConnectionQuery.ToString
The QuestionConnectionQuery object is much more than just the text of your command. It's also the parameters, execution type, a timeout, and a number of other things. If you want the command text, ask for it:
QuestionConnectionQuery.CommandText
But that's only the first issue here.
Right now, your parameters are not defined correctly, so this query will never succeed. OleDb uses ? as the parameter placeholder. Then the order in which you add the parameters to the collection has to match the order in which the placeholder shows in the query. The code in your question just has X and Y directly for parameter placeholders. You want to do this:
Dim QuestionConnectionQuery AS New OleDb.OleDbCommand("SELECT Questions.QuestionID FROM Questions WHERE Questions.QuestionDifficulty= ? AND ( Questions.LastDateRevealed Is Null OR Questions.LastDateRevealed < DateAdd('d',-2, Date() ) AND Questions.LastUsedKey NOT LIKE ? );", QuestionConnection)
QuestionConnectionQuery.Parameters.Add("?", OleDbType.Integer).Value = questionDifficulty
QuestionConnectionQuery.Parameters.Add("?", OleDbType.VarChar, 20).Value = strDatabaseKey
I had to guess at the type and lengths of your parameters. Adjust that to match the actual types and lengths of the columns in your database.
Once you have made these fixes, this next thing to understand is that the completed query never exists. The whole point of parameterized queries is parameter data is never substituted directly into the sql command text, not even by the database engine. This keeps user data separated from the command and prevents any possibility of sql injection attacks.
While I'm here, you may also want to examine the WHERE conditions in your query. The WHERE clause currently looks like this:
WHERE A AND ( B OR C AND D )
Whenever you see an AND next to an OR like that, within the same parenthetical section, I have to stop and ask if that's what is really intended, or whether you should instead close the parentheses before the final AND condition:
WHERE A AND (B OR C) AND D
This will fetch the command text and swap in the parameter values. It isnt necessarily valid SQL, the NET Provider objects haven't escaped things yet, but you can see what the values are and what the order is for debugging:
Function GetFullCommandSQL(cmd As Data.Common.DbCommand) As String
Dim sql = cmd.CommandText
For Each p As Data.Common.DbParameter In cmd.Parameters
If sql.Contains(p.ParameterName) AndAlso p.Value IsNot Nothing Then
If p.Value.GetType Is GetType(String) Then
sql = sql.Replace(p.ParameterName,
String.Format("'{0}'", p.Value.ToString))
Else
sql = sql.Replace(p.ParameterName, p.Value.ToString)
End If
End If
Next
Return sql
End Function
Given the following SQL:
Dim sql = "INSERT INTO Demo (`Name`, StartDate, HP, Active) VALUES (#name, #start, #hp, #act)"
After parameters are supplied, you can get back this:
INSERT INTO Demo (`Name`, StartDate, HP, Active) VALUES ('johnny', 2/11/2010 12:00:00 AM, 6, True)
It would need to be modified to work with OleDB '?' type parameter placeholders. But it will work if the DbCommand object was created by an OleDBCOmmandBuilder, since it uses "#pN" internally.
To get or set the text of the command that will be run, use the CommandText property.
To print the results, you need to actually execute the query. Call its ExecuteReader method to get an OleDbDataReader. You can use that to iterate over the rows.
Dim reader = QuestionConnectionQuery.ExecuteReader()
While reader.Read
Console.WriteLine(reader.GetValue(0))
End While
reader.Close()
If you know the data type of the column(s) ahead of time, you can use the type-specific methods like GetInt32. If you have multiple columns, change the 0 in this example to the zero-based index of the column you want.

Where clause in LINQ query is not recognized in vb.net?

The entity field is not recognized in the following Where clause. Is the VB wrong?
Dim projects = context.projects
.OrderBy(Function(x) x.name)
.Select(Function(x) {x.id, x.name})
.Where(Function(x) x.id <> sourceid)
If I take the Where off, it works fine. Also, if I flip the Where and the OrderBy, Where is fine, but now OrderBy fails.
Try this:
Dim projects = context.projects
.OrderBy(Function(x) x.name)
.Select(Function(x) New With {x.id, x.name})
.Where(Function(x) x.id <> sourceid)
The New With keyword should create an IEnumerable of anonymous type. You can then work with the id property in the Where clause without having to change the order of your operations.
There is nothing stopping you from doing the operations OrderBy, Select, Where in that order. The above code will certainly compile and run. However, logically you need to do the Where before Select, since the former is a filtering operation, while the latter is a projection.
Can you please try with the below code snippet.
Dim projects = context.projects.Where(Function(x) x.id <> sourceid).OrderBy(Function(x) x.name).Select(Function(x) {x.id, x.name})
This {x.id, x.name} is most likely an array of object (assuming id is integer and name is string, VB would infer Object). It is not an instance of a class with properties of id and name. #shree.pat18 explained how it can be adjusted to return what you want, but I would suggest using query syntax for clarity, and also putting your where clause before Select (should be slightly faster, because it does not create anonymous objects from the values you don't need included in results):
Dim projects = From p In context.projects
OrderBy p.name
Where p.Id <> sourceid
Select Id = p.Id, Name = p.Name

How to add custom columns to a table that LINQ to SQL can translate to SQL

I have a table that contains procedure codes among other data (let's call it "MyData"). I have another table that contains valid procedure codes, their descriptions, and the dates on which those codes are valid. Every time I want to report on MyData and include the procedure description, I have to do a lookup similar to this:
From m in dc.MyDatas _
Join p in dc.Procedures On m.proc_code Equals p.proc_code _
Where p.start_date <= m.event_date _
And If(p.end_date.HasValue, p.end_date.Value, Now) >= m.event_date _
Select m.proc_code, p.proc_desc
Since there are many places where I want to show the procedure description, this gets messy. I'd like to have the lookup defined in one place, so I tried putting this in an extension of MyData:
Partial Public Class MyData
Public ReadOnly Property ProcedureDescription() As String
Get
Dim dc As New MyDataContext
Return _
(From p in dc.Procedures _
Where p.proc_code = Me.proc_code _
And p.start_date <= Me.event_date _
And If(p.end_date.HasValue, p.end_date.Value, Now) >= Me.event_date _
Select p.proc_desc).SingleOrDefault
End Get
End Property
End Class
Which works when displaying data, but you can't use it in a query, because it doesn't know how to turn it into a SQL statement:
Dim test = _
From x In dc.MyDatas _
Select x.proc_code _
Where x.ProcedureDescription.Contains("test")
Error: The member 'MyProject.MyData.ProcedureDescription' has no supported translation to SQL.
Is there a way to turn a complex lookup (i.e. a non-trivial join) like this into something SQL can recognize so that I can define it in one place and just reference the description as if it were a field in MyData? So far the only thing I can think of is to create a SQL view on MyData that does the linking and bring that into my data context, but I'd like to try to avoid that. Any ideas would be welcomed. Thanks.
You can insert an AsEnumerable() into your query expression. That will convert everything to that point to a result set so that your custom properties can be included in the remainder of the expression (sorry I don't do VB):
var test = _
(from x in dc.MyDatas
(select x.proc_code).AsEnumerable().
Where(x => x.ProcedureDescription.Contains("test"));
this is not an extension method, it's a property
VB.net Extension methods: http://msdn.microsoft.com/en-us/library/bb384936.aspx
Consider using this query as a stored procedure if it's really important.