Skip to content

Instantly share code, notes, and snippets.

@jbe2277
Last active July 13, 2018 17:58
Show Gist options
  • Save jbe2277/dbd1b554f3613a9d7b8b31fa3c90a61c to your computer and use it in GitHub Desktop.
Save jbe2277/dbd1b554f3613a9d7b8b31fa3c90a61c to your computer and use it in GitHub Desktop.
Automatic Equals and GetHashCode of Properties
internal static class EquatableHelper
{
private static readonly ConcurrentDictionary<Type, Func<object, object, bool>> getEqualsFunctions
= new ConcurrentDictionary<Type, Func<object, object, bool>>();
private static readonly ConcurrentDictionary<Type, Func<object, int>> getHashCodeFunctions
= new ConcurrentDictionary<Type, Func<object, int>>();
public static bool PropertiesEquals(object x, object y)
{
if (ReferenceEquals(x, y)) { return true; }
if (x == null || y == null) { return false; }
Type type = x.GetType();
var getEqualsFunction = getEqualsFunctions.GetOrAdd(type, MakeEqualsMethod);
return getEqualsFunction(x, y);
}
public static int PropertiesGetHashCode(object obj)
{
if (obj == null) { return 0; }
Type type = obj.GetType();
var getHashCodeFunction = getHashCodeFunctions.GetOrAdd(type, MakeGetHashCodeMethod);
return getHashCodeFunction(obj);
}
public static bool DtoSequenceEqual<TSource>(IEnumerable<TSource> first, IEnumerable<TSource> second)
{
if (first == null || second == null)
{
return object.Equals(first, second);
}
return Enumerable.SequenceEqual(first, second);
}
public static int DtoSequenceGetHashCode<TSource>(IEnumerable<TSource> collection)
{
if (collection == null) { return 0; }
var filteredCollection = collection.Where(x => x != null);
if (!filteredCollection.Any()) { return 0; }
return filteredCollection.Select(x => x.GetHashCode()).Aggregate((result, next) => next ^ result);
}
private static Func<object, object, bool> MakeEqualsMethod(Type type)
{
var paramThis = Expression.Parameter(typeof(object), "x");
var paramThat = Expression.Parameter(typeof(object), "y");
var paramCastThis = Expression.Convert(paramThis, type);
var paramCastThat = Expression.Convert(paramThat, type);
Expression last = null;
var equalsMethod = typeof(object).GetMethod(nameof(Equals), BindingFlags.Public | BindingFlags.Static);
var sequenceEqualMethod = typeof(EquatableHelper).GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(x => x.Name == nameof(DtoSequenceEqual))
.Select(x => new
{
Method = x,
Parameters = x.GetParameters(),
}).ToArray()
.Single(x => x.Parameters.Length == 2
&& x.Parameters[0].ParameterType.IsGenericType
&& x.Parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>)
&& x.Parameters[1].ParameterType.IsGenericType
&& x.Parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.Method;
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
MethodCallExpression equals;
var genericTypeDefinition = property.PropertyType.IsGenericType ? property.PropertyType.GetGenericTypeDefinition() : null;
if (genericTypeDefinition != null &&
(genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition.GetInterfaces().Any(x => x.GUID == typeof(IEnumerable<>).GUID)))
{
var propertyAccessX = Expression.Property(paramCastThis, property);
var propertyAccessY = Expression.Property(paramCastThat, property);
var constructedMethod = sequenceEqualMethod.MakeGenericMethod(property.PropertyType.GetGenericArguments().Single());
equals = Expression.Call(constructedMethod, propertyAccessX, propertyAccessY);
}
else
{
// Boxing is necessary for the call of the Equals method.
var propertyAccessX = Expression.Convert(Expression.Property(paramCastThis, property), typeof(object));
var propertyAccessY = Expression.Convert(Expression.Property(paramCastThat, property), typeof(object));
// Use the Equals method instead of Expression.Equals which calls the "== operator"
equals = Expression.Call(equalsMethod, propertyAccessX, propertyAccessY);
}
if (last == null)
{
last = equals;
}
else
{
last = Expression.AndAlso(last, equals);
}
}
if (last == null)
{
// Type has no public instance properties: true if both types are the same
last = Expression.Condition(Expression.TypeIs(paramThat, type),
Expression.Constant(true), Expression.Constant(false));
}
return Expression.Lambda<Func<object, object, bool>>(last, paramThis, paramThat).Compile();
}
private static Func<object, int> MakeGetHashCodeMethod(Type type)
{
ParameterExpression paramThis = Expression.Parameter(typeof(object), "obj");
UnaryExpression paramCastThis = Expression.Convert(paramThis, type);
Expression last = null;
Expression nullValue = Expression.Constant(null);
var sequenceGetHashCode = typeof(EquatableHelper).GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(x => x.Name == nameof(DtoSequenceGetHashCode))
.Select(x => new
{
Method = x,
Parameters = x.GetParameters(),
}).ToArray()
.Single(x => x.Parameters.Length == 1
&& x.Parameters[0].ParameterType.IsGenericType
&& x.Parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.Method;
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
Expression hash;
var genericTypeDefinition = property.PropertyType.IsGenericType ? property.PropertyType.GetGenericTypeDefinition() : null;
if (genericTypeDefinition != null &&
(genericTypeDefinition == typeof(IEnumerable<>) || genericTypeDefinition.GetInterfaces().Any(x => x.GUID == typeof(IEnumerable<>).GUID)))
{
var propertyAccess = Expression.Property(paramCastThis, property);
var constructedMethod = sequenceGetHashCode.MakeGenericMethod(property.PropertyType.GetGenericArguments().Single());
hash = Expression.Call(constructedMethod, propertyAccess);
}
else
{
// Boxing is necessary for the call of the GetHashCode method.
var propertyAccess = Expression.Convert(Expression.Property(paramCastThis, property), typeof(object));
hash = Expression.Condition(Expression.Equal(propertyAccess, nullValue),
Expression.Constant(0),
Expression.Call(propertyAccess, "GetHashCode", Type.EmptyTypes));
}
last = last == null ? hash : Expression.ExclusiveOr(last, hash);
}
if (last == null)
{
// Type has no public instance properties
last = Expression.Constant(0);
}
return Expression.Lambda<Func<object, int>>(last, paramThis).Compile();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment