├── .gitignore ├── README.md ├── ValueOf.Tests ├── Equals.cs ├── Example.cs ├── ToString.cs ├── TryValidate.cs ├── Validate.cs └── ValueOf.Tests.csproj ├── ValueOf.sln ├── ValueOf ├── ValueOf.cs └── ValueOf.csproj └── licence.md /.gitignore: -------------------------------------------------------------------------------- 1 | bin/debug*.dll 2 | bin/release*.dll 3 | obj 4 | bin/ 5 | deploy 6 | deploy/* 7 | _ReSharper.* 8 | *.csproj.user 9 | *.resharper.user 10 | *.ReSharper.user 11 | *.resharper 12 | *.suo 13 | *.cache 14 | ~$* 15 | *.suo 16 | packages/* 17 | /packages 18 | /*.user 19 | .vs 20 | *.user 21 | project.lock.json 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ValueOf 2 | 3 | > install-package ValueOf 4 | 5 | ## What is this library 6 | 7 | > The Smell: Primitive Obsession is using primitive data types to represent domain ideas. For example, we use a String to represent a message, an Integer to represent an amount of money, or a Struct/Dictionary/Hash to represent a specific object. 8 | > The Fix: Typically, we introduce a ValueObject in place of the primitive data, then watch like magic as code from all over the system shows FeatureEnvySmell and wants to be on the new ValueObject. We move those methods, and everything becomes right with the world. 9 | > - http://wiki.c2.com/?PrimitiveObsession 10 | 11 | ValueOf lets you define ValueObject Types in a single line of code. Use them everywhere to strengthen your codebase. 12 | 13 | ``` 14 | public class EmailAddress : ValueOf { } 15 | 16 | ... 17 | 18 | EmailAddress emailAddress = EmailAddress.From("foo@bar.com"); 19 | 20 | ``` 21 | 22 | The ValueOf class implements `.Equals` and `.GetHashCode()` for you. 23 | 24 | You can use C# 7 Tuples for more complex Types with multiple values: 25 | 26 | ``` 27 | public class Address : ValueOf<(string firstLine, string secondLine, Postcode postcode), Address> { } 28 | 29 | ``` 30 | 31 | ### Validation 32 | 33 | You can add validation to your Types by overriding the `protected void Validate() { } ` method: 34 | 35 | ``` 36 | public class ValidatedClientRef : ValueOf 37 | { 38 | protected override void Validate() 39 | { 40 | if (string.IsNullOrWhiteSpace(Value)) 41 | throw new ArgumentException("Value cannot be null or empty"); 42 | } 43 | } 44 | 45 | ``` 46 | 47 | ## See Also 48 | 49 | If you liked this, you'll probably like another project of mine [OneOf](https://github.com/mcintyre321/OneOf) which provides Discriminated Unions for C#, allowing stronger compile time guarantees when writing branching logic. -------------------------------------------------------------------------------- /ValueOf.Tests/Equals.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System.Collections.Generic; 3 | 4 | namespace ValueOf.Tests 5 | { 6 | 7 | public class CaseInsensitiveClientRef : ValueOf 8 | { 9 | protected override bool Equals(ValueOf other) 10 | { 11 | return EqualityComparer.Default.Equals(Value.ToLower(), other.Value.ToLower()); 12 | } 13 | 14 | public override int GetHashCode() 15 | { 16 | return EqualityComparer.Default.GetHashCode(Value.ToLower()); 17 | } 18 | } 19 | 20 | public class Equals 21 | { 22 | [Test] 23 | public void CaseInsensitiveEquals() 24 | { 25 | CaseInsensitiveClientRef clientRef1 = CaseInsensitiveClientRef.From("ASDF12345"); 26 | CaseInsensitiveClientRef clientRef2 = CaseInsensitiveClientRef.From("asdf12345"); 27 | Assert.AreEqual(clientRef1, clientRef2); 28 | Assert.AreEqual(clientRef1.GetHashCode(), clientRef2.GetHashCode()); 29 | Assert.IsTrue(clientRef1 == clientRef2); 30 | Assert.IsTrue(clientRef1.Value == "ASDF12345"); 31 | 32 | CaseInsensitiveClientRef clientRef3 = CaseInsensitiveClientRef.From("QWER98765"); 33 | Assert.AreNotEqual(clientRef1, clientRef3); 34 | Assert.AreNotEqual(clientRef1.GetHashCode(), clientRef3.GetHashCode()); 35 | Assert.IsFalse(clientRef1 == clientRef3); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ValueOf.Tests/Example.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace ValueOf.Tests 4 | { 5 | public class ClientRef : ValueOf { } 6 | public class Postcode : ValueOf { } 7 | public class Address : ValueOf<(string firstLine, string secondLine, Postcode postcode), Address> { } 8 | 9 | public class Example 10 | { 11 | [Test] 12 | public void SingleValuedExample() 13 | { 14 | ClientRef clientRef1 = ClientRef.From("ASDF12345"); 15 | ClientRef clientRef2 = ClientRef.From("ASDF12345"); 16 | Assert.AreEqual(clientRef1, clientRef2); 17 | Assert.AreEqual(clientRef1.GetHashCode(), clientRef2.GetHashCode()); 18 | 19 | ClientRef clientRef3 = ClientRef.From("QWER98765"); 20 | Assert.AreNotEqual(clientRef1, clientRef3); 21 | Assert.AreNotEqual(clientRef1.GetHashCode(), clientRef3.GetHashCode()); 22 | } 23 | 24 | [Test] 25 | public void ValueTupleValuedExample() 26 | { 27 | Address address1 = Address.From(("16 Food Street", "London", Postcode.From("N1 1LT"))); 28 | Address address2 = Address.From(("16 Food Street", "London", Postcode.From("N1 1LT"))); 29 | Assert.AreEqual(address1, address2); 30 | Assert.AreEqual(address1.GetHashCode(), address2.GetHashCode()); 31 | 32 | Address address3 = Address.From(("17 Food Street", "London", Postcode.From("N1 1LT"))); 33 | Assert.AreNotEqual(address1, address3); 34 | Assert.AreNotEqual(address1.GetHashCode(), address3.GetHashCode()); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ValueOf.Tests/ToString.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace ValueOf.Tests 4 | { 5 | public class ToString 6 | { 7 | [Test] 8 | public void ToStringReturnsValueToStringForSingleValuedObjects() 9 | { 10 | ClientRef clientRef1 = ClientRef.From("ASDF12345"); 11 | 12 | Assert.AreEqual(clientRef1.Value, clientRef1.ToString()); 13 | } 14 | 15 | [Test] 16 | public void ToStringReturnsValueOfTupleForTupleValuedObjects() 17 | { 18 | Address address1 = Address.From(("16 Food Street", "London", Postcode.From("N1 1LT"))); 19 | 20 | Assert.AreEqual(address1.Value.ToString(), address1.ToString()); 21 | 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ValueOf.Tests/TryValidate.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | namespace ValueOf.Tests 4 | { 5 | public class TryValidateClientRef : ValueOf 6 | { 7 | protected override bool TryValidate() 8 | { 9 | return !string.IsNullOrWhiteSpace(Value); 10 | } 11 | } 12 | 13 | public class TryValidation 14 | { 15 | [Test] 16 | public void TryValidateReturnsFalse() 17 | { 18 | bool isValid = TryValidateClientRef.TryFrom("", out TryValidateClientRef valueObject); 19 | 20 | Assert.IsFalse(isValid); 21 | Assert.IsNull(valueObject); 22 | } 23 | 24 | [Test] 25 | public void TryValidateReturnsTrue() 26 | { 27 | bool isValid = TryValidateClientRef.TryFrom("something", out TryValidateClientRef valueObject); 28 | 29 | Assert.IsTrue(isValid); 30 | Assert.IsNotNull(valueObject); 31 | Assert.AreEqual("something", valueObject.Value); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ValueOf.Tests/Validate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace ValueOf.Tests 5 | { 6 | public class ValidatedClientRef : ValueOf 7 | { 8 | protected override void Validate() 9 | { 10 | if (string.IsNullOrWhiteSpace(Value)) 11 | throw new ArgumentException("Value cannot be null or empty"); 12 | } 13 | } 14 | 15 | public class Validation 16 | { 17 | [Test] 18 | public void SingleValuedExample() 19 | { 20 | Assert.Throws(() => ValidatedClientRef.From(""), "Value cannot be null or empty"); 21 | } 22 | 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /ValueOf.Tests/ValueOf.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net48 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ValueOf.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32112.339 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValueOf", "ValueOf\ValueOf.csproj", "{5C838C39-616F-4252-89E7-F19DE1065CB1}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValueOf.Tests", "ValueOf.Tests\ValueOf.Tests.csproj", "{B3131BBB-0C2E-451E-8B72-43B60B345530}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EC3E8502-674A-4C72-B9BC-D879F91742D8}" 11 | ProjectSection(SolutionItems) = preProject 12 | .gitignore = .gitignore 13 | licence.md = licence.md 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {5C838C39-616F-4252-89E7-F19DE1065CB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {5C838C39-616F-4252-89E7-F19DE1065CB1}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {5C838C39-616F-4252-89E7-F19DE1065CB1}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {5C838C39-616F-4252-89E7-F19DE1065CB1}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {B3131BBB-0C2E-451E-8B72-43B60B345530}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {B3131BBB-0C2E-451E-8B72-43B60B345530}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {B3131BBB-0C2E-451E-8B72-43B60B345530}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {B3131BBB-0C2E-451E-8B72-43B60B345530}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | GlobalSection(ExtensibilityGlobals) = postSolution 36 | SolutionGuid = {529ABB6F-150F-407F-8CBB-59F8D15355E9} 37 | EndGlobalSection 38 | EndGlobal 39 | -------------------------------------------------------------------------------- /ValueOf/ValueOf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Reflection; 6 | 7 | namespace ValueOf 8 | { 9 | public class ValueOf where TThis : ValueOf, new() 10 | { 11 | private static readonly Func Factory; 12 | 13 | /// 14 | /// WARNING - THIS FEATURE IS EXPERIMENTAL. I may change it to do 15 | /// validation in a different way. 16 | /// Right now, override this method, and throw any exceptions you need to. 17 | /// Access this.Value to check the value 18 | /// 19 | protected virtual void Validate() 20 | { 21 | } 22 | 23 | protected virtual bool TryValidate() 24 | { 25 | return true; 26 | } 27 | 28 | static ValueOf() 29 | { 30 | ConstructorInfo ctor = typeof(TThis) 31 | .GetTypeInfo() 32 | .DeclaredConstructors 33 | .First(); 34 | 35 | var argsExp = new Expression[0]; 36 | NewExpression newExp = Expression.New(ctor, argsExp); 37 | LambdaExpression lambda = Expression.Lambda(typeof(Func), newExp); 38 | 39 | Factory = (Func)lambda.Compile(); 40 | } 41 | 42 | public TValue Value { get; protected set; } 43 | 44 | public static TThis From(TValue item) 45 | { 46 | TThis x = Factory(); 47 | x.Value = item; 48 | x.Validate(); 49 | 50 | return x; 51 | } 52 | 53 | public static bool TryFrom(TValue item, out TThis thisValue) 54 | { 55 | TThis x = Factory(); 56 | x.Value = item; 57 | 58 | thisValue = x.TryValidate() 59 | ? x 60 | : null; 61 | 62 | return thisValue != null; 63 | } 64 | 65 | protected virtual bool Equals(ValueOf other) 66 | { 67 | return EqualityComparer.Default.Equals(Value, other.Value); 68 | } 69 | 70 | public override bool Equals(object obj) 71 | { 72 | if (obj is null) 73 | return false; 74 | 75 | if (ReferenceEquals(this, obj)) 76 | return true; 77 | 78 | return obj.GetType() == GetType() && Equals((ValueOf)obj); 79 | } 80 | 81 | public override int GetHashCode() 82 | { 83 | return EqualityComparer.Default.GetHashCode(Value); 84 | } 85 | 86 | public static bool operator ==(ValueOf a, ValueOf b) 87 | { 88 | if (a is null && b is null) 89 | return true; 90 | 91 | if (a is null || b is null) 92 | return false; 93 | 94 | return a.Equals(b); 95 | } 96 | 97 | public static bool operator !=(ValueOf a, ValueOf b) 98 | { 99 | return !(a == b); 100 | } 101 | 102 | // Implicit operator removed. See issue #14. 103 | 104 | public override string ToString() 105 | { 106 | return Value.ToString(); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /ValueOf/ValueOf.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net48;netstandard2.0 5 | x64 6 | Harry McIntyre 7 | ValueOf - Easy 'One-Liner' Value Objects for c#. Get over your Primitive Obsession! 8 | Harry McIntyre 9 | 1.0.0 10 | Harry McIntyre 11 | Declare Value Objects in one line e.g. `class ClientRef : ValueOf<string, ClientRef> { }`), create using `ClientRef.From(someString)` 12 | The base Type ValueOf<TValue, TThis>, provides Equals, GetHashcode. 13 | Use ValueTuples for multi property values e.g `class Address : ValueOf<(string firstLine, string secondLine, Postcode postcode), Address> {}` 14 | 15 | https://github.com/mcintyre321/ValueOf/ 16 | https://github.com/mcintyre321/ValueOf/blob/master/licence.md 17 | Value Object, ValueObject, DDD, Primitive Obsession, Domain Modelling 18 | True 19 | ValueOf 20 | Harry McIntyre 21 | AnyCPU 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /licence.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Harry McIntyre 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | --------------------------------------------------------------------------------