Last active
July 13, 2018 17:58
-
-
Save jbe2277/dbd1b554f3613a9d7b8b31fa3c90a61c to your computer and use it in GitHub Desktop.
Automatic Equals and GetHashCode of Properties
This file contains 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
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