Monday, January 28, 2019

c# - Dynamic LINQ OrderBy on IEnumerable / IQueryable




I found an example in the VS2008 Examples for Dynamic LINQ that allows you to use a sql-like string (e.g. OrderBy("Name, Age DESC")) for ordering. Unfortunately, the method included only works on IQueryable;. Is there any way to get this functionality on IEnumerable?


Answer



Just stumbled into this oldie...



To do this without the dynamic LINQ library, you just need the code as below. This covers most common scenarios including nested properties.



To get it working with IEnumerable you could add some wrapper methods that go via AsQueryable - but the code below is the core Expression logic needed.



public static IOrderedQueryable OrderBy(

this IQueryable source,
string property)
{
return ApplyOrder(source, property, "OrderBy");
}

public static IOrderedQueryable OrderByDescending(
this IQueryable source,
string property)
{

return ApplyOrder(source, property, "OrderByDescending");
}

public static IOrderedQueryable ThenBy(
this IOrderedQueryable source,
string property)
{
return ApplyOrder(source, property, "ThenBy");
}


public static IOrderedQueryable ThenByDescending(
this IOrderedQueryable source,
string property)
{
return ApplyOrder(source, property, "ThenByDescending");
}

static IOrderedQueryable ApplyOrder(
IQueryable 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) {
// use reflection (not ComponentModel) to mirror LINQ
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)result;
}





Edit: it gets more fun if you want to mix that with dynamic - although note that dynamic only applies to LINQ-to-Objects (expression-trees for ORMs etc can't really represent dynamic queries - MemberExpression doesn't support it). But here's a way to do it with LINQ-to-Objects. Note that the choice of Hashtable is due to favorable locking semantics:




using Microsoft.CSharp.RuntimeBinder;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Runtime.CompilerServices;
static class Program
{
private static class AccessorCache

{
private static readonly Hashtable accessors = new Hashtable();

private static readonly Hashtable callSites = new Hashtable();

private static CallSite> GetCallSiteLocked(
string name)
{
var callSite = (CallSite>)callSites[name];
if(callSite == null)

{
callSites[name] = callSite = CallSite>
.Create(Binder.GetMember(
CSharpBinderFlags.None,
name,
typeof(AccessorCache),
new CSharpArgumentInfo[] {
CSharpArgumentInfo.Create(
CSharpArgumentInfoFlags.None,
null)

}));
}
return callSite;
}

internal static Func GetAccessor(string name)
{
Func accessor = (Func)accessors[name];
if (accessor == null)
{

lock (accessors )
{
accessor = (Func)accessors[name];
if (accessor == null)
{
if(name.IndexOf('.') >= 0) {
string[] props = name.Split('.');
CallSite>[] 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;
}
}

public static IOrderedEnumerable OrderBy(
this IEnumerable source,
string property)
{
return Enumerable.OrderBy(
source,

AccessorCache.GetAccessor(property),
Comparer.Default);
}

public static IOrderedEnumerable OrderByDescending(
this IEnumerable source,
string property)
{
return Enumerable.OrderByDescending(
source,

AccessorCache.GetAccessor(property),
Comparer.Default);
}

public static IOrderedEnumerable ThenBy(
this IOrderedEnumerable source,
string property)
{
return Enumerable.ThenBy(
source,

AccessorCache.GetAccessor(property),
Comparer.Default);
}

public static IOrderedEnumerable ThenByDescending(
this IOrderedEnumerable source,
string property)
{
return Enumerable.ThenByDescending(
source,

AccessorCache.GetAccessor(property),
Comparer.Default);
}

static void Main()
{
dynamic a = new ExpandoObject(),
b = new ExpandoObject(),
c = new ExpandoObject();
a.X = "abc";

b.X = "ghi";
c.X = "def";
dynamic[] data = new[] {
new { Y = a },
new { Y = b },
new { Y = c }
};

var ordered = data.OrderByDescending("Y.X").ToArray();
foreach (var obj in ordered)

{
Console.WriteLine(obj.Y.X);
}
}
}

No comments:

Post a Comment

plot explanation - Why did Peaches&#39; mom hang on the tree? - Movies &amp; TV

In the middle of the movie Ice Age: Continental Drift Peaches' mom asked Peaches to go to sleep. Then, she hung on the tree. This parti...