Klab.Toolkit.ValueObjects 2.9.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Klab.Toolkit.ValueObjects --version 2.9.0
                    
NuGet\Install-Package Klab.Toolkit.ValueObjects -Version 2.9.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Klab.Toolkit.ValueObjects" Version="2.9.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Klab.Toolkit.ValueObjects" Version="2.9.0" />
                    
Directory.Packages.props
<PackageReference Include="Klab.Toolkit.ValueObjects" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Klab.Toolkit.ValueObjects --version 2.9.0
                    
#r "nuget: Klab.Toolkit.ValueObjects, 2.9.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Klab.Toolkit.ValueObjects@2.9.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Klab.Toolkit.ValueObjects&version=2.9.0
                    
Install as a Cake Addin
#tool nuget:?package=Klab.Toolkit.ValueObjects&version=2.9.0
                    
Install as a Cake Tool

Klab.Toolkit.ValueObjects

Overview

The Klab.Toolkit.ValueObjects package provides a foundation for implementing Domain-Driven Design (DDD) value objects in .NET applications. Value objects are immutable objects that represent concepts defined by their attributes rather than their identity, ensuring data integrity, encapsulating business rules, and promoting a rich domain model.

Purpose

This package enables developers to:

  • Enforce Business Rules: Validate data at the object creation level
  • Ensure Immutability: Prevent accidental data modification after creation
  • Provide Type Safety: Replace primitive obsession with meaningful types
  • Centralize Validation: Keep validation logic close to the data it protects
  • Enhance Domain Models: Create expressive, self-documenting code

Key Features

Built-in Value Objects

  • Email: RFC-compliant email address validation
  • ComPort: Serial communication port representation (COM1-COM256)
  • IpAddress: IP address validation and formatting
  • Voltage: Physical voltage measurements with unit conversions
  • Pressure: Physical pressure measurements with unit conversions
  • Current: Electrical current measurements with unit conversions

Core Principles

  • Validation on Creation: Invalid objects cannot be instantiated
  • Immutability: All value objects are implemented as records
  • Self-Contained: Each value object encapsulates its own validation rules
  • Rich API: Meaningful methods and properties for domain operations

Installation

dotnet add package Klab.Toolkit.ValueObjects

Built-in Value Objects

Email Address

using Klab.Toolkit.ValueObjects;

// Valid email creation
Email validEmail = Email.Create("user@example.com");
Console.WriteLine(validEmail.Value); // "user@example.com"

// Invalid email throws exception
try
{
    Email invalidEmail = Email.Create("not-an-email");
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message); // "E-Mail is invalid"
}

// Empty email handling
try
{
    Email emptyEmail = Email.Create("");
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message); // "Empty E-Mail Address is not possible"
}
Email Integration Example
public class UserService
{
    public Result<User> CreateUser(string emailInput, string name)
    {
        try
        {
            var email = Email.Create(emailInput);
            var user = new User
            {
                Id = Guid.NewGuid(),
                Email = email,
                Name = name,
                CreatedAt = DateTime.UtcNow
            };

            return Result.Success(user);
        }
        catch (ArgumentException ex)
        {
            return Error.Create("INVALID_EMAIL", ex.Message);
        }
    }
}

public class User
{
    public Guid Id { get; set; }
    public Email Email { get; set; }
    public string Name { get; set; }
    public DateTime CreatedAt { get; set; }
}

COM Port

// Valid COM port creation
ComPort port1 = ComPort.Create("COM1");
ComPort port255 = ComPort.Create("COM255");

Console.WriteLine(port1.Value); // "COM1"

// Invalid COM port examples
try
{
    ComPort invalidPort = ComPort.Create("COM0"); // Invalid: starts from COM1
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message); // "COM Port is invalid"
}

try
{
    ComPort invalidPort = ComPort.Create("COM257"); // Invalid: max is COM256
}
catch (ArgumentException ex)
{
    Console.WriteLine(ex.Message); // "COM Port is invalid"
}
COM Port in Industrial Applications
public class SerialDeviceManager
{
    private readonly Dictionary<ComPort, ISerialDevice> _devices = new();

