using System.Collections.Generic;
using System.Runtime.CompilerServices;
public static void Main(string[] args)
var l1 = new List<string>
var l2 = new List<string>
var comparer1 = new ValueEqualityComparer<List<string>>();
Console.WriteLine(comparer1.Equals(l1, l2));
var comparer2 = new ValueEqualityComparer<Test>();
Console.WriteLine(comparer2.Equals(test1, test2));
Console.WriteLine(comparer2.Equals(test2, test3));
var testList1 = new List<Test> {test1, test2};
var testList2 = new List<Test> {test1, test2};
var testList3 = new List<Test> {test2, test3};
var comparer3 = new ValueEqualityComparer<List<Test>>();
Console.WriteLine(comparer3.Equals(testList1, testList2));
Console.WriteLine(comparer3.Equals(testList2, testList3));
public int X { get; set; }
public int Y { get; set; }
public Foo Foo { get; set; }
public class ValueEqualityComparer<T> : IEqualityComparer<T>
private readonly Type _enumerableElementType;
private readonly bool _isEnumerable;
private readonly bool _isPrimitiveOrString;
public ValueEqualityComparer()
_isPrimitiveOrString = IsPrimitiveOrString(type);
if (!_isPrimitiveOrString)
(_isEnumerable, _enumerableElementType) = IsEnumerableAndElementType(type);
public bool Equals(T left, T right)
if (_isPrimitiveOrString)
return left?.Equals(right) ?? false;
var elementComparer = typeof(ValueEqualityComparer<>).MakeGenericType(_enumerableElementType)
.GetConstructor(new Type[] { })?.Invoke(null);
var sequenceEquals = typeof(Enumerable).Assembly.GetTypes()
assemblyType.IsSealed && !assemblyType.IsGenericType && !assemblyType.IsNested)
.SelectMany(assemblyType => assemblyType.GetMethods(BindingFlags.Static | BindingFlags.Public),
(assemblyType, method) => new {assemblyType, method})
.Where(x => x.method.IsDefined(typeof(ExtensionAttribute), false))
.First(x => x.method.Name == nameof(Enumerable.SequenceEqual) &&
x.method.GetParameters().Length == 3)
.MakeGenericMethod(_enumerableElementType);
return (bool) sequenceEquals.Invoke(null, new[] {left, right, elementComparer});
var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var property in properties)
var leftProp = property.GetValue(left);
var rightProp = property.GetValue(right);
var propComparerType = typeof(ValueEqualityComparer<>).MakeGenericType(property.PropertyType);
var propComparer = propComparerType.GetConstructor(new Type[] { })?.Invoke(null);
var equalsMethod = propComparerType.GetMethod(nameof(Equals),
new[] {property.PropertyType, property.PropertyType});
if (equalsMethod == null)
if (!(bool) equalsMethod.Invoke(propComparer, new[] {leftProp, rightProp}))
public int GetHashCode(T x)
return Tuple.Create(_isEnumerable, _enumerableElementType, _isPrimitiveOrString)
private static bool IsPrimitiveOrString(Type t)
return t.IsPrimitive || t == typeof(string);
private static (bool, Type) IsEnumerableAndElementType(Type t)
var enumerableType = t.GetInterfaces()
.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>));
return (enumerableType != null, enumerableType?.GetGenericArguments().Single());