diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..c85887d
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,52 @@
+# EditorConfig: https://EditorConfig.org
+
+root = true
+
+# All Files
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 4
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+# XML Configuration Files
+[*.{xml,config,props,targets,nuspec,resx,ruleset,vsixmanifest,vsct,refactorlog,runsettings}]
+indent_size = 2
+
+# JSON Files
+[*.{json,json5,webmanifest}]
+indent_size = 2
+
+# Project Files
+[*.{csproj,sqlproj}]
+indent_size = 2
+
+# YAML Files
+[*.{yml,yaml}]
+indent_size = 2
+
+# Markdown Files
+[*.md]
+trim_trailing_whitespace = false
+
+# Web Files
+[*.{htm,html,js,jsm,ts,tsx,css,sass,scss,less,pcss,svg,vue}]
+indent_size = 2
+
+# Batch Files
+[*.{cmd,bat}]
+end_of_line = crlf
+
+# Bash Files
+[*.sh]
+end_of_line = lf
+
+[*.{cs,vb}]
+dotnet_sort_system_directives_first = true
+dotnet_separate_import_directive_groups = true
+dotnet_style_namespace_match_folder = true
+
+[*.cs]
+csharp_using_directive_placement = outside_namespace
+csharp_style_namespace_declarations = file_scoped:warning
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..fa8c583
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,57 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "01:00"
+ timezone: "America/Chicago"
+ open-pull-requests-limit: 10
+
+ - package-ecosystem: nuget
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "02:00"
+ timezone: "America/Chicago"
+ open-pull-requests-limit: 10
+ groups:
+ Azure:
+ patterns:
+ - "Azure.*"
+ - "Microsoft.Azure.*"
+ - "Microsoft.Extensions.Azure"
+ AspNetCoreHealthChecks:
+ patterns:
+ - "AspNetCore.HealthChecks.*"
+ AspNetCore:
+ patterns:
+ - "Microsoft.AspNetCore.*"
+ - "Microsoft.Extensions.Features"
+ MicrosoftExtensions:
+ patterns:
+ - "Microsoft.Extensions.*"
+ EntityFrameworkCore:
+ patterns:
+ - "Microsoft.EntityFrameworkCore.*"
+ OpenTelemetry:
+ patterns:
+ - "OpenTelemetry.*"
+ Serilog:
+ patterns:
+ - "Serilog"
+ - "Serilog.*"
+ Hangfire:
+ patterns:
+ - "Hangfire"
+ - "Hangfire.*"
+ Testcontainers:
+ patterns:
+ - "Testcontainers.*"
+ xUnit:
+ patterns:
+ - "xunit"
+ - "xunit.assert"
+ - "xunit.core"
+ - "xunit.extensibility.*"
+ - "xunit.runner.*"
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
new file mode 100644
index 0000000..ca18873
--- /dev/null
+++ b/.github/workflows/dotnet.yml
@@ -0,0 +1,96 @@
+name: Build
+
+env:
+ DOTNET_NOLOGO: true
+ DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
+ DOTNET_ENVIRONMENT: github
+ ASPNETCORE_ENVIRONMENT: github
+ BUILD_PATH: "${{github.workspace}}/artifacts"
+ COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+
+on:
+ push:
+ branches:
+ - main
+ - develop
+ tags:
+ - "v*"
+ pull_request:
+ branches:
+ - main
+ - develop
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Install .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 8.0.x
+
+ - name: Restore Dependencies
+ run: dotnet restore
+
+ - name: Build Solution
+ run: dotnet build --no-restore --configuration Release
+
+ - name: Run Test
+ run: dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --settings coverlet.runsettings
+
+ - name: Report Coverage
+ if: success()
+ uses: coverallsapp/github-action@v2
+ with:
+ file: "${{github.workspace}}/test/*/TestResults/*/coverage.info"
+ format: lcov
+
+ - name: Create Packages
+ if: success() && github.event_name != 'pull_request'
+ run: dotnet pack --configuration Release --no-build --output "${{env.BUILD_PATH}}"
+
+ - name: Upload Packages
+ if: success() && github.event_name != 'pull_request'
+ uses: actions/upload-artifact@v4
+ with:
+ name: packages
+ path: "${{env.BUILD_PATH}}"
+
+ deploy:
+ runs-on: ubuntu-latest
+ needs: build
+ if: success() && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
+
+ steps:
+ - name: Download Artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: packages
+
+ - name: Publish Packages GitHub
+ run: |
+ for package in $(find -name "*.nupkg"); do
+ echo "${0##*/}": Pushing $package...
+ dotnet nuget push $package --source https://nuget.pkg.github.com/loresoft/index.json --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
+ done
+
+ - name: Publish Packages feedz
+ run: |
+ for package in $(find -name "*.nupkg"); do
+ echo "${0##*/}": Pushing $package...
+ dotnet nuget push $package --source https://f.feedz.io/loresoft/open/nuget/index.json --api-key ${{ secrets.FEEDDZ_KEY }} --skip-duplicate
+ done
+
+ - name: Publish Packages Nuget
+ if: startsWith(github.ref, 'refs/tags/v')
+ run: |
+ for package in $(find -name "*.nupkg"); do
+ echo "${0##*/}": Pushing $package...
+ dotnet nuget push $package --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_KEY }} --skip-duplicate
+ done
diff --git a/Equatable.Generator.sln b/Equatable.Generator.sln
new file mode 100644
index 0000000..a57cace
--- /dev/null
+++ b/Equatable.Generator.sln
@@ -0,0 +1,69 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equatable.Generator", "src\Equatable.Generator\Equatable.Generator.csproj", "{7CEA78BA-DFE9-4B0E-8837-11850D7C197B}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equatable.SourceGenerator", "src\Equatable.SourceGenerator\Equatable.SourceGenerator.csproj", "{919F5AB0-47F6-4B38-B934-E5ADE8C8BA11}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{9ABD9B14-9E53-463A-96B0-FA7F503BA826}"
+ ProjectSection(SolutionItems) = preProject
+ src\Directory.Build.props = src\Directory.Build.props
+ .github\workflows\dotnet.yml = .github\workflows\dotnet.yml
+ README.md = README.md
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Equatable.Generator.Tests", "test\Equatable.Generator.Tests\Equatable.Generator.Tests.csproj", "{FC77B226-0FC3-4AD4-8364-761D5FC419E5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equatable.SourceGenerator.Tests", "test\Equatable.SourceGenerator.Tests\Equatable.SourceGenerator.Tests.csproj", "{75E86899-4DD9-4493-8EE4-CF8237F2539D}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{EA286CB8-AC01-452B-9092-97DCE3817093}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equatable.Entities", "test\Equatable.Entities\Equatable.Entities.csproj", "{E1C35715-F118-433E-B456-11FD4B7B3079}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Equatable.Comparers", "src\Equatable.Comparers\Equatable.Comparers.csproj", "{E8A054EF-222A-4CD8-9177-643050501FB1}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {7CEA78BA-DFE9-4B0E-8837-11850D7C197B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7CEA78BA-DFE9-4B0E-8837-11850D7C197B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7CEA78BA-DFE9-4B0E-8837-11850D7C197B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7CEA78BA-DFE9-4B0E-8837-11850D7C197B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {919F5AB0-47F6-4B38-B934-E5ADE8C8BA11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {919F5AB0-47F6-4B38-B934-E5ADE8C8BA11}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {919F5AB0-47F6-4B38-B934-E5ADE8C8BA11}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {919F5AB0-47F6-4B38-B934-E5ADE8C8BA11}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FC77B226-0FC3-4AD4-8364-761D5FC419E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC77B226-0FC3-4AD4-8364-761D5FC419E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC77B226-0FC3-4AD4-8364-761D5FC419E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC77B226-0FC3-4AD4-8364-761D5FC419E5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {75E86899-4DD9-4493-8EE4-CF8237F2539D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {75E86899-4DD9-4493-8EE4-CF8237F2539D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {75E86899-4DD9-4493-8EE4-CF8237F2539D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {75E86899-4DD9-4493-8EE4-CF8237F2539D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E1C35715-F118-433E-B456-11FD4B7B3079}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E1C35715-F118-433E-B456-11FD4B7B3079}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E1C35715-F118-433E-B456-11FD4B7B3079}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E1C35715-F118-433E-B456-11FD4B7B3079}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E8A054EF-222A-4CD8-9177-643050501FB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E8A054EF-222A-4CD8-9177-643050501FB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E8A054EF-222A-4CD8-9177-643050501FB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E8A054EF-222A-4CD8-9177-643050501FB1}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {FC77B226-0FC3-4AD4-8364-761D5FC419E5} = {EA286CB8-AC01-452B-9092-97DCE3817093}
+ {75E86899-4DD9-4493-8EE4-CF8237F2539D} = {EA286CB8-AC01-452B-9092-97DCE3817093}
+ {E1C35715-F118-433E-B456-11FD4B7B3079} = {EA286CB8-AC01-452B-9092-97DCE3817093}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1A9BD8E7-57D6-44B0-BFC7-B7378C05D4F5}
+ EndGlobalSection
+EndGlobal
diff --git a/README.md b/README.md
index 286593e..f03bbd0 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
# Equatable.Generator
+
Source generator for Equals and GetHashCode
diff --git a/coverlet.runsettings b/coverlet.runsettings
new file mode 100644
index 0000000..b28750f
--- /dev/null
+++ b/coverlet.runsettings
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ lcov
+
+
+
+
+
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..6482efc
Binary files /dev/null and b/logo.png differ
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
new file mode 100644
index 0000000..b27b658
--- /dev/null
+++ b/src/Directory.Build.props
@@ -0,0 +1,59 @@
+
+
+
+ Source generator for Equals and GetHashCode
+ Copyright © $([System.DateTime]::Now.ToString(yyyy)) LoreSoft
+ LoreSoft
+ en-US
+ true
+ dotnet roslyn source-generator generator
+ https://github.com/loresoft/Equatable.Generator
+ MIT
+ logo.png
+ README.md
+ git
+ https://github.com/loresoft/Equatable.Generator
+ true
+
+
+
+ embedded
+ true
+ false
+
+
+
+ true
+
+
+
+ en-US
+ latest
+ enable
+ enable
+ 1591
+
+
+
+ v
+
+
+
+
+
+
+
+
+
+ true
+ \
+ false
+
+
+ true
+ \
+ false
+
+
+
+
diff --git a/src/Equatable.Comparers/DictionaryEqualityComparer.cs b/src/Equatable.Comparers/DictionaryEqualityComparer.cs
new file mode 100644
index 0000000..27c92f6
--- /dev/null
+++ b/src/Equatable.Comparers/DictionaryEqualityComparer.cs
@@ -0,0 +1,85 @@
+namespace Equatable.Comparers;
+
+///
+/// equality comparer instance
+///
+/// The type of the keys in the dictionary.
+/// The type of the values in the dictionary.
+public class DictionaryEqualityComparer : IEqualityComparer>
+{
+ ///
+ /// Gets the default equality comparer for specified generic argument.
+ ///
+ public static DictionaryEqualityComparer Default { get; } = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DictionaryEqualityComparer() : this(EqualityComparer.Default, EqualityComparer.Default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The that is used to determine equality of keys in a dictionary
+ /// The that is used to determine equality of values in a dictionary
+ /// or is null
+ public DictionaryEqualityComparer(IEqualityComparer keyComparer, IEqualityComparer valueComparer)
+ {
+ KeyComparer = keyComparer ?? throw new ArgumentNullException(nameof(keyComparer));
+ ValueComparer = valueComparer ?? throw new ArgumentNullException(nameof(valueComparer));
+ }
+
+ ///
+ /// Gets the that is used to determine equality of keys in a dictionary
+ ///
+ public IEqualityComparer KeyComparer { get; }
+
+ ///
+ /// Gets the that is used to determine equality of values in a dictionary
+ ///
+ public IEqualityComparer ValueComparer { get; }
+
+ ///
+ public bool Equals(IDictionary? x, IDictionary? y)
+ {
+ if (ReferenceEquals(x, y))
+ return true;
+
+ if (x == null || y == null)
+ return false;
+
+ if (x.Count != y.Count)
+ return false;
+
+ foreach (var pair in x)
+ {
+ if (!y.TryGetValue(pair.Key, out var value))
+ return false;
+
+ if (!ValueComparer.Equals(pair.Value, value))
+ return false;
+ }
+
+ return true;
+ }
+
+ ///
+ public int GetHashCode(IDictionary obj)
+ {
+ if (obj == null)
+ return 0;
+
+ var hash = new HashCode();
+
+ // sort by key to ensure dictionary with different order are the same
+ foreach (var pair in obj.OrderBy(d => d.Key))
+ {
+ hash.Add(pair.Key, KeyComparer);
+ hash.Add(pair.Value, ValueComparer);
+ }
+
+ return hash.ToHashCode();
+ }
+}
diff --git a/src/Equatable.Comparers/Equatable.Comparers.csproj b/src/Equatable.Comparers/Equatable.Comparers.csproj
new file mode 100644
index 0000000..815d7e5
--- /dev/null
+++ b/src/Equatable.Comparers/Equatable.Comparers.csproj
@@ -0,0 +1,11 @@
+
+
+
+ netstandard2.0;net6.0;net8.0
+
+
+
+
+
+
+
diff --git a/src/Equatable.Comparers/HashSetEqualityComparer.cs b/src/Equatable.Comparers/HashSetEqualityComparer.cs
new file mode 100644
index 0000000..9c78883
--- /dev/null
+++ b/src/Equatable.Comparers/HashSetEqualityComparer.cs
@@ -0,0 +1,67 @@
+namespace Equatable.Comparers;
+
+///
+/// equality comparer instance
+///
+/// The type of the values.
+public class HashSetEqualityComparer : IEqualityComparer>
+{
+ ///
+ /// Gets the default equality comparer for specified generic argument.
+ ///
+ public static HashSetEqualityComparer Default { get; } = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public HashSetEqualityComparer() : this(EqualityComparer.Default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public HashSetEqualityComparer(IEqualityComparer comparer)
+ {
+ Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
+ }
+
+ ///
+ /// Gets the that is used to determine equality of values
+ ///
+ public IEqualityComparer Comparer { get; }
+
+ ///
+ public bool Equals(IEnumerable? x, IEnumerable? y)
+ {
+ if (ReferenceEquals(x, y))
+ return true;
+
+ if (x == null || y == null)
+ return false;
+
+ if (x is ISet xSet)
+ return xSet.SetEquals(y);
+
+ if (y is ISet ySet)
+ return ySet.SetEquals(x);
+
+ xSet = new HashSet(x, Comparer);
+ return xSet.SetEquals(y);
+ }
+
+ ///
+ public int GetHashCode(IEnumerable obj)
+ {
+ if (obj == null)
+ return 0;
+
+ var hashCode = new HashCode();
+
+ // sort to ensure set with different order are the same
+ foreach (var item in obj.OrderBy(s => s))
+ hashCode.Add(item, Comparer);
+
+ return hashCode.ToHashCode();
+ }
+}
diff --git a/src/Equatable.Comparers/ReferenceEqualityComparer.cs b/src/Equatable.Comparers/ReferenceEqualityComparer.cs
new file mode 100644
index 0000000..a3db55a
--- /dev/null
+++ b/src/Equatable.Comparers/ReferenceEqualityComparer.cs
@@ -0,0 +1,27 @@
+using System.Runtime.CompilerServices;
+
+namespace Equatable.Comparers;
+
+///
+/// Reference equality comparer instance
+///
+/// The type of the value to compare.
+public class ReferenceEqualityComparer : IEqualityComparer where T : class
+{
+ ///
+ /// Gets the default equality comparer
+ ///
+ public static ReferenceEqualityComparer Default { get; } = new();
+
+ ///
+ public bool Equals(T? x, T? y)
+ {
+ return ReferenceEquals(x, y);
+ }
+
+ ///
+ public int GetHashCode(T obj)
+ {
+ return RuntimeHelpers.GetHashCode(obj);
+ }
+}
diff --git a/src/Equatable.Comparers/SequenceEqualityComparer.cs b/src/Equatable.Comparers/SequenceEqualityComparer.cs
new file mode 100644
index 0000000..8c80f73
--- /dev/null
+++ b/src/Equatable.Comparers/SequenceEqualityComparer.cs
@@ -0,0 +1,59 @@
+namespace Equatable.Comparers;
+
+///
+/// equality comparer instance
+///
+/// The type of the values.
+public class SequenceEqualityComparer : IEqualityComparer>
+{
+ ///
+ /// Gets the default equality comparer for specified generic argument.
+ ///
+ public static SequenceEqualityComparer Default { get; } = new();
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SequenceEqualityComparer() : this(EqualityComparer.Default)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SequenceEqualityComparer(IEqualityComparer comparer)
+ {
+ Comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
+ }
+
+ ///
+ /// Gets the that is used to determine equality of values
+ ///
+ public IEqualityComparer Comparer { get; }
+
+ ///
+ public bool Equals(IEnumerable? x, IEnumerable? y)
+ {
+ if (ReferenceEquals(x, y))
+ return true;
+
+ if (x == null || y == null)
+ return false;
+
+ return x.SequenceEqual(y, Comparer);
+ }
+
+ ///
+ public int GetHashCode(IEnumerable obj)
+ {
+ if (obj == null)
+ return 0;
+
+ var hashCode = new HashCode();
+
+ foreach (var item in obj)
+ hashCode.Add(item, Comparer);
+
+ return hashCode.ToHashCode();
+ }
+}
diff --git a/src/Equatable.Generator/Attributes/DictionaryEqualityAttribute.cs b/src/Equatable.Generator/Attributes/DictionaryEqualityAttribute.cs
new file mode 100644
index 0000000..8286772
--- /dev/null
+++ b/src/Equatable.Generator/Attributes/DictionaryEqualityAttribute.cs
@@ -0,0 +1,7 @@
+using System.Diagnostics;
+
+namespace Equatable.Attributes;
+
+[Conditional("EQUATABLE_GENERATOR")]
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public class DictionaryEqualityAttribute : Attribute;
diff --git a/src/Equatable.Generator/Attributes/EqualityComparerAttribute.cs b/src/Equatable.Generator/Attributes/EqualityComparerAttribute.cs
new file mode 100644
index 0000000..114492e
--- /dev/null
+++ b/src/Equatable.Generator/Attributes/EqualityComparerAttribute.cs
@@ -0,0 +1,12 @@
+using System.Diagnostics;
+
+namespace Equatable.Attributes;
+
+[Conditional("EQUATABLE_GENERATOR")]
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public class EqualityComparerAttribute(Type equalityType, string instanceName = "Default") : Attribute
+{
+ public Type EqualityType { get; } = equalityType;
+
+ public string InstanceName { get; } = instanceName;
+}
diff --git a/src/Equatable.Generator/Attributes/EquatableAttribute.cs b/src/Equatable.Generator/Attributes/EquatableAttribute.cs
new file mode 100644
index 0000000..7d4dda1
--- /dev/null
+++ b/src/Equatable.Generator/Attributes/EquatableAttribute.cs
@@ -0,0 +1,18 @@
+using System.Diagnostics;
+
+namespace Equatable.Attributes;
+
+[Conditional("EQUATABLE_GENERATOR")]
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
+public class EquatableAttribute : Attribute
+{
+ ///
+ /// Only members marked with equality attributes will be generated for Equal and GetHashCode.
+ ///
+ public bool Explicit { get; set; }
+
+ ///
+ /// Equal and GetHashCode generation do not consider members of base classes.
+ ///
+ public bool IgnoreInherited { get; set; }
+}
diff --git a/src/Equatable.Generator/Attributes/HashSetEqualityAttribute.cs b/src/Equatable.Generator/Attributes/HashSetEqualityAttribute.cs
new file mode 100644
index 0000000..fb4931e
--- /dev/null
+++ b/src/Equatable.Generator/Attributes/HashSetEqualityAttribute.cs
@@ -0,0 +1,7 @@
+using System.Diagnostics;
+
+namespace Equatable.Attributes;
+
+[Conditional("EQUATABLE_GENERATOR")]
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public class HashSetEqualityAttribute : Attribute;
diff --git a/src/Equatable.Generator/Attributes/IgnoreEqualityAttribute.cs b/src/Equatable.Generator/Attributes/IgnoreEqualityAttribute.cs
new file mode 100644
index 0000000..d525e17
--- /dev/null
+++ b/src/Equatable.Generator/Attributes/IgnoreEqualityAttribute.cs
@@ -0,0 +1,7 @@
+using System.Diagnostics;
+
+namespace Equatable.Attributes;
+
+[Conditional("EQUATABLE_GENERATOR")]
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public class IgnoreEqualityAttribute : Attribute;
diff --git a/src/Equatable.Generator/Attributes/ReferenceEqualityAttribute.cs b/src/Equatable.Generator/Attributes/ReferenceEqualityAttribute.cs
new file mode 100644
index 0000000..54550fa
--- /dev/null
+++ b/src/Equatable.Generator/Attributes/ReferenceEqualityAttribute.cs
@@ -0,0 +1,7 @@
+using System.Diagnostics;
+
+namespace Equatable.Attributes;
+
+[Conditional("EQUATABLE_GENERATOR")]
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public class ReferenceEqualityAttribute : Attribute;
diff --git a/src/Equatable.Generator/Attributes/SequenceEqualityAttribute.cs b/src/Equatable.Generator/Attributes/SequenceEqualityAttribute.cs
new file mode 100644
index 0000000..f5a73a4
--- /dev/null
+++ b/src/Equatable.Generator/Attributes/SequenceEqualityAttribute.cs
@@ -0,0 +1,7 @@
+using System.Diagnostics;
+
+namespace Equatable.Attributes;
+
+[Conditional("EQUATABLE_GENERATOR")]
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public class SequenceEqualityAttribute : Attribute;
diff --git a/src/Equatable.Generator/Attributes/StringEqualityAttribute.cs b/src/Equatable.Generator/Attributes/StringEqualityAttribute.cs
new file mode 100644
index 0000000..6c5e797
--- /dev/null
+++ b/src/Equatable.Generator/Attributes/StringEqualityAttribute.cs
@@ -0,0 +1,10 @@
+using System.Diagnostics;
+
+namespace Equatable.Attributes;
+
+[Conditional("EQUATABLE_GENERATOR")]
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+public class StringEqualityAttribute(StringComparison comparisonType) : Attribute
+{
+ public StringComparison ComparisonType { get; } = comparisonType;
+}
diff --git a/src/Equatable.Generator/Equatable.Generator.csproj b/src/Equatable.Generator/Equatable.Generator.csproj
new file mode 100644
index 0000000..b5cb236
--- /dev/null
+++ b/src/Equatable.Generator/Equatable.Generator.csproj
@@ -0,0 +1,12 @@
+
+
+
+ netstandard2.0;net6.0;net8.0
+ Equatable
+
+
+
+
+
+
+
diff --git a/src/Equatable.SourceGenerator/Equatable.SourceGenerator.csproj b/src/Equatable.SourceGenerator/Equatable.SourceGenerator.csproj
new file mode 100644
index 0000000..9d4a2be
--- /dev/null
+++ b/src/Equatable.SourceGenerator/Equatable.SourceGenerator.csproj
@@ -0,0 +1,28 @@
+
+
+
+ netstandard2.0
+ true
+ false
+ true
+ true
+ true
+ false
+ true
+ false
+ cs
+ 4.3
+ latest
+ true
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/src/Equatable.SourceGenerator/EquatableGenerator.cs b/src/Equatable.SourceGenerator/EquatableGenerator.cs
new file mode 100644
index 0000000..40a1dd9
--- /dev/null
+++ b/src/Equatable.SourceGenerator/EquatableGenerator.cs
@@ -0,0 +1,237 @@
+using Equatable.SourceGenerator.Models;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Equatable.SourceGenerator;
+
+[Generator]
+public class EquatableGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var provider = context.SyntaxProvider.ForAttributeWithMetadataName(
+ fullyQualifiedMetadataName: "Equatable.Attributes.EquatableAttribute",
+ predicate: SyntacticPredicate,
+ transform: SemanticTransform
+ )
+ .Where(static context => context is not null);
+
+ // Emit the diagnostics, if needed
+ var diagnostics = provider
+ .Select(static (item, _) => item?.Diagnostics)
+ .Where(static item => item?.Count > 0);
+
+ context.RegisterSourceOutput(diagnostics, ReportDiagnostic);
+
+ // output code
+ var entityClasses = provider
+ .Select(static (item, _) => item?.EntityClass)
+ .Where(static item => item is not null);
+
+ context.RegisterSourceOutput(entityClasses, Execute);
+ }
+
+ private static void ReportDiagnostic(SourceProductionContext context, EquatableArray? diagnostics)
+ {
+ if (diagnostics == null)
+ return;
+
+ foreach (var diagnostic in diagnostics)
+ context.ReportDiagnostic(diagnostic);
+ }
+
+ private static void Execute(SourceProductionContext context, EquatableClass? entityClass)
+ {
+ if (entityClass == null)
+ return;
+
+ var qualifiedName = entityClass.EntityNamespace is null
+ ? entityClass.EntityName
+ : $"{entityClass.EntityNamespace}.{entityClass.EntityName}";
+
+ var source = EquatableWriter.Generate(entityClass);
+
+ context.AddSource($"{qualifiedName}.Equatable.g.cs", source);
+ }
+
+
+ private static bool SyntacticPredicate(SyntaxNode syntaxNode, CancellationToken cancellationToken)
+ {
+ return (syntaxNode is ClassDeclarationSyntax classDeclaration && !classDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword))
+ || (syntaxNode is RecordDeclarationSyntax recordDeclaration && !recordDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword))
+ || (syntaxNode is StructDeclarationSyntax structDeclaration && !structDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword));
+ }
+
+ private static EquatableContext? SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken)
+ {
+ if (context.TargetSymbol is not INamedTypeSymbol targetSymbol)
+ return null;
+
+ var classNamespace = targetSymbol.ContainingNamespace.ToDisplayString();
+ var className = targetSymbol.Name;
+
+ var propertySymbols = GetProperties(targetSymbol);
+
+ var propertyArray = propertySymbols
+ .Select(p => CreateProperty(p))
+ .ToArray() ?? [];
+
+ var entity = new EquatableClass(classNamespace, className, propertyArray);
+ return new EquatableContext(entity, null);
+ }
+
+
+ private static IEnumerable GetProperties(INamedTypeSymbol targetSymbol)
+ {
+ var properties = new Dictionary();
+
+ var currentSymbol = targetSymbol;
+
+ // get nested properties
+ while (currentSymbol != null)
+ {
+ var propertySymbols = currentSymbol
+ .GetMembers()
+ .Where(m => m.Kind == SymbolKind.Property)
+ .OfType()
+ .Where(IsIncluded)
+ .Where(p => !properties.ContainsKey(p.Name));
+
+ foreach (var propertySymbol in propertySymbols)
+ properties.Add(propertySymbol.Name, propertySymbol);
+
+ currentSymbol = currentSymbol.BaseType;
+ }
+
+ return properties.Values;
+ }
+
+ private static EquatableProperty CreateProperty(IPropertySymbol propertySymbol)
+ {
+ var propertyType = propertySymbol.Type.ToDisplayString();
+ var propertyName = propertySymbol.Name;
+
+ // look for custom equality
+ var attributes = propertySymbol.GetAttributes();
+ if (attributes == null || attributes.Length == 0)
+ {
+ return new EquatableProperty(
+ propertyName,
+ propertyType,
+ ComparerTypes.Default);
+ }
+
+ // search for known attribute
+ foreach (var attribute in attributes)
+ {
+ (var comparerType, var comparerName, var comparerInstance) = GetComparer(attribute);
+
+ if (!comparerType.HasValue)
+ continue;
+
+ return new EquatableProperty(
+ propertyName,
+ propertyType,
+ comparerType.Value,
+ comparerName,
+ comparerInstance);
+ }
+
+ return new EquatableProperty(
+ propertyName,
+ propertyType,
+ ComparerTypes.Default);
+ }
+
+
+ private static (ComparerTypes? comparerType, string? comparerName, string? comparerInstance) GetComparer(AttributeData? attribute)
+ {
+ // known attributes
+ if (attribute == null || attribute.AttributeClass is not { ContainingNamespace: { Name: "Attributes", ContainingNamespace.Name: "Equatable" } })
+ return (null, null, null);
+
+ var className = attribute.AttributeClass?.Name;
+
+ return className switch
+ {
+ "DictionaryEqualityAttribute" => (ComparerTypes.Dictionary, null, null),
+ "HashSetEqualityAttribute" => (ComparerTypes.HashSet, null, null),
+ "ReferenceEqualityAttribute" => (ComparerTypes.Reference, null, null),
+ "SequenceEqualityAttribute" => (ComparerTypes.Sequence, null, null),
+ "StringEqualityAttribute" => GetStringComparer(attribute),
+ "EqualityComparerAttribute" => GetEqualityComparer(attribute),
+ _ => (null, null, null),
+ };
+ }
+
+ private static (ComparerTypes? comparerType, string? comparerName, string? comparerInstance) GetStringComparer(AttributeData? attribute)
+ {
+ var argument = attribute?.ConstructorArguments.FirstOrDefault();
+ if (argument == null || !argument.HasValue)
+ return (ComparerTypes.String, "CurrentCulture", null);
+
+ var comparerName = argument?.Value switch
+ {
+ 0 => "CurrentCulture",
+ 1 => "CurrentCultureIgnoreCase",
+ 2 => "InvariantCulture",
+ 3 => "InvariantCultureIgnoreCase",
+ 4 => "Ordinal",
+ 5 => "OrdinalIgnoreCase",
+ _ => "CurrentCulture"
+ };
+
+ return (ComparerTypes.String, comparerName, null);
+ }
+
+ private static (ComparerTypes? comparerType, string? comparerName, string? comparerInstance) GetEqualityComparer(AttributeData? attribute)
+ {
+ if (attribute == null)
+ return (ComparerTypes.Default, null, null);
+
+ // attribute constructor
+ var comparerType = attribute.ConstructorArguments.FirstOrDefault();
+ if (comparerType.Value is INamedTypeSymbol typeSymbol)
+ {
+ return (ComparerTypes.Custom, typeSymbol.ToDisplayString(), null);
+ }
+
+ // generic attribute
+ var attributeClass = attribute.AttributeClass;
+ if (attributeClass is { IsGenericType: true }
+ && attributeClass.TypeArguments.Length == attributeClass.TypeParameters.Length
+ && attributeClass.TypeArguments.Length == 1)
+ {
+ var typeArgument = attributeClass.TypeArguments[0];
+ var comparerName = typeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ return (ComparerTypes.Custom, comparerName, null);
+ }
+
+
+ return (ComparerTypes.Default, null, null);
+ }
+
+
+ private static bool IsIncluded(IPropertySymbol propertySymbol)
+ {
+ var attributes = propertySymbol.GetAttributes();
+ if (attributes.Length > 0 && attributes.Any(
+ a => a.AttributeClass is
+ {
+ Name: "IgnoreEqualityAttribute",
+ ContainingNamespace:
+ {
+ Name: "Attributes",
+ ContainingNamespace.Name: "Equatable"
+ }
+ }))
+ {
+ return false;
+ }
+
+ return !propertySymbol.IsIndexer && propertySymbol.DeclaredAccessibility == Accessibility.Public;
+ }
+}
diff --git a/src/Equatable.SourceGenerator/EquatableWriter.cs b/src/Equatable.SourceGenerator/EquatableWriter.cs
new file mode 100644
index 0000000..b47bacd
--- /dev/null
+++ b/src/Equatable.SourceGenerator/EquatableWriter.cs
@@ -0,0 +1,435 @@
+using Equatable.SourceGenerator.Models;
+
+using Microsoft.CodeAnalysis;
+
+namespace Equatable.SourceGenerator;
+
+public static class EquatableWriter
+{
+ public static string Generate(EquatableClass entityClass)
+ {
+ if (entityClass == null)
+ throw new ArgumentNullException(nameof(entityClass));
+
+ var codeBuilder = new IndentedStringBuilder();
+ codeBuilder
+ .AppendLine("// ")
+ .AppendLine("#nullable enable")
+ .AppendLine();
+
+ codeBuilder
+ .Append("namespace ")
+ .AppendLine(entityClass.EntityNamespace)
+ .AppendLine("{")
+ .IncrementIndent();
+
+ codeBuilder
+ .Append("partial class ")
+ .Append(entityClass.EntityName)
+ .Append(" : global::System.IEquatable<")
+ .Append(entityClass.EntityName)
+ .AppendLine("?>")
+ .AppendLine("{")
+ .IncrementIndent();
+
+ GenerateEquatable(codeBuilder, entityClass);
+ GenerateEquals(codeBuilder, entityClass);
+ GenerateHashCode(codeBuilder, entityClass);
+
+ codeBuilder
+ .DecrementIndent()
+ .AppendLine("}") // class
+ .DecrementIndent()
+ .AppendLine("}"); // namespace
+
+ return codeBuilder.ToString();
+ }
+
+ private static void GenerateEquatable(IndentedStringBuilder codeBuilder, EquatableClass entityClass)
+ {
+ codeBuilder
+ .AppendLine("/// ")
+ .Append("public bool Equals(")
+ .Append(entityClass.EntityName)
+ .AppendLine("? other)")
+ .AppendLine("{")
+ .IncrementIndent();
+
+ codeBuilder
+ .Append("return other is not null");
+
+ foreach (var entityProperty in entityClass.Properties)
+ {
+ switch (entityProperty.ComparerType)
+ {
+ case ComparerTypes.Dictionary:
+ codeBuilder
+ .AppendLine()
+ .Append(" && ")
+ .Append("DictionaryEquals(")
+ .Append(entityProperty.PropertyName)
+ .Append(", other.")
+ .Append(entityProperty.PropertyName)
+ .Append(")");
+ break;
+ case ComparerTypes.HashSet:
+ codeBuilder
+ .AppendLine()
+ .Append(" && ")
+ .Append("HashSetEquals(")
+ .Append(entityProperty.PropertyName)
+ .Append(", other.")
+ .Append(entityProperty.PropertyName)
+ .Append(")");
+ break;
+ case ComparerTypes.Reference:
+ break;
+ case ComparerTypes.Sequence:
+ codeBuilder
+ .AppendLine()
+ .Append(" && ")
+ .Append("SequenceEquals(")
+ .Append(entityProperty.PropertyName)
+ .Append(", other.")
+ .Append(entityProperty.PropertyName)
+ .Append(")");
+ break;
+ case ComparerTypes.String:
+ codeBuilder
+ .AppendLine()
+ .Append(" && ")
+ .Append("global::System.StringComparer.")
+ .Append(entityProperty.ComparerName)
+ .Append(".Equals(")
+ .Append(entityProperty.PropertyName)
+ .Append(", other.")
+ .Append(entityProperty.PropertyName)
+ .Append(")");
+ break;
+ case ComparerTypes.Custom:
+ codeBuilder
+ .AppendLine()
+ .Append(" && ")
+ .Append(entityProperty.ComparerName)
+ .Append(".")
+ .Append(entityProperty.ComparerInstance ?? "Default")
+ .Append(".Equals(")
+ .Append(entityProperty.PropertyName)
+ .Append(", other.")
+ .Append(entityProperty.PropertyName)
+ .Append(")");
+ break;
+ default:
+ codeBuilder
+ .AppendLine()
+ .Append(" && ")
+ .Append("global::System.Collections.Generic.EqualityComparer<")
+ .Append(entityProperty.PropertyType)
+ .Append(">.Default.Equals(")
+ .Append(entityProperty.PropertyName)
+ .Append(", other.")
+ .Append(entityProperty.PropertyName)
+ .Append(")");
+ break;
+ }
+ }
+
+ codeBuilder
+ .AppendLine(";")
+ .AppendLine();
+
+ GenerateEquatableFunctions(codeBuilder, entityClass);
+
+ codeBuilder
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+ }
+
+ private static void GenerateEquatableFunctions(IndentedStringBuilder codeBuilder, EquatableClass entityClass)
+ {
+ if (entityClass == null)
+ return;
+
+ if (entityClass.Properties.Any(p => p.ComparerType == ComparerTypes.Dictionary))
+ {
+ codeBuilder
+ .AppendLine("static bool DictionaryEquals(global::System.Collections.Generic.IDictionary? left, global::System.Collections.Generic.IDictionary? right)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .AppendLine("if (global::System.Object.ReferenceEquals(left, right))")
+ .AppendLine(" return true;")
+ .AppendLine()
+ .AppendLine("if (left == null || right == null)")
+ .AppendLine(" return false;")
+ .AppendLine()
+ .AppendLine("if (left.Count != right.Count)")
+ .AppendLine(" return false;")
+ .AppendLine();
+
+ codeBuilder
+ .AppendLine("foreach (var pair in left)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .AppendLine("if (!right.TryGetValue(pair.Key, out var value))")
+ .AppendLine(" return false;")
+ .AppendLine()
+ .AppendLine("if (!global::System.Collections.Generic.EqualityComparer.Default.Equals(pair.Value, value))")
+ .AppendLine(" return false;")
+ .AppendLine()
+ .DecrementIndent()
+ .AppendLine("}"); // foreach
+
+ codeBuilder
+ .AppendLine()
+ .AppendLine("return true;")
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+ }
+
+ if (entityClass.Properties.Any(p => p.ComparerType == ComparerTypes.HashSet))
+ {
+ codeBuilder
+ .AppendLine("static bool HashSetEquals(global::System.Collections.Generic.IEnumerable? left, global::System.Collections.Generic.IEnumerable? right)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .AppendLine("if (global::System.Object.ReferenceEquals(left, right))")
+ .AppendLine(" return true;")
+ .AppendLine()
+ .AppendLine("if (left == null || right == null)")
+ .AppendLine(" return false;")
+ .AppendLine()
+ .AppendLine("if (left is ISet leftSet)")
+ .AppendLine(" return leftSet.SetEquals(right);")
+ .AppendLine()
+ .AppendLine("if (right is ISet rightSet)")
+ .AppendLine(" return rightSet.SetEquals(left);")
+ .AppendLine()
+ .AppendLine("var hashSet = new HashSet(left, global::System.Collections.Generic.EqualityComparer.Default);")
+ .AppendLine("return hashSet.SetEquals(right);")
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+ }
+
+ if (entityClass.Properties.Any(p => p.ComparerType == ComparerTypes.Sequence))
+ {
+ codeBuilder
+ .AppendLine("static bool SequenceEquals(global::System.Collections.Generic.IEnumerable? left, global::System.Collections.Generic.IEnumerable? right)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .AppendLine("if (global::System.Object.ReferenceEquals(left, right))")
+ .AppendLine(" return true;")
+ .AppendLine()
+ .AppendLine("if (left == null || right == null)")
+ .AppendLine(" return false;")
+ .AppendLine()
+ .AppendLine("return global::System.Linq.Enumerable.SequenceEqual(left, right, global::System.Collections.Generic.EqualityComparer.Default);")
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+ }
+ }
+
+ private static void GenerateEquals(IndentedStringBuilder codeBuilder, EquatableClass entityClass)
+ {
+ codeBuilder
+ .AppendLine("/// ")
+ .AppendLine("public override bool Equals(object? obj)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .Append("return Equals(obj as ")
+ .Append(entityClass.EntityName)
+ .AppendLine(");")
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+
+ codeBuilder
+ .AppendLine("/// ")
+ .Append("public static bool operator ==(")
+ .Append(entityClass.EntityName)
+ .Append("? left, ")
+ .Append(entityClass.EntityName)
+ .AppendLine("? right)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .Append("return Equals(left, right);")
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+
+ codeBuilder
+ .AppendLine("/// ")
+ .Append("public static bool operator !=(")
+ .Append(entityClass.EntityName)
+ .Append("? left, ")
+ .Append(entityClass.EntityName)
+ .AppendLine("? right)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .Append("return !Equals(left, right);")
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+ }
+
+ private static void GenerateHashCode(IndentedStringBuilder codeBuilder, EquatableClass entityClass)
+ {
+ codeBuilder
+ .AppendLine("/// ")
+ .Append("public override int GetHashCode()")
+ .AppendLine("{")
+ .IncrementIndent();
+
+ codeBuilder
+ .AppendLine("int hashCode = -510739267;");
+
+ foreach (var entityProperty in entityClass.Properties)
+ {
+ switch (entityProperty.ComparerType)
+ {
+ case ComparerTypes.Dictionary:
+ codeBuilder
+ .Append("hashCode = (hashCode * -1521134295) + ")
+ .Append("DictionaryHashCode(")
+ .Append(entityProperty.PropertyName)
+ .AppendLine(");");
+ break;
+ case ComparerTypes.HashSet:
+ codeBuilder
+ .Append("hashCode = (hashCode * -1521134295) + ")
+ .Append("HashSetHashCode(")
+ .Append(entityProperty.PropertyName)
+ .AppendLine(");");
+ break;
+ case ComparerTypes.Reference:
+ break;
+ case ComparerTypes.Sequence:
+ codeBuilder
+ .Append("hashCode = (hashCode * -1521134295) + ")
+ .Append("SequenceHashCode(")
+ .Append(entityProperty.PropertyName)
+ .AppendLine(");");
+ break;
+ case ComparerTypes.String:
+ codeBuilder
+ .Append("hashCode = (hashCode * -1521134295) + ")
+ .Append("global::System.StringComparer.")
+ .Append(entityProperty.ComparerName)
+ .Append(".GetHashCode(")
+ .Append(entityProperty.PropertyName)
+ .AppendLine("!);");
+ break;
+ case ComparerTypes.Custom:
+ codeBuilder
+ .Append("hashCode = (hashCode * -1521134295) + ")
+ .Append(entityProperty.ComparerName)
+ .Append(".")
+ .Append(entityProperty.ComparerInstance ?? "Default")
+ .Append(".GetHashCode(")
+ .Append(entityProperty.PropertyName)
+ .AppendLine("!);");
+ break;
+ default:
+ codeBuilder
+ .Append("hashCode = (hashCode * -1521134295) + ")
+ .Append("global::System.Collections.Generic.EqualityComparer<")
+ .Append(entityProperty.PropertyType)
+ .Append(">.Default.GetHashCode(")
+ .Append(entityProperty.PropertyName)
+ .AppendLine("!);");
+ break;
+ }
+ }
+
+ codeBuilder
+ .AppendLine("return hashCode;")
+ .AppendLine();
+
+ GenerateHashCodeFunctions(codeBuilder, entityClass);
+
+ codeBuilder
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+ }
+
+ private static void GenerateHashCodeFunctions(IndentedStringBuilder codeBuilder, EquatableClass entityClass)
+ {
+ if (entityClass == null)
+ return;
+
+ if (entityClass.Properties.Any(p => p.ComparerType == ComparerTypes.Dictionary))
+ {
+ codeBuilder
+ .AppendLine("static int DictionaryHashCode(global::System.Collections.Generic.IDictionary? items)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .AppendLine("if (items == null)")
+ .AppendLine(" return 0;")
+ .AppendLine();
+
+ codeBuilder
+ .AppendLine("int hashCode = -510739267;")
+ .AppendLine()
+ .AppendLine("// sort by key to ensure dictionary with different order are the same")
+ .AppendLine("foreach (var item in items.OrderBy(d => d.Key))")
+ .AppendLine("{")
+ .IncrementIndent()
+ .AppendLine("hashCode = (hashCode * -1521134295) + global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(item.Key!);")
+ .AppendLine("hashCode = (hashCode * -1521134295) + global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(item.Value!);")
+ .DecrementIndent()
+ .AppendLine("}"); // foreach
+
+ codeBuilder
+ .AppendLine()
+ .AppendLine("return hashCode;")
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+ }
+
+ if (entityClass.Properties.Any(p => p.ComparerType == ComparerTypes.HashSet))
+ {
+ codeBuilder
+ .AppendLine("static int HashSetHashCode(global::System.Collections.Generic.IEnumerable? items)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .AppendLine("if (items == null)")
+ .AppendLine(" return 0;")
+ .AppendLine()
+ .AppendLine("int hashCode = -114976970;")
+ .AppendLine()
+ .AppendLine("// sort to ensure set with different order are the same")
+ .AppendLine("foreach (var item in items.OrderBy(s => s))")
+ .AppendLine(" hashCode = (hashCode * -1521134295) + global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(item!);")
+ .AppendLine()
+ .AppendLine("return hashCode;")
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+ }
+
+ if (entityClass.Properties.Any(p => p.ComparerType == ComparerTypes.Sequence))
+ {
+ codeBuilder
+ .AppendLine("static int SequenceHashCode(global::System.Collections.Generic.IEnumerable? items)")
+ .AppendLine("{")
+ .IncrementIndent()
+ .AppendLine("if (items == null)")
+ .AppendLine(" return 0;")
+ .AppendLine()
+ .AppendLine("int hashCode = -114976970;")
+ .AppendLine()
+ .AppendLine("foreach (var item in items)")
+ .AppendLine(" hashCode = (hashCode * -1521134295) + global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(item!);")
+ .AppendLine()
+ .AppendLine("return hashCode;")
+ .DecrementIndent()
+ .AppendLine("}")
+ .AppendLine();
+ }
+ }
+}
diff --git a/src/Equatable.SourceGenerator/IndentedStringBuilder.cs b/src/Equatable.SourceGenerator/IndentedStringBuilder.cs
new file mode 100644
index 0000000..efd5bf7
--- /dev/null
+++ b/src/Equatable.SourceGenerator/IndentedStringBuilder.cs
@@ -0,0 +1,360 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+
+namespace Equatable.SourceGenerator;
+
+///
+/// A thin wrapper over that adds indentation to each line built.
+///
+[ExcludeFromCodeCoverage]
+public class IndentedStringBuilder
+{
+ private const byte IndentSize = 4;
+ private byte _indent;
+ private bool _indentPending = true;
+
+ private readonly StringBuilder _stringBuilder = new();
+
+ ///
+ /// The current length of the built string.
+ ///
+ public virtual int Length
+ => _stringBuilder.Length;
+
+ ///
+ /// Appends the current indent and then the given string to the string being built.
+ ///
+ /// The string to append.
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder Append(string? value)
+ {
+ DoIndent();
+
+ _stringBuilder.Append(value);
+
+ return this;
+ }
+
+ ///
+ /// Appends the current indent and then the given string to the string being built.
+ ///
+ /// The value to append.
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder Append(T? value)
+ {
+ if (value == null)
+ return this;
+
+ DoIndent();
+
+ _stringBuilder.Append(value.ToString());
+
+ return this;
+ }
+
+ ///
+ /// Appends the current indent and then the given char to the string being built.
+ ///
+ /// The char to append.
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder Append(char value)
+ {
+ DoIndent();
+
+ _stringBuilder.Append(value);
+
+ return this;
+ }
+
+ ///
+ /// Appends the current indent and then the given strings to the string being built.
+ ///
+ /// The strings to append.
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder Append(IEnumerable value)
+ {
+ DoIndent();
+
+ foreach (var str in value)
+ _stringBuilder.Append(str);
+
+ return this;
+ }
+
+ ///
+ /// Appends the current indent and then the given chars to the string being built.
+ ///
+ /// The chars to append.
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder Append(IEnumerable value)
+ {
+ DoIndent();
+
+ foreach (var chr in value)
+ _stringBuilder.Append(chr);
+
+ return this;
+ }
+
+ ///
+ /// Appends the current indent and then the given strings to the string being built.
+ ///
+ /// The type of the members of values.
+ /// The string to use as a separator. separator is included in the concatenated and appended strings only if values has more than one element.
+ /// A collection that contains the objects to concatenate and append to the current instance of the string builder.
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder AppendJoin(IEnumerable values, string separator)
+ {
+ if (values is null)
+ throw new ArgumentNullException(nameof(values));
+
+ separator ??= string.Empty;
+
+ DoIndent();
+
+ var wroteValue = false;
+
+ foreach (var value in values)
+ {
+ if (wroteValue)
+ _stringBuilder.Append(separator);
+
+ _stringBuilder.Append(value);
+ wroteValue = true;
+ }
+
+ return this;
+ }
+
+ /// Appends the current indent and then the given strings to the string being built.
+ /// The type of the members of values.
+ /// A collection that contains the objects to concatenate and append to the current instance of the string builder.
+ /// The string to use as a separator. separator is included in the concatenated and appended strings only if values has more than one element.
+ ///
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder AppendJoinIf(IEnumerable values, string separator, Func, bool>? condition = null)
+ {
+ var c = condition ?? (s => s.Any());
+
+ if (c(values))
+ AppendJoin(values, separator);
+
+ return this;
+ }
+
+ /// Appends the current indent and then the given strings to the string being built.
+ /// The type of the members of values.
+ /// A collection that contains the objects to concatenate and append to the current instance of the string builder.
+ /// The string to use as a separator. separator is included in the concatenated and appended strings only if values has more than one element.
+ ///
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder AppendJoinIf(IEnumerable values, string separator, bool condition)
+ {
+ if (condition)
+ AppendJoin(values, separator);
+
+ return this;
+ }
+
+ ///
+ /// Appends a new line to the string being built.
+ ///
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder AppendLine()
+ {
+ AppendLine(string.Empty);
+
+ return this;
+ }
+
+ ///
+ ///
+ /// Appends the current indent, the given string, and a new line to the string being built.
+ ///
+ ///
+ /// If the given string itself contains a new line, the part of the string after that new line will not be indented.
+ ///
+ ///
+ /// The string to append.
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder AppendLine(string value)
+ {
+ if (value.Length != 0)
+ DoIndent();
+
+ _stringBuilder.AppendLine(value);
+
+ _indentPending = true;
+
+ return this;
+ }
+
+ ///
+ /// Separates the given string into lines, and then appends each line, prefixed
+ /// by the current indent and followed by a new line, to the string being built.
+ ///
+ /// The string to append.
+ /// If , then the terminating new line is not added after the last line.
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder AppendLines(string value, bool skipFinalNewline = false)
+ {
+ using (var reader = new StringReader(value))
+ {
+ var first = true;
+ string line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ if (first)
+ first = false;
+ else
+ AppendLine();
+
+ if (line.Length != 0)
+ Append(line);
+ }
+ }
+
+ if (!skipFinalNewline)
+ AppendLine();
+
+ return this;
+ }
+
+ ///
+ /// Appends a copy of the specified string if is met.
+ ///
+ /// The string to append.
+ /// The condition delegate to evaluate. If condition is null, String.IsNullOrWhiteSpace method will be used.
+ public IndentedStringBuilder AppendIf(string text, Func? condition = null)
+ {
+ var c = condition ?? (s => !string.IsNullOrEmpty(s));
+
+ if (c(text))
+ Append(text);
+
+ return this;
+ }
+
+ ///
+ /// Appends a copy of the specified string if is met.
+ ///
+ /// The string to append.
+ /// The condition delegate to evaluate. If condition is null, String.IsNullOrWhiteSpace method will be used.
+ public IndentedStringBuilder AppendIf(string? text, bool condition)
+ {
+ if (condition)
+ Append(text);
+
+ return this;
+ }
+
+ ///
+ /// Appends a copy of the specified string followed by the default line terminator if is met.
+ ///
+ /// The string to append.
+ /// The condition delegate to evaluate. If condition is null, String.IsNullOrWhiteSpace method will be used.
+ public IndentedStringBuilder AppendLineIf(string text, Func? condition = null)
+ {
+ var c = condition ?? (s => !string.IsNullOrEmpty(s));
+
+ if (c(text))
+ AppendLine(text);
+
+ return this;
+ }
+
+ ///
+ /// Resets this builder ready to build a new string.
+ ///
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder Clear()
+ {
+ _stringBuilder.Clear();
+ _indent = 0;
+
+ return this;
+ }
+
+ ///
+ /// Increments the indent.
+ ///
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder IncrementIndent()
+ {
+ _indent++;
+
+ return this;
+ }
+
+ ///
+ /// Decrements the indent.
+ ///
+ /// This builder so that additional calls can be chained.
+ public virtual IndentedStringBuilder DecrementIndent()
+ {
+ if (_indent > 0)
+ _indent--;
+
+ return this;
+ }
+
+ ///
+ /// Creates a scoped indenter that will increment the indent, then decrement it when disposed.
+ ///
+ /// An indenter.
+ public virtual IDisposable Indent()
+ => new Indenter(this);
+
+ ///
+ /// Temporarily disables all indentation. Restores the original indentation when the returned object is disposed.
+ ///
+ /// An object that restores the original indentation when disposed.
+ public virtual IDisposable SuspendIndent()
+ => new IndentSuspender(this);
+
+ ///
+ /// Returns the built string.
+ ///
+ /// The built string.
+ public override string ToString()
+ => _stringBuilder.ToString();
+
+ private void DoIndent()
+ {
+ if (_indentPending && _indent > 0)
+ _stringBuilder.Append(' ', _indent * IndentSize);
+
+ _indentPending = false;
+ }
+
+ private sealed class Indenter : IDisposable
+ {
+ private readonly IndentedStringBuilder _stringBuilder;
+
+ public Indenter(IndentedStringBuilder stringBuilder)
+ {
+ _stringBuilder = stringBuilder;
+
+ _stringBuilder.IncrementIndent();
+ }
+
+ public void Dispose()
+ => _stringBuilder.DecrementIndent();
+ }
+
+ private sealed class IndentSuspender : IDisposable
+ {
+ private readonly IndentedStringBuilder _stringBuilder;
+ private readonly byte _indent;
+
+ public IndentSuspender(IndentedStringBuilder stringBuilder)
+ {
+ _stringBuilder = stringBuilder;
+ _indent = _stringBuilder._indent;
+ _stringBuilder._indent = 0;
+ }
+
+ public void Dispose()
+ => _stringBuilder._indent = _indent;
+ }
+}
diff --git a/src/Equatable.SourceGenerator/Models/ComparerTypes.cs b/src/Equatable.SourceGenerator/Models/ComparerTypes.cs
new file mode 100644
index 0000000..28a118d
--- /dev/null
+++ b/src/Equatable.SourceGenerator/Models/ComparerTypes.cs
@@ -0,0 +1,12 @@
+namespace Equatable.SourceGenerator.Models;
+
+public enum ComparerTypes
+{
+ Default,
+ Dictionary,
+ HashSet,
+ Reference,
+ Sequence,
+ String,
+ Custom
+}
diff --git a/src/Equatable.SourceGenerator/Models/EquatableArray.cs b/src/Equatable.SourceGenerator/Models/EquatableArray.cs
new file mode 100644
index 0000000..2e84f77
--- /dev/null
+++ b/src/Equatable.SourceGenerator/Models/EquatableArray.cs
@@ -0,0 +1,45 @@
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Equatable.SourceGenerator.Models;
+
+public readonly struct EquatableArray(T[] array) : IEquatable>, IEnumerable
+ where T : IEquatable
+{
+ public T[] Array { get; } = array ?? [];
+
+ public int Count => Array.Length;
+
+ public ReadOnlySpan AsSpan() => Array.AsSpan();
+
+ public T[] AsArray() => Array;
+
+
+ public static bool operator ==(EquatableArray left, EquatableArray right) => left.Equals(right);
+
+ public static bool operator !=(EquatableArray left, EquatableArray right) => !left.Equals(right);
+
+ public bool Equals(EquatableArray array) => Array.AsSpan().SequenceEqual(array.AsSpan());
+
+ public override bool Equals(object? obj) => obj is EquatableArray array && Equals(this, array);
+
+ public override int GetHashCode()
+ {
+ if (Array is not T[] array)
+ return 0;
+
+ HashCode hashCode = default;
+ foreach (T item in array)
+ hashCode.Add(item);
+
+ return hashCode.ToHashCode();
+ }
+
+
+ IEnumerator IEnumerable.GetEnumerator() => (Array as IEnumerable).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => Array.GetEnumerator();
+
+
+ public static implicit operator EquatableArray(T[] array) => new(array);
+}
diff --git a/src/Equatable.SourceGenerator/Models/EquatableClass.cs b/src/Equatable.SourceGenerator/Models/EquatableClass.cs
new file mode 100644
index 0000000..3105654
--- /dev/null
+++ b/src/Equatable.SourceGenerator/Models/EquatableClass.cs
@@ -0,0 +1,6 @@
+namespace Equatable.SourceGenerator.Models;
+
+public record EquatableClass(
+ string EntityNamespace,
+ string EntityName,
+ EquatableArray Properties);
diff --git a/src/Equatable.SourceGenerator/Models/EquatableContext.cs b/src/Equatable.SourceGenerator/Models/EquatableContext.cs
new file mode 100644
index 0000000..2d26593
--- /dev/null
+++ b/src/Equatable.SourceGenerator/Models/EquatableContext.cs
@@ -0,0 +1,7 @@
+using Microsoft.CodeAnalysis;
+
+namespace Equatable.SourceGenerator.Models;
+
+public record EquatableContext(
+ EquatableClass? EntityClass,
+ EquatableArray? Diagnostics);
diff --git a/src/Equatable.SourceGenerator/Models/EquatableProperty.cs b/src/Equatable.SourceGenerator/Models/EquatableProperty.cs
new file mode 100644
index 0000000..652a353
--- /dev/null
+++ b/src/Equatable.SourceGenerator/Models/EquatableProperty.cs
@@ -0,0 +1,8 @@
+namespace Equatable.SourceGenerator.Models;
+
+public record EquatableProperty(
+ string PropertyName,
+ string PropertyType,
+ ComparerTypes ComparerType = ComparerTypes.Default,
+ string? ComparerName = null,
+ string? ComparerInstance = null);
diff --git a/src/Equatable.SourceGenerator/Properties/launchSettings.json b/src/Equatable.SourceGenerator/Properties/launchSettings.json
new file mode 100644
index 0000000..a0eae00
--- /dev/null
+++ b/src/Equatable.SourceGenerator/Properties/launchSettings.json
@@ -0,0 +1,9 @@
+{
+ "profiles": {
+ "Equatable.Entities": {
+ "commandName": "DebugRoslynComponent",
+ "targetProject": "..\\..\\test\\Equatable.Entities\\Equatable.Entities.csproj"
+ }
+
+ }
+}
diff --git a/test/Equatable.Entities/Audit.cs b/test/Equatable.Entities/Audit.cs
new file mode 100644
index 0000000..947ef29
--- /dev/null
+++ b/test/Equatable.Entities/Audit.cs
@@ -0,0 +1,15 @@
+using System;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class Audit : ModelBase
+{
+ public DateTime Date { get; set; }
+ public int? UserId { get; set; }
+ public int? TaskId { get; set; }
+ public string? Content { get; set; }
+ public string? UserName { get; set; }
+}
diff --git a/test/Equatable.Entities/DataType.cs b/test/Equatable.Entities/DataType.cs
new file mode 100644
index 0000000..2bac84c
--- /dev/null
+++ b/test/Equatable.Entities/DataType.cs
@@ -0,0 +1,65 @@
+using System;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class DataType
+{
+ public long Id { get; set; }
+
+ public string Name { get; set; } = null!;
+
+ public bool Boolean { get; set; }
+
+ public short Short { get; set; }
+
+ public long Long { get; set; }
+
+ public float Float { get; set; }
+
+ public double Double { get; set; }
+
+ public decimal Decimal { get; set; }
+
+ public DateTime DateTime { get; set; }
+
+ public DateTimeOffset DateTimeOffset { get; set; }
+
+ public Guid Guid { get; set; }
+
+ public TimeSpan TimeSpan { get; set; }
+
+#if NET6_0_OR_GREATER
+ public DateOnly DateOnly { get; set; }
+
+ public TimeOnly TimeOnly { get; set; }
+#endif
+
+ public bool? BooleanNull { get; set; }
+
+ public short? ShortNull { get; set; }
+
+ public long? LongNull { get; set; }
+
+ public float? FloatNull { get; set; }
+
+ public double? DoubleNull { get; set; }
+
+ public decimal? DecimalNull { get; set; }
+
+ public DateTime? DateTimeNull { get; set; }
+
+ public DateTimeOffset? DateTimeOffsetNull { get; set; }
+
+ public Guid? GuidNull { get; set; }
+
+ public TimeSpan? TimeSpanNull { get; set; }
+
+#if NET6_0_OR_GREATER
+ public DateOnly? DateOnlyNull { get; set; }
+
+ public TimeOnly? TimeOnlyNull { get; set; }
+#endif
+}
diff --git a/test/Equatable.Entities/Equatable.Entities.csproj b/test/Equatable.Entities/Equatable.Entities.csproj
new file mode 100644
index 0000000..ea7d562
--- /dev/null
+++ b/test/Equatable.Entities/Equatable.Entities.csproj
@@ -0,0 +1,27 @@
+
+
+
+ netstandard2.0;net6.0;net8.0
+ false
+ enable
+ latest
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ Analyzer
+ false
+
+
+
+
+
diff --git a/test/Equatable.Entities/Member.cs b/test/Equatable.Entities/Member.cs
new file mode 100644
index 0000000..05a5335
--- /dev/null
+++ b/test/Equatable.Entities/Member.cs
@@ -0,0 +1,19 @@
+using System;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class Member
+{
+ public Guid Id { get; set; }
+
+ public string EmailAddress { get; set; } = null!;
+
+ public string DisplayName { get; set; } = null!;
+
+ public string? FirstName { get; set; }
+
+ public string? LastName { get; set; }
+}
diff --git a/test/Equatable.Entities/ModelBase.cs b/test/Equatable.Entities/ModelBase.cs
new file mode 100644
index 0000000..a73f93e
--- /dev/null
+++ b/test/Equatable.Entities/ModelBase.cs
@@ -0,0 +1,21 @@
+using System;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public abstract partial class ModelBase
+{
+ public int Id { get; set; }
+
+ public DateTimeOffset Created { get; set; }
+
+ public string? CreatedBy { get; set; }
+
+ public DateTimeOffset Updated { get; set; }
+
+ public string? UpdatedBy { get; set; }
+
+ public long RowVersion { get; set; }
+}
diff --git a/test/Equatable.Entities/Priority.cs b/test/Equatable.Entities/Priority.cs
new file mode 100644
index 0000000..19c8c81
--- /dev/null
+++ b/test/Equatable.Entities/Priority.cs
@@ -0,0 +1,12 @@
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class Priority : ModelBase
+{
+ public string Name { get; set; } = null!;
+ public string? Description { get; set; }
+ public int DisplayOrder { get; set; }
+ public bool IsActive { get; set; }
+}
diff --git a/test/Equatable.Entities/Role.cs b/test/Equatable.Entities/Role.cs
new file mode 100644
index 0000000..435c6c6
--- /dev/null
+++ b/test/Equatable.Entities/Role.cs
@@ -0,0 +1,12 @@
+using System;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class Role : ModelBase
+{
+ public string Name { get; set; } = null!;
+ public string? Description { get; set; }
+}
diff --git a/test/Equatable.Entities/Status.cs b/test/Equatable.Entities/Status.cs
new file mode 100644
index 0000000..8c7c1bb
--- /dev/null
+++ b/test/Equatable.Entities/Status.cs
@@ -0,0 +1,13 @@
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+
+[Equatable]
+public partial class Status : ModelBase
+{
+ public string Name { get; set; } = null!;
+ public string? Description { get; set; }
+ public int DisplayOrder { get; set; }
+ public bool IsActive { get; set; }
+}
diff --git a/test/Equatable.Entities/StatusConstructor.cs b/test/Equatable.Entities/StatusConstructor.cs
new file mode 100644
index 0000000..565cf7a
--- /dev/null
+++ b/test/Equatable.Entities/StatusConstructor.cs
@@ -0,0 +1,45 @@
+using System;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class StatusConstructor
+{
+ public StatusConstructor(
+ int id,
+ string name,
+ string description,
+ bool isActive,
+ int displayOrder,
+ DateTimeOffset created,
+ string createdBy,
+ DateTimeOffset updated,
+ string updatedBy,
+ long rowVersion)
+ {
+ Id = id;
+ Name = name;
+ Description = description;
+ DisplayOrder = displayOrder;
+ IsActive = isActive;
+ Created = created;
+ CreatedBy = createdBy;
+ Updated = updated;
+ UpdatedBy = updatedBy;
+ RowVersion = rowVersion;
+ }
+
+ public int Id { get; }
+ public string Name { get; }
+ public string Description { get; }
+ public int DisplayOrder { get; }
+ public bool IsActive { get; }
+ public DateTimeOffset Created { get; }
+ public string CreatedBy { get; }
+ public DateTimeOffset Updated { get; }
+ public string UpdatedBy { get; }
+
+ public long RowVersion { get; }
+}
diff --git a/test/Equatable.Entities/StatusReadOnly.cs b/test/Equatable.Entities/StatusReadOnly.cs
new file mode 100644
index 0000000..85d21c9
--- /dev/null
+++ b/test/Equatable.Entities/StatusReadOnly.cs
@@ -0,0 +1,22 @@
+using System;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class StatusReadOnly
+{
+ public int Id { get; init; }
+
+ public string Name { get; init; } = null!;
+ public string? Description { get; init; }
+ public int DisplayOrder { get; init; }
+ public bool IsActive { get; init; }
+
+ public DateTimeOffset Created { get; init; }
+ public string? CreatedBy { get; init; }
+ public DateTimeOffset Updated { get; init; }
+ public string? UpdatedBy { get; init; }
+ public long RowVersion { get; init; }
+}
diff --git a/test/Equatable.Entities/StatusRecord.cs b/test/Equatable.Entities/StatusRecord.cs
new file mode 100644
index 0000000..146243a
--- /dev/null
+++ b/test/Equatable.Entities/StatusRecord.cs
@@ -0,0 +1,18 @@
+using System;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+public partial record StatusRecord(
+ int Id,
+ string Name,
+ string? Description,
+ int DisplayOrder,
+ bool IsActive,
+ DateTimeOffset Created,
+ string? CreatedBy,
+ DateTimeOffset Updated,
+ string? UpdatedBy,
+ long RowVersion
+);
diff --git a/test/Equatable.Entities/StatusRecordList.cs b/test/Equatable.Entities/StatusRecordList.cs
new file mode 100644
index 0000000..11bf1f8
--- /dev/null
+++ b/test/Equatable.Entities/StatusRecordList.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+public partial record StatusRecordList(
+ int Id,
+ string Name,
+ string? Description,
+ int DisplayOrder,
+ bool IsActive,
+ DateTimeOffset Created,
+ string? CreatedBy,
+ DateTimeOffset Updated,
+ string? UpdatedBy,
+ long RowVersion,
+ List Versions
+);
diff --git a/test/Equatable.Entities/Task.cs b/test/Equatable.Entities/Task.cs
new file mode 100644
index 0000000..66146eb
--- /dev/null
+++ b/test/Equatable.Entities/Task.cs
@@ -0,0 +1,33 @@
+using System;
+
+using Equatable.Attributes;
+using Equatable.Generator.Entities;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class Task : ModelBase
+{
+ public int? StatusId { get; set; }
+
+ public int? PriorityId { get; set; }
+
+ public string Title { get; set; } = null!;
+
+ public string? Description { get; set; }
+
+ public DateTimeOffset? StartDate { get; set; }
+
+ public DateTimeOffset? DueDate { get; set; }
+
+ public DateTimeOffset? CompleteDate { get; set; }
+
+ public int? AssignedId { get; set; }
+
+
+ public Priority? Priority { get; set; }
+
+ public Status? Status { get; set; }
+
+ public User? AssignedUser { get; set; }
+}
diff --git a/test/Equatable.Entities/User.cs b/test/Equatable.Entities/User.cs
new file mode 100644
index 0000000..17f2eae
--- /dev/null
+++ b/test/Equatable.Entities/User.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+
+using Equatable.Attributes;
+using Equatable.Entities;
+
+namespace Equatable.Generator.Entities;
+
+[Equatable]
+public partial class User : ModelBase
+{
+ [StringEquality(StringComparison.OrdinalIgnoreCase)]
+ public string EmailAddress { get; set; } = null!;
+
+ public bool IsEmailAddressConfirmed { get; set; }
+
+ public string? DisplayName { get; set; }
+
+ public string? FirstName { get; set; }
+
+ public string? LastName { get; set; }
+
+ public string? PasswordHash { get; set; }
+
+ public string? ResetHash { get; set; }
+
+ public string? InviteHash { get; set; }
+
+ public int AccessFailedCount { get; set; }
+
+ public bool LockoutEnabled { get; set; }
+
+ public DateTimeOffset? LockoutEnd { get; set; }
+
+ public DateTimeOffset? LastLogin { get; set; }
+
+ public bool IsDeleted { get; set; }
+
+ [SequenceEquality]
+ public List? AssignedTasks { get; set; }
+
+ [SequenceEquality]
+ public List? Roles { get; set; }
+}
diff --git a/test/Equatable.Entities/UserImport.cs b/test/Equatable.Entities/UserImport.cs
new file mode 100644
index 0000000..1a8c2bd
--- /dev/null
+++ b/test/Equatable.Entities/UserImport.cs
@@ -0,0 +1,17 @@
+using System;
+
+using Equatable.Attributes;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class UserImport
+{
+ [StringEquality(StringComparison.OrdinalIgnoreCase)]
+ public string EmailAddress { get; set; } = null!;
+ public string? DisplayName { get; set; }
+ public string? FirstName { get; set; }
+ public string? LastName { get; set; }
+ public DateTimeOffset? LockoutEnd { get; set; }
+ public DateTimeOffset? LastLogin { get; set; }
+}
diff --git a/test/Equatable.Entities/UserLogin.cs b/test/Equatable.Entities/UserLogin.cs
new file mode 100644
index 0000000..add21e6
--- /dev/null
+++ b/test/Equatable.Entities/UserLogin.cs
@@ -0,0 +1,19 @@
+using System;
+
+using Equatable.Attributes;
+using Equatable.Generator.Entities;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class UserLogin : ModelBase
+{
+ public string? EmailAddress { get; set; }
+ public Guid? UserId { get; set; }
+ public string? UserAgent { get; set; }
+ public string? IpAddress { get; set; }
+ public bool IsSuccessful { get; set; }
+ public string? FailureMessage { get; set; }
+
+ public User? User { get; set; }
+}
diff --git a/test/Equatable.Entities/UserRole.cs b/test/Equatable.Entities/UserRole.cs
new file mode 100644
index 0000000..5eddf6a
--- /dev/null
+++ b/test/Equatable.Entities/UserRole.cs
@@ -0,0 +1,16 @@
+using System;
+
+using Equatable.Attributes;
+using Equatable.Generator.Entities;
+
+namespace Equatable.Entities;
+
+[Equatable]
+public partial class UserRole
+{
+ public Guid UserId { get; set; }
+ public Guid RoleId { get; set; }
+
+ public User User { get; set; } = null!;
+ public Role Role { get; set; } = null!;
+}
diff --git a/test/Equatable.Generator.Tests/Comparers/DictionaryEqualityComparerTests.cs b/test/Equatable.Generator.Tests/Comparers/DictionaryEqualityComparerTests.cs
new file mode 100644
index 0000000..87fb42e
--- /dev/null
+++ b/test/Equatable.Generator.Tests/Comparers/DictionaryEqualityComparerTests.cs
@@ -0,0 +1,173 @@
+using Equatable.Comparers;
+
+namespace Equatable.Generator.Tests.Comparers;
+
+public class DictionaryEqualityComparerTests
+{
+ [Fact]
+ public void DefaultEquals()
+ {
+ var a = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var b = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var comparer = DictionaryEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeTrue();
+ }
+
+ [Fact]
+ public void NotEqualsKeys()
+ {
+ var a = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var b = new Dictionary
+ {
+ ["c"] = -10,
+ ["b"] = 5
+ };
+
+ var comparer = DictionaryEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void NotEqualsValues()
+ {
+ var a = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var b = new Dictionary
+ {
+ ["a"] = -10,
+ ["b"] = 5
+ };
+
+ var comparer = DictionaryEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void NotEqualsValuesNull()
+ {
+ var a = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var b = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = null
+ };
+
+ var comparer = DictionaryEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void EqualsValuesNull()
+ {
+ var a = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = null
+ };
+
+ var b = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = null
+ };
+
+ var comparer = DictionaryEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeTrue();
+ }
+
+ [Fact]
+ public void NotEqualsCount()
+ {
+ var a = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var b = new Dictionary();
+
+ var comparer = DictionaryEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void NotEqualsNull()
+ {
+ var a = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var comparer = DictionaryEqualityComparer.Default;
+ comparer.Equals(a, null).Should().BeFalse();
+ }
+
+ [Fact]
+ public void GetHashCodeSame()
+ {
+ var a = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var b = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var comparer = DictionaryEqualityComparer.Default;
+ var aHash = comparer.GetHashCode(a);
+ var bHash = comparer.GetHashCode(b);
+
+ aHash.Should().Be(bHash);
+ }
+
+ [Fact]
+ public void GetHashCodeSameDifferentOrder()
+ {
+ var a = new Dictionary
+ {
+ ["a"] = 10,
+ ["b"] = 5
+ };
+
+ var b = new Dictionary
+ {
+ ["b"] = 5,
+ ["a"] = 10
+ };
+
+ var comparer = DictionaryEqualityComparer.Default;
+ var aHash = comparer.GetHashCode(a);
+ var bHash = comparer.GetHashCode(b);
+
+ aHash.Should().Be(bHash);
+ }
+}
diff --git a/test/Equatable.Generator.Tests/Comparers/HashSetEqualityComparerTests.cs b/test/Equatable.Generator.Tests/Comparers/HashSetEqualityComparerTests.cs
new file mode 100644
index 0000000..1a5a6a5
--- /dev/null
+++ b/test/Equatable.Generator.Tests/Comparers/HashSetEqualityComparerTests.cs
@@ -0,0 +1,96 @@
+using Equatable.Comparers;
+
+namespace Equatable.Generator.Tests.Comparers;
+
+public class HashSetEqualityComparerTests
+{
+ [Fact]
+ public void DefaultEquals()
+ {
+ var a = new HashSet([10, 5]);
+
+ var b = new HashSet([10, 5]);
+
+ var comparer = HashSetEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeTrue();
+ }
+
+ [Fact]
+ public void EqualsOutOfOrder()
+ {
+ var a = new HashSet([10, 5]);
+
+ var b = new HashSet([5, 10]);
+
+ var comparer = HashSetEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeTrue();
+ }
+
+ [Fact]
+ public void NotEqualsValues()
+ {
+ var a = new HashSet([10, 5]);
+
+ var b = new HashSet([-10, 5]);
+
+ var comparer = HashSetEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void NotEqualsValuesNull()
+ {
+ var a = new HashSet([10, 5]);
+
+ var b = new HashSet([10, null]);
+
+ var comparer = HashSetEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void EqualsValuesNull()
+ {
+ var a = new HashSet([10, null]);
+
+ var b = new HashSet([10, null]);
+
+
+ var comparer = HashSetEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeTrue();
+ }
+
+ [Fact]
+ public void NotEqualsCount()
+ {
+ var a = new HashSet([10, 5]);
+
+ var b = new HashSet();
+
+ var comparer = HashSetEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void NotEqualsNull()
+ {
+ var a = new HashSet([10, 5]);
+
+ var comparer = HashSetEqualityComparer.Default;
+ comparer.Equals(a, null).Should().BeFalse();
+ }
+
+ [Fact]
+ public void GetHashCodeSame()
+ {
+ var a = new HashSet([10, 5]);
+
+ var b = new HashSet([5, 10]);
+
+ var comparer = HashSetEqualityComparer.Default;
+ var aHash = comparer.GetHashCode(a);
+ var bHash = comparer.GetHashCode(b);
+
+ aHash.Should().Be(bHash);
+ }
+}
diff --git a/test/Equatable.Generator.Tests/Comparers/SequenceEqualityComparerTests.cs b/test/Equatable.Generator.Tests/Comparers/SequenceEqualityComparerTests.cs
new file mode 100644
index 0000000..da95763
--- /dev/null
+++ b/test/Equatable.Generator.Tests/Comparers/SequenceEqualityComparerTests.cs
@@ -0,0 +1,85 @@
+using Equatable.Comparers;
+
+namespace Equatable.Generator.Tests.Comparers;
+
+public class SequenceEqualityComparerTests
+{
+ [Fact]
+ public void DefaultEquals()
+ {
+ var a = new List([10, 5]);
+
+ var b = new List([10, 5]);
+
+ var comparer = SequenceEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeTrue();
+ }
+
+ [Fact]
+ public void NotEqualsValues()
+ {
+ var a = new List([10, 5]);
+
+ var b = new List([-10, 5]);
+
+ var comparer = SequenceEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void NotEqualsValuesNull()
+ {
+ var a = new List([10, 5]);
+
+ var b = new List([10, null]);
+
+ var comparer = SequenceEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void EqualsValuesNull()
+ {
+ var a = new List([10, null]);
+
+ var b = new List([10, null]);
+
+
+ var comparer = SequenceEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeTrue();
+ }
+
+ [Fact]
+ public void NotEqualsCount()
+ {
+ var a = new List([10, 5]);
+
+ var b = new List();
+
+ var comparer = SequenceEqualityComparer.Default;
+ comparer.Equals(a, b).Should().BeFalse();
+ }
+
+ [Fact]
+ public void NotEqualsNull()
+ {
+ var a = new List([10, 5]);
+
+ var comparer = SequenceEqualityComparer.Default;
+ comparer.Equals(a, null).Should().BeFalse();
+ }
+
+ [Fact]
+ public void GetHashCodeSame()
+ {
+ var a = new List([10, 5]);
+
+ var b = new List([10, 5]);
+
+ var comparer = SequenceEqualityComparer.Default;
+ var aHash = comparer.GetHashCode(a);
+ var bHash = comparer.GetHashCode(b);
+
+ aHash.Should().Be(bHash);
+ }
+}
diff --git a/test/Equatable.Generator.Tests/Equatable.Generator.Tests.csproj b/test/Equatable.Generator.Tests/Equatable.Generator.Tests.csproj
new file mode 100644
index 0000000..b017302
--- /dev/null
+++ b/test/Equatable.Generator.Tests/Equatable.Generator.Tests.csproj
@@ -0,0 +1,39 @@
+
+
+
+ net8.0
+ enable
+ enable
+ latest
+ true
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Equatable.SourceGenerator.Tests/Equatable.SourceGenerator.Tests.csproj b/test/Equatable.SourceGenerator.Tests/Equatable.SourceGenerator.Tests.csproj
new file mode 100644
index 0000000..b7f5521
--- /dev/null
+++ b/test/Equatable.SourceGenerator.Tests/Equatable.SourceGenerator.Tests.csproj
@@ -0,0 +1,41 @@
+
+
+
+ net8.0
+ enable
+ enable
+ latest
+ true
+ false
+ true
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Equatable.SourceGenerator.Tests/EquatableWriterTest.cs b/test/Equatable.SourceGenerator.Tests/EquatableWriterTest.cs
new file mode 100644
index 0000000..518cdee
--- /dev/null
+++ b/test/Equatable.SourceGenerator.Tests/EquatableWriterTest.cs
@@ -0,0 +1,31 @@
+using Equatable.SourceGenerator.Models;
+
+namespace Equatable.SourceGenerator.Tests;
+
+public class EquatableWriterTest
+{
+ [Fact]
+ public async Task GenerateBasicUser()
+ {
+ var entityClass = new EquatableClass(
+ "Equatable.Generator.Entities",
+ "User",
+ new EquatableArray([
+ new EquatableProperty("Id", "System.Guid"),
+ new EquatableProperty("FirstName", "string"),
+ new EquatableProperty("LastName", "string"),
+ new EquatableProperty("EmailAddress", "string", ComparerTypes.String, "OrdinalIgnoreCase"),
+ new EquatableProperty("Created", "System.DateTimeOffset"),
+ new EquatableProperty("Roles", "ICollection", ComparerTypes.Sequence),
+ ])
+ );
+
+ var output = EquatableWriter.Generate(entityClass);
+
+ await Verifier
+ .Verify(output)
+ .UseDirectory("Snapshots")
+ .ScrubLinesContaining("GeneratedCodeAttribute");
+ }
+
+}
diff --git a/test/Equatable.SourceGenerator.Tests/Snapshots/EquatableWriterTest.GenerateBasicUser.verified.txt b/test/Equatable.SourceGenerator.Tests/Snapshots/EquatableWriterTest.GenerateBasicUser.verified.txt
new file mode 100644
index 0000000..19373a0
--- /dev/null
+++ b/test/Equatable.SourceGenerator.Tests/Snapshots/EquatableWriterTest.GenerateBasicUser.verified.txt
@@ -0,0 +1,75 @@
+//
+#nullable enable
+
+namespace Equatable.Generator.Entities
+{
+ partial class User : global::System.IEquatable
+ {
+ ///
+ public bool Equals(User? other)
+ {
+ return other is not null
+ && global::System.Collections.Generic.EqualityComparer.Default.Equals(Id, other.Id)
+ && global::System.Collections.Generic.EqualityComparer.Default.Equals(FirstName, other.FirstName)
+ && global::System.Collections.Generic.EqualityComparer.Default.Equals(LastName, other.LastName)
+ && global::System.StringComparer.OrdinalIgnoreCase.Equals(EmailAddress, other.EmailAddress)
+ && global::System.Collections.Generic.EqualityComparer.Default.Equals(Created, other.Created)
+ && SequenceEquals(Roles, other.Roles);
+
+ static bool SequenceEquals(global::System.Collections.Generic.IEnumerable? left, global::System.Collections.Generic.IEnumerable? right)
+ {
+ if (global::System.Object.ReferenceEquals(left, right))
+ return true;
+
+ if (left == null || right == null)
+ return false;
+
+ return global::System.Linq.Enumerable.SequenceEqual(left, right, global::System.Collections.Generic.EqualityComparer.Default);
+ }
+
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ return Equals(obj as User);
+ }
+
+ ///
+ public static bool operator ==(User? left, User? right)
+ {
+ return Equals(left, right);}
+
+ ///
+ public static bool operator !=(User? left, User? right)
+ {
+ return !Equals(left, right);}
+
+ ///
+ public override int GetHashCode(){
+ int hashCode = -510739267;
+ hashCode = (hashCode * -1521134295) + global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(Id!);
+ hashCode = (hashCode * -1521134295) + global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(FirstName!);
+ hashCode = (hashCode * -1521134295) + global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(LastName!);
+ hashCode = (hashCode * -1521134295) + global::System.StringComparer.OrdinalIgnoreCase.GetHashCode(EmailAddress!);
+ hashCode = (hashCode * -1521134295) + global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(Created!);
+ hashCode = (hashCode * -1521134295) + SequenceHashCode(Roles);
+ return hashCode;
+
+ static int SequenceHashCode(global::System.Collections.Generic.IEnumerable? items)
+ {
+ if (items == null)
+ return 0;
+
+ int hashCode = -114976970;
+
+ foreach (var item in items)
+ hashCode = (hashCode * -1521134295) + global::System.Collections.Generic.EqualityComparer.Default.GetHashCode(item!);
+
+ return hashCode;
+ }
+
+ }
+
+ }
+}