Table of Contents

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

TSelf

The derived value object type itself (CRTP pattern).

T

The 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

value T

The 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

value T

The 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

TypeCode

The TypeCode of type T.

ToBoolean(IFormatProvider?)

Converts the wrapped value to a bool.

public bool ToBoolean(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

bool

The wrapped value converted to a bool.

ToByte(IFormatProvider?)

Converts the wrapped value to a byte.

public byte ToByte(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

byte

The wrapped value converted to a byte.

ToChar(IFormatProvider?)

Converts the wrapped value to a char.

public char ToChar(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

char

The wrapped value converted to a char.

ToDateTime(IFormatProvider?)

Converts the wrapped value to a DateTime.

public DateTime ToDateTime(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

DateTime

The wrapped value converted to a DateTime.

ToDecimal(IFormatProvider?)

Converts the wrapped value to a decimal.

public decimal ToDecimal(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

decimal

The wrapped value converted to a decimal.

ToDouble(IFormatProvider?)

Converts the wrapped value to a double.

public double ToDouble(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

double

The wrapped value converted to a double.

ToInt16(IFormatProvider?)

Converts the wrapped value to a short.

public short ToInt16(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

short

The wrapped value converted to a short.

ToInt32(IFormatProvider?)

Converts the wrapped value to an int.

public int ToInt32(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

int

The wrapped value converted to an int.

ToInt64(IFormatProvider?)

Converts the wrapped value to a long.

public long ToInt64(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

long

The wrapped value converted to a long.

ToSByte(IFormatProvider?)

Converts the wrapped value to an sbyte.

public sbyte ToSByte(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

sbyte

The wrapped value converted to an sbyte.

ToSingle(IFormatProvider?)

Converts the wrapped value to a float.

public float ToSingle(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

float

The wrapped value converted to a float.

ToString()

Returns a string representation of the wrapped value.

public override string ToString()

Returns

string

The string representation of Value, or an empty string if null.

ToString(IFormatProvider?)

Converts the wrapped value to a string using the specified format provider.

public string ToString(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

string

The wrapped value converted to a string, or an empty string if null.

ToType(Type, IFormatProvider?)

Converts the wrapped value to the specified type.

public object ToType(Type conversionType, IFormatProvider? provider)

Parameters

conversionType Type

The type to convert to.

provider IFormatProvider

An 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

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

ushort

The wrapped value converted to a ushort.

ToUInt32(IFormatProvider?)

Converts the wrapped value to a uint.

public uint ToUInt32(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

uint

The wrapped value converted to a uint.

ToUInt64(IFormatProvider?)

Converts the wrapped value to a ulong.

public ulong ToUInt64(IFormatProvider? provider)

Parameters

provider IFormatProvider

An IFormatProvider for culture-specific formatting.

Returns

ulong

The wrapped value converted to a ulong.

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

valueObject ScalarValueObject<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.