Skip to content

Commit

Permalink
Create special base class for single value value objects. (#10)
Browse files Browse the repository at this point in the history
* Updated packages.

* Added special base class for value objects containing only a single value.

* Added primitive value object base class (closes #7).
  • Loading branch information
mgernand authored Mar 24, 2022
1 parent 17aedbf commit aba289a
Show file tree
Hide file tree
Showing 17 changed files with 481 additions and 140 deletions.
2 changes: 1 addition & 1 deletion azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ trigger:

variables:
BuildConfiguration: Release
DotNetCoreVersion: 6.0.200
DotNetCoreVersion: 6.0.201

stages:
- stage: BuildAndTest
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"sdk": {
"version": "6.0.200"
"version": "6.0.201"
}
}
5 changes: 3 additions & 2 deletions src/Fluxera.ValueObject/Fluxera.ValueObject.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Fluxera.Guards" Version="6.0.10" />
<PackageReference Include="GitVersion.MsBuild" Version="5.8.2">
<PackageReference Include="Fluxera.Guards" Version="6.0.11" />
<PackageReference Include="Fluxera.Utilities" Version="6.0.12" />
<PackageReference Include="GitVersion.MsBuild" Version="5.9.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
43 changes: 43 additions & 0 deletions src/Fluxera.ValueObject/PrimitiveValueObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
namespace Fluxera.ValueObject
{
using System;
using System.Collections.Generic;
using Fluxera.Guards;
using Fluxera.Utilities.Extensions;
using JetBrains.Annotations;

/// <summary>
/// A base class for a value object that only contains a single primitive value.
/// </summary>
/// <remarks>
/// The following types are allowed to be used with this class:
/// Any type that returns <c>true</c>f or <see cref="Type.IsPrimitive" /> and
/// additionally <see cref="Enum" /> values, <see cref="string" />, <see cref="decimal" />,
/// <see cref="DateTime" />, <see cref="DateTimeOffset" />, <see cref="TimeSpan" /> and <see cref="Guid" />.
/// </remarks>
/// <typeparam name="TValueObject">The type of the value object.</typeparam>
/// <typeparam name="TValue">The type of the value.</typeparam>
[PublicAPI]
public abstract class PrimitiveValueObject<TValueObject, TValue> : ValueObject<TValueObject>
where TValueObject : ValueObject<TValueObject>
{
static PrimitiveValueObject()
{
Type valueType = typeof(TValue);
bool isPrimitive = valueType.IsPrimitive(true, true);

Guard.Against.False(isPrimitive, nameof(Value), "The value of a primitive value object must be a primitive, string or enum value.");
}

/// <summary>
/// Gets or sets the single value of the value object.
/// </summary>
public TValue? Value { get; set; }

/// <inheritdoc />
protected sealed override IEnumerable<object?> GetEqualityComponents()
{
yield return this.Value;
}
}
}
35 changes: 21 additions & 14 deletions src/Fluxera.ValueObject/ValueObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,58 +6,64 @@
using JetBrains.Annotations;

/// <summary>
/// A base class for any value object.
/// A base class for any value object.
/// </summary>
/// <typeparam name="TValueObject">The type of the value object.</typeparam>
[PublicAPI]
public abstract class ValueObject<TValueObject>
where TValueObject : ValueObject<TValueObject>
{
/// <summary>
/// To ensure hashcode uniqueness, a carefully selected random number multiplier
/// is used within the calculation.
/// To ensure hashcode uniqueness, a carefully selected random number multiplier
/// is used within the calculation.
/// </summary>
/// <remarks>
/// See http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/
/// See http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/
/// </remarks>
private const int HashMultiplier = 37;

/// <summary>
/// Checks if the given value objects are equal.
/// </summary>
public static bool operator ==(ValueObject<TValueObject>? left, ValueObject<TValueObject>? right)
{
if (left is null)
if(left is null)
{
return right is null;
}

return left.Equals(right);
}

/// <summary>
/// Checks if the given value objects are not equal.
/// </summary>
public static bool operator !=(ValueObject<TValueObject>? left, ValueObject<TValueObject>? right)
{
return !(left == right);
}

/// <inheritdoc />
public override bool Equals(object? obj)
public sealed override bool Equals(object? obj)
{
if(obj is null)
{
return false;
}

if (object.ReferenceEquals(this, obj))
if(object.ReferenceEquals(this, obj))
{
return true;
}

ValueObject<TValueObject>? other = obj as ValueObject<TValueObject>;
return other != null
&& this.GetType() == other.GetType()
return (other != null)
&& (this.GetType() == other.GetType())
&& this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}

/// <inheritdoc />
public override int GetHashCode()
public sealed override int GetHashCode()
{
unchecked
{
Expand All @@ -79,7 +85,7 @@ public override int GetHashCode()
}

/// <inheritdoc />
public override string ToString()
public sealed override string ToString()
{
using(IEnumerator<object?> enumerator = this.GetEqualityComponents().GetEnumerator())
{
Expand All @@ -94,16 +100,17 @@ public override string ToString()
{
builder.Append(" ,").Append(enumerator.Current);
}

builder.Append(" }");

return builder.ToString();
}
}

/// <summary>
/// Gets all components of the value object that are used for equality. <br/>
/// The default implementation get all properties via reflection. One
/// can at any time override this behavior with a manual or custom implementation.
/// Gets all components of the value object that are used for equality. <br />
/// The default implementation get all properties via reflection. One
/// can at any time override this behavior with a manual or custom implementation.
/// </summary>
/// <returns>The components to use for equality.</returns>
protected virtual IEnumerable<object?> GetEqualityComponents()
Expand Down
173 changes: 122 additions & 51 deletions tests/Fluxera.ValueObject.UnitTests/EqualsOperatorsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,135 @@
{
using System.Collections.Generic;
using FluentAssertions;
using Model;
using Fluxera.ValueObject.UnitTests.Model;
using NUnit.Framework;

[TestFixture]
public class EqualsOperatorsTests
{
[Test]
[TestCaseSource(nameof(OperatorTestData))]
public void EqualOperatorShouldReturnExpectedValue(Address first, Address second, bool expected)
private static IEnumerable<object[]> OperatorTestData = new List<object[]>
{
new object[]
{
null!,
null!,
true
},
new object[]
{
new Address("Testgasse", "50", "11111", "Bremen"),
new Address("Testgasse", "50", "11111", "Bremen"),
true
},
new object[]
{
new Address("Testgasse", "50", "11111", "Bremen"),
new Address("Testweg", "50", "11111", "Bremen"),
false
},
new object[]
{
new Address("Testgasse", "50", "11111", "Bremen"),
null!,
false
}
};

private static IEnumerable<object[]> OperatorPrimitiveTestData = new List<object[]>
{
new object[]
{
new PostCode("12345"),
new PostCode("12345"),
true
},
new object[]
{
new PostCode("12345")
{
WillNotBeConsideredForEqualityAndHashCode = "ABC"
},
new PostCode("12345")
{
WillNotBeConsideredForEqualityAndHashCode = "ABC"
},
true
},
new object[]
{
new PostCode("12345")
{
WillNotBeConsideredForEqualityAndHashCode = "ABC"
},
new PostCode("12345")
{
WillNotBeConsideredForEqualityAndHashCode = "XYZ"
},
true
},
new object[]
{
new PostCode("12345")
{
WillNotBeConsideredForEqualityAndHashCode = "ABC"
},
new PostCode("54321")
{
WillNotBeConsideredForEqualityAndHashCode = "ABC"
},
false
},
new object[]
{
new PostCode("12345")
{
WillNotBeConsideredForEqualityAndHashCode = "ABC"
},
new PostCode("54321")
{
WillNotBeConsideredForEqualityAndHashCode = "XYZ"
},
false
}
};

[Test]
[TestCaseSource(nameof(OperatorPrimitiveTestData))]
public void EqualOperatorPrimitiveShouldReturnExpectedValue(PostCode first, PostCode second, bool expected)
{
bool result = first == second;
result.Should().Be(expected);
}

[Test]
[Test]
[TestCaseSource(nameof(OperatorTestData))]
public void NotEqualOperatorShouldReturnExpectedValue(Address first, Address second, bool expected)
public void EqualOperatorShouldReturnExpectedValue(Address first, Address second, bool expected)
{
bool result = first != second;
result.Should().Be(!expected);
bool result = first == second;
result.Should().Be(expected);
}

[Test]
public void EqualOperatorShouldReturnTrueForEmptyValueObject()
[Test]
public void EqualOperatorShouldReturnFalseForDerivedTypes()
{
Empty first = new Empty();
Empty second = new Empty();
BankAccount first = new BankAccount("Tester", "DE0000000000000", "ABCDFFXXX");
GermanBankAccount second = new GermanBankAccount("Tester", "DE0000000000000", "ABCDFFXXX");

bool result = first == second;
result.Should().BeTrue();
result.Should().BeFalse();
}

[Test]
public void EqualOperatorShouldReturnFalseForDerivedTypes()
[Test]
public void EqualOperatorShouldReturnFalseForDerivedTypesWithDifferentData()
{
BankAccount first = new BankAccount("Tester", "DE0000000000000", "ABCDFFXXX");
GermanBankAccount second = new GermanBankAccount("Tester", "DE0000000000000", "ABCDFFXXX");
GermanBankAccount first = new GermanBankAccount("Tester", "DE0000000000000", "ABCDFFXXX");
GermanBankAccount second = new GermanBankAccount("Testonius", "DE0000000000000", "ABCDFFXXX");

bool result = first == second;
result.Should().BeFalse();
}

[Test]
[Test]
public void EqualOperatorShouldReturnTrueForDerivedTypesWithSameData()
{
GermanBankAccount first = new GermanBankAccount("Tester", "DE0000000000000", "ABCDFFXXX");
Expand All @@ -54,45 +140,30 @@ public void EqualOperatorShouldReturnTrueForDerivedTypesWithSameData()
result.Should().BeTrue();
}

[Test]
public void EqualOperatorShouldReturnFalseForDerivedTypesWithDifferentData()
[Test]
public void EqualOperatorShouldReturnTrueForEmptyValueObject()
{
GermanBankAccount first = new GermanBankAccount("Tester", "DE0000000000000", "ABCDFFXXX");
GermanBankAccount second = new GermanBankAccount("Testonius", "DE0000000000000", "ABCDFFXXX");
Empty first = new Empty();
Empty second = new Empty();

bool result = first == second;
result.Should().BeFalse();
result.Should().BeTrue();
}

private static IEnumerable<object[]> OperatorTestData = new List<object[]>
[Test]
[TestCaseSource(nameof(OperatorTestData))]
public void NotEqualOperatorPrimitiveShouldReturnExpectedValue(Address first, Address second, bool expected)
{
new object[]
{
null!,
null!,
true
},

new object[]
{
new Address("Testgasse", "50", "11111", "Bremen"),
new Address("Testgasse", "50", "11111", "Bremen"),
true
},

new object[]
{
new Address("Testgasse", "50", "11111", "Bremen"),
new Address("Testweg", "50", "11111", "Bremen"),
false
},
bool result = first != second;
result.Should().Be(!expected);
}

new object[]
{
new Address("Testgasse", "50", "11111", "Bremen"),
null!,
false
},
};
[Test]
[TestCaseSource(nameof(OperatorPrimitiveTestData))]
public void NotEqualOperatorShouldReturnExpectedValue(PostCode first, PostCode second, bool expected)
{
bool result = first != second;
result.Should().Be(!expected);
}
}
}
Loading

0 comments on commit aba289a

Please sign in to comment.