    public Result ConnectDevice(string portName, ISerialDevice device)
    {
        try
        {
            var comPort = ComPort.Create(portName);

            if (_devices.ContainsKey(comPort))
            {
                return Error.Create("PORT_IN_USE", $"Port {comPort.Value} is already in use");
            }

            // Attempt connection
            device.Connect(comPort.Value);
            _devices[comPort] = device;

            return Result.Success();
        }
        catch (ArgumentException ex)
        {
            return Error.Create("INVALID_PORT", ex.Message);
        }
        catch (Exception ex)
        {
            return Error.Create("CONNECTION_FAILED", $"Failed to connect to {portName}: {ex.Message}");
        }
    }

    public IEnumerable<ComPort> GetConnectedPorts()
    {
        return _devices.Keys;
    }
}

Physical Measurements

Voltage
// Create voltage measurements
Voltage voltage1 = Voltage.Create(5.0); // 5 volts
Voltage voltage2 = Voltage.FromMillivolt(3300); // 3.3 volts from millivolts

// Access different units
Console.WriteLine($"Voltage: {voltage1.Volts}V"); // 5V
Console.WriteLine($"Millivolts: {voltage1.Millivolts}mV"); // 5000mV
Console.WriteLine($"Microvolts: {voltage1.Microvolts}μV"); // 5000000μV
Console.WriteLine($"Kilovolts: {voltage1.Kilovolts}kV"); // 0.005kV

// Voltage from different units
Voltage fromMicrovolts = Voltage.FromMicrovolt(1500000); // 1.5V
Voltage fromKilovolts = Voltage.FromKilovolt(0.012); // 12V
Voltage fromMegavolts = Voltage.FromMegavolt(0.000001); // 1V
Voltage in Electronics Applications
public class PowerSupplyController
{
    private readonly Dictionary<string, VoltageRange> _allowedRanges = new()
    {
        ["cpu"] = new VoltageRange(Voltage.Create(0.8), Voltage.Create(1.4)),
        ["memory"] = new VoltageRange(Voltage.Create(1.2), Voltage.Create(1.35)),
        ["io"] = new VoltageRange(Voltage.Create(3.0), Voltage.Create(3.6))
    };

    public Result<string> SetVoltage(string component, double volts)
    {
        var voltage = Voltage.Create(volts);

        if (!_allowedRanges.TryGetValue(component, out var range))
        {
            return Error.Create("UNKNOWN_COMPONENT", $"Component '{component}' is not recognized");
        }

        if (!range.Contains(voltage))
        {
            return Error.Create("VOLTAGE_OUT_OF_RANGE",
                $"Voltage {voltage.Volts}V is outside safe range {range.Min.Volts}V - {range.Max.Volts}V for {component}");
        }

        // Set the voltage (hardware interaction)
        return Result.Success($"Voltage set to {voltage.Volts}V for {component}");
    }
}

public record VoltageRange(Voltage Min, Voltage Max)
{
    public bool Contains(Voltage voltage) =>
        voltage.Volts >= Min.Volts && voltage.Volts <= Max.Volts;
}
Pressure
// Create pressure measurements
Pressure pressure1 = Pressure.Create(101325); // 1 atmosphere in pascals
Pressure pressure2 = Pressure.FromBar(1.5); // 1.5 bar

// Access different units
Console.WriteLine($"Pascals: {pressure1.Pascals}Pa"); // 101325Pa
Console.WriteLine($"Bar: {pressure1.Bar}bar"); // 1.01325bar
Console.WriteLine($"PSI: {pressure1.Psi}psi"); // ~14.7psi
Console.WriteLine($"Atmosphere: {pressure1.Atmosphere}atm"); // 1atm

// Create from different units
Pressure fromPsi = Pressure.FromPsi(30); // Tire pressure
Pressure fromAtmosphere = Pressure.FromAtmosphere(2); // 2 atmospheres
Current
// Create current measurements
Current current1 = Current.Create(2.5); // 2.5 amperes
Current current2 = Current.FromMilliamp(750); // 0.75 amperes

