Dynamic query Linq to xml VB.NET - vb.net

Hey,
I want to write a query that the "where" in the query is a string something like"
Dim query as string= "Name =xxxx and Date > 10 "
Dim t = from book in doc.Descendants("books") Select _
[Name] = book..value, [Date] = book..value....
Where (query)
I build the query string on run time
Thanks...

I'm not saying this is your case but I see this a lot from people that came from ASP classic where we used to build dynamic SQL strings all of the time. We tend to hope that LINQ will give us some more power in part of the code but let us use strings elsewhere. Unfortunately this isn't true. Where takes a boolean argument and there's no way around that. You can write your own parser that uses reflection and eventually returns a boolean but you'd be writing a lot of code that could be error prone. Here's how you really should do it:
Assuming this is our data class:
Public Class TestObject
Public Property Name As String
Public Property Job As String
End Class
And here's our test data:
Dim Objects As New List(Of TestObject)
Objects.Add(New TestObject() With {.Name = "A", .Job = "Baker"})
Objects.Add(New TestObject() With {.Name = "B", .Job = "President"})
Objects.Add(New TestObject() With {.Name = "C", .Job = "Bus Driver"})
Objects.Add(New TestObject() With {.Name = "D", .Job = "Trainer"})
What you want to do is create a variable that represents the data to search for:
''//This variable simulates our choice. Normally we would be parsing the querystring, form data, XML values, etc
Dim RandNum = New Random().Next(0, 3)
Dim LookForName As String = Nothing
Select Case RandNum
Case 0 : LookForName = "A"
Case 1 : LookForName = "B"
Case 2 : LookForName = "C"
End Select
''//Query based on our name
Dim Subset = (From O In Objects Select O Where (O.Name = LookForName)).ToList()
If sometimes you need to search on Job and sometimes and sometimes you don't you just might have to write a couple of queries:
Dim Subset As List(Of TestObject)
Select Case RandNum
Case 0
Subset = (From O In Objects Select O Where (O.Name = "A" And O.Job = "Baker")).ToList()
Case Else
Select Case RandNum
Case 1 : LookForName = "B"
Case 2 : LookForName = "C"
End Select
Subset = (From O In Objects Select O Where (O.Name = LookForName)).ToList()
End Select
And just to explain writing your own query parser (which is a path that I recommend you DO NOT go down), here is a very, very, very rough start. It only supports = and only strings and can break at multiple points.
Public Shared Function QueryParser(ByVal obj As Object, ByVal ParamArray queries() As String) As Boolean
''//Sanity check
If obj Is Nothing Then Throw New ArgumentNullException("obj")
If (queries Is Nothing) OrElse (queries.Count = 0) Then Throw New ArgumentNullException("queries")
''//Array of property/value
Dim NameValue() As String
''//Loop through each query
For Each Q In queries
''//Remove whitespace around equals sign
Q = System.Text.RegularExpressions.Regex.Replace(Q, "\s+=\s+", "=")
''//Break the query into two parts.
''//NOTE: this only supports the equal sign right now
NameValue = Q.Split("="c)
''//NOTE: if either part of the query also contains an equal sign then this exception will be thrown
If NameValue.Length <> 2 Then Throw New ArgumentException("Queries must be in the format X=Y")
''//Grab the property by name
Dim P = obj.GetType().GetProperty(NameValue(0))
''//Make sure it exists
If P Is Nothing Then Throw New ApplicationException(String.Format("Cannot find property {0}", NameValue(0)))
''//We only support strings right now
If Not P.PropertyType Is GetType(String) Then Throw New ApplicationException("Only string property types are support")
''//Get the value of the property for the supplied object
Dim V = P.GetValue(obj, Nothing)
''//Assumming null never equals null return false for a null value
If V Is Nothing Then Return False
''//Compare the two strings, return false if something doesn't match.
''//You could use String.Compare here, too, but this will use the current Option Compare rules
If V.ToString() <> NameValue(1) Then Return False
Next
''//The above didn't fail so return true
Return True
End Function
This code would allow you to write:
Dim Subset = (From O In Objects Select O Where (QueryParser(O, "Name = A", "Job = Baker"))).ToList()

