├── .vs └── Value │ ├── FileContentIndex │ ├── read.lock │ └── 028162b3-4b40-4b33-8be6-a021bd5c9cc8.vsidx │ ├── v15 │ └── Server │ │ └── sqlite3 │ │ ├── db.lock │ │ ├── storage.ide │ │ ├── storage.ide-shm │ │ └── storage.ide-wal │ └── DesignTimeBuild │ └── .dtbcache.v2 ├── Value.jpg ├── Value.snk ├── Value-icon.jpg ├── Value-small.jpg ├── Value.Net40 ├── Value.snk └── Value.Net40.csproj ├── Value.Net45 ├── Value.snk ├── Value.Net45.csproj.DotSettings └── Value.Net45.csproj ├── Value.Standard ├── Value.snk └── Value.Standard.csproj ├── Value.Tests ├── packages.config ├── Value.Tests.csproj.DotSettings ├── Domain.Helpers.Tests.csproj.DotSettings ├── Samples │ ├── Suit.cs │ ├── Currency.cs │ ├── ItemPriceWithBadImplementationForEqualityAndUnicity.cs │ ├── ThreeCards.cs │ ├── ItemPrice.cs │ ├── ThreeCardsBadlyImplementedAsValueType.cs │ ├── Amount.cs │ └── Card.cs ├── AmountShould.cs ├── Value.Tests.csproj ├── ValueTypeShould.cs ├── ListByValueShould.cs ├── DictionaryByValueShould.cs └── SetByValueShould.cs ├── ReleaseNoteContentToBeInsertedWithinNuspecFile.txt ├── BackLog.md ├── appveyor.yml ├── Value ├── Value.Shared.projitems ├── Value.shproj ├── ValueType.cs ├── EquatableByValueWithoutOrder.cs ├── ListByValue.cs ├── EquatableByValue.cs ├── DictionaryByValue.cs └── SetByValue.cs ├── Version.cs ├── Value.nuspec ├── ImplementationDetails.md ├── .build ├── Build.tasks └── Build.proj ├── .gitignore ├── Value.sln ├── Readme.md └── LICENSE /.vs/Value/FileContentIndex/read.lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vs/Value/v15/Server/sqlite3/db.lock: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Value.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/Value.jpg -------------------------------------------------------------------------------- /Value.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/Value.snk -------------------------------------------------------------------------------- /Value-icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/Value-icon.jpg -------------------------------------------------------------------------------- /Value-small.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/Value-small.jpg -------------------------------------------------------------------------------- /Value.Net40/Value.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/Value.Net40/Value.snk -------------------------------------------------------------------------------- /Value.Net45/Value.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/Value.Net45/Value.snk -------------------------------------------------------------------------------- /Value.Standard/Value.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/Value.Standard/Value.snk -------------------------------------------------------------------------------- /.vs/Value/DesignTimeBuild/.dtbcache.v2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/.vs/Value/DesignTimeBuild/.dtbcache.v2 -------------------------------------------------------------------------------- /.vs/Value/v15/Server/sqlite3/storage.ide: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/.vs/Value/v15/Server/sqlite3/storage.ide -------------------------------------------------------------------------------- /.vs/Value/v15/Server/sqlite3/storage.ide-shm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/.vs/Value/v15/Server/sqlite3/storage.ide-shm -------------------------------------------------------------------------------- /.vs/Value/v15/Server/sqlite3/storage.ide-wal: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/.vs/Value/v15/Server/sqlite3/storage.ide-wal -------------------------------------------------------------------------------- /.vs/Value/FileContentIndex/028162b3-4b40-4b33-8be6-a021bd5c9cc8.vsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpierrain/Value/HEAD/.vs/Value/FileContentIndex/028162b3-4b40-4b33-8be6-a021bd5c9cc8.vsidx -------------------------------------------------------------------------------- /Value.Tests/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ReleaseNoteContentToBeInsertedWithinNuspecFile.txt: -------------------------------------------------------------------------------- 1 | New feature(s): 2 | * Support for .Net Standard (ex .Net Core), .NET 4.0 and .NET 4.5 3 | * Add support for Dictionary with the new DictionaryByValue<T> decorator. 4 | 5 | -------------- 6 | 7 | Change(s): 8 | * 9 | 10 | -------------- 11 | Bug Fixe(s): 12 | * 13 | -------------------------------------------------------------------------------- /BackLog.md: -------------------------------------------------------------------------------- 1 | # Value's Backlog 2 | 3 | 1. Extract Concrete type from base class EquatableByValue that may be agreggated and reused by it and other types (i.e. favor composition over inheritance) 4 | 1. Implement decorators: DictionaryByValue, StackByValue, QueueByValue 5 | 1. Find better names for ....ByValue 6 | 1. Profile perf and improve implementations if necessary 7 | -------------------------------------------------------------------------------- /Value.Net45/Value.Net45.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | Off -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.1.1.{build} 2 | image: Visual Studio 2017 3 | branches: 4 | only: 5 | - master 6 | configuration: Release 7 | platform: Any CPU 8 | build_script: 9 | - cmd: build.cmd Release 10 | build: 11 | verbosity: minimal 12 | test: auto 13 | 14 | artifacts: 15 | - path: 'Artifacts\Packages\*.nupkg' 16 | 17 | assembly_info: 18 | patch: true 19 | file: version.cs 20 | assembly_version: '{version}' 21 | assembly_file_version: '{version}' 22 | assembly_informational_version: '{version}' 23 | -------------------------------------------------------------------------------- /Value.Tests/Value.Tests.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True 3 | False -------------------------------------------------------------------------------- /Value.Standard/Value.Standard.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard1.3 5 | Value 6 | false 7 | Value 8 | true 9 | Value.snk 10 | false 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Value.Tests/Domain.Helpers.Tests.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | True 3 | True -------------------------------------------------------------------------------- /Value/Value.Shared.projitems: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | 44709b35-26e8-4588-a87a-1727cdb11255 7 | 8 | 9 | Value.Shared 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Value/Value.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 44709b35-26e8-4588-a87a-1727cdb11255 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Value.Tests/Samples/Suit.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests.Samples 17 | { 18 | /// 19 | /// Possible Suits for poker cards. 20 | /// 21 | public enum Suit 22 | { 23 | C, // Club 24 | 25 | D, // Diamond 26 | 27 | H, // Heart 28 | 29 | S // Spade 30 | } 31 | } -------------------------------------------------------------------------------- /Value.Tests/Samples/Currency.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests.Samples 17 | { 18 | /// 19 | /// Any form of money issued by a government or central bank and used as legal tender and a basis for trade. 20 | /// 21 | public enum Currency 22 | { 23 | Euro, 24 | 25 | Dollar, 26 | 27 | Pound, 28 | 29 | Yuan, 30 | 31 | Yen 32 | } 33 | } -------------------------------------------------------------------------------- /Version.cs: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // http://www.apache.org/licenses/LICENSE-2.0 5 | // Unless required by applicable law or agreed to in writing, software 6 | // distributed under the License is distributed on an "AS IS" BASIS, 7 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | // See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | // 11 | // This file contains common assembly attributes 12 | // -------------------------------------------------------------------------------------------------------------------- 13 | 14 | using System.Reflection; 15 | 16 | [assembly: AssemblyProduct("Value")] 17 | [assembly: AssemblyCopyright("Copyright © Thomas PIERRAIN 2016-2018")] 18 | [assembly: AssemblyCompany("Thomas PIERRAIN")] 19 | 20 | // Version information for an assembly consists of the following four values: 21 | // 22 | // Major Version 23 | // Minor Version 24 | // Build Number 25 | // Revision 26 | // 27 | // You can specify all the values or you can default the Build and Revision Numbers 28 | // by using the '*' as shown below: 29 | // [assembly: AssemblyVersion("1.0.*")] 30 | [assembly: AssemblyVersion("1.0.0.0")] 31 | [assembly: AssemblyFileVersion("1.0.0.0")] 32 | -------------------------------------------------------------------------------- /Value/ValueType.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value 17 | { 18 | /// 19 | /// Base class for any Value Type (i.e. the 'Value Object' oxymoron of DDD). 20 | /// All you have to do is to implement the abstract methods: 21 | /// 22 | /// Domain type to be 'turned' into a Value Type. 23 | public abstract class ValueType : EquatableByValue 24 | { 25 | } 26 | } -------------------------------------------------------------------------------- /Value.Tests/Samples/ItemPriceWithBadImplementationForEqualityAndUnicity.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests.Samples 17 | { 18 | /// 19 | /// Price for an item. Bad Value Type implementation because doesn't override GetAllAttributesToBeUsedForEquality() 20 | /// 21 | public class ItemPriceWithBadImplementationForEqualityAndUnicity : Amount 22 | { 23 | public ItemPriceWithBadImplementationForEqualityAndUnicity(string itemName, decimal quantity, Currency currency) : base(quantity, currency) 24 | { 25 | this.ItemName = itemName; 26 | } 27 | 28 | public string ItemName { get; private set; } 29 | 30 | public override string ToString() 31 | { 32 | return $"{this.ItemName} - price: {this.Quantity} {this.Currency}."; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Value.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Value 5 | Value 6 | 2.0.0.00 7 | Thomas PIERRAIN 8 | Thomas PIERRAIN 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | https://github.com/tpierrain/Value/ 11 | false 12 | https://raw.githubusercontent.com/tpierrain/Value/master/Value-icon.jpg 13 | Value is a pico library (or code snippets shed) to help you to easily implement Value Types (also known as Value Objects in DDD) in your C# projects without making implementation errors nor polluting your domain logic with boiler-plate code. 14 | Less Entities, more Value Types with easy-to-use and lightweight helpers. 15 | New feature(s): 16 | 17 | * Support for .Net Standard (ex .Net Core) 18 | 19 | * Provide the following helpers: ValueType<T>, ListByValue<T>, SetByValue<T> 20 | 21 | -------------- 22 | 23 | Change(s): 24 | 25 | * 26 | 27 | -------------- 28 | 29 | Bug Fixe(s): 30 | 31 | * 32 | Copyright 2016-2017 Thomas PIERRAIN 33 | en-US 34 | DDD domain value object type tactical pattern pico library 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Value.Tests/Samples/ThreeCards.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests.Samples 17 | { 18 | using System.Collections.Generic; 19 | 20 | /// 21 | /// Proper implementation of a ThreeeCards ValueType since the order of the cards doesn't matter during 22 | /// Equality. 23 | /// 24 | public class ThreeCards : ValueType 25 | { 26 | private readonly HashSet cards; 27 | 28 | public ThreeCards(string card1Description, string card2Description, string card3Description) 29 | { 30 | this.cards = new HashSet(); 31 | 32 | this.cards.Add(Card.Parse(card1Description)); 33 | this.cards.Add(Card.Parse(card2Description)); 34 | this.cards.Add(Card.Parse(card3Description)); 35 | } 36 | 37 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 38 | { 39 | return new List { new SetByValue(this.cards) }; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Value/EquatableByValueWithoutOrder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Value 5 | { 6 | /// 7 | /// Support a by-Value Equality and Unicity where order of the elements that belongs 8 | /// to the Unicity/Equality doesn't matter. 9 | /// 10 | /// Type of the elements. 11 | public abstract class EquatableByValueWithoutOrder : EquatableByValue 12 | { 13 | protected override bool EqualsImpl(EquatableByValue other) 14 | { 15 | var otherEquatable = (EquatableByValueWithoutOrder) other; 16 | 17 | return this.EqualsWithoutOrderImpl(otherEquatable); 18 | } 19 | 20 | public override bool Equals(object obj) 21 | { 22 | return EqualsImpl(obj as EquatableByValue); 23 | } 24 | 25 | // Force all derived types to implement a specific equal implementation 26 | protected abstract bool EqualsWithoutOrderImpl(EquatableByValueWithoutOrder obj); 27 | 28 | public override int GetHashCode() 29 | { 30 | if (base.HashCode == Undefined) 31 | { 32 | var code = 0; 33 | 34 | // Two instances with same elements added in different order must return the same hashcode 35 | // Let's compute and sort hashcodes of all elements (always in the same order) 36 | var sortedHashCodes = new SortedSet(); 37 | 38 | foreach (var element in this.GetAllAttributesToBeUsedForEquality()) 39 | { 40 | sortedHashCodes.Add(element.GetHashCode()); 41 | } 42 | 43 | foreach (var elementHashCode in sortedHashCodes) 44 | { 45 | code = (code * 397) ^ elementHashCode; 46 | } 47 | 48 | // Cache the result in a field 49 | this.HashCode = code; 50 | } 51 | 52 | return this.HashCode; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Value.Tests/Samples/ItemPrice.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests.Samples 17 | { 18 | using System.Collections.Generic; 19 | using System.Linq; 20 | 21 | /// 22 | /// Price for an item. 23 | /// 24 | public class ItemPrice : Amount 25 | { 26 | private readonly string itemName; 27 | 28 | public ItemPrice(string itemName, decimal quantity, Currency currency) : base(quantity, currency) 29 | { 30 | this.itemName = itemName; 31 | } 32 | 33 | public string ItemName { get { return this.itemName; } } 34 | 35 | public override string ToString() 36 | { 37 | return string.Format("{0} - price: {1} {2}.", this.ItemName, this.Quantity, this.Currency); 38 | } 39 | 40 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 41 | { 42 | return base.GetAllAttributesToBeUsedForEquality().Concat(new List() { this.ItemName }); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Value.Tests/AmountShould.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests 17 | { 18 | using System; 19 | using NFluent; 20 | using NUnit.Framework; 21 | using Value.Tests.Samples; 22 | 23 | [TestFixture] 24 | internal class AmountShould 25 | { 26 | [Test] 27 | public void Add_Amount_instances_in_a_Closure_of_operations_style() 28 | { 29 | var firstAmount = new Amount(new decimal(1), Currency.Euro); 30 | var secondAmount = new Amount(new decimal(1), Currency.Euro); 31 | 32 | var thirdAmount = firstAmount.Add(secondAmount); 33 | Check.That(thirdAmount.Quantity).IsEqualTo(new decimal(2)); 34 | } 35 | 36 | [Test] 37 | public void Throw_exception_when_trying_to_add_Amounts_with_different_currencies() 38 | { 39 | var firstAmount = new Amount(new decimal(1), Currency.Euro); 40 | var secondAmount = new Amount(new decimal(1), Currency.Yuan); 41 | 42 | Check.ThatCode(() => firstAmount.Add(secondAmount)) 43 | .Throws() 44 | .WithMessage("Can't add amounts with different currencies: Amount= 1 Euro and other amount= 1 Yuan."); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Value.Tests/Samples/ThreeCardsBadlyImplementedAsValueType.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests.Samples 17 | { 18 | using System.Collections.Generic; 19 | 20 | /// 21 | /// Bad ValueType implementation of ThreeCards since the GetAllAttributesToBeUsedForEquality() method 22 | /// returns the set directly, without decoring it with the SetByValue helper. 23 | /// 24 | public class ThreeCardsBadlyImplementedAsValueType : ValueType 25 | { 26 | private readonly HashSet cards; 27 | 28 | public ThreeCardsBadlyImplementedAsValueType(string card1Description, string card2Description, string card3Description) 29 | { 30 | this.cards = new HashSet(); 31 | 32 | this.cards.Add(Card.Parse(card1Description)); 33 | this.cards.Add(Card.Parse(card2Description)); 34 | this.cards.Add(Card.Parse(card3Description)); 35 | } 36 | 37 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 38 | { 39 | // Here, should have done returned "new SetByValue(this.cards)" instead of "this.cards" 40 | return new List { this.cards }; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Value.Tests/Samples/Amount.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests.Samples 17 | { 18 | using System; 19 | using System.Collections.Generic; 20 | 21 | /// 22 | /// A quantity of money. 23 | /// 24 | /// Value type. 25 | public class Amount : ValueType 26 | { 27 | private readonly Currency currency; 28 | 29 | private readonly decimal quantity; 30 | 31 | public Amount(decimal quantity, Currency currency) 32 | { 33 | this.quantity = quantity; 34 | this.currency = currency; 35 | } 36 | 37 | public Currency Currency { get { return this.currency; } } 38 | 39 | public decimal Quantity { get { return this.quantity; } } 40 | 41 | public Amount Add(Amount otherAmount) 42 | { 43 | if (this.Currency != otherAmount.Currency) 44 | { 45 | throw new InvalidOperationException( 46 | string.Format("Can't add amounts with different currencies: Amount= {0} and other amount= {1}.", this, otherAmount)); 47 | } 48 | 49 | return new Amount(this.Quantity + otherAmount.Quantity, this.Currency); 50 | } 51 | 52 | public override string ToString() 53 | { 54 | return string.Format("{0} {1}", this.Quantity, this.Currency); 55 | } 56 | 57 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 58 | { 59 | return new List() { this.quantity, this.currency }; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Value.Net40/Value.Net40.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {833BCC87-B759-4502-8C32-93D396AE1357} 8 | Library 9 | Properties 10 | Value 11 | Value 12 | v4.8 13 | 512 14 | 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | false 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | false 34 | 35 | 36 | true 37 | 38 | 39 | Value.snk 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Version.cs 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /ImplementationDetails.md: -------------------------------------------------------------------------------- 1 | # Implementation details about *Value* 2 | 3 | (*may be interesting if you want to understand the library before you use it, or if you want to contribute to the project*) 4 | 5 | ![Value](https://github.com/tpierrain/Value/blob/master/Value-small.jpg?raw=true) 6 | 7 | ## External-design drivers 8 | 9 | The *Value* library: 10 | 11 | 1. __must be very easy and straightforward to use__ (must follow [the pit-of-success](https://blog.codinghorror.com/falling-into-the-pit-of-success/)) 12 | 2. __must not add accidental complexity(1) to the value types of the developers using the library__ (called *Devs* hereafter). Thus, it must not force the Devs to add boiler-plate or complicated code (like Equality or Hashcode implementations) in their own Value Types. 13 | - (1) *accidental complexity* means plumbering or technical code that has nothing to deal with the domain logic 14 | 3. __packages must be available for most of the .NET platforms and versions__ ( >= .NET 4.0, >= dotnet standard 1.3 and thus dotnet core compliant) 15 | 16 | ## Internal-design drivers 17 | 18 | Here are some drivers and insights about the implementation design of the *Value* library: 19 | 20 | - 100% tests coverage for the entire library 21 | - __*ByValue* collections (e.g. ListByValue, DictionaryByValue,..) are NOT *ValueTypes*__ (since we can modify them). Reason why they must NOT inherit from ValueType 22 | - ValueType and all *ByValue* collections __MUST accept struct as well as reference types__ (i.e. classes) 23 | - All *ByValue* collections should behave the same way as the collection they are decorating/wrapping (by delegation). Reason why they are derived from them. 24 | - Hashcode of EquatableByValue types must not be computed more than once (thus be cached) for performance purpose. Nonetheless, the resulted hashcode of all the *ByValue* collections must be reset every time the collection is changed (e.g. Add, Remove, etc). 25 | 26 | - TODO: All the implementation must follow the StyleCop rules provided with the project 27 | - Whenever possible (and if it doesn't violate the External design drivers for the library), __foster *Aggregation* over *Inheritance*__. If you can't, try to minimize the level of inheritance. 28 | - All implementation types should be easily copied-pasted for those that want to use the library in a .NET platform or version that aren't cover yet by *Value*. 29 | 30 | 31 | ## Misc implementation details/insights 32 | 33 | - I tried to foster aggregation (with an EquatableStrategy) over inheritance for the implementation of the library, but it forced me to publicly expose stuffs (like ResetHashcode() method) in the EquatableByValue type in order for the aggregated strategy (pattern) to be able to use it (and thus making it appeared in the ValueType) that I didn't want to expose for all Value types at the end of the day (since it violated the rule #2: *don't add accidental complexity to the library's users value types* ) 34 | - Thus, I rollbacked the implementation to a classic inheritance things. Sad Panda. I have other ideas to explore in order to retry getting rid of this inheritance but I just need time ;) 35 | 36 | 37 | -------------------------------------------------------------------------------- /.build/Build.tasks: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /Value.Net45/Value.Net45.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {9D20346D-AD57-40AA-A438-59D8245F8AA3} 8 | Library 9 | Properties 10 | Value 11 | Value 12 | v4.8 13 | 512 14 | 15 | 16 | 17 | false 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | true 38 | 39 | 40 | Value.snk 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Version.cs 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | Artifacts/ 11 | Artifacts/Binaries/*.* 12 | Artifacts/Binaries/*.* 13 | Artifacts/Packages/*.* 14 | Artifacts/Docs/*.* 15 | TestResult.xml 16 | 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | x64/ 20 | build/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | 24 | # KDiff3 backup files 25 | *.orig 26 | 27 | # MSTest test Results 28 | [Tt]est[Rr]esult*/ 29 | [Bb]uild[Ll]og.* 30 | 31 | *_i.c 32 | *_p.c 33 | *.ilk 34 | *.meta 35 | *.obj 36 | *.pch 37 | *.pdb 38 | *.pgc 39 | *.pgd 40 | *.rsp 41 | *.sbr 42 | *.tlb 43 | *.tli 44 | *.tlh 45 | *.tmp 46 | *.tmp_proj 47 | *.log 48 | *.vspscc 49 | *.vssscc 50 | .builds 51 | *.pidb 52 | *.log 53 | *.scc 54 | 55 | # Visual C++ cache files 56 | ipch/ 57 | *.aps 58 | *.ncb 59 | *.opensdf 60 | *.sdf 61 | *.cachefile 62 | 63 | # Visual Studio profiler 64 | *.psess 65 | *.vsp 66 | *.vspx 67 | 68 | # Guidance Automation Toolkit 69 | *.gpState 70 | 71 | # ReSharper is a .NET coding add-in 72 | _ReSharper*/ 73 | *.[Rr]e[Ss]harper 74 | 75 | # TeamCity is a build add-in 76 | _TeamCity* 77 | 78 | # DotCover is a Code Coverage Tool 79 | *.dotCover 80 | 81 | # NCrunch 82 | *.ncrunch* 83 | .*crunch*.local.xml 84 | 85 | # Installshield output folder 86 | [Ee]xpress/ 87 | 88 | # DocProject is a documentation generator add-in 89 | DocProject/buildhelp/ 90 | DocProject/Help/*.HxT 91 | DocProject/Help/*.HxC 92 | DocProject/Help/*.hhc 93 | DocProject/Help/*.hhk 94 | DocProject/Help/*.hhp 95 | DocProject/Help/Html2 96 | DocProject/Help/html 97 | 98 | # Click-Once directory 99 | publish/ 100 | 101 | # Publish Web Output 102 | *.Publish.xml 103 | *.pubxml 104 | 105 | # NuGet Packages Directory 106 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 107 | packages/ 108 | 109 | # Windows Azure Build Output 110 | csx 111 | *.build.csdef 112 | 113 | # Windows Store app package directory 114 | AppPackages/ 115 | 116 | # Others 117 | sql/ 118 | *.Cache 119 | ClientBin/ 120 | [Ss]tyle[Cc]op.* 121 | ~$* 122 | *~ 123 | *.dbmdl 124 | *.[Pp]ublish.xml 125 | *.pfx 126 | *.publishsettings 127 | 128 | # RIA/Silverlight projects 129 | Generated_Code/ 130 | 131 | # Backup & report files from converting an old project file to a newer 132 | # Visual Studio version. Backup files are not needed, because we have git ;-) 133 | _UpgradeReport_Files/ 134 | Backup*/ 135 | UpgradeLog*.XML 136 | UpgradeLog*.htm 137 | 138 | # SQL Server files 139 | App_Data/*.mdf 140 | App_Data/*.ldf 141 | 142 | # ========================= 143 | # Windows detritus 144 | # ========================= 145 | 146 | # Windows image file caches 147 | Thumbs.db 148 | ehthumbs.db 149 | 150 | # Folder config file 151 | Desktop.ini 152 | 153 | # Recycle Bin used on file shares 154 | $RECYCLE.BIN/ 155 | 156 | # Mac crap 157 | .DS_Store 158 | ._.DS_Store 159 | 160 | NFluent.userprefs 161 | 162 | NFluent.Tests/test-results/NFluent.Tests.csproj-Debug-2014-02-24.xml 163 | 164 | NFluent.Tests/test-results/NFluent.Tests.csproj.test-cache 165 | -------------------------------------------------------------------------------- /Value.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33213.308 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Value.Net45", "Value.Net45\Value.Net45.csproj", "{9D20346D-AD57-40AA-A438-59D8245F8AA3}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Value.Tests", "Value.Tests\Value.Tests.csproj", "{6570B926-81EC-4171-91FB-BBDBE55934D1}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{44F51B3E-5774-4F04-89A6-2B125CFE67C1}" 11 | ProjectSection(SolutionItems) = preProject 12 | .gitignore = .gitignore 13 | appveyor.yml = appveyor.yml 14 | BackLog.md = BackLog.md 15 | build.cmd = build.cmd 16 | .build\Build.proj = .build\Build.proj 17 | .build\Build.tasks = .build\Build.tasks 18 | ImplementationDetails.md = ImplementationDetails.md 19 | Readme.md = Readme.md 20 | ReleaseNoteContentToBeInsertedWithinNuspecFile.txt = ReleaseNoteContentToBeInsertedWithinNuspecFile.txt 21 | Value.nuspec = Value.nuspec 22 | EndProjectSection 23 | EndProject 24 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Value", "Value\Value.shproj", "{44709B35-26E8-4588-A87A-1727CDB11255}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Value.Standard", "Value.Standard\Value.Standard.csproj", "{E34876F1-8A7D-4B5C-940B-D1DFF1B639CD}" 27 | EndProject 28 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Value.Net40", "Value.Net40\Value.Net40.csproj", "{833BCC87-B759-4502-8C32-93D396AE1357}" 29 | EndProject 30 | Global 31 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 32 | Debug|Any CPU = Debug|Any CPU 33 | Release|Any CPU = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 36 | {9D20346D-AD57-40AA-A438-59D8245F8AA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {9D20346D-AD57-40AA-A438-59D8245F8AA3}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {9D20346D-AD57-40AA-A438-59D8245F8AA3}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {9D20346D-AD57-40AA-A438-59D8245F8AA3}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {6570B926-81EC-4171-91FB-BBDBE55934D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {6570B926-81EC-4171-91FB-BBDBE55934D1}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {6570B926-81EC-4171-91FB-BBDBE55934D1}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {6570B926-81EC-4171-91FB-BBDBE55934D1}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {E34876F1-8A7D-4B5C-940B-D1DFF1B639CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {E34876F1-8A7D-4B5C-940B-D1DFF1B639CD}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {E34876F1-8A7D-4B5C-940B-D1DFF1B639CD}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {E34876F1-8A7D-4B5C-940B-D1DFF1B639CD}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {833BCC87-B759-4502-8C32-93D396AE1357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {833BCC87-B759-4502-8C32-93D396AE1357}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {833BCC87-B759-4502-8C32-93D396AE1357}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {833BCC87-B759-4502-8C32-93D396AE1357}.Release|Any CPU.Build.0 = Release|Any CPU 52 | EndGlobalSection 53 | GlobalSection(SolutionProperties) = preSolution 54 | HideSolutionNode = FALSE 55 | EndGlobalSection 56 | GlobalSection(ExtensibilityGlobals) = postSolution 57 | SolutionGuid = {12957C47-723B-4BCF-B959-79D5BB8C2711} 58 | EndGlobalSection 59 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 60 | Value\Value.Shared.projitems*{44709b35-26e8-4588-a87a-1727cdb11255}*SharedItemsImports = 13 61 | Value\Value.Shared.projitems*{833bcc87-b759-4502-8c32-93d396ae1357}*SharedItemsImports = 4 62 | Value\Value.Shared.projitems*{9d20346d-ad57-40aa-a438-59d8245f8aa3}*SharedItemsImports = 4 63 | Value\Value.Shared.projitems*{e34876f1-8a7d-4b5c-940b-d1dff1b639cd}*SharedItemsImports = 5 64 | EndGlobalSection 65 | EndGlobal 66 | -------------------------------------------------------------------------------- /Value/ListByValue.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | 17 | using System.Linq; 18 | 19 | namespace Value 20 | { 21 | using System.Collections; 22 | using System.Collections.Generic; 23 | 24 | /// 25 | /// A list with equality based on its content and not on the list's reference 26 | /// (i.e.: 2 different instances containing the same items in the same order will be equals). 27 | /// 28 | /// This type is not thread-safe (for hashcode updates). 29 | /// Type of the listed items. 30 | public class ListByValue : EquatableByValue>, IList 31 | { 32 | private readonly IList list; 33 | 34 | public ListByValue() : this(new List()) 35 | { 36 | } 37 | 38 | public ListByValue(IList list) 39 | { 40 | this.list = list; 41 | } 42 | 43 | public int Count => this.list.Count; 44 | 45 | public bool IsReadOnly => ((ICollection)this.list).IsReadOnly; 46 | 47 | public T this[int index] 48 | { 49 | get => this.list[index]; 50 | set 51 | { 52 | this.ResetHashCode(); 53 | this.list[index] = value; 54 | } 55 | } 56 | 57 | public void Add(T item) 58 | { 59 | this.ResetHashCode(); 60 | this.list.Add(item); 61 | } 62 | 63 | public void Clear() 64 | { 65 | this.ResetHashCode(); 66 | this.list.Clear(); 67 | } 68 | 69 | public bool Contains(T item) 70 | { 71 | return this.list.Contains(item); 72 | } 73 | 74 | public void CopyTo(T[] array, int arrayIndex) 75 | { 76 | this.list.CopyTo(array, arrayIndex); 77 | } 78 | 79 | public IEnumerator GetEnumerator() 80 | { 81 | return this.list.GetEnumerator(); 82 | } 83 | 84 | public int IndexOf(T item) 85 | { 86 | return this.list.IndexOf(item); 87 | } 88 | 89 | public void Insert(int index, T item) 90 | { 91 | this.ResetHashCode(); 92 | this.list.Insert(index, item); 93 | } 94 | 95 | public bool Remove(T item) 96 | { 97 | this.ResetHashCode(); 98 | return this.list.Remove(item); 99 | } 100 | 101 | public void RemoveAt(int index) 102 | { 103 | this.ResetHashCode(); 104 | this.list.RemoveAt(index); 105 | } 106 | 107 | IEnumerator IEnumerable.GetEnumerator() 108 | { 109 | return ((IEnumerable)this.list).GetEnumerator(); 110 | } 111 | 112 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 113 | { 114 | return this.list.Cast(); 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /Value/EquatableByValue.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | using System; 17 | using System.Collections.Generic; 18 | using System.Linq; 19 | 20 | namespace Value 21 | { 22 | /// 23 | /// Support a by-Value Equality and Unicity. 24 | /// 25 | /// This latest implementation has been inspired from Scott Millett's book (Patterns, Principles, and Practices of Domain-Driven Design). 26 | /// Type of the elements. 27 | public abstract class EquatableByValue : IEquatable 28 | { 29 | protected const int Undefined = -1; 30 | 31 | protected volatile int HashCode = Undefined; 32 | 33 | protected void ResetHashCode() 34 | { 35 | HashCode = Undefined; 36 | } 37 | 38 | public static bool operator ==(EquatableByValue x, EquatableByValue y) 39 | { 40 | if (x is null && y is null) 41 | { 42 | return true; 43 | } 44 | 45 | if (x is null || y is null) 46 | { 47 | return false; 48 | } 49 | 50 | return x.Equals(y); 51 | } 52 | 53 | public static bool operator !=(EquatableByValue x, EquatableByValue y) 54 | { 55 | return !(x == y); 56 | } 57 | 58 | public bool Equals(T other) 59 | { 60 | var otherEquatable = other as EquatableByValue; 61 | if (otherEquatable == null) 62 | { 63 | return false; 64 | } 65 | 66 | return EqualsImpl(otherEquatable); 67 | } 68 | 69 | public override bool Equals(object obj) 70 | { 71 | if (obj == null) 72 | { 73 | return false; 74 | } 75 | 76 | T other; 77 | 78 | try 79 | { 80 | // we use a static cast here since we can't use the 'as' operator for structs and other value type primitives 81 | other = (T)obj; 82 | } 83 | catch (InvalidCastException e) 84 | { 85 | return false; 86 | } 87 | 88 | return Equals(other); 89 | } 90 | 91 | protected abstract IEnumerable GetAllAttributesToBeUsedForEquality(); 92 | 93 | protected virtual bool EqualsImpl(EquatableByValue otherEquatable) 94 | { 95 | // Implementation where orders of the elements matters. 96 | return GetAllAttributesToBeUsedForEquality().SequenceEqual(otherEquatable.GetAllAttributesToBeUsedForEquality()); 97 | } 98 | 99 | public override int GetHashCode() 100 | { 101 | // Implementation where orders of the elements matters. 102 | if (HashCode == Undefined) 103 | { 104 | var code = 0; 105 | 106 | foreach (var attribute in GetAllAttributesToBeUsedForEquality()) 107 | { 108 | code = (code * 397) ^ (attribute == null ? 0 : attribute.GetHashCode()); 109 | } 110 | 111 | HashCode = code; 112 | } 113 | 114 | return HashCode; 115 | } 116 | 117 | } 118 | } -------------------------------------------------------------------------------- /Value/DictionaryByValue.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2017 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | using System.Linq; 17 | 18 | namespace Value.Shared 19 | { 20 | using System.Collections; 21 | using System.Collections.Generic; 22 | 23 | /// 24 | /// A dictionary with equality based on its content and not on the dictionary's reference 25 | /// (i.e.: 2 different instances containing the same entries will be equals). 26 | /// 27 | /// This type is not thread-safe (for hashcode updates). 28 | /// The type of keys in the dictionary. 29 | /// The type of values in the dictionary. 30 | public class DictionaryByValue : EquatableByValueWithoutOrder>, IDictionary 31 | { 32 | private readonly IDictionary dictionary; 33 | 34 | public DictionaryByValue(IDictionary dictionary) 35 | { 36 | this.dictionary = dictionary; 37 | } 38 | 39 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 40 | { 41 | foreach (var kv in dictionary) 42 | { 43 | yield return kv; 44 | } 45 | } 46 | 47 | protected override bool EqualsWithoutOrderImpl(EquatableByValueWithoutOrder> obj) 48 | { 49 | var other = obj as DictionaryByValue; 50 | if (other == null) 51 | { 52 | return false; 53 | } 54 | 55 | return !this.dictionary.Except(other).Any(); 56 | } 57 | 58 | IEnumerator IEnumerable.GetEnumerator() 59 | { 60 | return dictionary.GetEnumerator(); 61 | } 62 | 63 | public IEnumerator> GetEnumerator() 64 | { 65 | return dictionary.GetEnumerator(); 66 | } 67 | 68 | public void Add(KeyValuePair item) 69 | { 70 | base.ResetHashCode(); 71 | dictionary.Add(item); 72 | } 73 | 74 | public void Clear() 75 | { 76 | base.ResetHashCode(); 77 | dictionary.Clear(); 78 | } 79 | 80 | public bool Contains(KeyValuePair item) 81 | { 82 | return dictionary.Contains(item); 83 | } 84 | 85 | public void CopyTo(KeyValuePair[] array, int arrayIndex) 86 | { 87 | dictionary.CopyTo(array, arrayIndex); 88 | } 89 | 90 | public bool Remove(KeyValuePair item) 91 | { 92 | base.ResetHashCode(); 93 | return dictionary.Remove(item); 94 | } 95 | 96 | public int Count => dictionary.Count; 97 | 98 | public bool IsReadOnly => dictionary.IsReadOnly; 99 | 100 | public bool ContainsKey(K key) 101 | { 102 | return dictionary.ContainsKey(key); 103 | } 104 | 105 | public void Add(K key, V value) 106 | { 107 | base.ResetHashCode(); 108 | dictionary.Add(key, value); 109 | } 110 | 111 | public bool Remove(K key) 112 | { 113 | base.ResetHashCode(); 114 | return dictionary.Remove(key); 115 | } 116 | 117 | public bool TryGetValue(K key, out V value) 118 | { 119 | return dictionary.TryGetValue(key, out value); 120 | } 121 | 122 | public V this[K key] 123 | { 124 | get => dictionary[key]; 125 | set 126 | { 127 | base.ResetHashCode(); 128 | dictionary[key] = value; 129 | } 130 | } 131 | 132 | public ICollection Keys => dictionary.Keys; 133 | 134 | public ICollection Values => dictionary.Values; 135 | } 136 | } -------------------------------------------------------------------------------- /Value.Tests/Value.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Debug 7 | AnyCPU 8 | {6570B926-81EC-4171-91FB-BBDBE55934D1} 9 | Library 10 | Properties 11 | Value.Tests 12 | Value.Tests 13 | v4.8 14 | 512 15 | 16 | 17 | 18 | 19 | 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | pdbonly 30 | true 31 | bin\Release\ 32 | TRACE 33 | prompt 34 | 4 35 | 36 | 37 | 38 | ..\packages\NFluent.2.8.0\lib\net45\NFluent.dll 39 | 40 | 41 | ..\packages\NUnit.3.13.3\lib\net45\nunit.framework.dll 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {9d20346d-ad57-40aa-a438-59d8245f8aa3} 69 | Value.Net45 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 85 | 86 | 87 | 88 | 95 | -------------------------------------------------------------------------------- /Value/SetByValue.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value 17 | { 18 | using System.Collections; 19 | using System.Collections.Generic; 20 | 21 | /// 22 | /// A Set with equality based on its content and not on the Set's reference 23 | /// (i.e.: 2 different instances containing the same items will be equals whatever their storage order). 24 | /// 25 | /// This type is not thread-safe (for hashcode updates). 26 | /// Type of the listed items. 27 | public class SetByValue : EquatableByValueWithoutOrder, ISet 28 | { 29 | private readonly ISet hashSet; 30 | 31 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 32 | { 33 | return (IEnumerable)this.hashSet; 34 | } 35 | 36 | protected override bool EqualsWithoutOrderImpl(EquatableByValueWithoutOrder obj) 37 | { 38 | var other = obj as SetByValue; 39 | if (other == null) 40 | { 41 | return false; 42 | } 43 | 44 | return this.hashSet.SetEquals(other); 45 | } 46 | 47 | public SetByValue(ISet hashSet) 48 | { 49 | this.hashSet = hashSet; 50 | } 51 | 52 | public SetByValue() : this(new HashSet()) 53 | { 54 | } 55 | 56 | public int Count => this.hashSet.Count; 57 | 58 | public bool IsReadOnly => this.hashSet.IsReadOnly; 59 | 60 | public void Add(T item) 61 | { 62 | base.ResetHashCode(); 63 | this.hashSet.Add(item); 64 | } 65 | 66 | public void Clear() 67 | { 68 | base.ResetHashCode(); 69 | this.hashSet.Clear(); 70 | } 71 | 72 | public bool Contains(T item) 73 | { 74 | return this.hashSet.Contains(item); 75 | } 76 | 77 | public void CopyTo(T[] array, int arrayIndex) 78 | { 79 | this.hashSet.CopyTo(array, arrayIndex); 80 | } 81 | 82 | public void ExceptWith(IEnumerable other) 83 | { 84 | base.ResetHashCode(); 85 | this.hashSet.ExceptWith(other); 86 | } 87 | 88 | public IEnumerator GetEnumerator() 89 | { 90 | return this.hashSet.GetEnumerator(); 91 | } 92 | 93 | public void IntersectWith(IEnumerable other) 94 | { 95 | base.ResetHashCode(); 96 | this.hashSet.IntersectWith(other); 97 | } 98 | 99 | public bool IsProperSubsetOf(IEnumerable other) 100 | { 101 | return this.hashSet.IsProperSubsetOf(other); 102 | } 103 | 104 | public bool IsProperSupersetOf(IEnumerable other) 105 | { 106 | return this.hashSet.IsProperSupersetOf(other); 107 | } 108 | 109 | public bool IsSubsetOf(IEnumerable other) 110 | { 111 | return this.hashSet.IsSubsetOf(other); 112 | } 113 | 114 | public bool IsSupersetOf(IEnumerable other) 115 | { 116 | return this.hashSet.IsSupersetOf(other); 117 | } 118 | 119 | public bool Overlaps(IEnumerable other) 120 | { 121 | return this.hashSet.Overlaps(other); 122 | } 123 | 124 | public bool Remove(T item) 125 | { 126 | base.ResetHashCode(); 127 | return this.hashSet.Remove(item); 128 | } 129 | 130 | public bool SetEquals(IEnumerable other) 131 | { 132 | return this.hashSet.SetEquals(other); 133 | } 134 | 135 | public void SymmetricExceptWith(IEnumerable other) 136 | { 137 | base.ResetHashCode(); 138 | this.hashSet.SymmetricExceptWith(other); 139 | } 140 | 141 | public void UnionWith(IEnumerable other) 142 | { 143 | base.ResetHashCode(); 144 | this.hashSet.UnionWith(other); 145 | } 146 | 147 | bool ISet.Add(T item) 148 | { 149 | base.ResetHashCode(); 150 | return this.hashSet.Add(item); 151 | } 152 | 153 | IEnumerator IEnumerable.GetEnumerator() 154 | { 155 | return ((IEnumerable)this.hashSet).GetEnumerator(); 156 | } 157 | } 158 | } -------------------------------------------------------------------------------- /Value.Tests/Samples/Card.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests.Samples 17 | { 18 | using System; 19 | using System.Collections.Generic; 20 | 21 | /// 22 | /// One of the 52 cards from a poker deck. Every card has a value, a suit and a name. 23 | /// 24 | public class Card : ValueType, IComparable 25 | { 26 | // TODO: make cards ranking dependant on poker game type (high-low, omaha, etc)? 27 | private const int AceValue = 14; 28 | 29 | private const int JackValue = 11; 30 | 31 | private const int KingValue = 13; 32 | 33 | private const int QueenValue = 12; 34 | 35 | private Card(string cardDescription, int value, Suit suit) 36 | { 37 | this.CardDescription = cardDescription; 38 | this.Value = value; 39 | this.Suit = suit; 40 | } 41 | 42 | public string CardDescription { get; private set; } 43 | 44 | public string DisplayName 45 | { 46 | get 47 | { 48 | var displayName = this.Value.ToString(); 49 | 50 | if (this.Value == AceValue) 51 | { 52 | displayName = "Ace"; 53 | } 54 | else if (this.Value == KingValue) 55 | { 56 | displayName = "King"; 57 | } 58 | else if (this.Value == QueenValue) 59 | { 60 | displayName = "Queen"; 61 | } 62 | else if (this.Value == JackValue) 63 | { 64 | displayName = "Jack"; 65 | } 66 | 67 | return displayName; 68 | } 69 | } 70 | 71 | public Suit Suit { get; private set; } 72 | 73 | public int Value { get; private set; } 74 | 75 | public static Card Parse(string cardDescription) 76 | { 77 | return new Card(cardDescription, ExtractValue(cardDescription), ExtractSuit(cardDescription)); 78 | } 79 | 80 | public int CompareTo(Card other) 81 | { 82 | return this.Value.CompareTo(other.Value); 83 | } 84 | 85 | public override string ToString() 86 | { 87 | return $"{this.CardDescription}"; 88 | } 89 | 90 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 91 | { 92 | return new List() { this.Value, this.CardDescription, this.DisplayName, this.Suit }; 93 | } 94 | 95 | private static Suit ExtractSuit(string cardDescription) 96 | { 97 | switch (cardDescription[1]) 98 | { 99 | case 'C': 100 | return Suit.C; 101 | 102 | case 'D': 103 | return Suit.D; 104 | 105 | case 'H': 106 | return Suit.H; 107 | 108 | case 'S': 109 | return Suit.S; 110 | 111 | default: 112 | throw new InvalidOperationException(); 113 | } 114 | } 115 | 116 | private static int ExtractValue(string cardDescription) 117 | { 118 | int result; 119 | var valueOrFace = cardDescription[0]; 120 | if (!int.TryParse(valueOrFace.ToString(), out result)) 121 | { 122 | switch (valueOrFace) 123 | { 124 | case 'T': 125 | result = 10; 126 | break; 127 | 128 | case 'J': 129 | return JackValue; 130 | 131 | break; 132 | 133 | case 'Q': 134 | return QueenValue; 135 | 136 | break; 137 | 138 | case 'K': 139 | return KingValue; 140 | 141 | break; 142 | 143 | case 'A': 144 | return AceValue; 145 | 146 | break; 147 | 148 | default: 149 | throw new InvalidOperationException(); 150 | } 151 | } 152 | 153 | return result; 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /Value.Tests/ValueTypeShould.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests 17 | { 18 | using System.Collections.Generic; 19 | using NFluent; 20 | using NUnit.Framework; 21 | using Value.Tests.Samples; 22 | 23 | [TestFixture] 24 | public class ValueTypeShould 25 | { 26 | [Test] 27 | public void Distinguish_a_derived_instance_from_a_base_value_type_with_same_sub_common_values() 28 | { 29 | var amount = new Amount(new decimal(50.3), Currency.Dollar); 30 | var itemPrice = new ItemPrice("movie", new decimal(50.3), Currency.Dollar); 31 | 32 | Check.That(amount.Equals((object)itemPrice)).IsFalse(); 33 | } 34 | 35 | [Test] 36 | public void Distinguish_a_value_type_from_null() 37 | { 38 | var amount = new Amount(new decimal(50.3), Currency.Dollar); 39 | 40 | Check.That(amount).IsNotEqualTo(null); 41 | Check.That(amount == null).IsFalse(); 42 | Check.That(amount != null).IsTrue(); 43 | Check.That(amount.Equals((object)null)).IsFalse(); 44 | Check.That(amount.Equals(null)).IsFalse(); 45 | } 46 | 47 | [Test] 48 | public void Distinguish_a_value_type_instance_from_WTF_is_this_other_type() 49 | { 50 | var amount = new Amount(new decimal(50.3), Currency.Dollar); 51 | Check.That(amount.Equals(Card.Parse("QS"))).IsFalse(); 52 | } 53 | 54 | [Test] 55 | public void Distinguish_a_value_type_instances_from_one_of_its_derived_type_instance() 56 | { 57 | var amount = new Amount(new decimal(50.3), Currency.Dollar); 58 | var itemPrice = new ItemPrice("movie", new decimal(50.3), Currency.Dollar); 59 | 60 | Check.That((Amount)itemPrice).IsNotEqualTo(amount); 61 | Check.That(itemPrice == amount).IsFalse(); 62 | Check.That(itemPrice.Equals(amount)).IsFalse(); 63 | Check.That(itemPrice.Equals((object)amount)).IsFalse(); 64 | } 65 | 66 | [Test] 67 | public void Distinguish_properly_implemented_derived_value_type_instances() 68 | { 69 | var itemPrice = new ItemPrice("movie", new decimal(50.3), Currency.Dollar); 70 | var itemPriceWithOtherValues = new ItemPrice("not a movie", new decimal(50.3), Currency.Dollar); 71 | var otherPriceInstanceWithSameValues = new ItemPrice("movie", new decimal(50.3), Currency.Dollar); 72 | 73 | Check.That(itemPrice).IsNotEqualTo(itemPriceWithOtherValues); 74 | Check.That(otherPriceInstanceWithSameValues).IsEqualTo(itemPrice); 75 | } 76 | 77 | [Test] 78 | public void Find_equals_2_different_instances_of_ValueType_with_different_values_when_equality_is_badly_implemented() 79 | { 80 | var itemPrice = new ItemPriceWithBadImplementationForEqualityAndUnicity("movie", new decimal(50.3), Currency.Dollar); 81 | var differentItemPriceValue = new ItemPriceWithBadImplementationForEqualityAndUnicity("not a movie", new decimal(50.3), Currency.Dollar); 82 | 83 | Check.That(itemPrice).IsEqualTo(differentItemPriceValue); // because bad implementation of equality 84 | } 85 | 86 | [Test] 87 | public void Find_equals_2_different_instances_of_ValueType_with_same_values() 88 | { 89 | var amount = new Amount(new decimal(50.3), Currency.Dollar); 90 | var sameAmountValue = new Amount(new decimal(50.3), Currency.Dollar); 91 | 92 | Check.That(amount).IsEqualTo(sameAmountValue); 93 | } 94 | 95 | [Test] 96 | public void Retrieve_properly_implemented_derived_value_type_with_same_values_in_a_set() 97 | { 98 | var itemPrice = new ItemPrice("movie", new decimal(50.3), Currency.Dollar); 99 | var itemPriceWithSameValues = new ItemPrice("movie", new decimal(50.3), Currency.Dollar); 100 | 101 | var set = new HashSet { itemPrice }; 102 | 103 | Check.That(set).ContainsExactly(itemPriceWithSameValues); 104 | } 105 | 106 | [Test] 107 | public void Retrieve_value_type_with_same_values_in_a_set() 108 | { 109 | var amount = new Amount(new decimal(50.3), Currency.Dollar); 110 | var amountWithSameValues = new Amount(new decimal(50.3), Currency.Dollar); 111 | 112 | var set = new HashSet { amount }; 113 | 114 | Check.That(set).ContainsExactly(amountWithSameValues); 115 | } 116 | 117 | [Test] 118 | public void Retrieve_wrong_instances_from_a_set_when_unicity_is_badly_implemented_on_a_derived_value_type() 119 | { 120 | var itemPrice = new ItemPriceWithBadImplementationForEqualityAndUnicity("movie", new decimal(50.3), Currency.Dollar); 121 | var itemPriceWithOtherValues = new ItemPriceWithBadImplementationForEqualityAndUnicity("XXX movie", new decimal(50.3), Currency.Dollar); 122 | 123 | var set = new HashSet { itemPrice }; 124 | 125 | Check.That(set).ContainsExactly(itemPriceWithOtherValues); // because bad implementation of unicity 126 | } 127 | 128 | [Test] 129 | public void Work_with_null_when_using_equality_operator() 130 | { 131 | Amount amount1 = null; 132 | Amount amount2 = null; 133 | 134 | Check.That(amount1 == amount2).IsTrue(); 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /Value.Tests/ListByValueShould.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests 17 | { 18 | using System.Collections.Generic; 19 | using NFluent; 20 | using NUnit.Framework; 21 | using Value.Tests.Samples; 22 | 23 | [TestFixture] 24 | public class ListByValueShould 25 | { 26 | [Test] 27 | public void Accept_list_as_constructor_argument() 28 | { 29 | var list = new List() { 1, 2, 3 }; 30 | 31 | Check.That(new ListByValue(list)).ContainsExactly(1, 2, 3); 32 | } 33 | 34 | [Test] 35 | public void Change_its_hashcode_everytime_the_list_is_updated() 36 | { 37 | var list = new ListByValue() { Card.Parse("QC"), Card.Parse("TS") }; 38 | 39 | var previousHashcode = list.GetHashCode(); 40 | list.Add(Card.Parse("3H")); // ---update the list --- 41 | var currentHashcode = list.GetHashCode(); 42 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 43 | 44 | 45 | previousHashcode = list.GetHashCode(); 46 | list.Remove(Card.Parse("QC")); // ---update the list --- 47 | currentHashcode = list.GetHashCode(); 48 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 49 | 50 | previousHashcode = list.GetHashCode(); 51 | list.Clear(); // ---update the list --- 52 | currentHashcode = list.GetHashCode(); 53 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 54 | 55 | previousHashcode = list.GetHashCode(); 56 | list.Insert(0, Card.Parse("AS")); // ---update the list --- 57 | currentHashcode = list.GetHashCode(); 58 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 59 | 60 | previousHashcode = list.GetHashCode(); 61 | list[0] = Card.Parse("QH"); 62 | currentHashcode = list.GetHashCode(); 63 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 64 | 65 | previousHashcode = list.GetHashCode(); 66 | list.RemoveAt(0); 67 | currentHashcode = list.GetHashCode(); 68 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 69 | } 70 | 71 | [Test] 72 | public void Consider_two_Lists_of_the_same_integers_Equals() 73 | { 74 | var listA = new List() {1, 2, 3, 4, 5}; 75 | var listB = new List() {1, 2, 3, 4, 5}; 76 | 77 | var listByValueA = new ListByValue(listA); 78 | var listByValueB = new ListByValue(listB); 79 | 80 | Check.That(listByValueB).IsEqualTo(listByValueA); 81 | } 82 | 83 | [Test] 84 | public void Consider_Equals_two_instances_with_same_reference_types_elements_in_same_order() 85 | { 86 | var firstElement = new object(); 87 | var secondElement = new object(); 88 | 89 | var listA = new ListByValue() { firstElement, secondElement }; 90 | var listB = new ListByValue() { firstElement, secondElement }; 91 | 92 | Check.That(listA).IsEqualTo(listB).And.ContainsExactly(firstElement, secondElement); 93 | } 94 | 95 | [Test] 96 | public void Consider_Equals_two_instances_with_same_ValueType_elements_in_same_order() 97 | { 98 | var listA = new ListByValue() { Card.Parse("QC"), Card.Parse("TS") }; 99 | var listB = new ListByValue() { Card.Parse("QC"), Card.Parse("TS") }; 100 | 101 | Check.That(listA).IsEqualTo(listB).And.ContainsExactly(Card.Parse("QC"), Card.Parse("TS")); 102 | } 103 | 104 | [Test] 105 | public void Consider_Not_Equals_two_instances_with_same_elements_in_different_order() 106 | { 107 | var firstElement = new object(); 108 | var secondElement = new object(); 109 | 110 | var listA = new ListByValue() { firstElement, secondElement }; 111 | var listB = new ListByValue() { secondElement, firstElement }; 112 | 113 | Check.That(listB).IsNotEqualTo(listA).And.ContainsExactly(secondElement, firstElement); 114 | } 115 | 116 | [Test] 117 | public void Properly_expose_Count() 118 | { 119 | var list = new ListByValue() { Card.Parse("QC"), Card.Parse("TS") }; 120 | Check.That(list.Count).IsEqualTo(2); 121 | 122 | list.Add(Card.Parse("4D")); 123 | Check.That(list.Count).IsEqualTo(3); 124 | } 125 | 126 | [Test] 127 | public void Properly_expose_Contains() 128 | { 129 | var list = new ListByValue() { Card.Parse("QC"), Card.Parse("TS") }; 130 | Check.That(list.Contains(Card.Parse("TS"))).IsTrue(); 131 | Check.That(list.Contains(Card.Parse("4D"))).IsFalse(); 132 | } 133 | 134 | [Test] 135 | public void Properly_expose_CopyTo() 136 | { 137 | var list = new ListByValue() { Card.Parse("QC"), Card.Parse("TS") }; 138 | var cards = new Card[5]; 139 | list.CopyTo(cards, 2); 140 | 141 | Check.That(cards).ContainsExactly(null, null, Card.Parse("QC"), Card.Parse("TS"), null); 142 | } 143 | 144 | [Test] 145 | public void Properly_expose_IEnumerable() 146 | { 147 | var list = new ListByValue() { Card.Parse("QC"), Card.Parse("TS") }; 148 | 149 | foreach (var card in list) 150 | { 151 | Check.That(card == Card.Parse("QC") || card == Card.Parse("TS")).IsTrue(); 152 | } 153 | } 154 | 155 | [Test] 156 | public void Properly_expose_indexer() 157 | { 158 | var list = new ListByValue() { Card.Parse("QC"), Card.Parse("TS") }; 159 | Check.That(list[0]).IsEqualTo(Card.Parse("QC")); 160 | Check.That(list[1]).IsEqualTo(Card.Parse("TS")); 161 | } 162 | 163 | [Test] 164 | public void Properly_expose_IndexOf() 165 | { 166 | var list = new ListByValue() { Card.Parse("QC"), Card.Parse("TS") }; 167 | Check.That(list.IndexOf(Card.Parse("QC"))).IsEqualTo(0); 168 | Check.That(list.IndexOf(Card.Parse("TS"))).IsEqualTo(1); 169 | } 170 | 171 | [Test] 172 | public void Properly_expose_IsReadOnly() 173 | { 174 | var originalList = new List() { 0, 1, 2 }; 175 | var listByValue = new ListByValue(originalList); 176 | 177 | ICollection original = originalList; 178 | Check.That(listByValue.IsReadOnly).IsEqualTo(original.IsReadOnly); 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /Value.Tests/DictionaryByValueShould.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using NFluent; 9 | using NUnit.Framework; 10 | using Value.Shared; 11 | 12 | namespace Value.Tests 13 | { 14 | [TestFixture] 15 | public class DictionaryByValueShould 16 | { 17 | [Test] 18 | public void Consider_two_instances_with_same_elements_inserted_in_same_order_Equals() 19 | { 20 | var dico1 = new Dictionary() { {1, "uno" }, { 4, "quatro" }, { 3, "tres" } }; 21 | var dico2 = new Dictionary() { { 1, "uno" }, { 4, "quatro" }, { 3, "tres" } }; 22 | 23 | var byValue1 = new DictionaryByValue(dico1); 24 | var byValue2 = new DictionaryByValue(dico2); 25 | 26 | Check.That(dico1==dico2).IsFalse(); 27 | Check.That(dico1.Equals(dico2)).IsFalse(); 28 | 29 | Check.That(byValue1 == byValue2).IsTrue(); 30 | Check.That(byValue1.Equals(byValue2)).IsTrue(); 31 | } 32 | 33 | [Test] 34 | public void Consider_two_instances_with_same_elements_inserted_in_different_order_Equals() 35 | { 36 | var dico1 = new Dictionary() { {1, "uno" }, { 4, "quatro" }, { 3, "tres" } }; 37 | var dico2 = new Dictionary() { { 1, "uno" }, { 3, "tres" }, { 4, "quatro" } }; 38 | 39 | var byValue1 = new DictionaryByValue(dico1); 40 | var byValue2 = new DictionaryByValue(dico2); 41 | 42 | Check.That(dico1).IsNotEqualTo(dico2); 43 | Check.That(byValue1).IsEqualTo(byValue2); 44 | } 45 | 46 | [Test] 47 | public void Consider_two_instances_with_different_elements_not_Equals() 48 | { 49 | var dico1 = new Dictionary() { { 1, "uno" }, { 4, "quatro" }, { 3, "tres" } }; 50 | var dico2 = new Dictionary() { { 1, "uno" }, { 79, "setenta y nueve" }, { 4, "quatro" } }; 51 | 52 | var byValue1 = new DictionaryByValue(dico1); 53 | var byValue2 = new DictionaryByValue(dico2); 54 | 55 | Check.That(byValue1).IsNotEqualTo(byValue2); 56 | } 57 | 58 | [Test] 59 | public void Consider_two_instances_with_same_integer_elements_Equals() 60 | { 61 | var dico1 = new Dictionary() { { 1, 1 }, { 4, 4 }, { 3, 3 } }; 62 | var dico2 = new Dictionary() { { 1, 1 }, { 4, 4 }, { 3, 3 } }; 63 | 64 | var byValue1 = new DictionaryByValue(dico1); 65 | var byValue2 = new DictionaryByValue(dico2); 66 | 67 | Check.That(byValue1).IsEqualTo(byValue2); 68 | } 69 | 70 | [Test] 71 | public void Consider_an_instance_not_equals_with_SetByValue_instance() 72 | { 73 | var dictionaryByValue = new DictionaryByValue(new Dictionary() { { 1, "uno" }, { 4, "quatro" }, { 3, "tres" } }); 74 | var setByValue = new SetByValue>() { new KeyValuePair(1, "uno"), 75 | new KeyValuePair(4, "quatro"), 76 | new KeyValuePair(3, "tres") }; 77 | 78 | Check.That(dictionaryByValue.Equals(setByValue)).IsFalse(); 79 | } 80 | 81 | [Test] 82 | public void Change_its_hashcode_everytime_the_dictionary_is_updated() 83 | { 84 | var dico = new DictionaryByValue(new Dictionary() { { 1, "uno" }, { 4, "quatro" }, { 3, "tres" } }); 85 | 86 | var previousHashcode = dico.GetHashCode(); 87 | dico.Add(79, "Setenta y nueve"); 88 | var currentHashcode = dico.GetHashCode(); 89 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 90 | 91 | previousHashcode = dico.GetHashCode(); 92 | dico.Remove(79); 93 | currentHashcode = dico.GetHashCode(); 94 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 95 | 96 | previousHashcode = dico.GetHashCode(); 97 | var keyValuePair = new KeyValuePair(42, "quarenta y dos"); 98 | dico.Add(keyValuePair); 99 | currentHashcode = dico.GetHashCode(); 100 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 101 | 102 | previousHashcode = dico.GetHashCode(); 103 | dico.Remove(keyValuePair); 104 | currentHashcode = dico.GetHashCode(); 105 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 106 | 107 | previousHashcode = dico.GetHashCode(); 108 | dico[33] = "trenta y tres"; 109 | currentHashcode = dico.GetHashCode(); 110 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 111 | 112 | previousHashcode = dico.GetHashCode(); 113 | dico.Clear(); 114 | currentHashcode = dico.GetHashCode(); 115 | Check.That(currentHashcode).IsNotEqualTo(previousHashcode); 116 | } 117 | 118 | [Test] 119 | public void Properly_expose_Contains() 120 | { 121 | var firstDate = DateTime.ParseExact("2017-05-04", "yyyy-MM-dd", CultureInfo.InvariantCulture); 122 | var secondDate = DateTime.ParseExact("2017-05-27", "yyyy-MM-dd", CultureInfo.InvariantCulture); 123 | 124 | var dico = new DictionaryByValue(new Dictionary() { { firstDate, "uno" }, { secondDate, "quatro" } }); 125 | 126 | Check.That(dico.Contains(new KeyValuePair(firstDate, "uno"))); 127 | } 128 | 129 | [Test] 130 | public void Properly_expose_ContainsKey() 131 | { 132 | var firstDate = DateTime.ParseExact("2017-05-04", "yyyy-MM-dd", CultureInfo.InvariantCulture); 133 | var secondDate = DateTime.ParseExact("2017-05-27", "yyyy-MM-dd", CultureInfo.InvariantCulture); 134 | var missingDate = DateTime.ParseExact("1974-12-24", "yyyy-MM-dd", CultureInfo.InvariantCulture); 135 | 136 | var dico = new DictionaryByValue(new Dictionary() { { firstDate, "uno" }, { secondDate, "quatro" } }); 137 | 138 | Check.That(dico.ContainsKey(secondDate)).IsTrue(); 139 | Check.That(dico.ContainsKey(missingDate)).IsFalse(); 140 | } 141 | 142 | [Test] 143 | public void Properly_expose_Count() 144 | { 145 | var dico = new DictionaryByValue(new Dictionary() { { 1, "uno" }, { 2, "quatro" } }); 146 | 147 | Check.That(dico.Count).IsEqualTo(2); 148 | } 149 | 150 | [Test] 151 | public void Properly_expose_Keys() 152 | { 153 | var dico = new DictionaryByValue(new Dictionary() { { 1, "uno" }, { 2, "quatro" } }); 154 | 155 | Check.That(dico.Keys).ContainsExactly(1, 2); 156 | } 157 | 158 | [Test] 159 | public void Properly_expose_Values() 160 | { 161 | var dico = new DictionaryByValue(new Dictionary() { { 1, "uno" }, { 2, "quatro" } }); 162 | 163 | Check.That(dico.Values).ContainsExactly("uno", "quatro"); 164 | } 165 | 166 | [Test] 167 | public void Properly_expose_Indexer() 168 | { 169 | var dico = new DictionaryByValue(new Dictionary() { { 1, "uno" }, { 2, "quatro" } }); 170 | 171 | Check.That(dico[1]).IsEqualTo("uno"); 172 | 173 | dico[42] = "quarenta y dos"; 174 | Check.That(dico[42]).IsEqualTo("quarenta y dos"); 175 | } 176 | 177 | [Test] 178 | public void Properly_expose_TryGetValue() 179 | { 180 | var dico = new DictionaryByValue(new Dictionary() { { 1, "uno" }, { 2, "quatro" } }); 181 | 182 | string result; 183 | dico.TryGetValue(1, out result); 184 | Check.That(result).IsEqualTo("uno"); 185 | } 186 | 187 | [Test] 188 | public void Properly_expose_CopyTo() 189 | { 190 | var dico = new DictionaryByValue(new Dictionary() { { 1, "uno" }, { 2, "quatro" } }); 191 | 192 | KeyValuePair[] array = new KeyValuePair[2]; 193 | dico.CopyTo(array, 0); 194 | 195 | Check.That(array).ContainsExactly(new KeyValuePair(1, "uno"), new KeyValuePair(2, "quatro")); 196 | } 197 | 198 | [Test] 199 | public void Properly_expose_IsReadOnly() 200 | { 201 | var dictionary = new Dictionary() { { 1, "uno" }, { 2, "quatro" } }; 202 | var wrappedDictionary = new DictionaryByValue(dictionary); 203 | 204 | Check.That(wrappedDictionary.IsReadOnly).IsEqualTo(((ICollection>)dictionary).IsReadOnly); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/ju5m6t3fm2xsl0o9/branch/master?svg=true)](https://ci.appveyor.com/project/tpierrain/value/branch/master) 2 | 3 | # Value 4 | 5 | is a pico library (or code snippets shed) to help you to __easily implement Value Types__ in your C# projects without making errors nor polluting your domain logic with boiler-plate code. 6 | 7 | ![Value](https://github.com/tpierrain/Value/blob/master/Value-small.jpg?raw=true) 8 | 9 | ## Value Types? 10 | __Domain Driven Design (DDD)__'s *Value Object* being an oxymoron (indeed, an object has a changing state by nature), we now rather use the "*Value Type*" (instance) terminology. But the concept is the same as described within Eric Evan's Blue book. 11 | 12 | __A Value Type:__ 13 | - is __immutable__ (every field must be read-only after the Value Type instantiation; no 'setter' is allowed) 14 | - is __rich with domain logic and behaviours__. The idea is to swallow (and encapsulate) most of our business complexity within those classes 15 | - embraces the __Ubiquitous Language__ of our business context: cure to primitive obsession, the usage of Value Types is an opportunity for us to embrace the language of our business within our code base 16 | - __exposes, uses and combines functions__ to provide business (domain) value. Functions usually return new instance(s) of Value Types ('*closure of operations*' describing an operation whose return type is the same as the type of its argument(s)) 17 | 18 | ```c# 19 | // e.g.: the following Add function does not change any existing Amount instance, it just returns a new one 20 | public Amount Add(Amount otherAmount) 21 | ``` 22 | 23 | - __considers ALL its attributes for Equality and Uniqueness__ (and "all" *is-a-must* here) 24 | - is __auto-validating__ (via *transactional* constructors __with business validation inside__ and throwing exception if necessary) 25 | 26 | 27 | ### Reverse the trend! (we need more Value Types, and fewer Entities) 28 | 29 | An *Entity* is an object that has a changeable state (often made by combining Value Objects) for which we care about its identity. Whether to use an Entity or Value Object will strongly depend on your business context (*there is no silver bullet*). Here are some basic samples to better grasp the difference between *Value Types* and *Entities*: 30 | 31 | - __*Value types*__: cards of a poker hand, a speed of 10 mph, a bank note of 10 euros (unless you are working for a Central Bank which need then to trace every bank note --> Entity), a user address 32 | 33 | - __*Entities*__: a user account, a customer's basket with items, a customer, ... 34 | 35 | Our OO code bases are usually full of types with states and contain very few Value Type instances. 36 | DDD advises us to reverse the trend by not having *Entities* created by default, and to strongly increase our __usage of Value Types__. 37 | 38 | This __will helps us to reduce side-effects within our OO base code__. A simple reflex, for great benefits. 39 | 40 | ### Side effects, you said? 41 | 42 | Yes, because one of the problem we face when we code with Object Oriented (OO) languages like C# or java is the presence of __side-effects__. Indead, the ability for object instances to have their own state changed by other threads or by a specific combination of previous method calls (temporal coupling) __makes our reasoning harder__. Doing Test Driven Development helps a lot, but is not enough to ease the reasoning about our code. 43 | 44 | Being inspired by functional programming (FP) languages, __DDD suggests us to make our OO design more FP oriented in order to reduce those painful side-effects__. They are many things we can do for it. E.g.: 45 | - to use and combine __functions__ instead of methods (that impact object states) 46 | - to embrace __CQS pattern__ for *Entity* objects (i.e. a paradigm where read methods never change state and write methods never return data) 47 | - to implement *Closure of Operations* whenever it's possible (to reduce coupling with other types) 48 | - to use __*Value Types*__ by default and to keep *Entity* objects only when needed. 49 | 50 | 51 | ### You are not alone 52 | 53 | Since there is no first-class citizen for immutability and *Value Types* in C#, the goal of this pico library is to help you easily implement Value Types without caring too much on the boiler-plate code. 54 | 55 | __Yeah, let's focus on our business value now!__ 56 | 57 | --- 58 | 59 | ## What's inside the box? 60 | 61 | - __ValueType__: making all your Value Types deriving from this base class will allow you to __properly implement Equality__ (IEquatable) __and Unicity__ (GetHashCode()) on ALL your fields __in 1 line of code__. Very Handy! 62 | ```c# 63 | // 1. You inherit from ValueType like this: 64 | public class Amount : ValueType 65 | { 66 | private readonly decimal quantity; 67 | private readonly Currency currency; 68 | 69 | public decimal Quantity { get { return this.quantity; } } 70 | public Currency Currency { get { return this.currency; } } 71 | 72 | ... 73 | 74 | // 2. You (are then forced to) implement the abstract method returning the list of all your fields 75 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 76 | { 77 | return new List() { this.quantity, this.currency }; // The line of code I was talking about 78 | } 79 | 80 | // And that's all folks! 81 | } 82 | 83 | 84 | ``` 85 | 86 | - __ListByValue__: A list with equality based on its content and not on the list's reference (i.e.: 2 different instances containing the same items in the same order will be equals). This collection decorator is __very useful for any ValueType that aggregates a list__ 87 | 88 | ```c# 89 | // when one of your ValueType aggregates a IList like this 90 | private readonly List cards; 91 | 92 | //... 93 | 94 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 95 | { 96 | // here, you can use the ListByValue decorator to ensure a "ByValue" equality of your Type. 97 | return new List() { new ListByValue(this.cards) }; 98 | } 99 | ``` 100 | 101 | - __SetByValue__: A Set with equality based on its content and not on the Set's reference (i.e.: 2 different instances containing the same items will be equals whatever their storage order). This collection decorator is __very useful for any ValueType that aggregates a set__ 102 | 103 | ```c# 104 | // when one of your ValueType aggregates a Set like this 105 | private readonly HashSet cards; 106 | 107 | //... 108 | 109 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 110 | { 111 | // here, you can use the SetByValue decorator to ensure the "ByValue" equality of your Type. 112 | return new List() { new SetByValue(this.cards) }; 113 | } 114 | ``` 115 | 116 | --- 117 | 118 | ## Usage samples of ValueTypes 119 | 120 | __Disclaimer:__ for the sake of clarity, the following code samples don't have behaviours to only focus here on the Equality concern. __Of course, a ValueType in DDD must embed behaviours to swallow complexity (it's not just a DTO or a POCO without responsibilities).__ 121 | 122 | Code Sample of a properly implemented ValueType: 123 | 124 | ```c# 125 | /// 126 | /// Proper implementation of a ThreeeCards ValueType since the order of the cards doesn't matter during 127 | /// Equality. Note: the Card type is also a ValueType. 128 | /// 129 | public class ThreeCards : ValueType 130 | { 131 | private readonly HashSet cards; 132 | 133 | public ThreeCards(string card1Description, string card2Description, string card3Description) 134 | { 135 | this.cards = new HashSet(); 136 | 137 | this.cards.Add(Card.Parse(card1Description)); 138 | this.cards.Add(Card.Parse(card2Description)); 139 | this.cards.Add(Card.Parse(card3Description)); 140 | } 141 | 142 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 143 | { 144 | // we decorate our standard HashSet with the SetByValue helper class. 145 | return new List() { new SetByValue(this.cards) }; 146 | } 147 | } 148 | ``` 149 | 150 | Code Sample of a bad ValueType implementation: 151 | 152 | ```c# 153 | /// 154 | /// Bad ValueType implementation of ThreeCards since the GetAllAttributesToBeUsedForEquality() method 155 | /// returns the set directly, without decoring it with the SetByValue helper. 156 | /// 157 | public class ThreeCardsBadlyImplementedAsValueType : ValueType 158 | { 159 | private readonly HashSet cards; 160 | 161 | public ThreeCardsBadlyImplementedAsValueType(string card1Description, string card2Description, string card3Description) 162 | { 163 | this.cards = new HashSet(); 164 | 165 | this.cards.Add(Card.Parse(card1Description)); 166 | this.cards.Add(Card.Parse(card2Description)); 167 | this.cards.Add(Card.Parse(card3Description)); 168 | } 169 | 170 | protected override IEnumerable GetAllAttributesToBeUsedForEquality() 171 | { 172 | // BAD IMPLEMENTATION HERE: should have returned "new SetByValue(this.cards)" instead of "this.cards" 173 | return new List() { this.cards }; 174 | } 175 | } 176 | ``` 177 | 178 | Now, let's have a look a 2 tests that clarify the impact of those 2 implementations: 179 | 180 | ```c# 181 | [Test] 182 | public void Should_consider_equals_two_ValueType_instances_that_aggregates_equivalent_SetByValue() 183 | { 184 | var threeCards = new ThreeCards("AS", "QD", "2H"); 185 | var sameThreeCards = new ThreeCards("2H", "QD", "AS"); 186 | 187 | Check.That(threeCards).IsEqualTo(sameThreeCards); 188 | } 189 | 190 | [Test] 191 | public void Should_consider_Not_equals_two_badly_implemented_ValueType_instances_that_aggregates_equivalent_HashSet() 192 | { 193 | var threeCards = new ThreeCardsBadlyImplementedAsValueType("AS", "QD", "2H"); 194 | var sameThreeCards = new ThreeCardsBadlyImplementedAsValueType("2H", "QD", "AS"); 195 | 196 | Check.That(threeCards).IsNotEqualTo(sameThreeCards); 197 | } 198 | ``` 199 | 200 | -------------------------------------------------------------------------------- /.build/Build.proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(MSBuildProjectDirectory)\.. 6 | 7 | 8 | 9 | 10 | 11 | Debug 12 | $(MSBuildProjectDirectory)\.. 13 | Value 14 | $(SolutionRoot)\$(SolutionName) 15 | $(SolutionRoot)\tools\ 16 | $(SolutionRoot)\ 17 | $(SolutionRoot)\Artifacts 18 | $(ArtifactsPath)\Binaries 19 | $(ArtifactsPath)\Packages 20 | $(ArtifactsPath)\Docs 21 | 22 | $(NuGetToolsPath)nuget.exe 23 | $(SolutionRoot)\packages\NUnit.ConsoleRunner.3.6.1\tools\nunit3-console.exe 24 | 25 | alpha 26 | 27 | 28 | 29 | 30 | nightly 31 | 32 | 33 | 34 | 35 | RC 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 82 | 83 | 84 | 85 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | net40 137 | 138 | 139 | net45 140 | 141 | 142 | netstandard1.3 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | $(SolutionRoot)\$(SolutionName).nuspec 159 | %(AsmInfo.Version) 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | n/a 187 | 188 | 189 | 190 | @(FileContents,'%0a%0d') 191 | 192 | 193 | 194 | 199 | 200 | 206 | 207 | 208 | 209 | $(PackagesPath)\$(SolutionName).$(PrettyVersion).nupkg 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /Value.Tests/SetByValueShould.cs: -------------------------------------------------------------------------------- 1 | // // -------------------------------------------------------------------------------------------------------------------- 2 | // // 3 | // // Copyright 2016 4 | // // Thomas PIERRAIN (@tpierrain) 5 | // // Licensed under the Apache License, Version 2.0 (the "License"); 6 | // // you may not use this file except in compliance with the License. 7 | // // You may obtain a copy of the License at 8 | // // http://www.apache.org/licenses/LICENSE-2.0 9 | // // Unless required by applicable law or agreed to in writing, software 10 | // // distributed under the License is distributed on an "AS IS" BASIS, 11 | // // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // // See the License for the specific language governing permissions and 13 | // // limitations under the License.b 14 | // // 15 | // // -------------------------------------------------------------------------------------------------------------------- 16 | namespace Value.Tests.Samples 17 | { 18 | using System.Collections.Generic; 19 | using NFluent; 20 | using NUnit.Framework; 21 | 22 | [TestFixture] 23 | public class SetByValueShould 24 | { 25 | [Test] 26 | public void Consider_two_sets_with_same_items_equals() 27 | { 28 | var set1 = new SetByValue { "Achille", "Anton", "Maxime" }; 29 | var set2 = new SetByValue { "Achille", "Anton", "Maxime" }; 30 | 31 | Check.That(set2).IsEqualTo(set1); 32 | } 33 | 34 | [Test] 35 | public void Consider_two_sets_with_same_items_in_different_order_equals() 36 | { 37 | var set1 = new SetByValue { "Achille", "Anton", "Maxime" }; 38 | var set2 = new SetByValue { "Maxime", "Anton", "Achille" }; 39 | 40 | Check.That(set2).IsEqualTo(set1); 41 | } 42 | 43 | [Test] 44 | public void Not_consider_a_classic_hashSet_and_a_HashSetByValue_Equals() 45 | { 46 | var set1 = new HashSet { "Achille", "Anton", "Maxime" }; 47 | var set2 = new SetByValue { "Achille", "Anton", "Maxime" }; 48 | 49 | Check.That(set1.Equals(set2)).IsFalse(); 50 | } 51 | 52 | [Test] 53 | public void Consider_equals_two_ValueType_instances_that_aggregates_equivalent_SetByValue() 54 | { 55 | var threeCards = new ThreeCards("AS", "QD", "2H"); 56 | var sameThreeCards = new ThreeCards("2H", "QD", "AS"); 57 | 58 | Check.That(threeCards).IsEqualTo(sameThreeCards); 59 | } 60 | 61 | [Test] 62 | public void Consider_Not_equals_two_badly_implemented_ValueType_instances_that_aggregates_equivalent_HashSet() 63 | { 64 | var threeCards = new ThreeCardsBadlyImplementedAsValueType("AS", "QD", "2H"); 65 | var sameThreeCards = new ThreeCardsBadlyImplementedAsValueType("2H", "QD", "AS"); 66 | 67 | Check.That(threeCards).IsNotEqualTo(sameThreeCards); 68 | } 69 | 70 | [Test] 71 | public void Change_its_hashcode_everytime_the_set_is_updated() 72 | { 73 | var set = new SetByValue() { Card.Parse("QC"), Card.Parse("TS") }; 74 | var firstHashCode = set.GetHashCode(); 75 | 76 | set.Add(Card.Parse("3H")); // ---update the set --- 77 | var afterAddHash = set.GetHashCode(); 78 | Check.That(firstHashCode).IsNotEqualTo(afterAddHash); 79 | 80 | set.Clear(); 81 | var afterClearHash = set.GetHashCode(); 82 | Check.That(afterClearHash).IsNotEqualTo(afterAddHash); 83 | 84 | set.Add(Card.Parse("1C")); 85 | set.Add(Card.Parse("2C")); 86 | set.Add(Card.Parse("KD")); 87 | set.Add(Card.Parse("AC")); 88 | 89 | afterAddHash = set.GetHashCode(); 90 | 91 | set.ExceptWith(new List() { Card.Parse("AC") }); 92 | var afterExceptWithHash = set.GetHashCode(); 93 | Check.That(afterExceptWithHash).IsNotEqualTo(afterAddHash); 94 | 95 | set.IntersectWith(new[] { Card.Parse("2C"), Card.Parse("1C") }); 96 | var afterIntersectWithHash = set.GetHashCode(); 97 | Check.That(afterIntersectWithHash).IsNotEqualTo(afterExceptWithHash); 98 | 99 | Check.That(set).ContainsExactly(Card.Parse("1C"), Card.Parse("2C")); 100 | set.Remove(Card.Parse("1C")); 101 | var afterRemoveHash = set.GetHashCode(); 102 | Check.That(afterRemoveHash).IsNotEqualTo(afterIntersectWithHash); 103 | 104 | ((ISet)set).Add(Card.Parse("AD")); 105 | ((ISet)set).Add(Card.Parse("AS")); 106 | afterAddHash = set.GetHashCode(); 107 | Check.That(afterAddHash).IsNotEqualTo(afterRemoveHash); 108 | 109 | set.UnionWith(new[] { Card.Parse("7H") }); 110 | var afterUnionWithHash = set.GetHashCode(); 111 | Check.That(afterUnionWithHash).IsNotEqualTo(afterAddHash); 112 | 113 | set.SymmetricExceptWith(new[] { Card.Parse("AD") }); 114 | var afterSymetricExceptWithHash = set.GetHashCode(); 115 | Check.That(afterSymetricExceptWithHash).IsNotEqualTo(afterUnionWithHash); 116 | } 117 | 118 | [Test] 119 | public void Properly_expose_Contains() 120 | { 121 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 122 | var byValueSet = new SetByValue(originalSet); 123 | 124 | Check.That(byValueSet.Contains(Card.Parse("QC"))).IsEqualTo(originalSet.Contains(Card.Parse("QC"))); 125 | } 126 | 127 | [Test] 128 | public void Properly_expose_CopyTo() 129 | { 130 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 131 | var byValueSet = new SetByValue(originalSet); 132 | 133 | var firstCards = new Card[originalSet.Count]; 134 | var secondCards = new Card[originalSet.Count]; 135 | originalSet.CopyTo(firstCards, 0); 136 | byValueSet.CopyTo(secondCards, 0); 137 | 138 | Check.That(secondCards).ContainsExactly(firstCards); 139 | } 140 | 141 | [Test] 142 | public void Properly_expose_Count() 143 | { 144 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 145 | var byValueSet = new SetByValue(originalSet); 146 | 147 | Check.That(byValueSet.Count).IsEqualTo(originalSet.Count); 148 | } 149 | 150 | [Test] 151 | public void Properly_expose_IsProperSubsetOf() 152 | { 153 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 154 | var byValueSet = new SetByValue(originalSet); 155 | 156 | Check.That(byValueSet.IsProperSubsetOf(new[] { Card.Parse("QC") })).IsEqualTo(originalSet.IsProperSubsetOf(new[] { Card.Parse("QC") })); 157 | } 158 | 159 | [Test] 160 | public void Properly_expose_IsProperSupersetOf() 161 | { 162 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 163 | var byValueSet = new SetByValue(originalSet); 164 | 165 | Check.That(byValueSet.IsProperSupersetOf(new[] { Card.Parse("QC") })) 166 | .IsTrue() 167 | .And.IsEqualTo(originalSet.IsProperSupersetOf(new[] { Card.Parse("QC") })); 168 | } 169 | 170 | [Test] 171 | public void Properly_expose_IsReadOnly() 172 | { 173 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 174 | var byValueSet = new SetByValue(originalSet); 175 | 176 | Check.That(byValueSet.IsReadOnly).IsEqualTo(((ICollection)originalSet).IsReadOnly); 177 | } 178 | 179 | [Test] 180 | public void Properly_expose_IsSubsetOf() 181 | { 182 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 183 | var byValueSet = new SetByValue(originalSet); 184 | 185 | Check.That(byValueSet.IsSubsetOf(new[] { Card.Parse("QC") })).IsEqualTo(originalSet.IsSubsetOf(new[] { Card.Parse("QC") })); 186 | } 187 | 188 | [Test] 189 | public void Properly_expose_IsSupersetOf() 190 | { 191 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 192 | var byValueSet = new SetByValue(originalSet); 193 | 194 | Check.That(byValueSet.IsSupersetOf(new[] { Card.Parse("QC") })).IsEqualTo(originalSet.IsSupersetOf(new[] { Card.Parse("QC") })); 195 | } 196 | 197 | [Test] 198 | public void Properly_expose_Overlaps() 199 | { 200 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 201 | var byValueSet = new SetByValue(originalSet); 202 | 203 | Check.That(byValueSet.Overlaps(new[] { Card.Parse("QC") })).IsEqualTo(originalSet.Overlaps(new[] { Card.Parse("QC") })); 204 | } 205 | 206 | [Test] 207 | public void Properly_expose_SetEquals() 208 | { 209 | var originalSet = new HashSet() { Card.Parse("QC"), Card.Parse("TS") }; 210 | var byValueSet = new SetByValue(originalSet); 211 | 212 | Check.That(byValueSet.SetEquals(new[] { Card.Parse("QC") })).IsEqualTo(originalSet.SetEquals(new[] { Card.Parse("QC") })); 213 | } 214 | 215 | [Test] 216 | public void Provide_different_GetHashCode_for_two_different_sets() 217 | { 218 | var set1 = new SetByValue { "Achille", "Anton", "Maxime" }; 219 | var set2 = new SetByValue { "Hendrix", "De Lucia", "Reinhart" }; 220 | 221 | Check.That(set2.GetHashCode()).IsNotEqualTo(set1.GetHashCode()); 222 | } 223 | 224 | [Test] 225 | public void Provide_same_GetHashCode_from_two_sets_with_same_values() 226 | { 227 | var set1 = new SetByValue { "Achille", "Anton", "Maxime" }; 228 | var set2 = new SetByValue { "Achille", "Anton", "Maxime" }; 229 | 230 | Check.That(set2.GetHashCode()).IsEqualTo(set1.GetHashCode()); 231 | } 232 | 233 | [Test] 234 | public void Provide_same_GetHashCode_from_two_sets_with_same_values_in_different_order() 235 | { 236 | var set1 = new SetByValue { "Achille", "Anton", "Maxime" }; 237 | var set2 = new SetByValue { "Maxime", "Achille", "Anton" }; 238 | 239 | Check.That(set2.GetHashCode()).IsEqualTo(set1.GetHashCode()); 240 | } 241 | 242 | [Test] 243 | public void Consider_two_sets_of_the_same_integers_Equals() 244 | { 245 | var set1 = new SetByValue() { 1, 2, 3, 4, 5 }; 246 | var set2 = new SetByValue() { 1, 2, 3, 4, 5 }; 247 | 248 | Check.That(set2).IsEqualTo(set1); 249 | } 250 | } 251 | } -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------