// Access different units
Console.WriteLine($"Amperes: {current1.Amperes}A"); // 2.5A
Console.WriteLine($"Milliamperes: {current1.Milliamperes}mA"); // 2500mA
Console.WriteLine($"Microamperes: {current1.Microamperes}μA"); // 2500000μA

Creating Custom Value Objects

Basic Value Object Pattern

using Klab.Toolkit.ValueObjects;

// Simple value object with validation
public record ProductCode
{
    public string Value { get; }

    public static ProductCode Create(string code)
    {
        if (string.IsNullOrWhiteSpace(code))
            throw new ArgumentException("Product code cannot be empty");

        if (code.Length != 8)
            throw new ArgumentException("Product code must be exactly 8 characters");

        if (!code.All(char.IsLetterOrDigit))
            throw new ArgumentException("Product code must contain only letters and digits");

        return new ProductCode(code.ToUpperInvariant());
    }

    private ProductCode(string value)
    {
        Value = value;
    }
}

Value Object with Result Pattern

using Klab.Toolkit.Results;

public record Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public static Result<Money> Create(decimal amount, string currency)
    {
        if (amount < 0)
            return Error.Create("NEGATIVE_AMOUNT", "Money amount cannot be negative");

        if (string.IsNullOrWhiteSpace(currency))
            return Error.Create("INVALID_CURRENCY", "Currency code is required");

        if (currency.Length != 3)
            return Error.Create("INVALID_CURRENCY", "Currency code must be 3 characters");

        return Result.Success(new Money(amount, currency.ToUpperInvariant()));
    }

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

    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException($"Cannot add {other.Currency} to {Currency}");

        return new Money(Amount + other.Amount, Currency);
    }

    public override string ToString() => $"{Amount:F2} {Currency}";
}

Complex Value Object with Business Logic

public record Temperature
{
    public double Celsius { get; }
    public double Fahrenheit => (Celsius * 9 / 5) + 32;
    public double Kelvin => Celsius + 273.15;

    public static Result<Temperature> FromCelsius(double celsius)
    {
        if (celsius < -273.15)
            return Error.Create("INVALID_TEMPERATURE", "Temperature cannot be below absolute zero (-273.15°C)");

        return Result.Success(new Temperature(celsius));
    }

    public static Result<Temperature> FromFahrenheit(double fahrenheit)
    {
        var celsius = (fahrenheit - 32) * 5 / 9;
        return FromCelsius(celsius);
    }

    public static Result<Temperature> FromKelvin(double kelvin)
    {
        if (kelvin < 0)
            return Error.Create("INVALID_TEMPERATURE", "Temperature in Kelvin cannot be negative");

        return FromCelsius(kelvin - 273.15);
    }

    private Temperature(double celsius)
    {
        Celsius = celsius;
    }

    public bool IsFreezingPoint => Math.Abs(Celsius) < 0.01;
    public bool IsBoilingPoint => Math.Abs(Celsius - 100) < 0.01;

    public TemperatureRange GetPhase()
    {
        return Celsius switch
        {
            < 0 => TemperatureRange.Solid,
            >= 0 and < 100 => TemperatureRange.Liquid,
            >= 100 => TemperatureRange.Gas,
            _ => TemperatureRange.Unknown
        };
    }
}

public enum TemperatureRange
{
    Unknown,
    Solid,
    Liquid,
    Gas
}

Advanced Patterns

Value Object Collections

public record Coordinates
{
    public double Latitude { get; }
    public double Longitude { get; }

    public static Result<Coordinates> Create(double latitude, double longitude)
    {
        if (latitude < -90 || latitude > 90)
            return Error.Create("INVALID_LATITUDE", "Latitude must be between -90 and 90 degrees");

        if (longitude < -180 || longitude > 180)
            return Error.Create("INVALID_LONGITUDE", "Longitude must be between -180 and 180 degrees");

        return Result.Success(new Coordinates(latitude, longitude));
    }

    private Coordinates(double latitude, double longitude)
    {
        Latitude = latitude;
        Longitude = longitude;
    }

    public double DistanceTo(Coordinates other)
    {
        // Haversine formula implementation
        const double R = 6371; // Earth's radius in kilometers

        var lat1Rad = Latitude * Math.PI / 180;
        var lat2Rad = other.Latitude * Math.PI / 180;
        var deltaLatRad = (other.Latitude - Latitude) * Math.PI / 180;
        var deltaLonRad = (other.Longitude - Longitude) * Math.PI / 180;

        var a = Math.Sin(deltaLatRad / 2) * Math.Sin(deltaLatRad / 2) +
                Math.Cos(lat1Rad) * Math.Cos(lat2Rad) *
                Math.Sin(deltaLonRad / 2) * Math.Sin(deltaLonRad / 2);

        var c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));

        return R * c;
    }
}

public record Route
{
    private readonly List<Coordinates> _waypoints;

    public IReadOnlyList<Coordinates> Waypoints => _waypoints.AsReadOnly();
    public double TotalDistance => CalculateTotalDistance();

    public static Result<Route> Create(IEnumerable<Coordinates> waypoints)
    {
        var waypointList = waypoints.ToList();

        if (waypointList.Count < 2)
            return Error.Create("INSUFFICIENT_WAYPOINTS", "Route must have at least 2 waypoints");

        return Result.Success(new Route(waypointList));
    }

    private Route(List<Coordinates> waypoints)
    {
        _waypoints = waypoints;
    }

    private double CalculateTotalDistance()
    {
        double total = 0;
        for (int i = 0; i < _waypoints.Count - 1; i++)
        {
            total += _waypoints[i].DistanceTo(_waypoints[i + 1]);
        }
        return total;
    }
}

Entity Integration

public class Product
{
    public Guid Id { get; private set; }
    public ProductCode Code { get; private set; }
    public string Name { get; private set; }
    public Money Price { get; private set; }
    public Email ContactEmail { get; private set; }

    public static Result<Product> Create(string code, string name, decimal price, string currency, string contactEmail)
    {
        try
        {
            var productCode = ProductCode.Create(code);
            var email = Email.Create(contactEmail);

            var moneyResult = Money.Create(price, currency);
            if (!moneyResult.IsSuccess)
                return Result.Failure<Product>(moneyResult.Error);

            var product = new Product
            {
                Id = Guid.NewGuid(),
                Code = productCode,
                Name = name,
                Price = moneyResult.Value,
                ContactEmail = email
            };

            return Result.Success(product);
        }
        catch (ArgumentException ex)
        {
            return Error.Create("INVALID_PRODUCT_DATA", ex.Message);
        }
    }

    public Result UpdatePrice(decimal newPrice)
    {
        var newMoneyResult = Money.Create(newPrice, Price.Currency);
        if (!newMoneyResult.IsSuccess)
            return newMoneyResult;

        Price = newMoneyResult.Value;
        return Result.Success();
    }
}

Testing Value Objects

public class EmailTests
{
    [Test]
    public void Create_WithValidEmail_ShouldSucceed()
    {
        // Arrange
        string validEmail = "test@example.com";

        // Act
        var email = Email.Create(validEmail);

        // Assert
        email.Value.Should().Be(validEmail);
    }

    [Test]
    public void Create_WithInvalidEmail_ShouldThrowException()
    {
        // Arrange
        string invalidEmail = "not-an-email";

        // Act & Assert
        Action act = () => Email.Create(invalidEmail);
        act.Should().Throw<ArgumentException>()
           .WithMessage("E-Mail is invalid");
    }

