├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── scripts
└── nuget-pack.bat
└── src
├── T4Immutable-netstd2
├── Attributes.cs
├── Helpers.cs
├── KeyValuePairHelper.cs
├── OptParam.cs
├── T4Immutable-netstd2.csproj
└── T4Immutable.nuspec
├── T4Immutable.sln
├── T4Immutable.sln.DotSettings
├── content
└── T4Immutable
│ ├── Attributes.cs_
│ ├── Class.ttinclude
│ ├── Field.ttinclude
│ ├── Parser.ttinclude
│ ├── Prop.ttinclude
│ ├── Shared.ttinclude
│ ├── Struct.ttinclude
│ ├── T4Immutable.tt
│ ├── TemplateFileManagerV2.1.ttinclude
│ └── VisualStudioHelper.ttinclude
└── readme.txt
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /output/
2 |
3 | ## Ignore Visual Studio temporary files, build results, and
4 | ## files generated by popular Visual Studio add-ons.
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # DNX
46 | project.lock.json
47 | artifacts/
48 |
49 | *_i.c
50 | *_p.c
51 | *_i.h
52 | *.ilk
53 | *.meta
54 | *.obj
55 | *.pch
56 | *.pdb
57 | *.pgc
58 | *.pgd
59 | *.rsp
60 | *.sbr
61 | *.tlb
62 | *.tli
63 | *.tlh
64 | *.tmp
65 | *.tmp_proj
66 | *.log
67 | *.vspscc
68 | *.vssscc
69 | .builds
70 | *.pidb
71 | *.svclog
72 | *.scc
73 |
74 | # Chutzpah Test files
75 | _Chutzpah*
76 |
77 | # Visual C++ cache files
78 | ipch/
79 | *.aps
80 | *.ncb
81 | *.opendb
82 | *.opensdf
83 | *.sdf
84 | *.cachefile
85 | *.VC.db
86 | *.VC.VC.opendb
87 |
88 | # Visual Studio profiler
89 | *.psess
90 | *.vsp
91 | *.vspx
92 | *.sap
93 |
94 | # TFS 2012 Local Workspace
95 | $tf/
96 |
97 | # Guidance Automation Toolkit
98 | *.gpState
99 |
100 | # ReSharper is a .NET coding add-in
101 | _ReSharper*/
102 | *.[Rr]e[Ss]harper
103 | *.DotSettings.user
104 |
105 | # JustCode is a .NET coding add-in
106 | .JustCode
107 |
108 | # TeamCity is a build add-in
109 | _TeamCity*
110 |
111 | # DotCover is a Code Coverage Tool
112 | *.dotCover
113 |
114 | # NCrunch
115 | _NCrunch_*
116 | .*crunch*.local.xml
117 | nCrunchTemp_*
118 |
119 | # MightyMoose
120 | *.mm.*
121 | AutoTest.Net/
122 |
123 | # Web workbench (sass)
124 | .sass-cache/
125 |
126 | # Installshield output folder
127 | [Ee]xpress/
128 |
129 | # DocProject is a documentation generator add-in
130 | DocProject/buildhelp/
131 | DocProject/Help/*.HxT
132 | DocProject/Help/*.HxC
133 | DocProject/Help/*.hhc
134 | DocProject/Help/*.hhk
135 | DocProject/Help/*.hhp
136 | DocProject/Help/Html2
137 | DocProject/Help/html
138 |
139 | # Click-Once directory
140 | publish/
141 |
142 | # Publish Web Output
143 | *.[Pp]ublish.xml
144 | *.azurePubxml
145 | # TODO: Comment the next line if you want to checkin your web deploy settings
146 | # but database connection strings (with potential passwords) will be unencrypted
147 | *.pubxml
148 | *.publishproj
149 |
150 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
151 | # checkin your Azure Web App publish settings, but sensitive information contained
152 | # in these scripts will be unencrypted
153 | PublishScripts/
154 |
155 | # NuGet Packages
156 | *.nupkg
157 | # The packages folder can be ignored because of Package Restore
158 | **/packages/*
159 | # except build/, which is used as an MSBuild target.
160 | !**/packages/build/
161 | # Uncomment if necessary however generally it will be regenerated when needed
162 | #!**/packages/repositories.config
163 | # NuGet v3's project.json files produces more ignoreable files
164 | *.nuget.props
165 | *.nuget.targets
166 |
167 | # Microsoft Azure Build Output
168 | csx/
169 | *.build.csdef
170 |
171 | # Microsoft Azure Emulator
172 | ecf/
173 | rcf/
174 |
175 | # Windows Store app package directories and files
176 | AppPackages/
177 | BundleArtifacts/
178 | Package.StoreAssociation.xml
179 | _pkginfo.txt
180 |
181 | # Visual Studio cache files
182 | # files ending in .cache can be ignored
183 | *.[Cc]ache
184 | # but keep track of directories ending in .cache
185 | !*.[Cc]ache/
186 |
187 | # Others
188 | ClientBin/
189 | ~$*
190 | *~
191 | *.dbmdl
192 | *.dbproj.schemaview
193 | *.pfx
194 | *.publishsettings
195 | node_modules/
196 | orleans.codegen.cs
197 |
198 | # Since there are multiple workflows, uncomment next line to ignore bower_components
199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
200 | #bower_components/
201 |
202 | # RIA/Silverlight projects
203 | Generated_Code/
204 |
205 | # Backup & report files from converting an old project file
206 | # to a newer Visual Studio version. Backup files are not needed,
207 | # because we have git ;-)
208 | _UpgradeReport_Files/
209 | Backup*/
210 | UpgradeLog*.XML
211 | UpgradeLog*.htm
212 |
213 | # SQL Server files
214 | *.mdf
215 | *.ldf
216 |
217 | # Business Intelligence projects
218 | *.rdl.data
219 | *.bim.layout
220 | *.bim_*.settings
221 |
222 | # Microsoft Fakes
223 | FakesAssemblies/
224 |
225 | # GhostDoc plugin setting file
226 | *.GhostDoc.xml
227 |
228 | # Node.js Tools for Visual Studio
229 | .ntvs_analysis.dat
230 |
231 | # Visual Studio 6 build log
232 | *.plg
233 |
234 | # Visual Studio 6 workspace options file
235 | *.opt
236 |
237 | # Visual Studio LightSwitch build output
238 | **/*.HTMLClient/GeneratedArtifacts
239 | **/*.DesktopClient/GeneratedArtifacts
240 | **/*.DesktopClient/ModelManifest.xml
241 | **/*.Server/GeneratedArtifacts
242 | **/*.Server/ModelManifest.xml
243 | _Pvt_Extensions
244 |
245 | # Paket dependency manager
246 | .paket/paket.exe
247 | paket-files/
248 |
249 | # FAKE - F# Make
250 | .fake/
251 |
252 | # JetBrains Rider
253 | .idea/
254 | *.sln.iml
255 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # T4Immutable
2 | ### T4Immutable is a T4 template for C# .NET apps that generates code for immutable classes.
3 |
4 | [](https://nuget.org/packages/T4Immutable)
5 |
6 | ## Table of contents
7 | * [Why use this?](#why)
8 | * [How do I start?](#starting)
9 | * [What's needed to make an immutable class?](#basics)
10 | * [How are collection (Array, List, Set, Dictionary... plus their Immutable versions) based properties handled?](#collections)
11 | * [Do generated classes serialize/deserialize correctly with JSON.NET / Protobuf.NET / others?](#serialization)
12 | * [I don't want X. Can I control what gets generated?](#codegen-options)
13 | * [Can I control the access level (public/private/...) of the constructor or the builder?](#constructor-builder-access-level)
14 | * [Constructor post-initalization / validation](#constructor-init-validation)
15 | * [Can I add extra attributes to each constructor parameter?](#constructor-param-attribs)
16 | * [How do I enforce automatic null checking for the constructor parameters? What about for the properties?](#null-checks)
17 | * [Constructor overrides](#constructor-overrides)
18 | * [How do I change the order of the arguments in the generated constructor?](#constructor-argument-order)
19 | * [How to specify property default values?](#default-values)
20 | * [Does it work with generic classes? Custom methods? Nested classes?](#supported-features)
21 | * [What if I want to make the class smarter though not strictly immutable, like caching a point distance after it has been requested the first time?](#non-immutability)
22 | * [Does Intellisense and all that stuff work after using this?](#intellisense)
23 | * [Can I suggest new features or whatever?](#suggestions)
24 | * [Can I see the extra code generated for the very first example?](#generated-code-sample)
25 |
26 | #### Release notes
27 | * **[v1.4.4]** Added proper support for .NET Standard 2.0. Fixed an issued with ImmutableGetHashCode generation.
28 | * **[v1.3.3]** ImmutableEquals now uses ImmutableGetHashCode as a speed optimization.
29 | * **[v1.3.2]** Fixed the generated equals operator (sometimes it would crash when the first item was null).
30 | * **[v1.3.1]** Made the library portable, however please check the notes inside 'How do I start?' about portable projects.
31 | * **[v1.2.1]** Now supports generating ToBuilder() and a better OptParam implementation.
32 | * **[v1.2.0]** Now supports generating builders.
33 | * **[v1.2.0]** WithParam class is now called OptParam.
34 | * **[v1.1.5]** Added a PreConstructor option to write code such as atributtes before generated constructors.
35 | * **[v1.1.5]** Added ExcludeConstructor and AllowCustomConstructors options.
36 | * **[v1.1.4]** Collection special cases are done when they inherit from ICollection instead of IEnumerable.
37 | * **[v1.1.3]** Using the dynamic keyword instead of reflection for faster KeyValuePair handling.
38 | * **[v1.1.2]** Generated Equals, GetHashCode and ToString now properly support collections as long as they implement IEnumerator. This means that arrays, List, Set, Dictionary, plus its Immutable variants are properly handled.
39 | * **[v1.1.0]** ImmutableClassOptions.EnableXXX/DisableXXX have been renamed to ImmutableClassOptions.IncludeXXX/ExcludeXXX
40 | * **[v1.1.0]** preConstructorParam code comment has been changed to the PreConstructorParam attribute
41 |
42 | ## Why use this?
43 | Creating proper immutable objects in C# requires a lot boilerplate code. The aim of this project is to reduce this to a minimum by means of automatic code generation via T4 templates. For instance, given the following class:
44 | ```c#
45 | [ImmutableClass(Options = ImmutableClassOptions.IncludeOperatorEquals)]
46 | class Person {
47 | private const int AgeDefaultValue = 18;
48 |
49 | public string FirstName { get; }
50 | public string LastName { get; }
51 | public int Age { get; }
52 |
53 | [ComputedProperty]
54 | public string FullName => $"{FirstName} {LastName}";
55 | }
56 | ```
57 |
58 | It will automatically generate for you in a separate partial class file the following:
59 | * A constructor such as `public Person(string firstName, string lastName, int age = 18)` that will initialize the values.
60 | * Working implementations for `Equals(object other)` and `Equals(Person other)`.
61 | * Working implementations for `operator==` and `operator!=`
62 | * A working implementation of `GetHashCode()`.
63 | * A better `ToString()` with output such as `"Person { FirstName=John, LastName=Doe, Age=21 }"`
64 | * A `Person With(...)` method that can be used to generate a new immutable clone with 0 or more properties changed (e.g. `var janeDoe = johnDoe.With(firstName: "Jane", age: 20)`
65 | * A `Builder` subclass that can be used to create the objects with the builder pattern such as:
66 | ```c#
67 | var builder = new Person.Builder().With(firstName: "John").With(lastName: "Doe"); // fluid way
68 | builder.Age = 21; // or via properties
69 | // that can be read back
70 | string firstName = builder.FirstName; // "John"
71 | var lastName = builder.LastName.Value; // "Doe"
72 | Person johnDoe = b.Build();
73 | var janeDoe = johnDoe.ToBuilder().With(firstName: "Jane", age: 20).Build(); // back and forth
74 | ```
75 |
76 | ## How do I start?
77 | 1. Install the T4Immutable nuget package
78 | 2. For **.NET Framework projects** use "**Build - Transform All T4 Templates**" or right click on the _T4Immutable/T4Immutable.tt_ file and click "**Run custom tool**". The files will be generated inside _T4Immutable_ as children of _T4Immutable.tt_.
79 | 3. For **.NET Core/Standard/etc projects** right click on the _T4Immutable/T4Immutable.tt_ file and click "**Run custom tool**". The files will be generated in a folder named _T4Immutable_generated_. "**Build - Transform All T4 Templates**" **won't work**
80 |
81 | *Remember to do this everytime you update the package or any of your immutable classes change.* If you want to automate it there are plugins out there that auto-run T4 templates before build such as [AutoT4](https://github.com/bennor/AutoT4).
82 |
83 | ## What's needed to make an immutable class?
84 | Just mark the class with the use the `[ImmutableClass]` attribute. The class will be auto-checked to meet the following constraints before code generation takes place:
85 | * Any properties _not_ marked as `ComputedProperty` will need to be either auto properites or have a non-public setter.
86 | * It should not have any custom constructors since one will be auto-generated, however please check the "Constructor overrides" section below to see ways to overcome this limitation.
87 | * Any default values (see "How to specify property default values?") will be checked to have the same type than the properties.
88 | * It cannot be static.
89 | * It cannot have any extra partials besides the generated one (this support is still TODO).
90 | * It cannot have a base class (probably to be lifted in a future update if anybody can show a proper use case), it can however have any interfaces.
91 |
92 | Besides those checks it is your responsibility to make the immutable object behave correctly. For example you should use ImmutableList instead of List and so on. This project is just made to reduce the boilerplate after all, not ensure correctness.
93 |
94 | ## How are collection (Array, List, Set, Dictionary... plus their Immutable versions) based properties handled?
95 | They just work as long as they inherit from `ICollection` (as all of the basic ones do). The generated `Equals()` will check they are equivalent by checking their contents, as well as the generated `GetHashCode()`. Nested collections are not a problem as well.
96 |
97 | ## Do generated classes serialize/deserialize correctly with JSON.NET / Protobuf.NET / others?
98 | #### JSON.NET
99 | ##### If you use a *public generated constructor*
100 | Just add `JsonIgnore` to computed properties.
101 | ##### If you use a *non-public generated constructor*
102 | Use `PreConstructor = "[Newtonsoft.Json.JsonConstructor]"` inside the `ImmutableClass` attribute. (Recommended over the next option)
103 |
104 | Alternatively:
105 | 1. Add the `ImmutableClassOptions.AllowCustomConstructors` to the `Options` parameter of the `ImmutableClass` attribute.
106 | 2. Add a constructor with no arguments.
107 | 3. Add a private/protected setter to all your non-computed properties if they didn't have any.
108 | 4. Add `JsonIgnore` to computed properties.
109 | 5. NB: In this case JSON.net will call the constructor and then _later_ set the properties one by one.
110 |
111 | #### Protobuf.NET
112 | 1. Mark your class as `[ProtoContract]`.
113 | 2. Add the `ImmutableClassOptions.AllowCustomConstructors` to the `Options` parameter of the `ImmutableClass` attribute.
114 | 3. Add a constructor with no arguments.
115 | 4. Mark the non-computed properties with `[ProtoMember(unique number)]`.
116 | 5. Add to all non-computed properties a private/protected setter if they didn't have any.
117 | 6. NB: In this case Protobuf.NET will call the constructor and then _later_ set the properties one by one.
118 |
119 | #### Others
120 | * Let me know :)
121 |
122 | ## I don't want X. Can I control what gets generated?
123 | You sure can, just add to the ImmutableClass attribute something like this:
124 | ```c#
125 | [ImmutableClass(Options =
126 | ImmutableClassOptions.ExcludeEquals | // do not generate an Equals() method
127 | ImmutableClassOptions.ExcludeGetHashCode | // do not generate a GetHashCode() method
128 | ImmutableClassOptions.IncludeOperatorEquals | // generate operator== and operator!= methods
129 | ImmutableClassOptions.ExcludeToString | // do not generate a ToString() method
130 | ImmutableClassOptions.ExcludeWith | // do not generate a With() method
131 | ImmutableClassOptions.ExcludeConstructor | // do not generate a constructor
132 | ImmutableClassOptions.ExcludeBuilder | // do not generate a builder or ImmutableToBuilder() - implies ExcludeToBuilder (usually used alongside ExcludeConstructor)
133 | ImmutableClassOptions.ExcludeToBuilder | // do not generate a builder or ToBuilder() method
134 | ImmutableClassOptions.AllowCustomConstructors)] // allow custom constructors
135 | ```
136 | Note that even if you exclude for example the `Equals()` method implementation you can still use them internally by invoking the `private bool ImmutableEquals(...)` implementation. This is done in case you might want to write your own `Equals()` yet still use the generated one as a base.
137 | Take care you do *not* use "using Foo = ImmutableClassOptions" to save some typing. Due to limitations with T4 it won't work.
138 |
139 | ## Can I control the access level (public/private/...) of the constructor or the builder?
140 | Yes. Do something like this:
141 | ```
142 | [ImmutableClass(ConstructorAccessLevel = ConstructorAccessLevel.Private, BuilderAccessLevel = BuilderAccessLevel.Protected)]
143 | ```
144 | Valid options are `Public`, `Protected`, `ProtectedInternal`, `Internal` and `Private`.
145 |
146 | ## Constructor post-initalization / validation
147 | If you need to do extra initialization / validation on the generated constructor just define a `void PostConstructor()` method (access modifier doesn't matter) and do your work there. It will be invoked inside the generated constructor after all assignations are done.
148 |
149 | Alternatively (and recommended) it is of course also possible to do validation inside the properties private/protected setters. E.g:
150 | ```c#
151 | private int _Age;
152 | public int Age {
153 | get { return _Age; }
154 | set {
155 | if (value < 18) throw new Exception("You are too young!");
156 | _Age = value;
157 | }
158 | }
159 | ```
160 |
161 | ## Can I add extra attributes to each constructor parameter?
162 | Yes, use the following when defining a property:
163 | ```c#
164 | [PreConstructorParam("[JetBrains.Annotations.NotNull]")]
165 | public string FirstName { get; }
166 | ```
167 | Bear in mind that if you use it to specify attributes they must have the full name (including namespace) or else there would be compilation errors. Also bear in mind that due to T4 limitations the string has to be constant, this is, it shouldn't depend on other const values.
168 |
169 | ## How do I enforce automatic null checking for the constructor parameters? What about for the properties?
170 | If you use this:
171 | ```c#
172 | [PreNotNullCheck, PostNotNullCheck]
173 | public string FirstName { get; }
174 | ```
175 | The constructor will be this:
176 | ```c#
177 | public Person(string firstName) {
178 | // pre not null check
179 | if (firstName == null) throw new ArgumentNullException(nameof(firstName));
180 |
181 | // assignations + PostConstructor() if needed
182 |
183 | // post not null check
184 | if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
185 | }
186 | ```
187 |
188 | Having said this, if you use JetBrains Annotations for null checking, you can also do this:
189 | ```c#
190 | [JetBrains.Annotations.NotNull, ConstructorParamNotNull]
191 | public string FirstName { get; }
192 | ```
193 | And the constructor will be this:
194 | ```c#
195 | public Person([JetBrains.Annotations.NotNull] string firstName) {
196 | // pre not null check is implied by ConstructorParamNotNull
197 | if (firstName == null) throw new ArgumentNullException(nameof(firstName));
198 |
199 | // assignations + PostConstructor() if needed
200 |
201 | // post not null check implied by JetBrains.Annotations.NotNull on the property
202 | if (this.FirstName == null) throw new NullReferenceException(nameof(this.FirstName));
203 | }
204 | ```
205 |
206 | ## Constructor overrides
207 | If you need to do alternate constructors (for example having a `Point(T x, T y)` immutable class and you want to generate a point from a distance and an angle) then you can do something like:
208 | ```c#
209 | public static Point FromAngleAndDistance(T distance, double angle) {
210 | // your code here
211 | return new Point(x, y);
212 | }
213 | ```
214 | Still, if you still aren't satisfied by this you can enable the `ImmutableOptions.AllowCustomConstructors` and create your own alternate constructor to your own risk.
215 |
216 | ## How do I change the order of the arguments in the generated constructor?
217 | Just change the order of the properties.
218 |
219 | ## How to specify property default values?
220 | If you want a property to have a given default value on the auto-generated constructor there are two ways. Say that you have a property named int Age, and you want it to have the default value of 18:
221 | * Way 1: private/protected/public/whatever `const int AgeDefaultValue = 18;`
222 | * Way 2: private/protected/public/whatever `static readonly int AgeDefaultValue = 18;`
223 |
224 | If you wonder why there are two alternatives it is because sometimes it is possible to add stuff such as `new Foo()` as a default parameter for constructors and that expression works as a readonly but does not work as a const.
225 |
226 | Please note that default values, like in a constructor, should not have gaps.
227 | This is, if you have int x, int y then you should have a default value for y or for x and y.
228 | If you want a default value for x then move it to the end.
229 |
230 | ## Does it work with generic classes? Custom methods? Nested classes?
231 | It sure does!
232 |
233 | ## What if I want to make the class smarter though not strictly immutable, like caching a point distance after it has been requested the first time?
234 | This is more about reducing boilerplate than ensuring immutability, so you can. E.g.:
235 | ```c#
236 | [ImmutableClass]
237 | class Point {
238 | public double X { get; }
239 | public double Y { get; }
240 |
241 | private double _Distance;
242 |
243 | [ComputedProperty]
244 | public double Distance {
245 | get {
246 | if (_Distance == null) _Distance = Math.Sqrt(X*X + Y*Y);
247 | return _Distance.Value;
248 | }
249 | }
250 | }
251 | ```
252 | However if you do stuff like this, then since internally it has become mutable-ish you will need to use a lock or some other method if you want it to work properly when the object is used concurrently. A probably better solution would be to initialize the ```_Distance``` member inside the `PostConstructor()`. It all depends on your use case.
253 |
254 | ## Does Intellisense and all that stuff work after using this?
255 | Absolutely, since the generated files are .cs files Intellisense will pick the syntax without problems after the T4 template is built.
256 |
257 | ## Can I suggest new features or whatever?
258 | Please do!
259 |
260 | ## Can I see the extra code generated for the very first example?
261 | Here you go (excluding some redundant attributes):
262 | ```c#
263 | using System;
264 |
265 | partial class Person : IEquatable {
266 | public Person(string firstName, string lastName, int age = 18) {
267 | this.FirstName = firstName;
268 | this.LastName = lastName;
269 | this.Age = age;
270 | _ImmutableHashCode = T4Immutable.Helpers.GetHashCodeFor(this.FirstName, this.LastName, this.Age);
271 | }
272 |
273 | private bool ImmutableEquals(Person obj) {
274 | if (ReferenceEquals(this, obj)) return true;
275 | if (ReferenceEquals(obj, null)) return false;
276 | if (ImmutableGetHashCode() != obj.ImmutableGetHashCode()) return false;
277 | return T4Immutable.Helpers.AreEqual(this.FirstName, obj.FirstName) && T4Immutable.Helpers.AreEqual(this.LastName, obj.LastName) && T4Immutable.Helpers.AreEqual(this.Age, obj.Age);
278 | }
279 |
280 | public override bool Equals(object obj) {
281 | return ImmutableEquals(obj as Person);
282 | }
283 |
284 | public bool Equals(Person obj) {
285 | return ImmutableEquals(obj);
286 | }
287 |
288 | public static bool operator ==(Person a, Person b) {
289 | return T4Immutable.Helpers.BasicAreEqual(a, b);
290 | }
291 |
292 | public static bool operator !=(Person a, Person b) {
293 | return !T4Immutable.Helpers.BasicAreEqual(a, b);
294 | }
295 |
296 | private readonly int _ImmutableHashCode;
297 |
298 | private int ImmutableGetHashCode() {
299 | return _ImmutableHashCode;
300 | }
301 |
302 | public override int GetHashCode() {
303 | return ImmutableGetHashCode();
304 | }
305 |
306 | private string ImmutableToString() {
307 | return T4Immutable.Helpers.ToStringFor(nameof(Person), new System.Tuple(nameof(this.FirstName), this.FirstName), new System.Tuple(nameof(this.LastName), this.LastName), new System.Tuple(nameof(this.Age), this.Age));
308 | }
309 |
310 | public override string ToString() {
311 | return ImmutableToString();
312 | }
313 |
314 | private Person ImmutableWith(T4Immutable.OptParam firstName = default(T4Immutable.OptParam), T4Immutable.OptParam lastName = default(T4Immutable.OptParam), T4Immutable.OptParam age = default(T4Immutable.OptParam)) {
315 | return new Person(
316 | firstName.HasValue ? firstName.Value : this.FirstName,
317 | lastName.HasValue ? lastName.Value : this.LastName,
318 | age.HasValue ? age.Value : this.Age
319 | );
320 | }
321 |
322 | public Person With(T4Immutable.OptParam firstName = default(T4Immutable.OptParam), T4Immutable.OptParam lastName = default(T4Immutable.OptParam), T4Immutable.OptParam age = default(T4Immutable.OptParam)) {
323 | return ImmutableWith(firstName, lastName, age);
324 | }
325 |
326 | public Person.Builder ToBuilder() {
327 | return ImmutableToBuilder();
328 | }
329 |
330 | private Person.Builder ImmutableToBuilder() {
331 | return new Person.Builder().With(
332 | new T4Immutable.OptParam(this.FirstName),
333 | new T4Immutable.OptParam(this.LastName),
334 | new T4Immutable.OptParam(this.Age)
335 | );
336 | }
337 |
338 | public class Builder {
339 | public T4Immutable.OptParam FirstName { get; set; }
340 | public T4Immutable.OptParam LastName { get; set; }
341 | public T4Immutable.OptParam Age { get; set; }
342 |
343 | public Builder With(T4Immutable.OptParam firstName = default(T4Immutable.OptParam), T4Immutable.OptParam lastName = default(T4Immutable.OptParam), T4Immutable.OptParam age = default(T4Immutable.OptParam)) {
344 | if (firstName.HasValue) this.FirstName = firstName;
345 | if (lastName.HasValue) this.LastName = lastName;
346 | if (age.HasValue) this.Age = age;
347 | return this;
348 | }
349 |
350 | public Person Build() {
351 | if (!this.FirstName.HasValue) throw new InvalidOperationException("Builder property 'FirstName' cannot be left unassigned");
352 | if (!this.LastName.HasValue) throw new InvalidOperationException("Builder property 'LastName' cannot be left unassigned");
353 | if (!this.Age.HasValue) this.Age = 18;
354 | if (!this.Age.HasValue) throw new InvalidOperationException("Builder property 'Age' cannot be left unassigned");
355 | return new Person(this.FirstName.Value, this.LastName.Value, this.Age.Value);
356 | }
357 | }
358 | }
359 | ```
360 |
--------------------------------------------------------------------------------
/scripts/nuget-pack.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 | cd ..
3 | md output
4 | cd src\T4Immutable-netstd2
5 | nuget pack T4Immutable.nuspec -build -properties Configuration=Release -outputdirectory ..\..\output
6 | cd ..\..\scripts
7 |
--------------------------------------------------------------------------------
/src/T4Immutable-netstd2/Attributes.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | // ReSharper disable RedundantAttributeUsageProperty
4 |
5 | // note we can't use features > c#4 since it needs to be compiled by the template
6 | // this means for example no auto-property initializers
7 | // ReSharper disable ConvertToAutoProperty
8 |
9 | namespace T4Immutable {
10 |
11 | #region For classes
12 |
13 | ///
14 | /// Marks a class so it can be processed by T4Immutable.
15 | ///
16 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
17 | public sealed class ImmutableClassAttribute : Attribute {
18 | private ConstructorAccessLevel _constructorAccessLevel = ConstructorAccessLevel.Public;
19 | private ImmutableClassOptions _options = ImmutableClassOptions.None;
20 | private string _preConstructor;
21 | private BuilderAccessLevel _builderAccessLevel = BuilderAccessLevel.Public;
22 |
23 | ///
24 | /// Immutable class generation options.
25 | ///
26 | public ImmutableClassOptions Options {
27 | get { return _options; }
28 | set { _options = value; }
29 | }
30 |
31 | ///
32 | /// Generated constructor access level (modifier).
33 | ///
34 | public ConstructorAccessLevel ConstructorAccessLevel {
35 | get { return _constructorAccessLevel; }
36 | set { _constructorAccessLevel = value; }
37 | }
38 |
39 | ///
40 | /// String with code to add before the constructor.
41 | ///
42 | public string PreConstructor {
43 | get { return _preConstructor; }
44 | set { _preConstructor = value; }
45 | }
46 |
47 | ///
48 | /// Generated builder class access level (modifier).
49 | ///
50 | public BuilderAccessLevel BuilderAccessLevel {
51 | get { return _builderAccessLevel; }
52 | set { _builderAccessLevel = value; }
53 | }
54 | }
55 |
56 | ///
57 | /// Immutable class generation options for T4Immutable.
58 | ///
59 | [Flags]
60 | public enum ImmutableClassOptions {
61 | ///
62 | /// Default.
63 | ///
64 | None = 0,
65 |
66 | ///
67 | /// Do not generate Equals() implementation or add the IEquatable interface.
68 | ///
69 | ExcludeEquals = 1 << 0,
70 |
71 | ///
72 | /// Do not generate a GetHashCode() implementation.
73 | ///
74 | ExcludeGetHashCode = 1 << 1,
75 |
76 | ///
77 | /// Generate operator == and operator !=.
78 | ///
79 | IncludeOperatorEquals = 1 << 2,
80 |
81 | ///
82 | /// Do not generate a ToString() implementation.
83 | ///
84 | ExcludeToString = 1 << 3,
85 |
86 | ///
87 | /// Do not generate a With() implementation.
88 | ///
89 | ExcludeWith = 1 << 4,
90 |
91 | ///
92 | /// Do not generate a constructor.
93 | ///
94 | ExcludeConstructor = 1 << 5,
95 |
96 | ///
97 | /// Allow the user to define his own constructors.
98 | ///
99 | AllowCustomConstructors = 1 << 6,
100 |
101 | ///
102 | /// Do not generate a builder class or ImmutableToBuilder() implementation. Implies ExcludeToBuilder.
103 | ///
104 | ExcludeBuilder = 1 << 7,
105 |
106 | ///
107 | /// Do not generate a ToBuilder() implementation.
108 | ///
109 | ExcludeToBuilder = 1 << 8,
110 | }
111 |
112 | ///
113 | /// Access level (modifier) for constructors generated by T4Immutable.
114 | ///
115 | public enum ConstructorAccessLevel {
116 | Public,
117 | Protected,
118 | Internal,
119 | Private,
120 | ProtectedInternal
121 | }
122 |
123 | ///
124 | /// Access level (modifier) for builders generated by T4Immutable.
125 | ///
126 | public enum BuilderAccessLevel {
127 | Public,
128 | Protected,
129 | Internal,
130 | Private,
131 | ProtectedInternal
132 | }
133 |
134 | #endregion
135 |
136 | #region For properties
137 |
138 | ///
139 | /// Adds a JetBrains.Annotations.NotNull attribute to the constructor parameter.
140 | /// Also enables a not null precheck implicitely.
141 | ///
142 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
143 | public sealed class ConstructorParamNotNullAttribute : Attribute {
144 | }
145 |
146 | ///
147 | /// Generate a not null check at the beginning of the constructor.
148 | ///
149 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
150 | public sealed class PreNotNullCheckAttribute : Attribute {
151 | }
152 |
153 | ///
154 | /// Generate a not null check at the end of the constructor.
155 | ///
156 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
157 | public sealed class PostNotNullCheckAttribute : Attribute {
158 | }
159 |
160 | ///
161 | /// Marks a property as computed, effectively making T4Immutable ignore it.
162 | ///
163 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
164 | public sealed class ComputedPropertyAttribute : Attribute {
165 | }
166 |
167 | ///
168 | /// String with code to add before the constructor parameter.
169 | ///
170 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
171 | public sealed class PreConstructorParamAttribute : Attribute {
172 | public string Pre { get; private set; }
173 |
174 | public PreConstructorParamAttribute(string pre) {
175 | Pre = pre;
176 | }
177 | }
178 |
179 | #endregion
180 |
181 | #region Internal
182 |
183 | ///
184 | /// Attribute used internally by T4Immutable to mark generated code. Not for public usage.
185 | ///
186 | [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
187 | public sealed class GeneratedCodeAttribute : Attribute {
188 | }
189 |
190 | #endregion
191 | }
192 |
--------------------------------------------------------------------------------
/src/T4Immutable-netstd2/Helpers.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace T4Immutable {
8 | ///
9 | /// Collection of helper methods for T4Immutable.
10 | ///
11 | public static class Helpers {
12 | ///
13 | /// Check if two objects are equal.
14 | ///
15 | /// Object type.
16 | /// First object.
17 | /// Second object.
18 | /// true if they are equal, false otherwise.
19 | public static bool BasicAreEqual(T a, T b) {
20 | bool aIsNull = ReferenceEquals(a, null), bIsNull = ReferenceEquals(b, null);
21 | if (aIsNull && bIsNull) {
22 | return true;
23 | }
24 | if (aIsNull || bIsNull) {
25 | return false;
26 | }
27 | return a.Equals(b);
28 | }
29 |
30 | ///
31 | /// Check if two objects are equal, plus some special checking for KeyValuePairs and ICollections.
32 | ///
33 | /// Object type.
34 | /// First object.
35 | /// Second object.
36 | /// true if they are equal, false otherwise.
37 | public static bool AreEqual(T a, T b) {
38 | bool aIsNull = ReferenceEquals(a, null), bIsNull = ReferenceEquals(b, null);
39 | if (aIsNull && bIsNull) {
40 | return true;
41 | }
42 | if (aIsNull || bIsNull) {
43 | return false;
44 | }
45 | if (a.Equals(b)) {
46 | return true;
47 | }
48 |
49 | // check for the special case of KeyValuePair (items of a dictionary)
50 | var aKvp = KeyValuePairHelper.TryExtractKeyValuePair(a);
51 | if (aKvp != null) {
52 | var bKvp = KeyValuePairHelper.TryExtractKeyValuePair(b);
53 | return AreEqual(aKvp.Item1, bKvp.Item1) && AreEqual(aKvp.Item2, bKvp.Item2);
54 | }
55 |
56 | // one extra check for collections
57 |
58 | var aCollection = a as ICollection;
59 | var bCollection = b as ICollection;
60 | if ((aCollection == null) || (bCollection == null)) {
61 | return false;
62 | }
63 |
64 | if (aCollection.Count != bCollection.Count) {
65 | return false;
66 | }
67 |
68 | var aEnum = aCollection.GetEnumerator();
69 | var bEnum = bCollection.GetEnumerator();
70 |
71 | while (aEnum.MoveNext()) {
72 | if (!bEnum.MoveNext()) {
73 | return false;
74 | }
75 | object aCurrent = aEnum.Current, bCurrent = bEnum.Current;
76 | if (!AreEqual(aCurrent, bCurrent)) {
77 | return false;
78 | }
79 | }
80 |
81 | // all items so far are the same, but does b have one more?
82 | return !bEnum.MoveNext();
83 | }
84 |
85 | ///
86 | /// Gets the hashcode of a single object. If the object is a collection it will make a hashcode of the collection.
87 | ///
88 | /// Object to make the hashcode for.
89 | /// A hashcode.
90 | public static int GetHashCodeForSingleObject(object o) {
91 | if (ReferenceEquals(o, null)) {
92 | return 0;
93 | }
94 |
95 | var oCollection = o as ICollection;
96 | if (oCollection == null) {
97 | // check for the special case of KeyValuePair (items of a dictionary)
98 | var kvp = KeyValuePairHelper.TryExtractKeyValuePair(o);
99 | if (kvp != null) {
100 | return GetHashCodeFor(kvp.Item1, kvp.Item2);
101 | }
102 |
103 | return o.GetHashCode();
104 | }
105 |
106 | // make a hash of the items if it is a collection
107 | var oEnum = oCollection.GetEnumerator();
108 |
109 | var list = new List