Implementation and Comparison of Dynamic LINQ Ordering on IEnumerable<T> and IQueryable<T>

Nov 22, 2025 · Programming · 8 views · 7.8

Keywords: Dynamic LINQ | Expression Trees | IQueryable | IEnumerable | Dynamic Binding | CSLA Framework

Abstract: This article provides an in-depth exploration of two core methods for implementing dynamic LINQ ordering in C#: expression tree-based extensions for IQueryable<T> and dynamic binding-based extensions for IEnumerable<T>. Through detailed analysis of code implementation principles, performance characteristics, and applicable scenarios, it offers technical guidance for developers to choose the optimal sorting solution in different data source environments. The article also combines practical cases from the CSLA framework to demonstrate the practical value of dynamic ordering in enterprise-level applications.

Overview of Dynamic LINQ Ordering Technology

In C# development practice, dynamic ordering is a common requirement. Traditional LINQ queries require sorting fields to be determined at compile time, but in many business scenarios, sorting conditions need to be dynamically generated based on user input or configuration. Microsoft provided a dynamic LINQ library in VS2008 examples, but its core methods only support the IQueryable<T> interface, which limits its use in in-memory collections.

Expression Tree-Based Implementation for IQueryable<T>

For IQueryable<T> data sources, dynamic ordering logic can be constructed through expression trees. The core method ApplyOrder utilizes reflection and expression tree technology to convert string-form property paths into Lambda expressions.

public static IOrderedQueryable<T> OrderBy<T>(
    this IQueryable<T> source, 
    string property)
{
    return ApplyOrder<T>(source, property, "OrderBy");
}

static IOrderedQueryable<T> ApplyOrder<T>(
    IQueryable<T> source, 
    string property, 
    string methodName) 
{
    string[] props = property.Split('.');
    Type type = typeof(T);
    ParameterExpression arg = Expression.Parameter(type, "x");
    Expression expr = arg;
    foreach(string prop in props) {
        PropertyInfo pi = type.GetProperty(prop);
        expr = Expression.Property(expr, pi);
        type = pi.PropertyType;
    }
    Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
    LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);

    object result = typeof(Queryable).GetMethods().Single(
            method => method.Name == methodName
                    && method.IsGenericMethodDefinition
                    && method.GetGenericArguments().Length == 2
                    && method.GetParameters().Length == 2)
            .MakeGenericMethod(typeof(T), type)
            .Invoke(null, new object[] {source, lambda});
    return (IOrderedQueryable<T>)result;
}

This method supports nested property access (such as "Address.City") by recursively building member access expressions to achieve deep property ordering. The advantage of this approach is that it can fully utilize database query optimization and complete sorting operations at the data source level.

Dynamic Binding-Based Extension for IEnumerable<T>

For in-memory IEnumerable<T> collections, C# dynamic features can be used to implement sorting functionality. Through CallSite and runtime binders, a caching mechanism for property accessors is constructed.

public static IOrderedEnumerable<dynamic> OrderBy(
    this IEnumerable<dynamic> source, 
    string property)
{
    return Enumerable.OrderBy<dynamic, object>(
        source, 
        AccessorCache.GetAccessor(property), 
        Comparer<object>.Default);
}

private static class AccessorCache
{
    private static readonly Hashtable accessors = new Hashtable();
    private static readonly Hashtable callSites = new Hashtable();

    internal static Func<dynamic,object> GetAccessor(string name)
    {
        Func<dynamic, object> accessor = (Func<dynamic, object>)accessors[name];
        if (accessor == null)
        {
            lock (accessors)
            {
                accessor = (Func<dynamic, object>)accessors[name];
                if (accessor == null)
                {
                    if(name.IndexOf('.') >= 0) {
                        string[] props = name.Split('.');
                        CallSite<Func<CallSite, object, object>>[] arr 
                            = Array.ConvertAll(props, GetCallSiteLocked);
                        accessor = target =>
                        {
                            object val = (object)target;
                            for (int i = 0; i < arr.Length; i++)
                            {
                                var cs = arr[i];
                                val = cs.Target(cs, val);
                            }
                            return val;
                        };
                    } else {
                        var callSite = GetCallSiteLocked(name);
                        accessor = target =>
                        {
                            return callSite.Target(callSite, (object)target);
                        };
                    }
                    accessors[name] = accessor;
                }
            }
        }
        return accessor;
    }
}

This method is particularly suitable for handling dynamic objects or ExpandoObject, improving performance by caching accessors to avoid repeated creation of call sites. Hashtable is chosen as the cache container due to its favorable locking semantics.

Comparative Analysis of Technical Implementations

The two methods differ significantly in implementation principles and application scenarios. The expression tree method is suitable for structured data models and can optimize query performance at the database level, while the dynamic binding method is more appropriate for dynamic types or data structures determined at runtime.

In terms of performance, the expression tree method has some initial overhead when first constructed but offers higher efficiency in subsequent executions. The dynamic binding method reduces runtime overhead through caching mechanisms but may be less efficient than compile-time determined sorting when processing large amounts of data.

Practical Application in CSLA Framework

The reference article demonstrates the practical application of dynamic LINQ in the CSLA (Component-based, Scalable, Logical Architecture) framework. Developers encountered IQueryable support issues when using ReadOnlyListBase and resolved them by converting in-memory collections to queryable interfaces using the AsQueryable() extension method.

var list = productfilter.AsQueryable();
if (lv.SortExpression.Length > 1)
{
    string sort = lv.SortExpression;
    switch (lv.SortDirection)
    {
        case SortDirection.Ascending:
            list = list.OrderBy(sort + " Asc" );
            break;
        case SortDirection.Descending:
            list = list.OrderBy(sort + " Desc");
            break;
    }
}

This pattern is common in enterprise-level applications, particularly in data binding and user interface interaction scenarios. The CSLA framework enhanced support for dynamic LINQ in subsequent versions, addressing requirements for complex scenarios such as multi-column sorting.

Performance Optimization and Best Practices

In practical applications, it is recommended to choose the appropriate implementation based on the data source type:

In the CSLA framework discussion, developers also mentioned the implementation of the ToList() extension method, which is valuable for converting dynamic query results into concrete lists:

public static IList ToList(this IQueryable query)
{
    return (IList)Activator.CreateInstance(
        typeof(List<>).MakeGenericType(query.ElementType), 
        query);
}

Conclusion and Future Outlook

Dynamic LINQ ordering technology provides C# developers with flexible data processing capabilities. By deeply understanding the principles of expression trees and dynamic binding, developers can choose the most suitable implementation based on specific requirements. As the .NET ecosystem evolves, these technologies will continue to play important roles in enterprise-level applications, data binding, and user interaction scenarios.

Future development directions may include better performance optimization, stronger type safety support, and deeper integration with other .NET features (such as Source Generators), providing more efficient and secure solutions for dynamic data processing.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.