├── src ├── ClickToBuild.bat ├── .nuget │ ├── NuGet.Config │ └── NuGet.targets ├── EqualityComparer │ ├── Settings.StyleCop │ ├── DateComparisonType.cs │ ├── EqualityComparer.nuspec │ ├── DateTimeExtensions.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Reflection │ │ ├── EventInfoComparer.cs │ │ ├── PropertyInfoComparer.cs │ │ ├── ParameterInfoComparer.cs │ │ ├── FieldInfoComparer.cs │ │ ├── MethodInfoComparer.cs │ │ ├── ConstructorInfoComparer.cs │ │ ├── MemberInfoComparer.cs │ │ └── TypeExtensions.cs │ ├── DateComparer.cs │ ├── EqualityComparer.csproj │ ├── GenericEqualityComparer.cs │ └── MemberComparer.cs ├── EqualityComparer.Tests │ ├── Settings.StyleCop │ ├── packages.config │ ├── Reflection │ │ ├── MemberInfoComparerTest.cs │ │ ├── FieldInfoComparerTest.cs │ │ ├── ConstructorInfoComparerTest.cs │ │ ├── EventInfoComparerTest.cs │ │ ├── ParameterInfoComparerTest.cs │ │ ├── MethodInfoComparerTest.cs │ │ └── TypeExtensionsTest.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── DateComparerTest.cs │ ├── DateTimeExtensionsTest.cs │ ├── GenericEqualityComparerTest.cs │ ├── EqualityComparer.Tests.csproj │ └── MemberComparerTest.cs ├── gendarme.ignore ├── NuGetPack.ps1 └── EqualityComparer.sln ├── logo-128.png ├── .editorconfig ├── LICENSE.md ├── .hgignore ├── .gitignore └── README.md /src/ClickToBuild.bat: -------------------------------------------------------------------------------- 1 | PowerShell -Command ".\Build\build.ps1 BuildAll" 2 | pause -------------------------------------------------------------------------------- /logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Iristyle/EqualityComparer/HEAD/logo-128.png -------------------------------------------------------------------------------- /src/.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = crlf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /src/EqualityComparer/Settings.StyleCop: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ..\Build\Settings.StyleCop 5 | Linked 6 | 7 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/Settings.StyleCop: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ..\Build\Settings.Test.StyleCop 5 | Linked 6 | 7 | -------------------------------------------------------------------------------- /src/EqualityComparer/DateComparisonType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EqualityComparer 4 | { 5 | /// Defines how to compare dates when using DateComaper{T}. 6 | /// ebrown, 6/18/2011. 7 | public enum DateComparisonType 8 | { 9 | /// An exact comparison by ticks. 10 | Exact, 11 | /// A comparison truncated / always rounded down to the nearest second, which can be useful with data stores that do not roundtrip dates properly. 12 | TruncatedToSecond 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/gendarme.ignore: -------------------------------------------------------------------------------- 1 | # see https://github.com/mono/mono-tools/blob/master/gendarme/mono-options.ignore for complete example 2 | # implemented in https://github.com/mono/mono-tools/blob/master/gendarme/console/IgnoreFileList.cs 3 | # single rule must be followed by one or more targets 4 | 5 | # comment 6 | # R: Rule 7 | # M: Method 8 | # T: Type (no spaces allowed) 9 | # A: Assembly - we support Name, FullName and * 10 | # N: namespace - special case (no need to resolve) 11 | # @: include file 12 | # for instance 13 | # R: Gendarme.Rules.Design.Generic.PreferGenericsOverRefObjectRule 14 | # A: * -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/Reflection/MemberInfoComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace EqualityComparer.Reflection.Tests 6 | { 7 | public class MemberInfoComparerTest 8 | { 9 | class TypeA 10 | { 11 | public int Test { get; set; } 12 | } 13 | 14 | class TypeB 15 | { 16 | public int Test { get; set; } 17 | } 18 | 19 | [Fact] 20 | public void Equals_True_ForIdenticalTypes() 21 | { 22 | Assert.True(typeof(TypeA).GetMembers().SequenceEqual(typeof(TypeB).GetMembers(), MemberInfoComparer.Default)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/Reflection/FieldInfoComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace EqualityComparer.Reflection.Tests 6 | { 7 | public class FieldInfoComparerTest 8 | { 9 | class TypeA 10 | { 11 | [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Test Code")] 12 | public int Test = 0; 13 | } 14 | 15 | class TypeB 16 | { 17 | [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "Test Code")] 18 | public int Test = 0; 19 | } 20 | 21 | [Fact] 22 | public void Equals_True_OnTypesOfSameSignature() 23 | { 24 | Assert.True(typeof(TypeA).GetFields().SequenceEqual(typeof(TypeB).GetFields(), FieldInfoComparer.Default)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/Reflection/ConstructorInfoComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace EqualityComparer.Reflection.Tests 6 | { 7 | public class ConstructorInfoComparerTest 8 | { 9 | class TypeA 10 | { 11 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 12 | public TypeA(int test) { } 13 | } 14 | 15 | class TypeB 16 | { 17 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 18 | public TypeB(int test) { } 19 | } 20 | 21 | [Fact] 22 | public void Equals_True_OnTypesOfSameSignature() 23 | { 24 | Assert.True(typeof(TypeA).GetConstructors().SequenceEqual(typeof(TypeB).GetConstructors(), ConstructorInfoComparer.Default)); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/NuGetPack.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)] 3 | [string] 4 | $apiKey 5 | ) 6 | 7 | function Pack-And-Push 8 | { 9 | $thisName = $MyInvocation.MyCommand.Name 10 | $currentDirectory = [IO.Path]::GetDirectoryName((Get-Content function:$thisName).File) 11 | Write-Host "Running against $currentDirectory" 12 | $nuget = Get-ChildItem -Path $currentDirectory -Include 'nuget.exe' -Recurse | 13 | Select -ExpandProperty FullName -First 1 14 | 15 | Get-ChildItem -Path $currentDirectory -Include *.nuspec -Recurse | 16 | % { Join-Path ([IO.Path]::GetDirectoryName($_)) ([IO.Path]::GetFileNameWithoutExtension($_) + '.csproj') } | 17 | ? { Test-Path $_ } | 18 | % { Start-Process $nuget -ArgumentList "pack $_ -Build -Prop Configuration=Release -Exclude '**\*.CodeAnalysisLog.xml'" -NoNewWindow -Wait } 19 | 20 | Get-ChildItem *.nupkg | % { &$nuget push $_ $apiKey } 21 | } 22 | 23 | del *.nupkg 24 | Pack-And-Push 25 | del *.nupkg 26 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (C) 2011 East Point Systems, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/EqualityComparer/EqualityComparer.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $id$ 5 | $version$ 6 | $title$ 7 | Ethan Brown 8 | $author$ 9 | Opensource .NET helpers for easily comparing objects by their property / member values 10 | false 11 | $description$ 12 | Initial release 13 | https://github.com/EastPoint/EqualityComparer 14 | https://github.com/EastPoint/EqualityComparer/blob/master/LICENSE.md 15 | https://github.com/EastPoint/EqualityComparer/raw/master/logo-128.png 16 | equality comparer expression reflection 17 | en-US 18 | East Point Systems 2012 and contributors 19 | 20 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | *.*scc 4 | *.FileListAbsolute.txt 5 | *.aps 6 | *.bak 7 | *.[Cc]ache 8 | *.clw 9 | *.eto 10 | *.exe 11 | *.fb6lck 12 | *.fbl6 13 | *.fbpInf 14 | *.ilk 15 | *.lib 16 | *.log 17 | *.ncb 18 | *.nlb 19 | *.[Oo]bj 20 | *.patch 21 | *.pch 22 | *.pdb 23 | *.plg 24 | ipch/ 25 | *.[Pp]ublish.xml 26 | *.rdl.data 27 | *.sbr 28 | *.sdf 29 | *.opensdf 30 | *.unsuccessfulbuild 31 | *.opt 32 | *.scc 33 | *.sig 34 | *.sqlsuo 35 | *.suo 36 | *.svclog 37 | *.tlb 38 | *.tlh 39 | *.tli 40 | *.trends 41 | *.tmp 42 | *.user 43 | *.vshost.* 44 | *.vsmdi 45 | *DXCore.Solution 46 | *_i.c 47 | *_p.c 48 | Ankh.Load 49 | Ankh.NoLoad 50 | Backup* 51 | CVS/ 52 | .svn 53 | [Pp]recompiled[Ww]eb/ 54 | UpgradeLog*.* 55 | [Bb]uildArtifacts/ 56 | [Bb]in/ 57 | [Dd]ebug/ 58 | [Oo]bj/ 59 | [Rr]elease/ 60 | [Ss]andcastle/[Dd]ata 61 | [Tt]humbs.db 62 | _[Uu]pgradeReport_[Ff]iles 63 | _[Rr]e[Ss]harper*/ 64 | *.resharper 65 | [Tt]est[Rr]esult* 66 | hgignore[.-]* 67 | ignore[.-]* 68 | svnignore[.-]* 69 | lint.db 70 | .DS_Store 71 | [Cc]lientBin/ 72 | [Ii]ndex.dat 73 | [Ss]torage.dat 74 | *.sln.docstates 75 | ~$* 76 | packages/ 77 | *.orig 78 | *.dbmdl 79 | *.dbproj.schemaview 80 | *.xap -------------------------------------------------------------------------------- /src/EqualityComparer/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EqualityComparer 4 | { 5 | /// Date time extensions. 6 | /// 7/19/2011. 7 | public static class DateTimeExtensions 8 | { 9 | /// Truncates DateTime to second, so that JSON values with DateTimes can be roundtripped / compared properly. 10 | /// 7/19/2011. 11 | /// Original DateTime value. 12 | /// A new DateTime value truncated to the nearest second. 13 | public static DateTime TruncateToSecond(this DateTime value) 14 | { 15 | return new DateTime((value.Ticks / TimeSpan.TicksPerSecond) * TimeSpan.TicksPerSecond); 16 | } 17 | 18 | /// A DateTime extension method that rounds DateTimes to the nearest second. 19 | /// 7/19/2011. 20 | /// Original DateTime value. 21 | /// A new DateTime value rounded and truncated to the nearest second. 22 | public static DateTime RoundToNearestSecond(this DateTime value) 23 | { 24 | if (value.Millisecond >= 500) 25 | //account for tiny discrepancies with ms and ticks 26 | value = value.AddMilliseconds(502); 27 | 28 | return TruncateToSecond(value); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Resources; 4 | using System.Runtime.CompilerServices; 5 | using System.Runtime.InteropServices; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle("EqualityComparer.Tests")] 11 | [assembly: AssemblyDescription("")] 12 | [assembly: AssemblyProduct("EqualityComparer.Tests")] 13 | [assembly: AssemblyCulture("")] 14 | [assembly: AssemblyCompany("East Point Systems, Inc. http://www.eastpointsystems.com/")] 15 | [assembly: AssemblyCopyright("Copyright © 2012 East Point Systems, Inc.")] 16 | [assembly: AssemblyTrademark("")] 17 | [assembly: AssemblyConfiguration("")] 18 | [assembly: AssemblyVersion("0.1.2.0")] 19 | [assembly: AssemblyFileVersion("0.1.2.0")] 20 | [assembly: AssemblyInformationalVersion("0.1.2.0")] 21 | [assembly: CLSCompliant(true)] 22 | [assembly: NeutralResourcesLanguage("en")] 23 | 24 | // Setting ComVisible to false makes the types in this assembly not visible 25 | // to COM components. If you need to access a type in this assembly from 26 | // COM, set the ComVisible attribute to true on that type. 27 | [assembly: ComVisible(false)] 28 | 29 | // The following GUID is for the ID of the typelib if this project is exposed to COM 30 | [assembly: Guid("9cbd6f87-b7b8-448b-b197-ba9f33605f3b")] 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | 3 | #OS junk files 4 | [Tt]humbs.db 5 | *.DS_Store 6 | [Ii]ndex.dat 7 | [Ss]torage.dat 8 | 9 | #Visual Studio files 10 | *.[Oo]bj 11 | *.user 12 | *.aps 13 | *.pch 14 | *.pdb 15 | *.scc 16 | *.*scc 17 | *_i.c 18 | *_p.c 19 | *.ncb 20 | *.suo 21 | *.tlb 22 | *.tlh 23 | *.tli 24 | *.bak 25 | *.[Cc]ache 26 | *.ilk 27 | *.log 28 | *.lib 29 | *.sbr 30 | *.sdf 31 | *.opensdf 32 | *.unsuccessfulbuild 33 | *.opt 34 | *.plg 35 | ipch/ 36 | obj/ 37 | [Bb]in 38 | [Dd]ebug*/ 39 | [Rr]elease*/ 40 | Ankh.Load 41 | Ankh.NoLoad 42 | *.vshost.* 43 | *.FileListAbsolute.txt 44 | *.clw 45 | *.eto 46 | *.vsmdi 47 | *.dbmdl 48 | *.dbproj.schemaview 49 | 50 | #ASP.NET 51 | [Pp]recompiled[Ww]eb/ 52 | UpgradeLog*.* 53 | _[Uu]pgradeReport_[Ff]iles 54 | *.[Pp]ublish.xml 55 | 56 | #Silverlight 57 | [Cc]lientBin/ 58 | *.xap 59 | 60 | #WCF 61 | *.svclog 62 | 63 | #SSRS and SSMS 64 | *.rdl.data 65 | *.sqlsuo 66 | 67 | #Tooling 68 | _[Rr]e[Ss]harper*/ 69 | *.resharper 70 | [Tt]est[Rr]esult* 71 | *DXCore.Solution 72 | *.sln.docstates 73 | *.fbpInf 74 | lint.db 75 | 76 | #Build Scripts 77 | [Bb]uildArtifacts/ 78 | [Ss]andcastle/[Dd]ata 79 | *.trends 80 | 81 | #Project files 82 | [Bb]uild/ 83 | 84 | #TFS Files 85 | *.nlb 86 | 87 | #Other Source Control 88 | hgignore[.-]* 89 | ignore[.-]* 90 | svnignore[.-]* 91 | *.orig 92 | 93 | #CVS / Subversion files 94 | CVS/ 95 | .svn 96 | 97 | # Office Temp Files 98 | ~$* 99 | 100 | #NuGet 101 | [Pp]ackages/ 102 | *.nupkg 103 | 104 | #Miscellaneous Files 105 | *.exe 106 | *.fb6lck 107 | *.fbl6 108 | *.patch 109 | *.sig 110 | *.tmp 111 | Backup* 112 | 113 | #Test related files 114 | *.ncrunchsolution 115 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/Reflection/EventInfoComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using Xunit; 5 | 6 | namespace EqualityComparer.Reflection.Tests 7 | { 8 | public class EventInfoComparerTest 9 | { 10 | class A 11 | { 12 | public event EventHandler Test; 13 | public event EventHandler Test2; 14 | 15 | //placeholders to remove compiler warnings 16 | private void CallTest() { var TestCopy = Test; if (null != TestCopy) { TestCopy(this, EventArgs.Empty); } } 17 | private void CallTest2() { var TestCopy2 = Test2; if (null != TestCopy2) { TestCopy2(this, new UnhandledExceptionEventArgs(new DivideByZeroException(), false)); } } 18 | } 19 | 20 | class B 21 | { 22 | public event EventHandler Test; 23 | public event EventHandler Test2; 24 | 25 | //placeholders to remove compiler warnings 26 | private void CallTest() { var TestCopy = Test; if (null != TestCopy) { TestCopy(this, EventArgs.Empty); } } 27 | private void CallTest2() { var TestCopy2 = Test2; if (null != TestCopy2) { TestCopy2(this, new UnhandledExceptionEventArgs(new DivideByZeroException(), false)); } } 28 | } 29 | 30 | [Fact] 31 | public void Equals_True_OnTypesOfSameSignature() 32 | { 33 | Assert.True(typeof(A).GetEvents().SequenceEqual(typeof(B).GetEvents(), EventInfoComparer.Default)); 34 | } 35 | 36 | [Fact] 37 | public void Equals_False_OnNullFirstParameter() 38 | { 39 | Assert.False(EventInfoComparer.Default.Equals(null, (EventInfo)typeof(A).GetMember("Test")[0])); 40 | } 41 | 42 | [Fact] 43 | public void Equals_False_OnNullSecondParameter() 44 | { 45 | Assert.False(EventInfoComparer.Default.Equals((EventInfo)typeof(A).GetMember("Test")[0], null)); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/EqualityComparer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Reflection; 4 | using System.Resources; 5 | using System.Runtime.InteropServices; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle("EqualityComparer")] 11 | [assembly: AssemblyDescription("A set of Expression tree based object instance comparers")] 12 | [assembly: AssemblyProduct("EqualityComparer")] 13 | [assembly: AssemblyCulture("")] 14 | [assembly: AssemblyCompany("East Point Systems, Inc. http://www.eastpointsystems.com/")] 15 | [assembly: AssemblyCopyright("Copyright © 2012 East Point Systems, Inc.")] 16 | [assembly: AssemblyTrademark("")] 17 | [assembly: AssemblyConfiguration("")] 18 | [assembly: AssemblyVersion("0.1.2.0")] 19 | [assembly: AssemblyFileVersion("0.1.2.0")] 20 | [assembly: AssemblyInformationalVersion("0.1.2.0")] 21 | [assembly: CLSCompliant(true)] 22 | [assembly: NeutralResourcesLanguage("en")] 23 | 24 | // Setting ComVisible to false makes the types in this assembly not visible 25 | // to COM components. If you need to access a type in this assembly from 26 | // COM, set the ComVisible attribute to true on that type. 27 | [assembly: ComVisible(false)] 28 | 29 | // The following GUID is for the ID of the typelib if this project is exposed to COM 30 | [assembly: Guid("34a5780f-8fc9-49d4-90c4-67d866e3c693")] 31 | 32 | [assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Target = "EqualityComparer", Scope = "namespace", Justification = "Simple library!")] 33 | [assembly: SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Target = "EqualityComparer.Reflection", Scope = "namespace", Justification = "Helpers mirror .NET framework type layout")] 34 | -------------------------------------------------------------------------------- /src/EqualityComparer/Reflection/EventInfoComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Reflection; 5 | 6 | namespace EqualityComparer.Reflection 7 | { 8 | /// Event information comparer. 9 | /// ebrown, 2/3/2011. 10 | public class EventInfoComparer : EqualityComparer 11 | { 12 | private static Lazy _default = new Lazy(() => new EventInfoComparer()); 13 | 14 | /// Gets the default EventInfoComparer instance, rather than continually constructing new instances. 15 | /// The default. 16 | public static new EventInfoComparer Default { get { return _default.Value; } } 17 | 18 | /// Tests if two EventInfo objects are considered equal by our definition -- same Name, EventHandlerType, IsMulticast, Attributes. 19 | /// ebrown, 2/3/2011. 20 | /// EventInfo instance to be compared. 21 | /// EventInfo instance to be compared. 22 | /// true if the objects are considered equal, false if they are not. 23 | public override bool Equals(EventInfo x, EventInfo y) 24 | { 25 | if (x == y) { return true; } 26 | if ((x == null) || (y == null)) { return false; } 27 | 28 | return (x.Name == y.Name && 29 | x.EventHandlerType == y.EventHandlerType && 30 | x.IsMulticast == y.IsMulticast && 31 | x.Attributes == y.Attributes); 32 | } 33 | 34 | /// Calculates the hash code for this object. 35 | /// ebrown, 2/3/2011. 36 | /// The object. 37 | /// The hash code for this object. 38 | public override int GetHashCode(EventInfo obj) 39 | { 40 | if (null == obj) { return 0; } 41 | return string.Format(CultureInfo.CurrentCulture, "Member:{0}Name:{1}EventHandlerType:{2}", obj.MemberType, obj.Name, obj.EventHandlerType).GetHashCode(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/DateComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Xunit; 5 | using Xunit.Extensions; 6 | 7 | namespace EqualityComparer.Tests 8 | { 9 | public class DateComparerTest 10 | { 11 | private static DateTime wellKnownDate = new DateTime(2011, 6, 20, 13, 30, 1, 200); 12 | 13 | public static IEnumerable GetEqualToSecondDates 14 | { 15 | get 16 | { 17 | yield return new object[] { wellKnownDate, wellKnownDate }; 18 | yield return new object[] { wellKnownDate, wellKnownDate.AddMilliseconds(100) }; 19 | yield return new object[] { wellKnownDate, wellKnownDate.AddMilliseconds(799) }; 20 | } 21 | } 22 | 23 | public static IEnumerable GetUnequalToSecondDates 24 | { 25 | get 26 | { 27 | yield return new object[] { wellKnownDate, wellKnownDate.AddMilliseconds(800) }; 28 | yield return new object[] { wellKnownDate, wellKnownDate.AddMilliseconds(-201) }; 29 | } 30 | } 31 | 32 | [Theory] 33 | [PropertyData("GetEqualToSecondDates")] 34 | public void Equals_TruncatedToSecond_ReturnsTrue_OnComparisonsWithinSameSecond(DateTime testValue1, DateTime testValue2) 35 | { 36 | Assert.True(new DateComparer(DateComparisonType.TruncatedToSecond).Equals(testValue1, testValue2)); 37 | } 38 | 39 | [Theory] 40 | [PropertyData("GetUnequalToSecondDates")] 41 | public void Equals_TruncatedToSecond_ReturnsFalse_OnComparisonsOutsideSameSecond(DateTime testValue1, DateTime testValue2) 42 | { 43 | Assert.False(new DateComparer(DateComparisonType.TruncatedToSecond).Equals(testValue1, testValue2)); 44 | } 45 | 46 | [Fact] 47 | public void Equals_ExactComparison_ReturnsFalse_DifferentDateTime() 48 | { 49 | var now = DateTime.Now; 50 | Assert.False(new DateComparer(DateComparisonType.Exact).Equals(now, now.AddTicks(1))); 51 | } 52 | 53 | [Fact] 54 | public void Equals_ExactComparison_ReturnsTrue_SameTime() 55 | { 56 | var now = DateTime.Now; 57 | Assert.True(new DateComparer(DateComparisonType.Exact).Equals(now, now)); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/EqualityComparer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EqualityComparer", "EqualityComparer\EqualityComparer.csproj", "{10D51FFF-4F91-4917-82EA-B7426B2652F6}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EqualityComparer.Tests", "EqualityComparer.Tests\EqualityComparer.Tests.csproj", "{7E944761-20F9-48F3-8644-042AACBFD385}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{F49B64D7-4785-4E3B-94CC-1627D940E8FD}" 9 | ProjectSection(SolutionItems) = preProject 10 | .nuget\NuGet.Config = .nuget\NuGet.Config 11 | .nuget\NuGet.exe = .nuget\NuGet.exe 12 | .nuget\NuGet.targets = .nuget\NuGet.targets 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Analysis|Any CPU = Analysis|Any CPU 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {10D51FFF-4F91-4917-82EA-B7426B2652F6}.Analysis|Any CPU.ActiveCfg = Analysis|Any CPU 23 | {10D51FFF-4F91-4917-82EA-B7426B2652F6}.Analysis|Any CPU.Build.0 = Analysis|Any CPU 24 | {10D51FFF-4F91-4917-82EA-B7426B2652F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {10D51FFF-4F91-4917-82EA-B7426B2652F6}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {10D51FFF-4F91-4917-82EA-B7426B2652F6}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {10D51FFF-4F91-4917-82EA-B7426B2652F6}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {7E944761-20F9-48F3-8644-042AACBFD385}.Analysis|Any CPU.ActiveCfg = Analysis|Any CPU 29 | {7E944761-20F9-48F3-8644-042AACBFD385}.Analysis|Any CPU.Build.0 = Analysis|Any CPU 30 | {7E944761-20F9-48F3-8644-042AACBFD385}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {7E944761-20F9-48F3-8644-042AACBFD385}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {7E944761-20F9-48F3-8644-042AACBFD385}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {7E944761-20F9-48F3-8644-042AACBFD385}.Release|Any CPU.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /src/EqualityComparer/Reflection/PropertyInfoComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Reflection; 5 | 6 | namespace EqualityComparer.Reflection 7 | { 8 | /// A class that performs a rudimentary comparison of PropertyInfo instances. 9 | /// ebrown, 2/3/2011. 10 | public class PropertyInfoComparer : EqualityComparer 11 | { 12 | private static Lazy _default = new Lazy(() => new PropertyInfoComparer()); 13 | 14 | /// Gets the default PropertyInfoComparer instance, rather than continually constructing new instances. 15 | /// The default. 16 | public static new PropertyInfoComparer Default { get { return _default.Value; } } 17 | 18 | /// 19 | /// Tests if two PropertyInfo objects are considered equal by our definition -- same Name, PropertyType, CanRead / CanWrite, Attributes. 20 | /// 21 | /// ebrown, 2/3/2011. 22 | /// PropertyInfo to be compared. 23 | /// PropertyInfo to be compared. 24 | /// true if the objects are considered equal, false if they are not. 25 | public override bool Equals(PropertyInfo x, PropertyInfo y) 26 | { 27 | if (x == y) { return true; } 28 | if ((x == null) || (y == null)) { return false; } 29 | 30 | return (x.Name == y.Name && 31 | x.PropertyType == y.PropertyType && 32 | x.CanRead == y.CanRead && 33 | x.CanWrite == y.CanWrite && 34 | x.Attributes == y.Attributes); 35 | } 36 | 37 | /// Calculates the hash code for this object. 38 | /// ebrown, 2/3/2011. 39 | /// The object. 40 | /// The hash code for this object. 41 | public override int GetHashCode(PropertyInfo obj) 42 | { 43 | if (null == obj) { return 0; } 44 | return string.Format(CultureInfo.CurrentCulture, "Member:{0}Name:{1}PropertyType:{2}", obj.MemberType, obj.Name, obj.PropertyType).GetHashCode(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/EqualityComparer/Reflection/ParameterInfoComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Reflection; 5 | 6 | namespace EqualityComparer.Reflection 7 | { 8 | /// This is a *very* rudimentary comparison routine that examines two ParameterInfo definitions for signature compatibility. 9 | /// ebrown, 2/3/2011. 10 | public class ParameterInfoComparer : EqualityComparer 11 | { 12 | private static Lazy _default = new Lazy(() => new ParameterInfoComparer()); 13 | 14 | /// Gets the default ParameterInfoComparer instance, rather than continually constructing new instances. 15 | /// The default. 16 | public static new ParameterInfoComparer Default { get { return _default.Value; } } 17 | 18 | /// 19 | /// Tests if two ParameterInfo objects are considered equal 20 | /// -- same DefaultValue, IsIn, IsOptional, IsRetval, ParameterType, Position. 21 | /// 22 | /// ebrown, 2/3/2011. 23 | /// ParameterInfo instance to be compared. 24 | /// ParameterInfo instance to be compared. 25 | /// true if the objects are considered equal, false if they are not. 26 | public override bool Equals(ParameterInfo x, ParameterInfo y) 27 | { 28 | if (x == y) { return true; } 29 | if ((x == null) || (y == null)) { return false; } 30 | 31 | return (x.Name == y.Name && 32 | x.DefaultValue == y.DefaultValue && 33 | x.IsIn == y.IsIn && 34 | x.IsOptional == y.IsOptional && 35 | x.IsOut == y.IsOut && 36 | x.IsRetval == y.IsRetval && 37 | x.ParameterType == y.ParameterType && 38 | x.Position == y.Position); 39 | } 40 | 41 | /// Calculates the hash code for this object. 42 | /// ebrown, 2/3/2011. 43 | /// The object. 44 | /// The hash code for this object. 45 | public override int GetHashCode(ParameterInfo obj) 46 | { 47 | if (null == obj) { return 0; } 48 | return (String.Format(CultureInfo.CurrentCulture, "{0}{1}{2}{3}", obj.Name, obj.DefaultValue, obj.ParameterType, obj.Position)).GetHashCode(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/EqualityComparer/Reflection/FieldInfoComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Reflection; 5 | 6 | namespace EqualityComparer.Reflection 7 | { 8 | /// A rudimentary FieldInfo comparer that checks for equivalency. 9 | /// ebrown, 2/3/2011. 10 | public class FieldInfoComparer : EqualityComparer 11 | { 12 | private static Lazy _default = new Lazy(() => new FieldInfoComparer()); 13 | 14 | /// Gets the default FieldInfoComparer instance, rather than continually constructing new instances. 15 | /// The default. 16 | public static new FieldInfoComparer Default { get { return _default.Value; } } 17 | 18 | /// 19 | /// Tests if two FieldInfo objects are considered equal by our definition -- same Name, FieldType, InitOnly, Literal, Pinvoke, Private / 20 | /// Public, NotSerialized, Static, Attributes. 21 | /// 22 | /// ebrown, 2/3/2011. 23 | /// Field information to be compared. 24 | /// Field information to be compared. 25 | /// true if the objects are considered equal, false if they are not. 26 | public override bool Equals(FieldInfo x, FieldInfo y) 27 | { 28 | if (x == y) { return true; } 29 | if ((x == null) || (y == null)) { return false; } 30 | 31 | return (x.Name == y.Name && 32 | x.FieldType == y.FieldType && 33 | x.IsInitOnly == y.IsInitOnly && 34 | x.IsLiteral == y.IsLiteral && 35 | x.IsPinvokeImpl == y.IsPinvokeImpl && 36 | x.IsPrivate == y.IsPrivate && 37 | x.IsPublic == y.IsPublic && 38 | x.IsNotSerialized == y.IsNotSerialized && 39 | x.IsStatic == y.IsStatic && 40 | x.Attributes == y.Attributes); 41 | } 42 | 43 | /// Calculates the hash code for this object. 44 | /// ebrown, 2/3/2011. 45 | /// The object. 46 | /// The hash code for this object. 47 | public override int GetHashCode(FieldInfo obj) 48 | { 49 | if (null == obj) { return 0; } 50 | return string.Format(CultureInfo.CurrentCulture, "Member:{0}Name:{1}FieldType:{2}", obj.MemberType, obj.Name, obj.FieldType).GetHashCode(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/EqualityComparer/DateComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace EqualityComparer 6 | { 7 | /// An EqualityComparer implementation for DateTimes that can use a configurable comparison algorithm, 8 | /// such as comparing exactly and comparing to the second. 9 | /// ebrown, 6/18/2011. 10 | public class DateComparer : EqualityComparer 11 | { 12 | private DateComparisonType comparisonType; 13 | private static EqualityComparer _default = 14 | new DateComparer(DateComparisonType.Exact); 15 | 16 | /// Gets the default DateComparer instance, which is by DateComparisonType.Exact. 17 | /// The default. 18 | public static new EqualityComparer Default 19 | { 20 | get { return _default; } 21 | } 22 | 23 | /// Initializes a new instance of the DateComparer class. 24 | /// ebrown, 6/18/2011. 25 | /// The method by which dates should be compared. 26 | public DateComparer(DateComparisonType dateComparisonType) 27 | { 28 | this.comparisonType = dateComparisonType; 29 | } 30 | 31 | /// Tests if two DateTime objects are considered equal, given the DateComparisonType. 32 | /// ebrown, 6/18/2011. 33 | /// Date time to be compared. 34 | /// Date time to be compared. 35 | /// true if the objects are considered equal, false if they are not. 36 | public override bool Equals(DateTime x, DateTime y) 37 | { 38 | switch (comparisonType) 39 | { 40 | case DateComparisonType.TruncatedToSecond: 41 | return x.TruncateToSecond().Equals(y.TruncateToSecond()); 42 | case DateComparisonType.Exact: 43 | default: 44 | return x.Equals(y); 45 | } 46 | } 47 | 48 | /// Calculates the hash code for this object. 49 | /// ebrown, 6/18/2011. 50 | /// Date/Time of the object. 51 | /// The hash code for this object. 52 | public override int GetHashCode(DateTime obj) 53 | { 54 | switch (comparisonType) 55 | { 56 | case DateComparisonType.TruncatedToSecond: 57 | return obj.TruncateToSecond().GetHashCode(); 58 | case DateComparisonType.Exact: 59 | default: 60 | return obj.GetHashCode(); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/DateTimeExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Xunit; 4 | using Xunit.Extensions; 5 | 6 | namespace EqualityComparer.Tests 7 | { 8 | public class DateTimeExtensionsTest 9 | { 10 | private static DateTime wellKnownDate = new DateTime(2011, 6, 20, 13, 30, 1, 200); 11 | 12 | public static IEnumerable GetTruncatedExpectations 13 | { 14 | get 15 | { 16 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 499), new DateTime(2011, 6, 20, 13, 30, 1, 0) }; 17 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 499).AddTicks(TimeSpan.TicksPerSecond / 2), new DateTime(2011, 6, 20, 13, 30, 1, 0) }; 18 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 500), new DateTime(2011, 6, 20, 13, 30, 1, 0) }; 19 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 999), new DateTime(2011, 6, 20, 13, 30, 1, 0) }; 20 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 1), new DateTime(2011, 6, 20, 13, 30, 1, 0) }; 21 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 2, 1), new DateTime(2011, 6, 20, 13, 30, 2, 0) }; 22 | } 23 | } 24 | 25 | public static IEnumerable GetRoundedExpectations 26 | { 27 | get 28 | { 29 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 499), new DateTime(2011, 6, 20, 13, 30, 1, 0) }; 30 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 499).AddTicks(TimeSpan.TicksPerSecond / 2), new DateTime(2011, 6, 20, 13, 30, 2, 0) }; 31 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 500), new DateTime(2011, 6, 20, 13, 30, 2, 0) }; 32 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 999), new DateTime(2011, 6, 20, 13, 30, 2, 0) }; 33 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 1, 1), new DateTime(2011, 6, 20, 13, 30, 1, 0) }; 34 | yield return new object[] { new DateTime(2011, 6, 20, 13, 30, 2, 1), new DateTime(2011, 6, 20, 13, 30, 2, 0) }; 35 | } 36 | } 37 | 38 | [Theory] 39 | [PropertyData("GetTruncatedExpectations")] 40 | public void TruncateToSecond_ReturnsExpected(DateTime input, DateTime expected) 41 | { 42 | Assert.Equal(input.TruncateToSecond(), expected.TruncateToSecond()); 43 | } 44 | 45 | [Theory] 46 | [PropertyData("GetRoundedExpectations")] 47 | public void RoundToNearestSecond_ReturnsExpected(DateTime input, DateTime expected) 48 | { 49 | Assert.Equal(input.RoundToNearestSecond(), expected.RoundToNearestSecond()); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/EqualityComparer/Reflection/MethodInfoComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace EqualityComparer.Reflection 8 | { 9 | /// This is a *very* rudimentary comparison routine that examines two MethodInfo definitions for signature compatibility. 10 | /// ebrown, 2/3/2011. 11 | public class MethodInfoComparer : EqualityComparer 12 | { 13 | private static Lazy _default = new Lazy(() => new MethodInfoComparer()); 14 | 15 | /// Gets the default MethodInfoComparer instance, rather than continually constructing new instances. 16 | /// The default. 17 | public static new MethodInfoComparer Default { get { return _default.Value; } } 18 | 19 | /// Tests if two MethodInfo objects are considered equal. 20 | /// ebrown, 2/3/2011. 21 | /// MethodInfo instance to be compared. 22 | /// MethodInfo instance to be compared. 23 | /// true if the objects are considered equal, false if they are not. 24 | public override bool Equals(MethodInfo x, MethodInfo y) 25 | { 26 | if (x == y) { return true; } 27 | if ((x == null) || (y == null)) { return false; } 28 | 29 | if (x.Name != y.Name) 30 | { 31 | return false; 32 | } 33 | 34 | Type xReturnType = x.ReturnType, 35 | yReturnType = y.ReturnType; 36 | 37 | //comparing ReturnType doesn't work on generic methods -- so we have to do things a little different 38 | if (x.IsGenericMethod && y.IsGenericMethod) 39 | { 40 | if (xReturnType.IsGenericType && yReturnType.IsGenericType) 41 | return (xReturnType.GetGenericTypeDefinition() == yReturnType.GetGenericTypeDefinition()) 42 | && x.GetParameters().SequenceEqual(y.GetParameters(), ParameterInfoComparer.Default); 43 | 44 | //match type names 45 | if (xReturnType.IsGenericParameter && yReturnType.IsGenericParameter) 46 | return (xReturnType.Name == yReturnType.Name 47 | && ((!x.GetParameters().Any() && !y.GetParameters().Any()) 48 | || x.GetParameters().SequenceEqual(y.GetParameters(), ParameterInfoComparer.Default))); 49 | } 50 | 51 | //return types match and there are 0 params or param list sequences match 52 | return (xReturnType == yReturnType 53 | && ((!x.GetParameters().Any() && !y.GetParameters().Any()) 54 | || x.GetParameters().SequenceEqual(y.GetParameters(), ParameterInfoComparer.Default))); 55 | } 56 | 57 | /// Calculates the hash code for this object. 58 | /// ebrown, 2/3/2011. 59 | /// The object. 60 | /// The hash code for this object. 61 | public override int GetHashCode(MethodInfo obj) 62 | { 63 | if (null == obj) { return 0; } 64 | return string.Format(CultureInfo.CurrentCulture, "{0}{1}{2}", obj.MemberType, obj.Name, obj.ReturnType).GetHashCode(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/EqualityComparer/Reflection/ConstructorInfoComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace EqualityComparer.Reflection 8 | { 9 | /// This is a *very* rudimentary comparison routine that examines two ConstructorInfo definitions for signature compatibility. 10 | /// ebrown, 2/3/2011. 11 | public class ConstructorInfoComparer : EqualityComparer 12 | { 13 | private static Lazy _default = new Lazy(() => new ConstructorInfoComparer()); 14 | 15 | /// Gets the default ConstructorInfoComparer instance, rather than continually constructing new instances. 16 | /// The default. 17 | public static new ConstructorInfoComparer Default { get { return _default.Value; } } 18 | 19 | /// 20 | /// Tests if two ConstructorInfo objects are considered equal by our definition 21 | /// -- same Name, CallingConvention, Abstract, Final, Private, Public, Static, Virtual, Attributes and matching parameters order as defined by 22 | /// ParameterInfoComparer. 23 | /// 24 | /// ebrown, 2/3/2011. 25 | /// ConstructorInfo to be compared. 26 | /// ConstructorInfo to be compared. 27 | /// true if the objects are considered equal, false if they are not. 28 | public override bool Equals(ConstructorInfo x, ConstructorInfo y) 29 | { 30 | if (x == y) { return true; } 31 | if ((x == null) || (y == null)) { return false; } 32 | 33 | var basicRules = (x.Name == y.Name && 34 | x.CallingConvention == y.CallingConvention && 35 | x.IsAbstract == y.IsAbstract && 36 | x.IsFinal == y.IsFinal && 37 | x.IsPrivate == y.IsPrivate && 38 | x.IsPublic == y.IsPublic && 39 | x.IsStatic == y.IsStatic && 40 | x.IsVirtual == y.IsVirtual && 41 | x.Attributes == y.Attributes); 42 | 43 | if (!basicRules) { return false; } 44 | 45 | //generic constructors do things a little different 46 | if (x.IsGenericMethod && y.IsGenericMethod) 47 | { 48 | var xArgs = x.GetGenericArguments(); 49 | var yArgs = y.GetGenericArguments(); 50 | 51 | bool genericsMatch = ((!xArgs.Any() && !yArgs.Any()) || xArgs.SequenceEqual(yArgs)); 52 | if (!genericsMatch) { return false; } 53 | } 54 | 55 | ParameterInfo[] xParameters = x.GetParameters(), 56 | yParameters = y.GetParameters(); 57 | 58 | //return types match and there are 0 params or param list sequences match 59 | return ((!xParameters.Any() && !yParameters.Any()) 60 | || xParameters.SequenceEqual(yParameters, ParameterInfoComparer.Default)); 61 | } 62 | 63 | /// Calculates the hash code for this object. 64 | /// ebrown, 2/3/2011. 65 | /// The object. 66 | /// The hash code for this object. 67 | public override int GetHashCode(ConstructorInfo obj) 68 | { 69 | if (null == obj) { return 0; } 70 | return string.Format(CultureInfo.CurrentCulture, "{0}{1}", obj.MemberType, obj.Name).GetHashCode(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/GenericEqualityComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using Xunit; 5 | 6 | namespace EqualityComparer.Tests 7 | { 8 | public class GenericEqualityComparerTest 9 | { 10 | class A 11 | { 12 | public A(int integer, string @string) 13 | { 14 | Integer = integer; 15 | String = @string; 16 | } 17 | 18 | public int Integer { get; set; } 19 | public string String { get; set; } 20 | 21 | public static GenericEqualityComparer IntegerOnlyComparer = new GenericEqualityComparer((a1, a2) => a1.Integer == a2.Integer); 22 | public static GenericEqualityComparer AllPropertiesComparer = new GenericEqualityComparer((a1, a2) => a1.Integer == a2.Integer && a1.String == a2.String); 23 | } 24 | 25 | class B 26 | { 27 | public B(int integer, A a) 28 | { 29 | Integer = integer; 30 | A = @a; 31 | } 32 | 33 | public int Integer { get; set; } 34 | public A A { get; set; } 35 | } 36 | 37 | [Fact] 38 | [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Justification = "We're testing the constructor!")] 39 | public void Constructor_ThrowsOnNullFunc() 40 | { 41 | Assert.Throws(() => { var comparer = new GenericEqualityComparer(null); }); 42 | } 43 | 44 | [Fact] 45 | [SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Justification = "We're testing the constructor!")] 46 | public void Constructor_ThrowsOnNullHashGenerator() 47 | { 48 | Assert.Throws(() => { var comparer = new GenericEqualityComparer((a, b) => a.Integer == b.Integer, null); }); 49 | } 50 | 51 | [Fact] 52 | public void Compare_TrueOnMatchingObjectsWithSpecifiedPropertiesOnly() 53 | { 54 | A a = new A(1, "foo"), 55 | b = new A(1, "bar"); 56 | Assert.Equal(a, b, A.IntegerOnlyComparer); 57 | } 58 | 59 | [Fact] 60 | public void Compare_TrueOnMatchingObjectsWithMultipleProperties() 61 | { 62 | A a = new A(1, "string"), 63 | b = new A(a.Integer, a.String); 64 | Assert.Equal(a, b, A.AllPropertiesComparer); 65 | } 66 | 67 | [Fact] 68 | public void Compare_FalseOnMismatchedObjects() 69 | { 70 | A a = new A(1, string.Empty), 71 | b = new A(2, string.Empty); 72 | Assert.NotEqual(a, b, A.IntegerOnlyComparer); 73 | } 74 | 75 | [Fact] 76 | public void ByAllProperties_TrueOnMatchedObjectInstances() 77 | { 78 | A a = new A(3, "Foo"), 79 | b = new A(3, "Foo"); 80 | 81 | Assert.Equal(a, b, GenericEqualityComparer.ByAllMembers()); 82 | } 83 | 84 | [Fact] 85 | public void ByAllProperties_FalseOnUnMatchedObjectInstances() 86 | { 87 | A a = new A(5, "Foo"), 88 | b = new A(3, "Bar"); 89 | 90 | Assert.NotEqual(a, b, GenericEqualityComparer.ByAllMembers()); 91 | } 92 | 93 | [Fact] 94 | public void ByAllProperties_TrueOnMatchedObjectInstancesWithCustomComparer() 95 | { 96 | B b = new B(6, new A(5, "Foo")), 97 | b2 = new B(6, new A(5, "Bar")); 98 | 99 | Assert.Equal(b, b2, GenericEqualityComparer.ByAllMembers(new[] { A.IntegerOnlyComparer })); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/Reflection/ParameterInfoComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace EqualityComparer.Reflection.Tests 7 | { 8 | public class ParameterInfoComparerTest 9 | { 10 | class A 11 | { 12 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 13 | public void Test(int input, string input2) { } 14 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 15 | public void TestCopy(int input, string input2) { } 16 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 17 | public void TestOverload(string input, string input2) { } 18 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 19 | public void TestRearranged(string input, int input2) { } 20 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 21 | public void TestWithOptional(int input, string input2 = "optional") { } 22 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 23 | public void TestWithRef(int input, ref string input2) { } 24 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 25 | public void TestWithOut(int input, out string input2) { input2 = "out"; } 26 | } 27 | 28 | [Fact] 29 | public void Equals_True_OnIdenticalParameter() 30 | { 31 | Assert.True(ParameterInfoComparer.Default.Equals(typeof(A).GetMethod("Test").GetParameters()[0], 32 | typeof(A).GetMethod("Test").GetParameters()[0])); 33 | } 34 | 35 | [Fact] 36 | public void Equals_True_OnIdenticallyTypedParameters() 37 | { 38 | Assert.True(typeof(A).GetMethod("Test").GetParameters() 39 | .SequenceEqual(typeof(A).GetMethod("TestCopy").GetParameters(), ParameterInfoComparer.Default)); 40 | } 41 | 42 | [Fact] 43 | public void Equals_False_OnSameParameterNamesAndOrderButDifferentTypes() 44 | { 45 | Assert.False(typeof(A).GetMethod("Test").GetParameters() 46 | .SequenceEqual(typeof(A).GetMethod("TestOverload").GetParameters(), ParameterInfoComparer.Default)); 47 | } 48 | 49 | [Fact] 50 | public void Equals_False_OnReorderedParameters() 51 | { 52 | Assert.False(typeof(A).GetMethod("Test").GetParameters() 53 | .SequenceEqual(typeof(A).GetMethod("TestRearranged").GetParameters(), ParameterInfoComparer.Default)); 54 | } 55 | 56 | [Fact] 57 | public void Equals_False_OnParametersDifferingByOptional() 58 | { 59 | Assert.False(typeof(A).GetMethod("Test").GetParameters() 60 | .SequenceEqual(typeof(A).GetMethod("TestWithOptional").GetParameters(), ParameterInfoComparer.Default)); 61 | } 62 | 63 | [Fact] 64 | public void Equals_False_OnParametersDifferingByRef() 65 | { 66 | Assert.False(typeof(A).GetMethod("Test").GetParameters() 67 | .SequenceEqual(typeof(A).GetMethod("TestWithRef").GetParameters(), ParameterInfoComparer.Default)); 68 | } 69 | 70 | [Fact] 71 | public void Equals_False_OnParametersDifferingByOut() 72 | { 73 | Assert.False(typeof(A).GetMethod("Test").GetParameters() 74 | .SequenceEqual(typeof(A).GetMethod("TestWithOut").GetParameters(), ParameterInfoComparer.Default)); 75 | } 76 | 77 | [Fact] 78 | public void Equals_False_OnNullFirstParameter() 79 | { 80 | Assert.False(ParameterInfoComparer.Default.Equals(null, typeof(A).GetMethod("Test").GetParameters()[0])); 81 | } 82 | 83 | [Fact] 84 | public void Equals_False_OnNullSecondParameter() 85 | { 86 | Assert.False(ParameterInfoComparer.Default.Equals(typeof(A).GetMethod("Test").GetParameters()[0]), null); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/EqualityComparer/EqualityComparer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {10D51FFF-4F91-4917-82EA-B7426B2652F6} 5 | Library 6 | EqualityComparer 7 | EqualityComparer 8 | v4.0 9 | 512 10 | Client 11 | ..\..\src\ 12 | 8.0.30703 13 | 2.0 14 | Debug 15 | AnyCPU 16 | 4 17 | prompt 18 | TRACE;CODE_ANALYSIS 19 | Properties 20 | 24 | EPS 25 | true 26 | 27 | 28 | bin\Debug 29 | $(DefineConstants);DEBUG 30 | true 31 | full 32 | false 33 | 34 | 35 | bin\Release 36 | $(OutputPath)\$(AssemblyName).XML 37 | pdbonly 38 | true 39 | 40 | 41 | bin\Analysis 42 | $(DefineConstants);DEBUG 43 | true 44 | full 45 | true 46 | $(BuildingInsideVisualStudio) 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 80 | 82 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/Reflection/MethodInfoComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using Xunit; 4 | 5 | namespace EqualityComparer.Reflection.Tests 6 | { 7 | public class MethodInfoComparerTest 8 | { 9 | class A 10 | { 11 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 12 | public void Test(int input, string input2) { } 13 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 14 | public void TestCopy(int input, string input2) { } 15 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 16 | public void TestOverload(string input, string input2) { } 17 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 18 | public void TestRearranged(string input, int input2) { } 19 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 20 | public void TestWithOptional(int input, string input2 = "optional") { } 21 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 22 | public void TestWithRef(int input, ref string input2) { } 23 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 24 | public void TestWithOut(int input, out string input2) { input2 = "out"; } 25 | } 26 | 27 | class B 28 | { 29 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 30 | public void Test(int input, string input2) { } 31 | } 32 | 33 | class C : A 34 | { 35 | [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Test Code")] 36 | public void TestGeneric(T input) { } 37 | } 38 | 39 | [Fact] 40 | public void Equals_True_OnIdenticalMethod() 41 | { 42 | Assert.True(MethodInfoComparer.Default.Equals(typeof(A).GetMethod("Test"), 43 | typeof(A).GetMethod("Test"))); 44 | } 45 | 46 | [Fact] 47 | public void Equals_True_OnIdenticalMethodSignaturesOnDifferentTypes() 48 | { 49 | Assert.True(MethodInfoComparer.Default.Equals(typeof(A).GetMethod("Test"), 50 | typeof(B).GetMethod("Test"))); 51 | } 52 | 53 | [Fact] 54 | public void Equals_False_OnIdenticalMethodSignaturesWithDifferentNames() 55 | { 56 | Assert.False(MethodInfoComparer.Default.Equals(typeof(A).GetMethod("Test"), 57 | typeof(A).GetMethod("TestCopy"))); 58 | } 59 | 60 | [Fact] 61 | public void Equals_False_OnSameParameterNamesAndOrderButDifferentTypes() 62 | { 63 | Assert.False(MethodInfoComparer.Default.Equals(typeof(A).GetMethod("Test"), 64 | typeof(A).GetMethod("TestOverload"))); 65 | } 66 | 67 | [Fact] 68 | public void Equals_False_OnReorderedParameters() 69 | { 70 | Assert.False(MethodInfoComparer.Default.Equals(typeof(A).GetMethod("Test"), 71 | typeof(A).GetMethod("TestRearranged"))); 72 | } 73 | 74 | [Fact] 75 | public void Equals_False_OnParametersDifferingByOptional() 76 | { 77 | Assert.False(MethodInfoComparer.Default.Equals(typeof(A).GetMethod("Test"), 78 | typeof(A).GetMethod("TestWithOptional"))); 79 | } 80 | 81 | [Fact] 82 | public void Equals_False_OnParametersDifferingByRef() 83 | { 84 | Assert.False(MethodInfoComparer.Default.Equals(typeof(A).GetMethod("Test"), 85 | typeof(A).GetMethod("TestWithRef"))); 86 | } 87 | 88 | [Fact] 89 | public void Equals_False_OnParametersDifferingByOut() 90 | { 91 | Assert.False(MethodInfoComparer.Default.Equals(typeof(A).GetMethod("Test"), 92 | typeof(A).GetMethod("TestWithOut"))); 93 | } 94 | 95 | [Fact] 96 | public void Equals_False_OnNullFirstParameter() 97 | { 98 | Assert.False(MethodInfoComparer.Default.Equals(null, typeof(A).GetMethod("Test"))); 99 | } 100 | 101 | [Fact] 102 | public void Equals_False_OnNullSecondParameter() 103 | { 104 | Assert.False(MethodInfoComparer.Default.Equals(typeof(A).GetMethod("Test"), null)); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/EqualityComparer/GenericEqualityComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | 6 | namespace EqualityComparer 7 | { 8 | /// A generic comparer that takes accepts a Func{T, T, bool} to create simple on-the-fly comparison routines. 9 | /// ebrown, 2/7/2011. 10 | public class GenericEqualityComparer : EqualityComparer 11 | { 12 | private readonly Func _comparer; 13 | private readonly Func _hasher; 14 | 15 | /// Constructor accepting the comparison function. 16 | /// 17 | /// Uses a hasher function that always returns the default GetHashCode implementation for given instances. Don't use this for any 18 | /// sorting operations. 19 | /// 20 | /// The comparison function to use when comparing the two instances. 21 | /// 22 | /// Thrown when the comparer or hashers are null. 23 | public GenericEqualityComparer(Func comparer) : 24 | this(comparer, o => o.GetHashCode()) 25 | { } 26 | 27 | /// Constructor accepting the comparison function and hashing function. 28 | /// ebrown, 2/7/2011. 29 | /// Thrown when the comparer or hashers are null. 30 | /// The comparison function to use when comparing the two instances. 31 | /// The hash function used to generate object hashes on the instances. 32 | public GenericEqualityComparer(Func comparer, Func hasher) 33 | { 34 | if (comparer == null) 35 | throw new ArgumentNullException("comparer"); 36 | if (hasher == null) 37 | throw new ArgumentNullException("hasher"); 38 | 39 | this._comparer = comparer; 40 | this._hasher = hasher; 41 | } 42 | 43 | /// Tests if two T objects are considered equal. 44 | /// Uses the passed in Func{T, T, bool} for the comparison. 45 | /// T to be compared. 46 | /// T to be compared. 47 | /// true if the objects are considered equal, false if they are not. 48 | public override bool Equals(T x, T y) 49 | { 50 | return this._comparer(x, y); 51 | } 52 | 53 | /// Calculates the hash code for this object. 54 | /// 55 | /// If no hasher function was supplied, will always return the default GetHashCode implementation. Otherwise uses Func{T, int} hasher 56 | /// function supplied. 57 | /// 58 | /// The object. 59 | /// The hash code for this object. 60 | public override int GetHashCode(T obj) 61 | { 62 | return this._hasher(obj); 63 | } 64 | 65 | /// 66 | /// Shortcut method to get a simple generic IEqualityComparer{T} where the comparison is by all properties and fields on the instance. 67 | /// 68 | /// ebrown, 6/6/2011. 69 | /// A GenericEqualityComparer{T}. 70 | [SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "A static factory method is perfectly acceptable in this context")] 71 | public static GenericEqualityComparer ByAllMembers() 72 | { 73 | return new GenericEqualityComparer((x, y) => MemberComparer.Equal(x, y)); 74 | } 75 | 76 | /// 77 | /// Shortcut method to get a simple generic IEqualityComparer{T} where the comparison is by all properties and fields on the instance, 78 | /// with user defined overrides available on specific encountered types. 79 | /// 80 | /// ebrown, 6/6/2011. 81 | /// A set of additional comparers to use to override default member by member comparison. 82 | /// A GenericEqualityComparer{T}. 83 | [SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "A static factory method is perfectly acceptable in this context")] 84 | public static GenericEqualityComparer ByAllMembers(IEnumerable customComparers) 85 | { 86 | return new GenericEqualityComparer((x, y) => MemberComparer.Equal(x, y, customComparers)); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/EqualityComparer/Reflection/MemberInfoComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace EqualityComparer.Reflection 8 | { 9 | /// A rudimentary comparison routine that Member information comparer. 10 | /// ebrown, 2/3/2011. 11 | public class MemberInfoComparer : EqualityComparer 12 | { 13 | private static Lazy _default = new Lazy(() => new MemberInfoComparer()); 14 | private static Lazy _ignoreCustom = new Lazy(() => new MemberInfoComparer(MemberTypes.Custom, MemberTypes.NestedType, MemberTypes.TypeInfo)); 15 | 16 | /// Gets the default MemberInfoComparer instance, rather than continually constructing new instances. 17 | /// The default. 18 | public static new MemberInfoComparer Default { get { return _default.Value; } } 19 | 20 | /// 21 | /// Gets the MemberInfoComparer instance that will ignore MemberTypes.Custom, MemberTypes.NestedType and MemberTypes.TypeInfo, rather 22 | /// than continually constructing new instances. 23 | /// 24 | /// A MemberInfoComparer following the specified rules. 25 | public static MemberInfoComparer IgnoreNestedTypes { get { return _ignoreCustom.Value; } } 26 | 27 | private MemberTypes[] _memberTypeIgnores; 28 | 29 | /// 30 | /// Initializes a new instance of the MemberInfoComparer class. 31 | /// 32 | public MemberInfoComparer() 33 | { } 34 | 35 | /// Initializes a new instance of the MemberInfoComparer class. 36 | /// ebrown, 2/3/2011. 37 | /// A variable-length parameters list containing MemberTypes to ignores. 38 | public MemberInfoComparer(params MemberTypes[] ignores) 39 | { 40 | if (null == ignores) { throw new ArgumentNullException("ignores"); } 41 | 42 | this._memberTypeIgnores = ignores; 43 | if (null != ignores) 44 | { 45 | if (ignores.Contains(MemberTypes.All)) 46 | { 47 | throw new ArgumentException("MemberTypes.All makes no sense within this context", "ignores"); 48 | } 49 | } 50 | } 51 | 52 | /// Tests if two MemberInfo objects are considered equal. 53 | /// ebrown, 2/3/2011. 54 | /// MemberInfo instance to be compared. 55 | /// MemberInfo instance to be compared. 56 | /// true if the objects are considered equal, false if they are not. 57 | public override bool Equals(MemberInfo x, MemberInfo y) 58 | { 59 | if (x == y) { return true; } 60 | if ((x == null) || (y == null)) { return false; } 61 | MemberTypes xMemberType = x.MemberType, 62 | yMemberType = y.MemberType; 63 | if (xMemberType != yMemberType) { return false; } 64 | 65 | //check against our ignore list -- return true if we're ignoring these types 66 | if (null != this._memberTypeIgnores && this._memberTypeIgnores.Contains(xMemberType)) { return true; } 67 | 68 | if (x.Name != y.Name) { return false; } 69 | 70 | switch (xMemberType) 71 | { 72 | case MemberTypes.Constructor: 73 | return ConstructorInfoComparer.Default.Equals((ConstructorInfo)x, (ConstructorInfo)y); 74 | case MemberTypes.Event: 75 | return EventInfoComparer.Default.Equals((EventInfo)x, (EventInfo)y); 76 | case MemberTypes.Field: 77 | return FieldInfoComparer.Default.Equals((FieldInfo)x, (FieldInfo)y); 78 | case MemberTypes.Method: 79 | return MethodInfoComparer.Default.Equals((MethodInfo)x, (MethodInfo)y); 80 | case MemberTypes.Property: 81 | return PropertyInfoComparer.Default.Equals((PropertyInfo)x, (PropertyInfo)y); 82 | 83 | //compare the NestedTypes for compatibility based on their Members (effectively making this a recursive call) 84 | case MemberTypes.NestedType: 85 | var xMembers = ((Type)x).GetMembers(); 86 | var yMembers = ((Type)y).GetMembers(); 87 | 88 | //empty list of members 89 | return ((!xMembers.Any() && !yMembers.Any()) 90 | //or the same list of members (using the same criteria that we're currently using) 91 | || (!xMembers.Except(yMembers, this).Any() && !yMembers.Except(xMembers, this).Any())); 92 | 93 | default: 94 | case MemberTypes.Custom: 95 | case MemberTypes.TypeInfo: 96 | return false; 97 | } 98 | } 99 | 100 | /// Calculates the hash code for this object. 101 | /// ebrown, 2/3/2011. 102 | /// The object. 103 | /// The hash code for this object. 104 | public override int GetHashCode(MemberInfo obj) 105 | { 106 | if (null == obj) { return 0; } 107 | return string.Format(CultureInfo.CurrentCulture, "{0}{1}", obj.MemberType, obj.Name).GetHashCode(); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://github.com/EastPoint/EqualityComparer/raw/master/logo-128.png) 2 | 3 | # EqualityComparer 4 | A super basic way of comparing object instances on a property by property / member by member basis. There are allowances for overriding default comparison behaviors, and it's mostly implemented using Expressions. 5 | 6 | The code was heavily inspired / derived from Marc Gravells original [post on StackOverflow](http://stackoverflow.com/questions/986572/hows-to-quick-check-if-data-transfer-two-objects-have-equal-properties-in-c) discussing a basic comparison method for DTOs. Like Marcs original post, this code also uses some caching of built Expression trees once an object type has been 'seen'. 7 | 8 | This variant is a little more general purpose with a few more features, and is designed primarily around testing semantics. 9 | 10 | ## Installation 11 | 12 | * Install-Package EqualityComparer 13 | 14 | ### Requirements 15 | 16 | * .NET Framework 4+ Client Profile 17 | 18 | ### Usage Examples 19 | 20 | There are plenty of examples in the Tests project. Look there for more details. 21 | 22 | Should work properly with: 23 | 24 | * Nested types 25 | * Anonymous types 26 | * Custom hand-rolled comparers (derived from IEqualityComparer or using the included Func based GenericEqualityComparer) 27 | * Fuzzy date comparison (i.e. where Redis stores an inexact date value vs what .NET has) 28 | 29 | ### Basic Comparison 30 | ```csharp 31 | Guid sharedGuid = Guid.NewGuid(); 32 | DateTime now = DateTime.Now; 33 | Assert.True(MemberComparer.Equal(new { PropertyA = "A", Integer = 23, Guid = sharedGuid, Date = now }, 34 | new { PropertyA = "A", Integer = 23, Guid = sharedGuid, Date = now })); 35 | ``` 36 | 37 | ### Fuzzy date comparisons. 38 | 39 | ```csharp 40 | DateTime one = DateTime.Parse("07:27:15.01"), 41 | two = DateTime.Parse("07:27:15.49"); 42 | 43 | var a = new { Foo = 5, Bar = new { Now = one } }; 44 | var b = new { Foo = 5, Bar = new { Now = two } }; 45 | 46 | Assert.True(MemberComparer.Equal(one, two, new[] { new DateComparer(DateComparisonType.TruncatedToSecond) })); 47 | ``` 48 | 49 | ### Custom Comparer 50 | 51 | ```csharp 52 | class ClassWithFieldsAndProperties 53 | { 54 | public string Foo; 55 | public string Bar { get; set; } 56 | } 57 | 58 | string Bar = "bar"; 59 | Assert.True(MemberComparer.Equal(new { Integer = 5, Custom = new ClassWithFieldsAndProperties() { Foo = "456", Bar = Bar } }, 60 | new { Integer = 5, Custom = new ClassWithFieldsAndProperties() { Foo = "4567", Bar = Bar } }, 61 | new[] { new GenericEqualityComparer((a, b) => a.Bar == b.Bar) })); 62 | ``` 63 | 64 | ### GenericEqualityComparer 65 | 66 | In addition to being able to create a new instance of a GenericEqualityComparer with an anonymous Func, the GenericEqualityComparer class has a ```ByAllMembers``` static 67 | which will recursively examine the type and generate a (cached) comparison Expression. 68 | 69 | Given this class: 70 | 71 | ```csharp 72 | class A 73 | { 74 | public A(int integer, string @string) 75 | { 76 | Integer = integer; 77 | String = @string; 78 | } 79 | 80 | public int Integer { get; set; } 81 | public string String { get; set; } 82 | 83 | public static GenericEqualityComparer IntegerOnlyComparer = new GenericEqualityComparer((a1, a2) => a1.Integer == a2.Integer); 84 | } 85 | ``` 86 | 87 | We can use the standard ByAllMembers comparison. 88 | 89 | ```csharp 90 | A a = new A(3, "Foo"), 91 | b = new A(3, "Foo"); 92 | 93 | Assert.Equal(a, b, GenericEqualityComparer.ByAllMembers()); 94 | ``` 95 | 96 | ByAllMembers also has an overload that lets you specify comparers to use when a specific type is encountered. In this example, we tell it to only look at the Integer member of A when A instances are found in the object graph. 97 | 98 | ```csharp 99 | class B 100 | { 101 | public B(int integer, A a) 102 | { 103 | Integer = integer; 104 | A = @a; 105 | } 106 | 107 | public int Integer { get; set; } 108 | public A A { get; set; } 109 | } 110 | 111 | B b = new B(6, new A(5, "Foo")), 112 | b2 = new B(6, new A(5, "Bar")); 113 | 114 | Assert.Equal(b, b2, GenericEqualityComparer.ByAllMembers(new[] { A.IntegerOnlyComparer })); 115 | ``` 116 | 117 | ## Similar Projects 118 | 119 | * [AutoFixture](http://autofixture.codeplex.com/) includes a library called Ploeh.SemanticComparison . I haven't checked out all the details, but it does pack a Fluent interface. 120 | * [AnonymousComparer](http://linqcomparer.codeplex.com/) - the AnonymousComparer looks very similar to the GenericEqualityComparer class in our library, except for some syntactical differences. It doesn't look like there are options to override the behavior of comparisons either. 121 | * [System.DataStructures.FuncComparer](http://adjunct.codeplex.com/) - looks like a basic implementation of a Func IEqualityComparer. 122 | 123 | ## Future Improvements 124 | 125 | * Ensure Mono works properly (it *should* already) 126 | 127 | ## Contributing 128 | 129 | Fork the code, and submit a pull request! 130 | 131 | Any useful changes are welcomed. If you have an idea you'd like to see implemented that strays far from the simple spirit of the application, ping us first so that we're on the same page. 132 | 133 | ## Credits 134 | 135 | Creative Commons icon courtesy [WikiMedia](http://commons.wikimedia.org/wiki/File:Emblem-equal.svg) 136 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/Reflection/TypeExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace EqualityComparer.Reflection.Tests 8 | { 9 | public class TypeExtensionsTest 10 | { 11 | [Fact] 12 | public void IsGenericInterfaceAssignableFrom_FindsGenericInterfaceDefinitionsProperly() 13 | { 14 | List ints = new List(); 15 | Assert.True(typeof(IList<>).IsGenericInterfaceAssignableFrom(ints.GetType())); 16 | } 17 | 18 | [Fact] 19 | public void GetGenericInterfaceTypeParameters_FindsGenericInterfaceDefinitionTypesProperly() 20 | { 21 | List ints = new List(); 22 | var genericTypeParams = typeof(IList<>).GetGenericInterfaceTypeParameters(ints.GetType()).ToList(); 23 | Assert.Equal(1, genericTypeParams.Count); 24 | Assert.Equal(typeof(int), genericTypeParams[0]); 25 | } 26 | 27 | [Fact] 28 | public void IsGenericInterfaceAssignableFrom_ReturnsFalseOnIsAssignableForUnimplementedInterface() 29 | { 30 | Dictionary dictionary = new Dictionary(); 31 | Assert.Equal(false, typeof(IList<>).IsGenericInterfaceAssignableFrom(dictionary.GetType())); 32 | } 33 | 34 | [Fact] 35 | public void GetGenericInterfaceTypeParameters_ThrowsOnUnimplementedInterface() 36 | { 37 | Dictionary dictionary = new Dictionary(); 38 | Assert.Throws(() => typeof(IList<>).GetGenericInterfaceTypeParameters(dictionary.GetType())); 39 | } 40 | 41 | [Fact] 42 | public void IsGenericInterfaceAssignableFrom_ThrowsOnNonGenericInterface() 43 | { 44 | Assert.Throws(() => typeof(ICollection).IsGenericInterfaceAssignableFrom(typeof(int))); 45 | } 46 | 47 | [Fact] 48 | public void GetGenericInterfaceTypeParameters_ThrowsOnNonGenericInterface() 49 | { 50 | Assert.Throws(() => typeof(ICollection).GetGenericInterfaceTypeParameters(typeof(int))); 51 | } 52 | 53 | [Fact] 54 | public void IsGenericInterfaceAssignableFrom_ThrowsOnNullParameterCombinations() 55 | { 56 | Assert.Throws(() => (null as Type).IsGenericInterfaceAssignableFrom(typeof(int))); 57 | Assert.Throws(() => typeof(int).IsGenericInterfaceAssignableFrom(null)); 58 | Assert.Throws(() => (null as Type).IsGenericInterfaceAssignableFrom(null)); 59 | } 60 | 61 | [Fact] 62 | public void GetGenericInterfaceTypeParameters_ThrowsOnNullParameterCombinations() 63 | { 64 | Assert.Throws(() => (null as Type).GetGenericInterfaceTypeParameters(typeof(int))); 65 | Assert.Throws(() => typeof(int).GetGenericInterfaceTypeParameters(null)); 66 | Assert.Throws(() => (null as Type).GetGenericInterfaceTypeParameters(null)); 67 | } 68 | 69 | [Fact] 70 | public void GetAllBaseTypesAndInterfaces_FindsGenericDerivedTypesAndInterfaces() 71 | { 72 | //not the simplest example -- but should find these guys 73 | var interfaces = typeof(Dictionary).GetAllBaseTypesAndInterfaces(); 74 | 75 | //.NET 4 has 10 ifaces / .NET 4.5 adds IReadOnlyDictionary and IReadOnlyCollection> 76 | Assert.True(interfaces.Count >= 10); 77 | Assert.True(interfaces[typeof(Dictionary)] == 0); 78 | Assert.True(interfaces[typeof(IDictionary)] == 1); 79 | Assert.True(interfaces[typeof(ICollection>)] == 1); 80 | Assert.True(interfaces[typeof(IEnumerable>)] == 1); 81 | Assert.True(interfaces[typeof(IEnumerable)] == 1); 82 | Assert.True(interfaces[typeof(IDictionary)] == 1); 83 | Assert.True(interfaces[typeof(ICollection)] == 1); 84 | Assert.True(interfaces[typeof(System.Runtime.Serialization.ISerializable)] == 1); 85 | Assert.True(interfaces[typeof(System.Runtime.Serialization.IDeserializationCallback)] == 1); 86 | Assert.True(interfaces[typeof(object)] == 2); 87 | } 88 | 89 | [Fact] 90 | public void GetAllBaseTypesAndInterfaces_FindsDerivedInterfaces() 91 | { 92 | var interfaces = typeof(IEnumerable).GetAllBaseTypesAndInterfaces(); 93 | //should find these guys 94 | Assert.Equal(2, interfaces.Count); 95 | Assert.True(interfaces[typeof(IEnumerable)] == 0); 96 | Assert.True(interfaces[typeof(IEnumerable)] == 1); 97 | } 98 | 99 | [Fact] 100 | public void GetAllBaseTypesAndInterfaces_ThrowsOnNullType() 101 | { 102 | Assert.Throws(() => (null as Type).GetAllBaseTypesAndInterfaces()); 103 | } 104 | 105 | interface IMarker { } 106 | interface IMarker2 : IMarker { } 107 | class A : IMarker { } 108 | class B : A { } 109 | class C : B, IMarker2 { } 110 | 111 | [Fact] 112 | public void GetAllBaseTypesAndInterfaces_FindsAllDerivations() 113 | { 114 | var types = typeof(C).GetAllBaseTypesAndInterfaces(); 115 | 116 | Assert.Equal(6, types.Count); 117 | Assert.True(types[typeof(C)] == 0); 118 | Assert.True(types[typeof(IMarker2)] == 1); 119 | Assert.True(types[typeof(IMarker)] == 1); 120 | Assert.True(types[typeof(B)] == 2); 121 | Assert.True(types[typeof(A)] == 3); 122 | Assert.True(types[typeof(object)] == 4); 123 | } 124 | 125 | class D : IMarker, IDisposable 126 | { 127 | protected virtual void Dispose(bool disposing) { } 128 | public void Dispose() { } 129 | } 130 | 131 | [Fact] 132 | public void GetAllBaseTypesAndInterfaces_FindsAllDerivationsAcrossAssemblies() 133 | { 134 | var types = typeof(D).GetAllBaseTypesAndInterfaces(); 135 | 136 | Assert.Equal(4, types.Count); 137 | Assert.True(types[typeof(D)] == 0); 138 | Assert.True(types[typeof(IDisposable)] == 1); 139 | Assert.True(types[typeof(IMarker)] == 1); 140 | Assert.True(types[typeof(object)] == 2); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/EqualityComparer.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {7E944761-20F9-48F3-8644-042AACBFD385} 5 | Library 6 | EqualityComparer.Tests 7 | EqualityComparer.Tests 8 | v4.0 9 | 512 10 | ..\..\src\ 11 | 8.0.30703 12 | 2.0 13 | Debug 14 | AnyCPU 15 | 4 16 | prompt 17 | TRACE;CODE_ANALYSIS 18 | Properties 19 | 23 | EPS.Test 24 | 3016 25 | Client 26 | true 27 | 28 | 29 | bin\Debug 30 | $(DefineConstants);DEBUG 31 | true 32 | full 33 | false 34 | 35 | 36 | bin\Release 37 | pdbonly 38 | true 39 | 40 | 41 | bin\Analysis 42 | $(DefineConstants);DEBUG 43 | true 44 | full 45 | true 46 | $(BuildingInsideVisualStudio) 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | {10D51FFF-4F91-4917-82EA-B7426B2652F6} 77 | EqualityComparer 78 | 79 | 80 | 81 | 82 | ..\packages\Castle.Core.2.5.2\lib\NET35\Castle.Core.dll 83 | True 84 | 85 | 86 | ..\packages\FakeItEasy.1.7.4257.42\lib\NET40\FakeItEasy.dll 87 | True 88 | 89 | 90 | ..\packages\AutoFixture.2.8.0\lib\Ploeh.AutoFixture.dll 91 | True 92 | 93 | 94 | ..\packages\AutoFixture.AutoFakeItEasy.2.8.0\lib\Ploeh.AutoFixture.AutoFakeItEasy.dll 95 | True 96 | 97 | 98 | ..\packages\AutoFixture.Xunit.2.8.0\lib\Ploeh.AutoFixture.Xunit.dll 99 | True 100 | 101 | 102 | ..\packages\AutoFixture.2.8.0\lib\Ploeh.SemanticComparison.dll 103 | True 104 | 105 | 106 | ..\packages\xunit.1.9.0.1566\lib\xunit.dll 107 | True 108 | 109 | 110 | ..\packages\xunit.extensions.1.9.0.1566\lib\xunit.extensions.dll 111 | True 112 | 113 | 114 | ..\packages\xunit.runners.1.9.0.1566\tools\xunit.runner.msbuild.dll 115 | True 116 | 117 | 118 | ..\packages\xunit.runners.1.9.0.1566\tools\xunit.runner.utility.dll 119 | True 120 | 121 | 122 | 123 | 124 | 125 | 128 | 129 | 132 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /src/EqualityComparer/Reflection/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace EqualityComparer.Reflection 9 | { 10 | /// A set of useful extension methods built on top of 11 | /// ebrown, 11/9/2010. 12 | public static class TypeExtensions 13 | { 14 | /// Determines whether a given type implements a specified interface, where the interface *must* be generic. 15 | /// ebrown, 11/9/2010. 16 | /// Thrown when one or more required arguments are null. 17 | /// Thrown when one or more arguments have unsupported or illegal values. 18 | /// The Type of the interface to search for. 19 | /// The concrete object Type to examine. 20 | /// true if the concrete type specified implements the interface type specified; otherwise, false. 21 | public static bool IsGenericInterfaceAssignableFrom(this Type interfaceType, Type concreteType) 22 | { 23 | return GetGenericInterfaces(interfaceType, concreteType).Any(); 24 | } 25 | 26 | /// 27 | /// For a given type implementing a specified interface, where the interface *must* be generic, this returns the parameters passed to the 28 | /// generic interface. 29 | /// 30 | /// ebrown, 11/9/2010. 31 | /// Thrown when one or more required arguments are null. 32 | /// Thrown when one or more arguments have unsupported or illegal values. 33 | /// The Type of the interface to search for. 34 | /// The concrete object Type to examine. 35 | /// An enumeration of the Types being used in the generic interface declaration. 36 | public static IEnumerable GetGenericInterfaceTypeParameters(this Type interfaceType, Type concreteType) 37 | { 38 | var implementedInterface = GetGenericInterfaces(interfaceType, concreteType).FirstOrDefault(); 39 | if (implementedInterface == default(Type)) 40 | { 41 | throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, "Interface {0} not implemented by {1}", interfaceType, concreteType), "concreteType"); 42 | } 43 | 44 | return implementedInterface.GetGenericArguments(); 45 | } 46 | 47 | private static IEnumerable GetGenericInterfaces(Type interfaceType, Type concreteType) 48 | { 49 | if (null == interfaceType) { throw new ArgumentNullException("interfaceType"); } 50 | if (null == concreteType) { throw new ArgumentNullException("concreteType"); } 51 | 52 | if (!interfaceType.IsGenericType || !interfaceType.IsInterface) 53 | { 54 | throw new ArgumentException("interfaceType must be a generic interface such as IInterface"); 55 | } 56 | 57 | return concreteType.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType); 58 | } 59 | /// Determines whether the specified type is anonymous. 60 | /// ebrown, 11/9/2010. 61 | /// Thrown when one or more required arguments are null. 62 | /// The type. 63 | /// true if the specified type is anonymous; otherwise, false. 64 | public static bool IsAnonymous(this Type type) 65 | { 66 | if (type == null) { throw new ArgumentNullException("type"); } 67 | 68 | string typeName = type.Name; 69 | // HACK: The only way to detect anonymous types right now. 70 | return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false) 71 | && type.IsGenericType && typeName.Contains("AnonymousType") 72 | && (typeName.StartsWith("<>", StringComparison.OrdinalIgnoreCase) || typeName.StartsWith("VB$", StringComparison.OrdinalIgnoreCase)) 73 | && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic; 74 | } 75 | 76 | /// Determines whether the specified object is anonymous. 77 | /// ebrown, 11/9/2010. 78 | /// Thrown when one or more required arguments are null. 79 | /// The object to inspect. 80 | /// true if the specified object is based on an anonymous type; otherwise, false. 81 | public static bool IsAnonymous(this T value) 82 | { 83 | if (null == value) { throw new ArgumentNullException("value"); } 84 | 85 | return IsAnonymous(typeof(T)); 86 | } 87 | 88 | 89 | /// A Type extension method that gets all base types and interfaces for a given type by recursing the type hierarchy. 90 | /// 91 | /// Given Type is explored for all derived types and interfaces. Types are returned in the following depth order: 92 | /// - The type itself at depth 0 93 | /// - Implemented interfaces at depth 1 94 | /// - All based types, in order of derivation with an appropriate depth of 2+. 95 | /// 96 | /// Thrown when one or more required arguments are null. 97 | /// The type. 98 | /// all base types and interfaces in order of depth. 99 | public static IDictionary GetAllBaseTypesAndInterfaces(this Type type) 100 | { 101 | if (null == type) { throw new ArgumentNullException("type"); } 102 | 103 | //start with interfaces at depth of 1 104 | var types = type.GetInterfaces().ToDictionary(i => i, i => 1); 105 | //original type at depth of 0 106 | types.Add(type, 0); 107 | 108 | //base types at depth of 2+ up the tree -- if we're object / have no BaseType 109 | RecurseTypeHierarchy(type.BaseType, types, 2); 110 | 111 | return types; 112 | } 113 | 114 | private static void RecurseTypeHierarchy(Type derivedType, IDictionary discoveredTypes, int depth) 115 | { 116 | if (null == derivedType) { return; } 117 | 118 | if (derivedType.BaseType != null) 119 | { 120 | //plow all the way to the bottom -- largest depth further down 121 | RecurseTypeHierarchy(derivedType.BaseType, discoveredTypes, depth + 1); 122 | } 123 | discoveredTypes.Add(derivedType, depth); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | true 8 | 9 | 10 | false 11 | 12 | 13 | false 14 | 15 | 16 | true 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 30 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 31 | 32 | 33 | 34 | 35 | $(SolutionDir).nuget 36 | packages.config 37 | 38 | 39 | 40 | 41 | $(NuGetToolsPath)\nuget.exe 42 | @(PackageSource) 43 | 44 | "$(NuGetExePath)" 45 | mono --runtime=v4.0.30319 $(NuGetExePath) 46 | 47 | $(TargetDir.Trim('\\')) 48 | 49 | -RequireConsent 50 | 51 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -solutionDir "$(SolutionDir) " 52 | $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols 53 | 54 | 55 | 56 | RestorePackages; 57 | $(ResolveReferencesDependsOn); 58 | 59 | 60 | 61 | 62 | $(BuildDependsOn); 63 | BuildPackage; 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /src/EqualityComparer/MemberComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | using EqualityComparer.Reflection; 9 | 10 | namespace EqualityComparer 11 | { 12 | /// A class that performs a public property by property and field by field comparison of two object instances. Useful for testing. 13 | /// http://stackoverflow.com/questions/986572/hows-to-quick-check-if-data-transfer-two-objects-have-equal-properties-in-c 14 | [SuppressMessage("Gendarme.Rules.Smells", "AvoidSpeculativeGeneralityRule", Justification = "This is a class useful to testing and does not represent speculation")] 15 | public static class MemberComparer 16 | { 17 | /// Does a public property by property and field by field comparison of the two objects. 18 | /// ebrown, 1/19/2011. 19 | /// Generic type parameter - inferred by compiler. 20 | /// The first instance. 21 | /// The second instance. 22 | /// true if the objects are equivalent by comparison of properties OR both instances are NULL, false if not. 23 | public static bool Equal(T instanceX, T instanceY) 24 | { 25 | return Equal(instanceX, instanceY, new IEqualityComparer[] { }); 26 | } 27 | 28 | /// Does a public property by property and field by field comparison of the two objects. 29 | /// ebrown, 1/19/2011. 30 | /// Thrown when the list of comparers is null, the comparers are not also instances of IEqualityComparer{} or any of the comparers are null. 31 | /// Thrown when there is more than one comparer for a given type. 32 | /// Generic type parameter - inferred by compiler. 33 | /// The first instance. 34 | /// The second instance. 35 | /// A variable-length parameters list containing custom comparers. 36 | /// true if the objects are equivalent by comparison of properties OR both instances are NULL, false if not. 37 | public static bool Equal(T instanceX, T instanceY, IEnumerable customComparers) 38 | { 39 | if (null == instanceX && null == instanceY) { return true; } 40 | if (null == instanceX || null == instanceY) { return false; } 41 | if (null == customComparers) 42 | { 43 | throw new ArgumentNullException("customComparers"); 44 | } 45 | 46 | if (customComparers.Any(comparer => null == comparer)) 47 | { 48 | throw new ArgumentNullException("customComparers", "List of comparers contains a null IEqualityComparer"); 49 | } 50 | 51 | Type genericEqualityComparer = typeof(IEqualityComparer<>); 52 | if (customComparers.Any(comparer => !genericEqualityComparer.IsGenericInterfaceAssignableFrom(comparer.GetType()))) 53 | { 54 | throw new ArgumentException("All comparer instances must implement IEqualityComparer<>", "customComparers"); 55 | } 56 | 57 | var comparerPairs = customComparers.Select(comparer => 58 | new KeyValuePair(genericEqualityComparer.GetGenericInterfaceTypeParameters(comparer.GetType()).First(), comparer)); 59 | 60 | if (comparerPairs.Select(pair => pair.Key).Distinct().Count() != comparerPairs.Count()) 61 | { 62 | throw new ArgumentException("Only one IEqualityComparer<> instance per Type is allowed in the list"); 63 | } 64 | 65 | var customComparerDictionary = comparerPairs.ToDictionary(pair => pair.Key, pair => pair.Value); 66 | 67 | return Cache.Compare(instanceX, instanceY, customComparerDictionary); 68 | } 69 | 70 | static bool ImplementsItsOwnEqualsMethod(this Type type) 71 | { 72 | var equalsMethod = type.GetMethod("Equals", new Type[] { type }); 73 | return (null != equalsMethod && equalsMethod.DeclaringType == type); 74 | } 75 | 76 | static class Cache 77 | { 78 | //instance, instance, 79 | internal static readonly Func, bool> Compare = delegate { return true; }; 80 | 81 | [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", Justification = "More readable with static constructor / no easy way to write without")] 82 | static Cache() 83 | { 84 | Type t = typeof(T); 85 | 86 | if (typeof(string) == t) 87 | { 88 | Compare = (stringX, stringY, comparers) => comparers.ContainsKey(t) ? 89 | comparers[t].Equals(stringX, stringY) : 90 | StringComparer.Ordinal.Equals(stringX, stringY); 91 | return; 92 | } 93 | //for now, do a ref check, since Exception is a bit of an oddball type 94 | else if (typeof(Exception).IsAssignableFrom(t)) 95 | { 96 | Compare = (exceptionX, exceptionY, comparers) => comparers.ContainsKey(t) ? 97 | comparers[t].Equals(exceptionX, exceptionY) : 98 | object.ReferenceEquals(exceptionX, exceptionY); 99 | return; 100 | } 101 | else if ((t.IsValueType || t.IsPrimitive) && 102 | !(t.IsGenericType && (t.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)))) 103 | { 104 | Compare = (valueX, valueY, comparers) => comparers.ContainsKey(t) ? 105 | comparers[t].Equals(valueX, valueY) : 106 | valueX.Equals(valueY); 107 | return; 108 | } 109 | 110 | var x = Expression.Parameter(t, "x"); 111 | var y = Expression.Parameter(t, "y"); 112 | var customComparers = Expression.Parameter(typeof(IDictionary), "customComparers"); 113 | 114 | if (typeof(IEnumerable<>).IsGenericInterfaceAssignableFrom(t)) 115 | { 116 | Type genericTypeParam = (typeof(IEnumerable<>)).GetGenericInterfaceTypeParameters(t).First(); 117 | //Type genericType = typeof(IEnumerable<>).MakeGenericType(genericTypeParam); 118 | Compare = Expression.Lambda, bool>>(SequencesOfTypeAreEqual(x, y, genericTypeParam, customComparers), x, y, customComparers) 119 | .Compile(); 120 | return; 121 | }; 122 | 123 | var members = t.GetProperties(BindingFlags.Public | BindingFlags.Instance).OfType().Union(t.GetFields(BindingFlags.Public | BindingFlags.Instance)).ToArray(); 124 | if (members.Length == 0) { return; } 125 | 126 | Expression body = CallExpressionIfNoComparer(BuildRecursiveComparison(members, x, y, customComparers, null, null), 127 | customComparers, t, x, y); 128 | Compare = Expression.Lambda, bool>>(body, x, y, customComparers) 129 | .Compile(); 130 | } 131 | private static MethodCallExpression SequencesOfTypeAreEqual(Expression xProperty, Expression yProperty, Type genericTypeParam, Expression comparers) 132 | { 133 | return Expression.Call(typeof(System.Linq.Enumerable), "SequenceEqual", new Type[] { genericTypeParam }, 134 | new Expression[] { xProperty, yProperty, Expression.Call(typeof(GenericEqualityComparer<>).MakeGenericType(genericTypeParam), "ByAllMembers", null, Expression.Property(comparers, "Values")) }); 135 | } 136 | 137 | private static BinaryExpression CallComparerIfAvailable(Expression comparers, Type memberType, Expression xPropertyOrField, Expression yPropertyOrField) 138 | { 139 | //(comparers.ContainsKey(memberType) && ((IEqualityComparer)comparers[memberType]).Equals(x, y)) 140 | return BinaryExpression.AndAlso( 141 | Expression.Call(comparers, "ContainsKey", null, Expression.Constant(memberType, typeof(Type))), 142 | Expression.Call(Expression.TypeAs( 143 | Expression.MakeIndex(comparers, typeof(IDictionary).GetProperty("Item", typeof(IEqualityComparer), new[] { typeof(Type) }), new[] { Expression.Constant(memberType, typeof(Type)) }), 144 | typeof(IEqualityComparer<>).MakeGenericType(memberType)), "Equals", null, xPropertyOrField, yPropertyOrField)); 145 | } 146 | 147 | private static BinaryExpression CallExpressionIfNoComparer(Expression comparison, ParameterExpression comparers, Type memberType, Expression xPropertyOrField, Expression yPropertyOrField) 148 | { 149 | return BinaryExpression.Or( 150 | CallComparerIfAvailable(comparers, memberType, xPropertyOrField, yPropertyOrField), 151 | Expression.AndAlso( 152 | Expression.IsFalse(Expression.Call(comparers, "ContainsKey", null, Expression.Constant(memberType, typeof(Type)))), 153 | comparison)); 154 | } 155 | 156 | private static BinaryExpression CustomPropertyComparison(MemberExpression xPropertyOrField, MemberExpression yPropertyOrField, BinaryExpression parentNullChecks, BinaryExpression recursiveProperties, 157 | Func customCheckToThisLevel) 158 | { 159 | var nullExpression = Expression.Constant(null, typeof(object)); 160 | //x.Property != null && y.Property != null 161 | var propertyNotNullCheck = Expression.AndAlso(Expression.NotEqual(xPropertyOrField, nullExpression), Expression.NotEqual(yPropertyOrField, nullExpression)); 162 | //combine with any parent null checking to this depth -- i.e. (x.Property != null && y.Property != null && x.Property.Property != null && y.Property.Property != null) 163 | var nullChecktoThisDepth = null == parentNullChecks ? propertyNotNullCheck : Expression.AndAlso(parentNullChecks, propertyNotNullCheck); 164 | 165 | //now combine that with a null for OKs -- i.e. x.Property != null && y.Property != null 166 | var propertiesAreBothNull = Expression.AndAlso(Expression.Equal(xPropertyOrField, nullExpression), Expression.Equal(yPropertyOrField, nullExpression)); 167 | var nullAllowedToThisDepth = null == parentNullChecks ? propertiesAreBothNull : Expression.AndAlso(parentNullChecks, propertiesAreBothNull); 168 | 169 | //prefixing with the null check to this point + our custom comparison on this property 170 | var completedExpression = Expression.Or(nullAllowedToThisDepth, Expression.AndAlso(nullChecktoThisDepth, customCheckToThisLevel(nullChecktoThisDepth))); 171 | 172 | //combine the running list of recursive properties for this parent 173 | return recursiveProperties == null ? completedExpression : Expression.AndAlso(recursiveProperties, completedExpression); 174 | } 175 | 176 | private static Expression BuildRecursiveComparison(MemberInfo[] members, Expression x, Expression y, ParameterExpression comparers, BinaryExpression parentNullChecks, Expression body) 177 | { 178 | //nothing to see here -- move on 179 | if (members.Length == 0) 180 | return body; 181 | 182 | BinaryExpression propertyEqualities = null; 183 | BinaryExpression recursiveProperties = null; 184 | 185 | for (int i = 0; i < members.Length; i++) 186 | { 187 | Type memberType = members[i] is PropertyInfo ? ((PropertyInfo)members[i]).PropertyType : ((FieldInfo)members[i]).FieldType; 188 | MemberExpression xPropertyOrField = Expression.PropertyOrField(x, members[i].Name), 189 | yPropertyOrField = Expression.PropertyOrField(y, members[i].Name); 190 | 191 | if (memberType.IsValueType || (memberType.ImplementsItsOwnEqualsMethod() && !memberType.IsAnonymous())) 192 | { 193 | var propEqual = CallExpressionIfNoComparer(Expression.Equal(xPropertyOrField, yPropertyOrField), comparers, memberType, xPropertyOrField, yPropertyOrField); 194 | 195 | // this type supports value comparison so we can just compare it. 196 | propertyEqualities = propertyEqualities == null ? propEqual : Expression.AndAlso(propertyEqualities, propEqual); 197 | //i.e. (x.Property == y.Property) && (x.Property2 == y.Property2) .... 198 | } 199 | //for now, do a ref check, since Exception is a bit of an oddball type 200 | else if (typeof(Exception).IsAssignableFrom(memberType)) 201 | { 202 | //the refs are the same OR 203 | var propEqual = CallExpressionIfNoComparer(Expression.ReferenceEqual(xPropertyOrField, yPropertyOrField), 204 | comparers, memberType, xPropertyOrField, yPropertyOrField); 205 | 206 | propertyEqualities = propertyEqualities == null ? propEqual : Expression.AndAlso(propertyEqualities, propEqual); 207 | //i.e. (x.Property == y.Property) && (x.Property2 == y.Property2) .... 208 | } 209 | else if (typeof(IEnumerable<>).IsGenericInterfaceAssignableFrom(memberType)) 210 | { 211 | Type genericTypeParam = (typeof(IEnumerable<>)).GetGenericInterfaceTypeParameters(memberType).First(); 212 | 213 | recursiveProperties = CustomPropertyComparison(xPropertyOrField, yPropertyOrField, parentNullChecks, recursiveProperties, (nullCheckToThisDepth) => 214 | //and call SequenceEqual on the two sequences, passing a custom IEqualityComparer 215 | SequencesOfTypeAreEqual(xPropertyOrField, yPropertyOrField, genericTypeParam, comparers)); 216 | } 217 | // this type does not support value comparison, so we must recurse and find it's properties. 218 | else 219 | { 220 | recursiveProperties = CustomPropertyComparison(xPropertyOrField, yPropertyOrField, parentNullChecks, recursiveProperties, (nullCheckToThisDepth) => 221 | //and either use a passed comparer or recurse the property and all it's nested properties 222 | CallExpressionIfNoComparer( 223 | BuildRecursiveComparison(memberType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OfType().Union(memberType.GetFields(BindingFlags.Public | BindingFlags.Instance)).ToArray(), 224 | xPropertyOrField, yPropertyOrField, comparers, nullCheckToThisDepth, recursiveProperties), 225 | comparers, memberType, xPropertyOrField, yPropertyOrField)); 226 | } 227 | } 228 | 229 | //to make it this far we either had simple properties or nested recursive stuff 230 | BinaryExpression depthCheck = null != propertyEqualities && null != recursiveProperties ? Expression.AndAlso(propertyEqualities, recursiveProperties) 231 | : propertyEqualities ?? recursiveProperties; 232 | 233 | //combine that with our main running expression if there is one 234 | return body == null ? depthCheck : Expression.AndAlso(body, depthCheck); 235 | } 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/EqualityComparer.Tests/MemberComparerTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using Xunit; 5 | 6 | namespace EqualityComparer.Tests 7 | { 8 | public class MemberComparerTest 9 | { 10 | [Fact] 11 | public void Equal_ThrowsOnNullEqualityComparerList() 12 | { 13 | Assert.Throws(() => MemberComparer.Equal(4, 4, null)); 14 | } 15 | 16 | [Fact] 17 | public void Equal_ThrowsOnNullComparerInComparerList() 18 | { 19 | Assert.Throws(() => MemberComparer.Equal(4, 4, new IEqualityComparer[] { null })); 20 | } 21 | 22 | private class FooComparer : IEqualityComparer 23 | { 24 | public bool Equals(object x, object y) 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | public int GetHashCode(object obj) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | } 34 | 35 | [Fact] 36 | public void Equal_ThrowsOnNonGenericEqualityComparerInList() 37 | { 38 | Assert.Throws(() => MemberComparer.Equal(4, 4, new[] { new FooComparer() })); 39 | } 40 | 41 | [Fact] 42 | public void Equal_ThrowsOnMoreThanOneComparerForATypeInList() 43 | { 44 | Assert.Throws(() => MemberComparer.Equal(4, 4, new[] { GenericEqualityComparer.ByAllMembers(), GenericEqualityComparer.ByAllMembers() })); 45 | } 46 | 47 | [Fact] 48 | public void Equal_TrueOnNullXNullY() 49 | { 50 | Assert.True(MemberComparer.Equal(null as List, null as List)); 51 | } 52 | 53 | [Fact] 54 | public void Equal_FalseOnNullXNonNullY() 55 | { 56 | Assert.False(MemberComparer.Equal(null as List, new List())); 57 | } 58 | 59 | [Fact] 60 | public void Equal_FalseOnNullYNonNullX() 61 | { 62 | Assert.False(MemberComparer.Equal(new List(), null as List)); 63 | } 64 | 65 | [Fact] 66 | public void Equal_TrueOnString() 67 | { 68 | Assert.True(MemberComparer.Equal("foo", "foo")); 69 | } 70 | 71 | [Fact] 72 | public void Equal_FalseOnMismatchedString() 73 | { 74 | Assert.False(MemberComparer.Equal("foo", "bar")); 75 | } 76 | 77 | [Fact] 78 | public void Equal_TrueOnPrimitive() 79 | { 80 | Assert.True(MemberComparer.Equal(3, 3)); 81 | } 82 | 83 | [Fact] 84 | public void Equal_TrueOnKeyValuePair() 85 | { 86 | Assert.True(MemberComparer.Equal(new KeyValuePair(1, "one"), 87 | new KeyValuePair(1, "one"))); 88 | } 89 | 90 | [Fact] 91 | public void Equal_FalseOnMismatchedPrimitive() 92 | { 93 | Assert.False(MemberComparer.Equal(5, 15)); 94 | } 95 | 96 | [Fact] 97 | public void Equal_TrueOnSameObject() 98 | { 99 | var anonymous = new { PropertyA = "A", Integer = 23, Guid = Guid.NewGuid() }; 100 | 101 | Assert.True(MemberComparer.Equal(anonymous, anonymous)); 102 | } 103 | 104 | [Fact] 105 | public void Equal_TrueOnDifferentObjectsWithSameValues() 106 | { 107 | Guid sharedGuid = Guid.NewGuid(); 108 | DateTime now = DateTime.Now; 109 | Assert.True(MemberComparer.Equal(new { PropertyA = "A", Integer = 23, Guid = sharedGuid, Date = now }, 110 | new { PropertyA = "A", Integer = 23, Guid = sharedGuid, Date = now })); 111 | } 112 | 113 | [Fact] 114 | public void Equal_FalseOnDifferentObjectsWithDifferentValues() 115 | { 116 | Guid sharedGuid = Guid.NewGuid(); 117 | Assert.False(MemberComparer.Equal(new { PropertyA = "B", Integer = 23, Guid = sharedGuid }, 118 | new { PropertyA = "A", Integer = 23, Guid = sharedGuid })); 119 | } 120 | 121 | [Fact] 122 | public void Equal_TrueOnAnonymousObjectsWithNestedObjects() 123 | { 124 | var sub1 = new { PropertyB = "b1" }; 125 | var sub2 = new { PropertyB = "b1" }; 126 | 127 | Assert.True(MemberComparer.Equal(sub1, sub2)); 128 | } 129 | 130 | [Fact] 131 | public void Equal_TrueOnObjectsWithNestedObjects() 132 | { 133 | var sub1 = new { PropertyB = "b1" }; 134 | var sub2 = new { PropertyB = "b1" }; 135 | 136 | Assert.True(MemberComparer.Equal(new { PropertyA = "A", Integer = 23, Sub = sub1 }, 137 | new { PropertyA = "A", Integer = 23, Sub = sub2 })); 138 | } 139 | 140 | [Fact] 141 | public void Equal_FalseOnAnonymousObjectsWithNestedObjectsWithDifferentValues() 142 | { 143 | var sub1 = new { PropertyB = "b1" }; 144 | var sub2 = new { PropertyB = "b2" }; 145 | 146 | Assert.False(MemberComparer.Equal(sub1, sub2)); 147 | } 148 | 149 | [Fact] 150 | public void Equal_FalseOnObjectsWithNestedObjectsWithDifferentValues() 151 | { 152 | var sub1 = new { PropertyB = "b1" }; 153 | var sub2 = new { PropertyB = "b2" }; 154 | 155 | Assert.False(MemberComparer.Equal(new { PropertyA = "A", Integer = 23, Sub = sub1 }, 156 | new { PropertyA = "A", Integer = 23, Sub = sub2 })); 157 | } 158 | 159 | [Fact] 160 | public void Equal_TrueOnNullAnonymousObjects() 161 | { 162 | //anonymous types that look the same like these actually share a static type (as constructed by the compiler) 163 | var sub1 = new { PropertyB = "b1" }; 164 | sub1 = null; 165 | var sub2 = new { PropertyB = "b1" }; 166 | sub2 = null; 167 | 168 | Assert.True(MemberComparer.Equal(sub1, sub2)); 169 | } 170 | 171 | [Fact] 172 | public void Equal_TrueOnObjectsWithNullNestedObjects() 173 | { 174 | //anonymous types that look the same like these actually share a static type (as constructed by the compiler) 175 | var sub1 = new { PropertyB = "b1" }; 176 | sub1 = null; 177 | var sub2 = new { PropertyB = "b1" }; 178 | sub2 = null; 179 | 180 | Assert.True(MemberComparer.Equal(new { PropertyA = "A", Integer = 23, Sub = sub1 }, 181 | new { PropertyA = "A", Integer = 23, Sub = sub2 })); 182 | } 183 | 184 | [Fact] 185 | public void Equal_FalseOnObjectsWithNullNestedObjectsWithDifferentValues() 186 | { 187 | var sub1 = new { PropertyB = "b1" }; 188 | sub1 = null; 189 | var sub2 = new { PropertyB = "b2" }; 190 | sub2 = null; 191 | 192 | Assert.False(MemberComparer.Equal(new { PropertyA = "A", Integer = 24, Sub = sub1 }, 193 | new { PropertyA = "A", Integer = 23, Sub = sub2 })); 194 | } 195 | 196 | [Fact] 197 | public void Equal_FalseOnObjectsWithOneNullNestedObjectAndOneNonNullNestedObject() 198 | { 199 | var sub1 = new { PropertyB = "b1" }; 200 | var sub2 = new { PropertyB = "b2" }; 201 | sub2 = null; 202 | 203 | Assert.False(MemberComparer.Equal(new { PropertyA = "A", Integer = 23, Sub = sub1 }, 204 | new { PropertyA = "A", Integer = 23, Sub = sub2 })); 205 | } 206 | 207 | [Fact] 208 | public void Equal_TrueOnDoubleNestedObjectsWithSameValues() 209 | { 210 | //we've nested two levels deep 211 | var sub2 = new { PropertyC = "b2" }; 212 | var sub1 = new { PropertyB = "b1", Sub = sub2 }; 213 | 214 | Assert.True(MemberComparer.Equal(new { PropertyA = "A", Integer = 23, Sub = sub1 }, 215 | new { PropertyA = "A", Integer = 23, Sub = sub1 })); 216 | } 217 | 218 | [Fact] 219 | public void Equal_TrueOnObjectsWithEnumProperties() 220 | { 221 | Assert.True(MemberComparer.Equal(new { Day = DayOfWeek.Monday }, new { Day = DayOfWeek.Monday })); 222 | } 223 | 224 | [Fact] 225 | public void Equal_TrueOnObjectsWithNestedCollections() 226 | { 227 | var nestedCollection1 = new { Property = "value", NestedProperties = new List() { "a", "b" } }; 228 | var nestedCollection2 = new { Property = "value", NestedProperties = new List() { "a", "b" } }; 229 | 230 | Assert.True(MemberComparer.Equal(nestedCollection1, nestedCollection2)); 231 | } 232 | 233 | class DictionaryObj 234 | { 235 | public Dictionary ObjectDictionary { get; set; } 236 | } 237 | 238 | [Fact] 239 | public void Equal_TrueOnMatchedDictionaryObjectInstances() 240 | { 241 | DictionaryObj dobj1 = new DictionaryObj { ObjectDictionary = new Dictionary 242 | { { 1, new ClassWithFieldsAndProperties() { Foo = "one", Bar= "two" } }, { 2, new ClassWithFieldsAndProperties() { Foo = "three", Bar= "four" } } } }; 243 | DictionaryObj dobj2 = new DictionaryObj { ObjectDictionary = new Dictionary 244 | { { 1, new ClassWithFieldsAndProperties() { Foo = "one", Bar= "two" } }, { 2, new ClassWithFieldsAndProperties() { Foo = "three", Bar= "four" } } } }; 245 | 246 | Assert.True(MemberComparer.Equal(dobj1, dobj2)); 247 | } 248 | 249 | [Fact] 250 | public void Equal_FalseOnObjectsWithNestedCollections() 251 | { 252 | var nestedCollection1 = new { Property = "value", NestedProperties = new List() { "a", "b" } }; 253 | var nestedCollection2 = new { Property = "value", NestedProperties = new List() { "b", "a" } }; 254 | 255 | Assert.False(MemberComparer.Equal(nestedCollection1, nestedCollection2)); 256 | } 257 | 258 | [Fact] 259 | public void Equal_TrueOnEqualCollections() 260 | { 261 | Assert.True(MemberComparer.Equal(new int[] { 5, 10 }, new int[] { 5, 10 })); 262 | } 263 | 264 | [Fact] 265 | public void Equal_FalseOnMismatchedCollections() 266 | { 267 | Assert.False(MemberComparer.Equal(new int[] { 5, 10 }, new int[] { 10, 5 })); 268 | } 269 | 270 | class ClassWithFieldsAndProperties 271 | { 272 | public string Foo; 273 | public string Bar { get; set; } 274 | } 275 | 276 | [Fact] 277 | public void Equal_TrueOnClassWithMismatchedPropertiesAndFieldsWithCustomComparer() 278 | { 279 | string Bar = "bar"; 280 | Assert.True(MemberComparer.Equal(new ClassWithFieldsAndProperties() { Foo = "456", Bar = Bar }, new ClassWithFieldsAndProperties() { Foo = "4567", Bar = Bar }, 281 | new[] { new GenericEqualityComparer((a, b) => a.Bar == b.Bar) })); 282 | } 283 | 284 | [Fact] 285 | public void Equal_TrueOnClassWithMismatchedPropertiesAndFieldsWithCustomComparerNested() 286 | { 287 | string Bar = "bar"; 288 | Assert.True(MemberComparer.Equal(new { Integer = 5, Custom = new ClassWithFieldsAndProperties() { Foo = "456", Bar = Bar } }, 289 | new { Integer = 5, Custom = new ClassWithFieldsAndProperties() { Foo = "4567", Bar = Bar } }, 290 | new[] { new GenericEqualityComparer((a, b) => a.Bar == b.Bar) })); 291 | } 292 | 293 | [Fact] 294 | public void Equal_TrueOnClasswithPropertiesAndFields() 295 | { 296 | string Bar = "123", Foo = "456"; 297 | Assert.True(MemberComparer.Equal(new ClassWithFieldsAndProperties() { Bar = Bar, Foo = Foo }, new ClassWithFieldsAndProperties() { Bar = Bar, Foo = Foo })); 298 | } 299 | 300 | [Fact] 301 | public void Equal_FalseOnClassWithMismatchFieldValues() 302 | { 303 | string Bar = "bar"; 304 | Assert.False(MemberComparer.Equal(new ClassWithFieldsAndProperties() { Foo = "456", Bar = Bar }, new ClassWithFieldsAndProperties() { Foo = "4567", Bar = Bar })); 305 | } 306 | 307 | [Fact] 308 | public void Equal_TrueOnExactDates() 309 | { 310 | DateTime now = DateTime.Now; 311 | Assert.True(MemberComparer.Equal(now, now)); 312 | } 313 | 314 | [Fact] 315 | public void Equal_TrueToSecondOnEqualDates() 316 | { 317 | DateTime now = DateTime.Now; 318 | Assert.True(MemberComparer.Equal(now, now, new[] { new DateComparer(DateComparisonType.TruncatedToSecond) })); 319 | } 320 | 321 | [Fact] 322 | public void Equal_FalseOnDatesDifferingByLessThanASecond() 323 | { 324 | DateTime one = DateTime.Parse("07:27:15.01"), 325 | two = DateTime.Parse("07:27:15.49"); 326 | 327 | Assert.False(MemberComparer.Equal(one, two)); 328 | } 329 | 330 | [Fact] 331 | public void Equal_TrueToSecondOnDatesDifferingByLessThanASecondWithCustomComparer() 332 | { 333 | DateTime one = DateTime.Parse("07:27:15.01"), 334 | two = DateTime.Parse("07:27:15.49"); 335 | 336 | Assert.True(MemberComparer.Equal(one, two, new[] { new DateComparer(DateComparisonType.TruncatedToSecond) })); 337 | } 338 | 339 | [Fact] 340 | public void Equal_TrueToSecondOnNestedDatesDifferingByLessThanASecondWithCustomComparer() 341 | { 342 | DateTime one = DateTime.Parse("07:27:15.01"), 343 | two = DateTime.Parse("07:27:15.49"); 344 | 345 | var a = new { Foo = 5, Bar = new { Now = one } }; 346 | var b = new { Foo = 5, Bar = new { Now = two } }; 347 | 348 | Assert.True(MemberComparer.Equal(one, two, new[] { new DateComparer(DateComparisonType.TruncatedToSecond) })); 349 | } 350 | 351 | [Fact] 352 | public void Equal_TrueToSecondOnNestedCollectionOfDatesDifferingByLessThanASecondWithCustomComparer() 353 | { 354 | var dates = new[] { DateTime.Parse("07:27:15.01"), DateTime.Parse("07:27:15.49") }; 355 | Assert.True(MemberComparer.Equal(new { A = 1, Dates = dates }, new { A = 1, Dates = dates }, new[] { new DateComparer(DateComparisonType.TruncatedToSecond) })); 356 | } 357 | 358 | class ClassWithStatics 359 | { 360 | public static int StaticInteger { get { return 12; } } 361 | public string Value { get; set; } 362 | } 363 | 364 | [Fact] 365 | public void Equal_IgnoresStatics() 366 | { 367 | var a = new ClassWithStatics() { Value = "Foo" }; 368 | var b = new ClassWithStatics() { Value = "Foo" }; 369 | 370 | Assert.True(MemberComparer.Equal(a, b)); 371 | } 372 | 373 | [Fact] 374 | public void Equal_TrueOnRefToSameException() 375 | { 376 | var exception = new ArgumentNullException("foo"); 377 | 378 | Assert.True(MemberComparer.Equal(exception, exception)); 379 | } 380 | 381 | class ExceptionHolder 382 | { 383 | public Exception Exception { get; set; } 384 | } 385 | 386 | [Fact] 387 | public void Equal_TrueOnNestedRefToSameException() 388 | { 389 | var exception = new ArgumentNullException("foo"); 390 | 391 | Assert.True(MemberComparer.Equal(new ExceptionHolder() { Exception = exception }, new ExceptionHolder() { Exception = exception })); 392 | } 393 | 394 | [Fact(Skip = "This will require quite a bit of effort to get right, so punted for now")] 395 | public void Equal_TrueOnExceptionsWithinSameScopeOfSameType() 396 | { 397 | var exception = new ArgumentNullException("foo"); 398 | var exception2 = new ArgumentNullException("foo"); 399 | 400 | Assert.True(MemberComparer.Equal(exception, exception2)); 401 | } 402 | 403 | interface IFoo 404 | { 405 | int Integer { get; } 406 | } 407 | 408 | class Foo : IFoo 409 | { 410 | public int Integer { get; set; } 411 | } 412 | 413 | [Fact] 414 | public void Equal_TrueOnInterfaces() 415 | { 416 | Assert.True(MemberComparer.Equal(new Foo() { Integer = 5 }, new Foo() { Integer = 5 })); 417 | } 418 | 419 | interface IBar 420 | { 421 | IFoo Foo { get; } 422 | } 423 | 424 | class Bar : IBar 425 | { 426 | public IFoo Foo { get; set; } 427 | } 428 | 429 | [Fact] 430 | public void Equal_TrueOnNestedInterfaces() 431 | { 432 | Assert.True(MemberComparer.Equal(new Bar() { Foo = new Foo() { Integer = 5 } }, new Bar() { Foo = new Foo() { Integer = 5 } })); 433 | } 434 | 435 | class A 436 | { 437 | public int Integer { get; set; } 438 | } 439 | 440 | class B : A 441 | { 442 | public string String { get; set; } 443 | } 444 | 445 | [Fact] 446 | public void Equal_ScopesComparisonToSpecifiedType() 447 | { 448 | Assert.True(MemberComparer.Equal(new B() { Integer = 4, String = "Foo" }, new B() { Integer = 4, String = "Bar" })); 449 | } 450 | } 451 | } 452 | --------------------------------------------------------------------------------