    [Test]
    [TestCase("", "Empty E-Mail Adress is not possible")]
    [TestCase("   ", "Empty E-Mail Adress is not possible")]
    [TestCase("invalid-email", "E-Mail is invalid")]
    [TestCase("@example.com", "E-Mail is invalid")]
    [TestCase("test@", "E-Mail is invalid")]
    public void Create_WithInvalidInput_ShouldThrowWithCorrectMessage(string input, string expectedMessage)
    {
        // Act & Assert
        Action act = () => Email.Create(input);
        act.Should().Throw<ArgumentException>()
           .WithMessage(expectedMessage);
    }
}

public class VoltageTests
{
    [Test]
    public void Create_ShouldSetVoltsCorrectly()
    {
        // Arrange
        double volts = 5.0;

        // Act
        var voltage = Voltage.Create(volts);

        // Assert
        voltage.Volts.Should().Be(volts);
        voltage.Millivolts.Should().Be(5000);
        voltage.Microvolts.Should().Be(5000000);
    }

    [Test]
    public void FromMillivolt_ShouldConvertCorrectly()
    {
        // Arrange
        double millivolts = 3300;

        // Act
        var voltage = Voltage.FromMillivolt(millivolts);

        // Assert
        voltage.Volts.Should().Be(3.3);
        voltage.Millivolts.Should().Be(millivolts);
    }
}

Best Practices

1. Validation at Creation

Always validate input during object creation, never allow invalid objects to exist.

// ✅ Good: Validation at creation
public static Email Create(string email)
{
    if (string.IsNullOrWhiteSpace(email))
        throw new ArgumentException("Email cannot be empty");

    if (!IsValidFormat(email))
        throw new ArgumentException("Invalid email format");

    return new Email(email);
}

// ❌ Bad: Validation after creation
public void SetEmail(string email)
{
    if (!IsValidFormat(email))
        throw new ArgumentException("Invalid email");
    _email = email;
}

2. Immutability

Make all value objects immutable to prevent accidental modification.

// ✅ Good: Immutable record
public record Email
{
    public string Value { get; }
    private Email(string value) => Value = value;
}

// ❌ Bad: Mutable class
public class Email
{
    public string Value { get; set; }
}

3. Rich Domain Models

Provide meaningful operations and properties that reflect the domain.

// ✅ Good: Rich domain model
public record Temperature
{
    public double Celsius { get; }
    public double Fahrenheit => (Celsius * 9 / 5) + 32;
    public bool IsFreezing => Celsius <= 0;
    public bool IsBoiling => Celsius >= 100;
}

// ❌ Bad: Anemic model
public record Temperature
{
    public double Value { get; }
}

4. Equality by Value

Ensure value objects compare by value, not reference (automatic with records).

var email1 = Email.Create("test@example.com");
var email2 = Email.Create("test@example.com");
Assert.True(email1 == email2); // Should be equal

Performance Considerations

  • Creation Cost: Value objects have validation overhead during creation
  • Memory Usage: Immutable objects may create more garbage, but improve thread safety
  • Comparison: Record-based equality is efficient for value comparison
  • Caching: Consider caching frequently used value objects (e.g., common currency codes)

Thread Safety

All value objects are inherently thread-safe due to their immutable nature:

  • No state can be modified after creation
  • Multiple threads can safely access the same value object instance
  • Validation logic should also be thread-safe (avoid static mutable state)
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.10.0 63 8/22/2025
2.9.0 130 8/3/2025
2.8.2 230 5/15/2025
2.8.1 163 4/24/2025
2.8.0 139 4/22/2025
2.7.3 132 4/13/2025
2.7.2 150 4/6/2025
2.7.1 159 4/3/2025
2.7.0 150 4/3/2025
2.6.0 488 3/24/2025
2.5.1 118 3/14/2025
2.5.0 107 2/24/2025
2.4.1 123 10/2/2024
2.4.0 107 10/2/2024
2.3.0 115 10/1/2024
2.2.4 113 9/30/2024
2.2.3 117 9/28/2024
2.2.2 121 9/20/2024
2.2.1 125 9/17/2024
2.2.0 123 9/17/2024
2.1.0 128 8/12/2024
2.0.0 202 12/28/2023