I am currently implementing some dynamic filtering/sorting and thought it was a good idea to do a benchmark to see how things are looking.
First, here's the method creating an expression acting as a "getter":
public static Expression<Func<TEntity, object>> GetPropertyGetter(string propertyName, bool useCache = false)
{
if (useCache && _propertyGetters.ContainsKey(propertyName))
return _propertyGetters[propertyName];
var entityType = typeof(TEntity);
var property = entityType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
if (property == null)
throw new Exception($"Property {propertyName} was not found in entity {entityType.Name}");
var param = Expression.Parameter(typeof(TEntity));
var prop = Expression.Property(param, propertyName);
var convertedProp = Expression.Convert(prop, typeof(object));
var expr = Expression.Lambda<Func<TEntity, object>>(convertedProp, param);
if (useCache)
{
_propertyGetters.Add(propertyName, expr);
}
return expr;
}
Here's the benchmark:
public class OrderBy
{
private readonly List<Entry> _entries;
public OrderBy()
{
_entries = new List<Entry>();
for (int i = 0; i < 1_000_000; i++)
{
_entries.Add(new Entry($"Title {i}", i));
}
}
[Benchmark(Baseline = true)]
public List<Entry> SearchTitle()
{
return _entries.AsQueryable().OrderByDescending(p => p.Title).ToList();
}
[Benchmark]
public List<Entry> SearchTitleDynamicallyWithoutCache()
{
var expr = DynamicExpressions<Entry>.GetPropertyGetter("Title");
return _entries.AsQueryable().OrderByDescending(expr).ToList();
}
[Benchmark]
public List<Entry> SearchTitleDynamicallyWithCache()
{
var expr = DynamicExpressions<Entry>.GetPropertyGetter("Title", useCache: true);
return _entries.AsQueryable().OrderByDescending(expr).ToList();
}
}
public class Entry
{
public string Title { get; set; }
public int Number { get; set; }
public Entry(string title, int number)
{
Title = title;
Number = number;
}
}
So my question is, why does creating an expression (which uses Reflection to get the property) faster than a direct access (p => p.Title
)?
The problem is your GetPropertyGetter
method generates a lambda that converts the result of the property into an object
. When OrderBy
sorts by object
instead of by string
, the comparison used is different. If you change the lambda to p => (object)p.Title
you will discover it is faster as well. If you change the OrderByDescending
to take a StringComparer.InvariantCulture
, you will see a slight speed up over your generated lambdas.
Of course, that also means your dynamic OrderBy
most likely doesn't handle other languages properly.
Unfortunately once you start to dynamically create code like the lambda for a LINQ method, you can't always just substitute object
and expect the same results (e.g. an int
field will be boxed, string
won't use the same comparer, types with custom comparers may not work, ...). Basically I think of Expression
building for dynamic type handling as like the GPL - it spreads out (and up) like a virus. If you replaced OrderByDescending(GetPropertyGetter)
with dynamic OrderByPropertyNameDescending(string)
and built the call to OrderBy
as well, you would get what you expect.
Consider:
public static class DynanmicExt {
public static IOrderedQueryable<TEntity> OrderByDescending<TEntity>(this IQueryable<TEntity> q, string propertyName) {
var entityType = typeof(TEntity);
var property = entityType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
if (property == null)
throw new Exception($"Property {propertyName} was not found in entity {entityType.Name}");
var param = Expression.Parameter(typeof(TEntity));
var prop = Expression.Property(param, propertyName);
var expr = Expression.Lambda<Func<TEntity,string>>(prop, param);
var OrderBymi = typeof(Queryable).GetGenericMethod("OrderByDescending", new[] { typeof(IQueryable<TEntity>), typeof(Expression<Func<TEntity, object>>) })
.MakeGenericMethod(typeof(TEntity), prop.Member.GetMemberType());
var obParam = Expression.Parameter(typeof(IQueryable<TEntity>));
var obBody = Expression.Call(null, OrderBymi, obParam, expr);
var obLambda = Expression.Lambda<Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>>(obBody, obParam).Compile();
return obLambda(q);
}
}
Oh, almost forgot it needs these handy Reflection helpers:
public static class MemberInfoExt {
public static Type GetMemberType(this MemberInfo member) {
switch (member) {
case FieldInfo mfi:
return mfi.FieldType;
case PropertyInfo mpi:
return mpi.PropertyType;
case EventInfo mei:
return mei.EventHandlerType;
default:
throw new ArgumentException("MemberInfo must be if type FieldInfo, PropertyInfo or EventInfo", nameof(member));
}
}
}
public static class TypeExt {
public static MethodInfo GetGenericMethod(this Type t, string methodName, params Type[] pt) =>
t.GetMethods().Where(mi => mi.Name == methodName && mi.IsGenericMethod && mi.GetParameters().Select(mip => mip.ParameterType.IfGetGenericTypeDefinition()).SequenceEqual(pt.Select(p => p.IfGetGenericTypeDefinition()))).Single();
public static Type IfGetGenericTypeDefinition(this Type aType) => aType.IsGenericType ? aType.GetGenericTypeDefinition() : aType;
}
Now you can use it with:
public List<Entry> SearchTitle2() =>
_entries.AsQueryable().OrderByDescending("Title").ToList();
This is now just as slow as the lambda is to run.