No, there is nothing directly like what you're looking for where you can pass in a string. As they say, when all you have is a hammer, everything looks like a nail...The real problem is that you need to learn what LINQ is good at and apply it to your code (if it is a good fit), rather than try and make it do what you could with a dynamically built SQL query string.
What you should probably be doing is making those "Where" clauses strongly typed anyway. Your current code has a lot of potential to blow up and be hard to debug.
What you could do instead is something like this (sorry, using C#, been a while since I've touched VB.NET):
var query = from book in doc.Descendants("books")
select book;
if(needsNameComparison)
{
query = query.where(book.Name == nameToCompare);
}
if(needsDateComparison)
{
query = query.Where(book.Date > 10);
}
List<book> bookList = query.ToList();
With LINQ, "query" isn't actually run until the "ToList()" call. Since it uses late execution, the query is dynamic in that it's being built on until it actually needs to run. This is similar to the code you were looking to use before since you were building a query string ahead of time, then executing it at a specific point.

Related

VB.NET - List.Contains always returns false when it should be true

I am more of a C# developer, and I am stuck on trying to implement something on my visual basic code;
TL;DR is I have a database table which holds all the IDs of Employees that have gone through a process. I want to be able to get those into a list, and see if any of the current members going through this process exist on this list, and if they do remove those members so they don't get reprocessed. The database table returns an object called of type "ProcessedTermedMember" which is a custom object with three string propertys (ID, FirstName, LastName)
I basically have this code here, which is returning false... I know that it is looking to see the Reference is equal by default with contains, so it would only be true when it is the exact same object, vs an object where all properties are the same. How can I override this?
Here is my code:
Dim termedMembers = (From ec In db.ECs
Where ec.AccountTypeCode = "WCS"
Where ec.AccountStatus = 5
Select ec).ToList()
Dim processedTermMembers = (From p In db.ProcessedTermedMembers
Select p).ToList()
For Each member In termedMembers
Dim term As ProcessedTermedMember = New ProcessedTermedMember With {.EmployeeID = member.EmployeeID, .FirstName = member.EmployeeFirstName, .LastName = member.EmployeeLastName}
If processedTermMembers.Contains(term) Then
termedMembers.Remove(member)
End If
Next
This 'If Then' statement in the loop always returns "false" how can I get it to return true if all properties in the "term" variable are equal to the properties of one of the list items in "processedTermMembers"?
Any help would be greatly appreciated.
This won't work for a couple of reasons, but namely because you are attempting to modify a collection inside of a For/Each loop.
The simplest solution would be to move the declaration of termedMembers to after the declaration of processedTermMembers and to add an additional where clause to termedMembers:
Dim processedTermMembers = (From p In db.ProcessedTermedMembers
Select p).ToList()
Dim termedMembers = (From ec In db.ECs
Where ec.AccountTypeCode = "WCS" AndAlso ec.AccountStatus = 5 AndAlso Not processedTermMembers.Any(Function(term) term.EmployeeID = ec.EmployeeID AndAlso term.FirstName = ec.EmployeeFirstName AndAlso term.LastName = ec.EmployeeLastName)).ToList()
What the modified LINQ statement does is pull in the records from db.ECs where the AccountTypeCode is not "WCS", the AccountStatus is not 5, and is not in the processedTermMembers collection.
Just some modification into #David code :
Dim processedTermMembers = (From p In db.ProcessedTermedMembers
Select p).ToList()
Dim termedMembers = (From ec In db.ECs
Where (ec.AccountTypeCode = "WCS" _
AndAlso ec.AccountStatus = 5 _
AndAlso Not (processedTermMembers.Any(Function(term) ec.EmployeeID.Equals(term.EmployeeID) AndAlso ec.EmployeeFirstName.Equals(term.FirstName) AndAlso ec.EmployeeLastName.Equals(term.LastName))))).ToList()

How to get the value of a column using a dataview in vb.net

I have a list of records and for Employee R1005, I need to check if that Employee has been Enabled for login alert (i.e EnableLoginAlert = Yes), then a button will be displayed.
CompanyID EmployeeNo EnableLoginAlert
10046 R1005 Yes
20041 Ajax12 No
47021 Drek Yes
I have tried the below codes:
If dCompanyDetails.Tables(0).Rows.Count > 0 Then
Dim dataView As DataView = dCompanyDetails.Tables(0).DefaultView
dataView.RowFilter = "EmployeeNo = '" & strEmployeeNumber & "'"
Dim svalue As String = dataView.Table.Rows(0).ItemArray(0).ToString()
If svalue = "No" Then
AlertButton.Visible = False
ElseIf svalue = "Yes" Then
{
//Do something else
}
End If
End If
If you are going to use a DataView then use it. This:
Dim svalue As String = dataView.Table.Rows(0).ItemArray(0).ToString()
is simply going back to the DataTable and using it, ignoring the DataView. The DataView contains DataRowView objects so get the one you need and use it. It is similar to a DataRow and you can use it the same way in this case:
Dim enableLoginAlert = CStr(dataView(0)("EnableLoginAlert")) = "Yes"
Now you have an actual Boolean that represents the state you want.
That's not how you should do it though. Generally speaking, you would use a DataView when you want to bind data. In fact, if you bind a DataTable then the data you see in the UI actually comes from the DefaultView. That's why you can filter and sort it. In this case, there are better options.
If you want to find a row by its primary key then the Rows collection of a DataTable has a Find method, e.g.
Dim row = dCompanyDetails.Tables(0).Rows.Find(strEmployeeNumber)
Dim enableLoginAlert = CStr(row("EnableLoginAlert")) = "Yes"
If you're searching by other than the primary key, the DataTable itself has a Select method. Because multiple rows may match, it returns an array, so you need to get the row out of that, e.g.
Dim row = dCompanyDetails.Tables(0).Select($"EmployeeNo = '{strEmployeeNumber}'").First()
Dim enableLoginAlert = CStr(row("EnableLoginAlert")) = "Yes"
If you want to look up a single row it's perhaps easiest to use LINQ:
Dim row = dCompanyDetails.Tables(0).Rows.Cast(Of DataRow).AsQueryable().FirstOrDefault(Function(r) r("EmployeeNo").ToString() = strEmployeeNumber)
If row IsNot Nothing AndAlso row("EnableLoginAlert").ToString() = "Yes" Then
...
..though I'd be the first to claim that using LINQ on base DataTables is very verbose, because of the Cast/AsQueryable. I'd use strongly typed DataTables (in a dataset); if you were to convert your code to using strongly typed tables it would look like:
Dim r = someDataSet.AProperTableName.FirstOrDefault(Function(r) r.EmployeeNo = strEmployeeNumber)
If r?.EnableLoginALert = "Yes" Then
...
...using strongly typed datatables is much less messy..
nb: You need to Imports System.Linq for these to work
That LINQ is the same thing as:
For Each r as DataRow in dCompanyDetails.Tables(0)
If r("EmployeeNo").ToString() = "R1005" AndAlso r("EnableLoginAlert").ToString() = "Yes" Then
...
You also have the option of using DataTable.Select (not a LINQ thing, though LINQ has a Select too)
Dim matchingRows = dCompanyDetails.Tables(0).Select($"[EmployeeNo] = '{strEmployeeNumber}'")
If matchingrows.Count > 0 AndAlso matchingRows(0)("EnableLoginAlert").ToString() = "Yes"

Use the contents of a string variable as a condition for an if statement?

Suppose I want to use an If statement, but I won't know until run-time what the actual condition of the If statement will be. Is there a way to do this by passing the condition as the contents of a string? As an example of the kind of thing I'm looking to acheive, consider the following bit of code;
Dim a as Integer = 1
Dim b as Integer = 2
Dim ConditionString As String = "<"
If a ConditionString b Then
...
End If
Mainly what I'm looking for is some way to leave the actual condition undefined until run-time. The reason I want to do this is because I need to have a set of threshold conditions in a database including not just the numeric values themselves, but also comparison operations. I might want to have something that amounts to "> 3.2 And < 5.6". As numbers are pulled in from data, the comparison operations need to be applied to the data depending on various conditions. Also, the database would be changed from time-to-time.
For such cases I love to use NCalc library, it has everything you need - it parses simple expressions (including logical and relational). Here is an example of it in C#:
var expr = new Expression("[X] > 3.2 and [X] < 5.6");
expr.Parameters["X"] = 10.0;
if (expr.Evaluate())
{
// ...
}
and VB.NET:
Dim expr As var = New Expression("[X] > 3.2 and [X] < 5.6")
expr.Parameters("X") = 10
If expr.Evaluate Then
' ...
End If
You can store a map of String to Func(Of Integer, Integer, Boolean) keyed by the strings "<", ">", "==", and so on, and take addresses of the functions that implement those conditions. For example:
Function LessThan(Integer a, Integer b) As Boolean
Return a < b
End Function
Dim Comparisons As New Map(Of String, Func(Of Integer, Integer, Boolean))
Comparisons.Add("<", AddressOf LessThan)
And then you can call it as such:
Dim a as Integer = 1
Dim b as Integer = 2
Dim ConditionString As String = "<"
If Comparisons(ConditionString)(a, b) Then
There are only a few possible conditions so I would just use a Select..Case statement:
Select Case ConditionString
Case "<"
Case ">"
'etc.
Case Else
End Select
Otherwise, you cannot (simply) convert a string "<" to an operator.
You just need a 'code', mapping some kind of value that you can store in a database to the various kinds of "conditions" you wish to test. Instead of a string, I'd suggest using an enum:
Enum ConditionEnum
LessThan
GreaterThan
Equal
SomeOtherVeryComplicatedBinaryFunction
End Enum
And then define a method that evaluates the condition along with the two arguments:
Public Sub EvaluateConditionWithArguments(ConditionEnum condition, Integer a, Integer b) As Boolean
EvaluateConditionWithArguments = False
Select Case condition
Case ConditionEnum.LessThan
If a < b Then
EvaluateConditionWithArguments = True
End If
...
End Select
End Sub

Linq query with multiple OrderBy statements added in a loop

I have a method in a webservice that has parameter with which users can decide how they want to order their results. This is a List(Of String) with the names of the fields in the order they want to sort them.
I know I can normally order on multiple columns by doing the following
Dim test = Bars.OrderBy(Function(x) x.Foo) _
.ThenBy(Function(x) x.Bar) _
.ThenBy(Function(x) x.Test)
However in this case this won't work since I can't chain the ThenBy function because I'm adding the sorting orders in a loop. To use ThenBy I need an IOrderedQueryable collection. This is how I would want it to work
Dim sortColumns = {"Foo", "Bar", "Test"}
Dim query = From b in Bars
For each column in sortColumns
Select Case column
Case "Foo"
query = query.Orderby(Function(x) x.Foo)
Case "Bar"
query = query.Orderby(Function(x) x.Bar)
Case "Test"
query = query.Orderby(Function(x) x.Test)
End Select
Next
Dim result = query.Select(Function(x) x.x).ToList()
Return result
This of course won't work because OrderBy will replace any previous ordering. The only solution I can think of is ordering the list on some other variable first so I already have an IOrderedQueryable collection but this just seems like the wrong approach.
Dim bars As New List(Of Bar)
Dim sortColumns = {"Foo", "Bar", "Test"}
Dim query = bars.Select(Function(x) New With {.Temp = 1, .x = x}) _
.OrderBy(Function(x) x.Temp)
For Each column In sortColumns
Select Case column
Case "Foo"
query = query.ThenBy(Function(x) x.x.Foo)
Case "Bar"
query = query.ThenBy(Function(x) x.x.Bar)
Case "Test"
query = query.ThenBy(Function(x) x.x.Test)
End Select
Next
Dim result = query.Select(Function(x) x.x).ToList()
Return result
You could write your own extension method OrderByOrThenBy which checks whether the value is already an IOrderedQueryable, uses ThenBy if so and OrderBy otherwise. Slightly smelly, but not terribly hard to do.
EDIT: C# sample (untested):
public static class QueryableOrdering
{
public static IOrderedQueryable<TElement> OrderByOrThenBy<TElement, TKey>
(this IQueryable<TElement> source,
Expression<Func<TElement, TKey>> ordering)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
if (ordering == null)
{
throw new ArgumentNullException("ordering");
}
var ordered = source as IOrderedQueryable<TElement>;
return ordered == null ? source.OrderBy(ordering)
: ordered.ThenBy(ordering);
}
}

How do I append a 'where' clause using VB.NET and LINQ?

I am pretty new to VB.NET and am having a bit of trouble here with something I thought should be simple.
Keeping it simple, let's say I have a Document table with "Name" that I want to search on (in reality there are several other tables, joins, etc. ..). I need to be able to build the query using a where clause based on string values passed in.
Example - the user may pass in "ABC", "ABC DEF", "ABC DEF GHI".
The final query would be (the syntax is not correct, I know):
Select * from Documents Where Name Like %ABC% AND Name Like %DEF% AND Name like %GHI%
So, I thought I could do something like this.
Dim query = From document In _context.Documents
<< loop based on number of strings passed in >>
query = query.Where( ... what goes here?? )
For some reason, being brain-dead or something, I can't figure out how to make this work in VB.NET, or if I'm doing it correctly.
I believe this is how you would do it in VB (I'm a C# developer):
query = query.Where(Function(s) s = "ABC")
See LINQ - Sample Queries for some examples.
I think the tricky part here is the unknown number of query parameters. You can use the underlying LINQ IQueryable(Of T) here to help.
I think the following would work (it's not compiled, just notepad code here):
Public Function GetDocuments(criteria as String)
Dim splitCriteria = SplitTheCriteria(criteria)
dim query = from document in _context.Documents
For Each item in splitCriteria
Dim localItem = item
query = AddCriteriaToQuery(query, localItem)
Next
dim matchingDocuments = query.ToList()
End Function
Private Function AddCriteriaToQuery(query as IQueryable(Of Document), criteria as string) as IQueryable(Of Document)
return query.Where(Function(doc) doc.Name = criteria)
End Function
Since LINQ will delay-execute the query you can append where clauses onto your query in the loop and then call .ToList() at the end to execute the query.
In LINQ to SQL you can add WHERE clauses to your query using the .Where method of the query object, as you noted in your question. To use the LIKE operator, try using the .Contains method of the object you're querying in the Lambda expression of your call to the Where method.
Here's a simplified example in a console application. Hopefully it will lead you in the correct direction.
Public Class Doc
Private _docName As String
Public Property DocName() As String
Get
Return _docName
End Get
Set(ByVal value As String)
_docName = value
End Set
End Property
Public Sub New(ByVal newDocName As String)
_docName = newDocName
End Sub
End Class
Sub Main()
Dim Documents As New List(Of Doc)
Documents.Add(New Doc("ABC"))
Documents.Add(New Doc("DEF"))
Documents.Add(New Doc("GHI"))
Documents.Add(New Doc("ABC DEF"))
Documents.Add(New Doc("DEF GHI"))
Documents.Add(New Doc("GHI LMN"))
Dim qry = From docs In Documents
qry = qry.Where(Function(d) d.DocName.Contains("GHI"))
Dim qryResults As List(Of Doc) = qry.ToList()
For Each d As Doc In qryResults
Console.WriteLine(d.DocName)
Next
End Sub
Note the .Contains("GHI") call in the Lambda expression of the .Where method. I'm referencing the parameter of the expression, "d", which exposes the DocName property, which further exposes the .Contains method. This should produce the LIKE query you're expecting.
This method is additive, i.e. the call to the .Where method could be enclosed in a loop to make additional LIKE operators added to the WHERE clause of your query.
Dim query = From document In _context.Documents where document.name = 'xpto' select document
Or
Dim query = From document In _context.Documents where document.name.contains('xpto') select document
If you do this in a loop, you can do something like this:
.Where(Function(i as mytype) i.myfiltervar = WhatIWantToSelect)