Entity Framework dynamic Order by (VB.NET)
Introduction
A common requirement when working with data from a database is to provide the ability to sort by columns read from a database table which done in a Window desktop solution poses several challenges ranging from how to enable a DataGridView to properly sort as with conventional data sources like DataTable containers to providing dynamic sorting from selections in ComboBox and ListBox controls and optionally remember the last sort.
Learn how to sort in a DataGridView and to present available columns in a ComboBox or ListBox dynamically where available columns are based off a standard or custom model which Entity Framework uses to contain table data.
Conventional OrderBy
When reading data which needs to be sorted a property within a specific model must be specified as shown below. Unlike conventional SQL where the column to sort is not a property of a class to sort by a column the column is appended to the SQL while with Entity Framework this is not possible without a method to dynamically creating the order by at runtime.
Which is equivalent to the following SQL
SELECT Cust.CustomerIdentifier,
Cust.CompanyName,
C.FirstName,
C.LastName,
Countries.Name
FROM Customers AS Cust
INNER JOIN Contacts AS C ON Cust.ContactId = C.ContactId
INNER JOIN Countries ON Cust.CountryIdentifier = Countries.CountryIdentifier;
Dynamic Order By
There are two challenges for this at runtime.
- Write code to present available columns in a model or DTO for a ListBox or ComboBox
- Write a generic method which will work againsts a model or DTO which accepts a string name of a column to perform an order by
- Consider that a view which joins several models together as it's rare to have a single table without the following so this means most likely the view may change and or property/column names may change so the code to perform order by must be fluid.
- References table e.g. Country, Contact Type, Category etc.
- Joined tabled e.g. Customers -> Orders
To accomplish this a lambda Expression and Func(Of T) can be used to build in this case a single column sort which by using slightly modified code could be written to perform one or more column sorts.
Written here as a language extension method:
Imports System.Linq.Expressions
Imports System.Runtime.CompilerServices
Namespace LanguageExtensions
Public Module GenericSorterExtension
''' <summary>
''' Specifies the sort direction against a property
''' </summary>
Public Enum SortDirection
''' <summary>
''' Sort ascending.
''' </summary>
Ascending
''' <summary>
''' Sort descending.
''' </summary>
Descending
End Enum
<Extension>
Public Function Sort(Of T)(list As List(Of T), propertyName As String, sortDirection As SortDirection) As List(Of T)
Dim param As ParameterExpression = Expression.Parameter(GetType(T), "item")
Dim sortExpression As Expression(Of Func(Of T, Object)) = Expression.Lambda(Of Func(Of T, Object))(
Expression.Convert(
Expression.Property(param, propertyName), GetType(Object)), param)
Select Case sortDirection
Case SortDirection.Ascending
list = list.AsQueryable().OrderBy(sortExpression).ToList()
Case Else
list = list.AsQueryable().OrderByDescending(sortExpression).ToList()
End Select
Return list
End Function
End Module
End Namespace
To get column names which can be passed to the language extension above.
''' <summary>
''' Get names of properties for CustomerItem which over
''' time CustomerItem have have properties added or removed
''' so this handles any changes rather than hard coded properties
''' or property name changes.
''' </summary>
''' <returns></returns>
Public Function CustomerItemPropertyNames() As List(Of String)
Dim list As New List(Of String)
Dim customerItemType As Type = GetType(CustomerItem)
For Each propertyInfo As PropertyInfo In
customerItemType.GetProperties(BindingFlags.Instance Or
BindingFlags.Public Or BindingFlags.NonPublic)
list.Add(propertyInfo.Name.SplitCamelCase())
Next
Return list
End Function
For this example there is a Customer model, Contact model and Country model. The following class provides a view for two Customer properties, two Contact properties and one Country property.
''' <summary>
''' Default view for presenting data
''' </summary>
''' <remarks>
''' Important, how things are setup calling context.Customers.ToList
''' will fail as FirstName and LastName are not part of the model.
''' </remarks>
Partial Public Class Customer
Public Property FirstName As String
Public Property LastName As String
Public Shared ReadOnly Property Projection() As Expression(Of Func(Of Customer, CustomerItem))
Get
Return Function(customer) New CustomerItem() With {
.CustomerIdentifier = customer.CustomerIdentifier,
.CompanyName = customer.CompanyName,
.CountryName = customer.Country.Name,
.FirstName = customer.Contact.FirstName,
.LastName = customer.Contact.LastName}
End Get
End Property
End Class
A ComboBox is populated with column/property names as follows.
ColumnNameListBox.DataSource = operations.CustomerItemPropertyNames()
In the screenshot below note the ComboBox populated with column names, below the ComboBox there is a CheckBox which specifies the sort direction, default is ascending.
To perform the sort the selected item in the ComboBox is passed to the CustomSort method.
There may be several parts of this code new to some developers.
- The method is asynchronous which ensure the front end remains responsive. This is important for a good user experience.
- How the lambda select is constructed. By creating a read only property the view can be easily maintained and used over and over again.
By following these steps for any model one column sorting is easy. There is a caveat, once data is presented into a DataGridView column sorting will not work. To enable sorting use the following component BindingListView which may be installed using NuGet package manager.
Install-Package Equin.ApplicationFramework.BindingListView
To use the BindingList, setup a private variable in form.
Private CustomerView As BindingListView(Of CustomerItem)
Replace CustomerItem with the class/model to work with in your project.
Load returning data into a variable e.g.
By following the steps above dynamic order by can be very easy.
Multiple columns
To provide multiple property/column sort the following requires a string array of property names and a sort direction. The enum for sort direction is done a different way then the order by for a single property/column, some may like this while other prefer the latter.
Note in the language extension the direction for ordering is the same for both properties.
<Extension>
Public Function SortMultiColumn(Of T)(source As IQueryable(Of T),
propertyNames() As String, sortOrder As SortDirection) As IOrderedQueryable(Of T)
If propertyNames.Length = 0 Then
Throw New InvalidOperationException()
End If
Dim param = Expression.Parameter(GetType(T), String.Empty)
Dim expressionPropField = Expression.PropertyOrField(param, propertyNames(0))
Dim sortExpression = Expression.Lambda(expressionPropField, param)
Dim orderByCall As MethodCallExpression = Expression.Call(GetType(Queryable), "OrderBy" &
((If(sortOrder = SortDirection.Descending, "Descending", String.Empty))),
{GetType(T), expressionPropField.Type},
source.Expression, Expression.Quote(sortExpression))
If propertyNames.Length > 1 Then
For index As Integer = 1 To propertyNames.Length - 1
Dim item = propertyNames(index)
param = Expression.Parameter(GetType(T), String.Empty)
expressionPropField = Expression.PropertyOrField(param, item)
sortExpression = Expression.Lambda(expressionPropField, param)
orderByCall = Expression.Call(GetType(Queryable), "ThenBy" &
((If(sortOrder = SortDirection.Descending, "Descending", String.Empty))),
{GetType(T), expressionPropField.Type},
orderByCall, Expression.Quote(sortExpression))
Next
End If
Return DirectCast(source.Provider.CreateQuery(Of T)(orderByCall), IOrderedQueryable(Of T))
End Function
The following is a wrapper for the extension method above.
Public Async Function CustomerSortTwoProperties(
propertyName As String(),
Optional SortDirection As SortDirection = SortDirection.Descending) As Task(Of List(Of CustomerItem))
Using context = New NorthWindAzureContext()
Return Await Task.Run(
Function()
Return context.Customers.
Select(Customer.Projection).
SortMultiColumn(propertyName, SortDirection).ToList()
End Function)
End Using
End Function
To implement the two property order by in a form.
Imports WindowsApp1.Classes
Imports WindowsApp1.LanguageExtensions
Public Class MultiColumnSortForm
Private operations As New Operations
Private Async Sub MultiColumnSortForm_Shown(sender As Object, e As EventArgs) Handles Me.Shown
Dim results = Await operations.
CustomerSortTwoProperties({"CompanyName", "CountryName"},
SortDirection.Descending)
DataGridView1.DataSource = results
End Sub
End Class
IMPORTANT In the language extension presented in this article should not require any modifications so do not get hung up on the complexities of the code but instead simply use them. |
Unusual order by
Even though this does not handle dynamic order (but could with some refactors) by it's worth mentioning an OrderBy can use a IComparer to provide specialized ordering.
For example, a business rule states that specific company names should appear at the top of the order then the remaining company names to follow.
The comparer
Namespace Classes
Public Class SpecificOrdering
Inherits OrderedPredicateComparer(Of String)
Private Shared ReadOnly Order() As Func(Of String, Boolean) = {
Function(value) value.StartsWith("Bon"),
Function(item) item.StartsWith("Cac"),
Function(item) item.StartsWith("Du")}
Public Sub New()
MyBase.New(Order)
End Sub
End Class
End Namespace
Usage in a project
- Copy GenericSorterExtensions.vb to your project, all code within is generic.
- Create a DTO class as per CustomerItem.
- Setup a project as shown in the partial class Customer.
- Write code in a form to get the data, use Form1 code as a guide.
Summary
Code presented to assist in learning how to provide dynamic ordering of properties for an Entity Framework model in which string values are used as properties where the string values for column names are evaluated so that if a column/property name changes they will be reflected at runtime.
There are modifications which may be done to suit business needs without touching the generic methods used to create the order by’s.
See also
ASP.NET Core (C#) Blazor sorting
Source code
The following GitHub repository see this project while a secondary class project is not needed. The secondary class project will be part of another article.