I've needed a couple of very special comparers recenty and didn't want to implement each one of them every time so I created a builder and a couple of supporting classes that do that for me.
Example
I'll start with an example. Given a collection of Product
s:
var products = new[] { new Product {Name = "Car", Price = 7 }, new Product {Name = "Table", Price = 3 }, new Product {Name = "Orange", Price = 1 }, }; private class Product { public int Id { get; set; } public string Name { get; set; } public int Price { get; set; } }
I'd like to sort them either by the length of their name or price. Instead of implementig this type of unusual sorting with a new comparer I can use my new builder that allows me to create a comparer for that on the fly:
var comparer = ComparerFactory<Product>.Create(
x => new { x.Name.Length, x.Price },
(builder, x, y) =>
{
builder
.LessThen(() => x.Length < y.Length || x.Price < y.Price)
.Equal(() => x.Length == y.Length || x.Price == y.Price)
.GreaterThan(() => x.Length > y.Length || x.Price > y.Price);
});
var sorted = products.OrderByDescending(p => p, comparer).ToList();
Implementation
On top of everything else I use the ComparerFactory<T>
that implements the tedious null checks and comparisons. No magic here.
internal static class ComparerFactory<T>
{
private class Comparer : IComparer<T>
{
private readonly IDictionary<CompareOperator, Func<T, T, bool>> _comparers;
internal Comparer([NotNull] IDictionary<CompareOperator, Func<T, T, bool>> comparers)
{
_comparers = comparers;
}
public int Compare(T x, T y)
{
if (ReferenceEquals(x, y)) return 0;
if (ReferenceEquals(x, null)) return -1;
if (ReferenceEquals(y, null)) return 1;
if (_comparers[CompareOperator.LessThan](x, y)) return -1;
if (_comparers[CompareOperator.Equal](x, y)) return 0;
if (_comparers[CompareOperator.GreaterThan](x, y)) return 1;
// Makes the compiler very happy.
return 0;
}
}
public static IComparer<T> Create<TComparable>(Expression<Func<T, TComparable>> selectComparable, Action<ComparerBuilder<T, TComparable>, TComparable, TComparable> create)
{
var builder = new ComparerBuilder<T, TComparable>(selectComparable);
create(builder, default, default);
var funcs = builder.Build();
return new Comparer(funcs);
}
}
The interesing part begins with the ComparerBuilder<T, TComparable>
. An instance of this class passed to the Create
call just after the expression that selects the relevant value or properties as an anonymous object:
var comparer = ComparerFactory<Product>.Create( x => new { x.Name.Length, x.Price }, (builder, x, y) => { ... });
The user also receives the two arguments that should be compared x
and y
. I'm doing this to not have to repeat myself when defining each comparison like I did before.
This was the first version but I didn't like it. Too much redundancy so I moved the two repeating variables to the top.
( isLessThan: (x, y) => x.Ordinal < y.Ordinal, areEqual: (x, y) => x.Ordinal == y.Ordinal, isGreaterThan: (x, y) => x.Ordinal > y.Ordinal );
The builder collects a couple of expressions. One for selecting the value or the properties that should be compared and one expression for each operation.
Finally the Build
method turns each three operation expressions into three Func<T, T, bool>
:
internal class ComparerBuilder<T, TComparable>
{
private readonly Expression<Func<T, TComparable>> _getComparable;
private readonly IDictionary<CompareOperator, Expression<Func<bool>>> _expressions = new Dictionary<CompareOperator, Expression<Func<bool>>>();
public ComparerBuilder(Expression<Func<T, TComparable>> getComparable)
{
_getComparable = getComparable;
}
public ComparerBuilder<T, TComparable> LessThen(Expression<Func<bool>> expression)
{
_expressions[CompareOperator.LessThan] = expression;
return this;
}
public ComparerBuilder<T, TComparable> Equal(Expression<Func<bool>> expression)
{
_expressions[CompareOperator.Equal] = expression;
return this;
}
public ComparerBuilder<T, TComparable> GreaterThan(Expression<Func<bool>> expression)
{
_expressions[CompareOperator.GreaterThan] = expression;
return this;
}
internal IDictionary<CompareOperator, Func<T, T, bool>> Build()
{
var left = Expression.Parameter(typeof(T), "left");
var right = Expression.Parameter(typeof(T), "right");
return _expressions.ToDictionary(x => x.Key, x => CompileComparer(x.Value, new[] { left, right }));
}
private Func<T, T, bool> CompileComparer(Expression compare, ParameterExpression[] parameters)
{
var rewritten = CompareRewriter.Rewrite(_getComparable, parameters, compare);
var lambda = Expression.Lambda<Func<T, T, bool>>(Expression.Invoke(rewritten), parameters);
return lambda.Compile();
}
}
But since the original operation expressions are incompatible with the Func<T, T, bool>
signature:
() => x < y
I cannot call them from the comparer yet. I need something else that wold accept parameters:
(x, y) => x < y
In order to achieve this I created the CompareRewriter
that is a ExpressionVisitor
and replaces closures with calls to the selector for the comparable type or value. In case it's an anonymous type an additional conversion is necessary. Here a property access needs to be done to get the result of the type selector and not the closure which is useless later.
This means that on the left and right side of each <
, >
and ==
operator I inject the selector specified as the first argument:
x => new { x.Name.Length, x.Price }
Rewriting the expressions was the trickiest part but I managed to create the correct expressions and it behaves just as I expect it to.
internal class CompareRewriter : ExpressionVisitor
{
private readonly Expression _getComparable;
private readonly ParameterExpression[] _parameters;
private int _param;
public CompareRewriter(Expression getComparable, ParameterExpression[] parameters)
{
_getComparable = getComparable;
_parameters = parameters;
}
public static Expression Rewrite(Expression getComparable, ParameterExpression[] parameters, Expression compare)
{
var visitor = new CompareRewriter(getComparable, parameters);
return visitor.Visit(compare);
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
var binary = Visit((BinaryExpression)node.Body);
return Expression.Lambda<Func<bool>>(binary);
}
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.NodeType == ExpressionType.Equal) return base.VisitBinary(node);
// Rewrite
// () => ClosureT1.x < ClosureT2.y
// to
// () => getComparable(T2).x < getComparable(T2).y
var getLeft = Expression.Invoke(_getComparable, _parameters[0]);
var getRight = Expression.Invoke(_getComparable, _parameters[1]);
_param = 0;
var left = Visit(node.Left);
_param++;
var right = Visit(node.Right);
// Determine whether a member-access is necessary or are we using pure values?
left = left == node.Left ? getLeft : left;
right = right == node.Right ? getRight : right;
switch (node.NodeType)
{
case ExpressionType.LessThan: return Expression.LessThan(left, right);
case ExpressionType.Equal: return Expression.Equal(left, right);
case ExpressionType.GreaterThan: return Expression.GreaterThan(left, right);
}
return base.VisitBinary(node);
}
protected override Expression VisitMember(MemberExpression node)
{
return
node.Member.MemberType == MemberTypes.Property
? Expression.MakeMemberAccess(Expression.Invoke(_getComparable, _parameters[_param]), node.Member)
: base.VisitMember(node);
}
}
internal enum CompareOperator
{
LessThan,
Equal,
GreaterThan
}
What do you think of this kind of a builder where you need to rewrite the expressions afterwards? What do you say about the expression-visitor?
The same code but as a single file can be found here and my two tests here.
[Orange, Car, Table]
because Orange is the longest word, Car has then the highest price and Table is inbetween. \$\endgroup\$