Class ScalarValueObject<TSelf, T>
- Namespace
- Trellis
- Assembly
- Trellis.DomainDrivenDesign.dll
Base class for value objects that wrap a single scalar value. Provides a strongly-typed wrapper around primitive types with domain semantics.
public abstract class ScalarValueObject<TSelf, T> : ValueObject, IComparable<ValueObject>, IEquatable<ValueObject>, IConvertible where TSelf : ScalarValueObject<TSelf, T>, IScalarValue<TSelf, T> where T : IComparable
Type Parameters
TSelfThe derived value object type itself (CRTP pattern).
TThe type of the wrapped scalar value. Must implement IComparable.
- Inheritance
-
ScalarValueObject<TSelf, T>
- Implements
- Derived
- Inherited Members
- Extension Methods
Examples
Simple scalar value object for a strongly-typed ID:
public class CustomerId : ScalarValueObject<CustomerId, Guid>
{
private CustomerId(Guid value) : base(value) { }
public static CustomerId NewUnique() => new(Guid.NewGuid());
public static Result<CustomerId> TryCreate(Guid value) =>
value.ToResult()
.Ensure(v => v != Guid.Empty, Error.Validation("Customer ID cannot be empty"))
.Map(v => new CustomerId(v));
public static Result<CustomerId> TryCreate(string? stringOrNull) =>
stringOrNull.ToResult(Error.Validation("Customer ID cannot be empty"))
.Bind(s => Guid.TryParse(s, out var guid)
? Result.Success(guid)
: Error.Validation("Invalid GUID format"))
.Bind(TryCreate);
}
// Usage
var id = CustomerId.NewUnique();
Guid guidValue = id; // Implicit conversion to Guid
Scalar value object with custom equality and validation:
public class Temperature : ScalarValueObject<Temperature, decimal>
{
private Temperature(decimal value) : base(value) { }
public static Result<Temperature> TryCreate(decimal value) =>
value.ToResult()
.Ensure(v => v >= -273.15m,
Error.Validation("Temperature cannot be below absolute zero"))
.Ensure(v => v <= 1_000_000m,
Error.Validation("Temperature exceeds physical limits"))
.Map(v => new Temperature(v));
// Custom equality - round to 2 decimal places
protected override IEnumerable<IComparable> GetEqualityComponents()
{
yield return Math.Round(Value, 2);
}
// Domain operations
public static Temperature FromCelsius(decimal celsius) => new(celsius);
public static Temperature FromFahrenheit(decimal fahrenheit) =>
new((fahrenheit - 32) * 5 / 9);
public decimal ToCelsius() => Value;
public decimal ToFahrenheit() => (Value * 9 / 5) + 32;
}
// Usage
var temp1 = Temperature.TryCreate(98.6m);
var temp2 = Temperature.TryCreate(98.60m);
temp1 == temp2; // true - rounded to same value
decimal celsius = temp1.Value; // Access underlying value
Scalar value object for email addresses:
public class EmailAddress : ScalarValueObject<EmailAddress, string>
{
private EmailAddress(string value) : base(value) { }
public static Result<EmailAddress> TryCreate(string email) =>
email.ToResult(Error.Validation("Email is required", "email"))
.Ensure(e => !string.IsNullOrWhiteSpace(e),
Error.Validation("Email cannot be empty", "email"))
.Ensure(e => e.Contains("@"),
Error.Validation("Email must contain @", "email"))
.Ensure(e => e.Length <= 254,
Error.Validation("Email too long", "email"))
.Map(e => new EmailAddress(e.Trim().ToLowerInvariant()));
public string Domain => Value.Split('@')[1];
public string LocalPart => Value.Split('@')[0];
}
Remarks
Scalar value objects wrap a single primitive value (int, string, decimal, Guid, etc.) to provide:
- Type safety: Prevents mixing of semantically different values (e.g., CustomerId vs OrderId)
- Domain semantics: Makes code more expressive and self-documenting
- Validation: Encapsulates validation rules for the wrapped value
- Implicit conversion: Allows transparent usage as the underlying type
- IConvertible support: Enables conversion to other types when needed
Common use cases:
- Entity identifiers (CustomerId, OrderId, ProductId)
- Domain primitives (EmailAddress, PhoneNumber, PostalCode)
- Measurements (Temperature, Distance, Weight)
- Quantifiers (Percentage, Quantity, Amount)
The class implements IConvertible to allow conversion operations, and provides an implicit operator to seamlessly convert to the underlying type.
Constructors
ScalarValueObject(T)
Initializes a new instance of the ScalarValueObject<TSelf, T> class with the specified value.
protected ScalarValueObject(T value)
Parameters
valueTThe value to wrap.
Remarks
This constructor is protected to enforce creation through factory methods (typically TryCreate) that implement validation logic.
Properties
Value
Gets the wrapped scalar value.
public T Value { get; }
Property Value
- T
The underlying primitive value wrapped by this value object.
Remarks
While the value is publicly accessible, the constructor is typically protected, forcing creation through factory methods that enforce validation.
Methods
Create(T)
Creates a validated value object from a primitive value. Throws an exception if validation fails.
public static TSelf Create(T value)
Parameters
valueTThe raw primitive value
Returns
- TSelf
The validated value object
Examples
Use in tests or with known-valid values:
// ✅ Good - Test data
var email = EmailAddress.Create("test@example.com");
// ✅ Good - Building from validated data
var temp = Temperature.Create(98.6m);
// ❌ Bad - User input (use TryCreate instead)
var email = EmailAddress.Create(userInput);
Remarks
Use this method when you know the value is valid (e.g., in tests, with constants,
or when building from other validated value objects). This provides cleaner code
than calling TryCreate().Value.
⚠️ Don't use this method with user input or uncertain data - use TryCreate
instead to handle validation errors gracefully.
This is a default implementation that can be overridden if custom behavior is needed.
Exceptions
- InvalidOperationException
Thrown when validation fails
GetEqualityComponents()
Returns the components used for equality comparison. By default, returns the wrapped Value.
protected override IEnumerable<IComparable> GetEqualityComponents()
Returns
- IEnumerable<IComparable>
An enumerable containing the scalar value.
Examples
// Custom equality for Temperature - round to 2 decimal places
protected override IEnumerable<IComparable> GetEqualityComponents()
{
yield return Math.Round(Value, 2);
}
Remarks
Override this method to customize equality comparison. For example, to compare email addresses case-insensitively or to round decimal values.
GetTypeCode()
Returns the type code of the wrapped value.
public TypeCode GetTypeCode()
Returns
ToBoolean(IFormatProvider?)
Converts the wrapped value to a bool.
public bool ToBoolean(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToByte(IFormatProvider?)
Converts the wrapped value to a byte.
public byte ToByte(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToChar(IFormatProvider?)
Converts the wrapped value to a char.
public char ToChar(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToDateTime(IFormatProvider?)
Converts the wrapped value to a DateTime.
public DateTime ToDateTime(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToDecimal(IFormatProvider?)
Converts the wrapped value to a decimal.
public decimal ToDecimal(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToDouble(IFormatProvider?)
Converts the wrapped value to a double.
public double ToDouble(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToInt16(IFormatProvider?)
Converts the wrapped value to a short.
public short ToInt16(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToInt32(IFormatProvider?)
Converts the wrapped value to an int.
public int ToInt32(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToInt64(IFormatProvider?)
Converts the wrapped value to a long.
public long ToInt64(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToSByte(IFormatProvider?)
Converts the wrapped value to an sbyte.
public sbyte ToSByte(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToSingle(IFormatProvider?)
Converts the wrapped value to a float.
public float ToSingle(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToString()
Returns a string representation of the wrapped value.
public override string ToString()
Returns
ToString(IFormatProvider?)
Converts the wrapped value to a string using the specified format provider.
public string ToString(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToType(Type, IFormatProvider?)
Converts the wrapped value to the specified type.
public object ToType(Type conversionType, IFormatProvider? provider)
Parameters
conversionTypeTypeThe type to convert to.
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
- object
The wrapped value converted to the specified type.
ToUInt16(IFormatProvider?)
Converts the wrapped value to a ushort.
public ushort ToUInt16(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToUInt32(IFormatProvider?)
Converts the wrapped value to a uint.
public uint ToUInt32(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
ToUInt64(IFormatProvider?)
Converts the wrapped value to a ulong.
public ulong ToUInt64(IFormatProvider? provider)
Parameters
providerIFormatProviderAn IFormatProvider for culture-specific formatting.
Returns
Operators
implicit operator T(ScalarValueObject<TSelf, T>)
Implicitly converts the scalar value object to its underlying type.
public static implicit operator T(ScalarValueObject<TSelf, T> valueObject)
Parameters
valueObjectScalarValueObject<TSelf, T>The scalar value object to convert.
Returns
- T
The wrapped value of type
T.
Examples
var customerId = CustomerId.NewUnique();
Guid guid = customerId; // Implicit conversion
var temperature = Temperature.TryCreate(98.6m).Value;
decimal value = temperature; // Implicit conversion
Remarks
This implicit operator allows scalar value objects to be used transparently as their underlying type in most contexts, reducing the need for explicit unwrapping.