Linq query with multiple OrderBy statements added in a loop - vb.net

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);
}
}

Related

How to Sort a Generic Collection With a Where Clause

I am using an IEnumerable as the dataSource for a Repeater control. I need to sort the IEnumerable WHERE an entities property value equals a given value, then ORDER BY some other value. In other words, I want to sort an IEnumerable after some selected entity.
I am guessing I will have to implement the IComparer interface, but not sure where to start. Perhaps it could be as simple as having 2 IEnumerables; one with the entity I want displayed first, a second with the other entities, then concatenate them.
Dim myEnumerable As IEnumerable(Of myEntity) = dbContext.myEntity
myEnumerable.OrderBy(Function(x) x.propertyA **where x.propertyA = "value"**) _
.OrderBy(function(x) x.propertyB)
For simplicity of an example, I will use an IEnumerable(of String)
Dim myEnumerable As IEnumerable(Of String) = {"1", "2", "3", "40", "50"}
myEnumerable.OrderBy(Function(x) where x = "3" Then Order By x.length)
I am looking for the resulting sort to be
3
1
2
40
50
Try This:-
Dim query = myEnumerable.Where(Function(x) x = "3").Concat(myEnumerable.Where(Function(x) x <> "3").OrderBy(Function(x) x)).ToList()
C# Equivalent:-
var query = myEnumerable.Where(x => x == "3").Concat(myEnumerable.Where(x => x != "3").OrderBy(x => x)).ToList();

How to return distinct result in linq to collection

Below query result duplicates of class code.
cboFilterValues.DataSource = (From i In allDetails Select New LookUpItem With {.ItemText = i.ClassCode, .ItemValue = i.ClassCode} Distinct).ToList()
Can any one suggest me how i could achieve distinct result for above query. I need result set as IList(Of LookupItems)
Thank You
Your Distinct is not working because (presumably - you didn't provide the code) you have not overridden the Equals and GetHashCode methods in your LookUpItem class, so instances are being compared using reference equality. If you implement those methods, the Distinct should work:
Public Overrides Function Equals(o As Object) As Boolean
If o Is Nothing OrElse Not Me.GetType().Equals(o.GetType()) Then Return False
Dim other = DirectCast(o, LookUpItem)
Return Me.ItemText = other.ItemText ' or some other fields
End Function
Public Overrides Function GetHashCode() As Integer
Return Me.ItemText.GetHashCode() ' or some other fields
End Function
Alternatively, you could modify your query a little, since you are only using the ClassCode property from allDetails, and put the distinct there (assuming that ClassCode is a String, or something else that uses value equality):
cboFilterValues.DataSource = (
From i In (From d In allDetails Select d.ClassCode Distinct)
Select New LookUpItem With {.ItemText = i, .ItemValue = i}
).ToList()
cboFilterValues.DataSource = (From x In (From i In allDetails Select i Distinct) Select New LookUpItem With {.ItemText = x.ClassCode, .ItemValue = x.ClassCode}).Tolist
I assuming what you have above doesn't work because of some problem with the select new, this should get round it if that's the problem.
Tim

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.

vb.net syntax for order by clause in linq to sql that has select new with

I have a LINQ to sql statement that joins 2 tables. I would like to add a order by clause on one of the columns. However the order by clause does not seem to take effect at all.
Could you please suggest the right syntax in VB.net to achieve order by in the following:
Dim query = From dtIt In dbsomecontext.mytable
Join dtIl In dbsomecontext.anothertable On dtIt.ItemID Equals dtIl.ItemID
Where dtIl.IsAvailable = True
Order By dtIt.manufacturer
Select New With {
.Alpha = UCase((dtIt.manufacturer).Substring(0, 1))
}
Dim dtManufacturer As DataTable = csLINQOperations.LINQToDataTable(query)
Return dtManufacturer
Have you put a break point on the line where to Dim dtManufacturer ?
I created some sample classes to repersent your data objects as you've defined it.
Dim linqQuery = From dtIT In myTables _
Join dtIL In otherTables On dtIT.ItemID Equals dtIL.ItemID _
Where dtIL.IsAvaliable = True _
Order By dtIT.Manufacturer Ascending _
Select New With {.Alpha = UCase((dtIT.Manufacturer).Substring(0, 1))}
Now, when I have a break point on the line after this LINQ Query I can inspect the object linqQuery by using "linqQuery.ToList" and see the order of the data. It does infact order the output in an ordered fashion, based on the Manufacturer name.
Why is it that you think your code is not ordering the data? Using the Break Points and Watch, inspect your "query" object (using "query.ToList" in the Quick Watch) and see if the results are ordered correctly.
Yes, I figured the results were not ordered by "QuickWatch"ing dtManufacturer in the line :
Dim dtManufacturer As DataTable = csLINQOperations.LINQToDataTable(query)
Now, I have changed the query to as follows and it works :
Dim query = From dtIt In dbinkAndToner.InkAndToners
Join dtIl In dbinkAndToner.ItemsLists On dtIt.ItemID Equals dtIl.ItemID
Where dtIl.IsAvailable = True
Select New With {
.Alpha = ((dtIt.Manufacturer).Substring(0, 1))
}
Distinct
query = From dtIt In query
Order By dtIt.Alpha
Dim dtManufacturer As DataTable = csLINQOperations.LINQToDataTable(query)
Return dtManufacturer

Dynamic query Linq to xml 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.