Created
June 17, 2015 20:17
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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