Value Object Design Patterns - stevedunn/vogen

Value Object Design Patterns

Value objects are immutable and have no identity, meaning that two value objects are equal if all their properties are equal. Value objects are often used in C# programming to represent concepts that have no distinct identity, such as numbers, dates, or geographical locations.

In the project “https://github.com/stevedunn/vogen/”, value object design patterns are implemented using the ValueObjectAttribute attribute, which is applied to classes that represent value objects. The ValueObjectAttribute attribute ensures that the class meets the requirements of a value object, such as being immutable and having a value-based equality implementation.

Here are some possible options for implementing value object design patterns in C#, along with examples from the project:

Option 1: Value Object Attribute

The ValueObjectAttribute attribute can be applied to a class to indicate that it is a value object. The attribute ensures that the class meets the requirements of a value object, such as having a value-based equality implementation.

Example:

[ValueObject]
public class Money
{
public decimal Amount { get; }
public string Currency { get; }

public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}

protected bool Equals(Money other)
{
return Amount == other.Amount && Currency == other.Currency;
}

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((Money)obj);
}

public override int GetHashCode()
{
unchecked
{
return (Amount.GetHashCode() * 397) ^ Currency.GetHashCode();
}
}
}

Source: docs/site/Writerside/topics/reference/ValueObjectAttribute.md

Option 2: Value Object Or Error

The ValueObjectOrError class can be used to represent a value object that may also contain an error message. This is useful when performing validation on a value object, as it allows you to return an error message if the validation fails.

Example:

public class EmailAddress
{
public string Value { get; }

public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Email address cannot be null or whitespace", nameof(value));
}

if (!new EmailAddressValidator().IsValid(value, out var errorMessage))
{
throw new ArgumentException(errorMessage, nameof(value));
}

Value = value;
}
}

public class EmailAddressValidator : AbstractValidator<EmailAddress>
{
public EmailAddressValidator()
{
RuleFor(x => x.Value)
.EmailAddress()
.WithMessage("Invalid email address format");
}
}

public class User
{
public EmailAddress Email { get; }

public User(EmailAddress email)
{
Email = email;
}
}

public class ValueObjectOrError<TValueObject, TError>
{
public bool IsSuccess { get; }
public TValueObject Value { get; }
public TError Error { get; }

public ValueObjectOrError(TValueObject value)
{
IsSuccess = true;
Value = value;
Error = default;
}

public ValueObjectOrError(TError error)
{
IsSuccess = false;
Value = default;
Error = error;
}
}

Source: src/Vogen.SharedTypes/ValueObjectOrError.cs

Option 3: Static Abstracts Generation

The StaticAbstractsGeneration class can be used to generate static abstract classes that can be used as base classes for value objects. This allows you to define common behavior for all value objects in a single place.

Example:

public abstract class ValueObject<T> where T : ValueObject<T>
{
protected abstract IEnumerable<object> GetEqualityComponents();

public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}

var valueObject = (T)obj;
return GetEqualityComponents().SequenceEqual(valueObject.GetEqualityComponents());
}

public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
}

public class Money : ValueObject<Money>
{
public decimal Amount { get; }
public string Currency { get; }

public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}

protected override IEnumerable<object> GetEqualityComponents()
{
yield return Amount;
yield return Currency;
}
}

Source: src/Vogen.SharedTypes/StaticAbstractsGeneration.cs

Option 4: Using Interfaces

Value objects can also be implemented using interfaces. This allows you to define a common contract for all value objects, while still allowing each value object to have its own implementation.

Example:

public interface IValueObject
{
bool Equals(IValueObject other);
int GetHashCode();
}

public class Money : IValueObject
{
public decimal Amount { get; }
public string Currency { get; }

public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}

public bool Equals(IValueObject other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
if (other.GetType() != GetType()) return false;
return Equals((Money)other);
}

public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((Money)obj);
}

public override int GetHashCode()
{
unchecked
{
return (Amount.GetHashCode() * 397) ^ Currency.GetHashCode();
}
}
}

Source: samples/Vogen.Examples/TypicalScenarios/UsingInterfaces.cs

Sources: