Skip to content

Instantly share code, notes, and snippets.

@RichardD2
Created June 17, 2015 20:17
Show Gist options
  • Save RichardD2/a4e4b06e6fe94096a772 to your computer and use it in GitHub Desktop.
Save RichardD2/a4e4b06e6fe94096a772 to your computer and use it in GitHub Desktop.
Build an IComparer<T> for multiple properties, based on a mapping to an anonymous type.
public static class AnonymousComparer
{
private static class DefaultComparerCache
{
private static readonly ConcurrentDictionary<Type, Expression> Cache = new ConcurrentDictionary<Type, Expression>();
private static Expression GetDefaultComparerCore(Type propertyType)
{
var genericTypeDefinition = typeof(Comparer<>);
var comparerType = genericTypeDefinition.MakeGenericType(propertyType);
var comparer = Expression.Property(null, comparerType, "Default");
var genericInterfaceDefinition = typeof(IComparer<>);
var interfaceType = genericInterfaceDefinition.MakeGenericType(propertyType);
return Expression.Convert(comparer, interfaceType);
}
public static Expression GetDefaultComparer(Type propertyType)
{
return Cache.GetOrAdd(propertyType, GetDefaultComparerCore);
}
}
private sealed class ReplacementVisitor : ExpressionVisitor
{
private IReadOnlyCollection<ParameterExpression> SourceParameters { get; set; }
private Expression ToFind { get; set; }
private Expression ReplaceWith { get; set; }
public static Expression Transform(IReadOnlyCollection<ParameterExpression> sourceParameters, Expression source, Expression find, Expression replace)
{
var visitor = new ReplacementVisitor
{
SourceParameters = sourceParameters,
ToFind = find,
ReplaceWith = replace,
};
return visitor.Visit(source);
}
private Expression ReplaceNode(Expression node)
{
return (node == ToFind) ? ReplaceWith : node;
}
protected override Expression VisitConstant(ConstantExpression node)
{
return ReplaceNode(node);
}
protected override Expression VisitBinary(BinaryExpression node)
{
var result = ReplaceNode(node);
if (result == node) result = base.VisitBinary(node);
return result;
}
protected override Expression VisitParameter(ParameterExpression node)
{
if (SourceParameters.Contains(node)) return ReplaceNode(node);
return SourceParameters.FirstOrDefault(p => p.Name == node.Name) ?? node;
}
}
private sealed class CompositeComparer<TSource> : IComparer<TSource>
{
private readonly IReadOnlyList<Func<TSource, TSource, int>> _comparisons;
public CompositeComparer(IEnumerable<Expression<Func<TSource, TSource, int>>> comparisons)
{
_comparisons = comparisons.Select(expr => expr.Compile()).ToList().AsReadOnly();
}
public int Compare(TSource x, TSource y)
{
return _comparisons
.Select(fn => fn(x, y))
.FirstOrDefault(result => result != 0);
}
}
public sealed class Builder<TSource>
{
private readonly IReadOnlyList<Expression<Func<TSource, TSource, int>>> _comparisons;
private Builder(IEnumerable<Expression<Func<TSource, TSource, int>>> comparisons)
{
_comparisons = comparisons.ToList().AsReadOnly();
}
public Builder() : this(Enumerable.Empty<Expression<Func<TSource, TSource, int>>>())
{
}
public Builder<TSource> AddFromAnonymousType<TResult>(Expression<Func<TSource, TResult>> expression)
{
if (expression == null) throw new ArgumentNullException("expression");
var body = expression.Body as NewExpression;
if (body == null) throw new ArgumentException("Invalid expression.", "body");
var newItems = body.Arguments.Select(expr => ConvertAnonymousProperty(expression.Parameters, expr));
return new Builder<TSource>(_comparisons.Concat(newItems));
}
private static Expression<Func<TSource, TSource, int>> ConvertAnonymousProperty(IReadOnlyList<ParameterExpression> sourceParameters, Expression property)
{
var x = Expression.Parameter(sourceParameters[0].Type, "x");
var xBody = ReplacementVisitor.Transform(sourceParameters, property, sourceParameters[0], x);
var y = Expression.Parameter(sourceParameters[0].Type, "y");
var yBody = ReplacementVisitor.Transform(sourceParameters, property, sourceParameters[0], y);
var comparer = DefaultComparerCache.GetDefaultComparer(property.Type);
var body = Expression.Call(comparer, "Compare", null, xBody, yBody);
return Expression.Lambda<Func<TSource, TSource, int>>(body, x, y);
}
public IComparer<TSource> Build()
{
return new CompositeComparer<TSource>(_comparisons);
}
}
public static Builder<TSource> Compare<TSource>()
{
return new Builder<TSource>();
}
}
sealed class Person
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime Birthday { get; set; }
}
// Compare by Name, then by Age:
IComparer<Person> comparer = AnonymousComparer.Compare<Person>()
.AddFromAnonymousType(p => new { p.Name, p.Age })
.Build();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment