ValueExtensions.ValueOf 0.1.3

dotnet add package ValueExtensions.ValueOf --version 0.1.3                
NuGet\Install-Package ValueExtensions.ValueOf -Version 0.1.3                
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="ValueExtensions.ValueOf" Version="0.1.3" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add ValueExtensions.ValueOf --version 0.1.3                
#r "nuget: ValueExtensions.ValueOf, 0.1.3"                
#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.
// Install ValueExtensions.ValueOf as a Cake Addin
#addin nuget:?package=ValueExtensions.ValueOf&version=0.1.3

// Install ValueExtensions.ValueOf as a Cake Tool
#tool nuget:?package=ValueExtensions.ValueOf&version=0.1.3                

ValueOf

A helper to deal with primitive obsession. Enables creation of types with value object semantics. Inspired by https://github.com/mcintyre321/ValueOf. This alternative version has the following enhancements:

  • Doesn't use exceptions to communicate validation failures - cleaner code, easier to integrate with validation frameworks.
  • Supports structs - no pressure on GC .

Scenarios

Scenario 1 - no validation is needed, reference type value object

Steps:

  • Create a record class derived from ValueOf<TValue, TThis>.AsClass.
  • Create a single-argument private constructor.
public record FirstName : ValueOf<string, FirstName>.AsClass
{
    private FirstName(string value) : base(value)
    {
    }
}

To construct an instance, use the following API:

FirstName firstName = FirstName.From("John");

Scenario 2 - validation is needed, reference type value object

Steps:

  • Create a record class derived from ValueOf<TValue, TThis>.AsClass record class.
  • Create a single-argument private constructor.
  • Define a bool-returning public static method named IsValid with the signature (TValue value) or alternatively (TValue value, out string? error) - see below for usage details.
  • Alternatively, you can create an arbitrarily named method with the same signature and mark it with the [Validator] attribute.
public record EmailAddress : ValueOf<string, EmailAddress>.AsClass
{
    private EmailAddress(string value) : base(value)
    {
    }

    public static bool IsValid(string value)
    {
        bool isValid = Regex.IsMatch(value, @"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.IgnoreCase);

        return isValid;
    }
}

To construct an instance, use the following API:

string someString = ...;

if (EmailAddress.TryFrom(someString, out EmailAddress? email))
{
    // validation passed, use the 'email' instance
}
else
{
    //validation failed
    Console.WriteLine($"Error occurred.");
}

EmailAddress.IsValid(...) validation method will be discovered and used by the TryFrom(...) method to validate the passed in value parameter.

You can also hook up your validation framework of choice to the EmailAddress.IsValid(...) method. This is a way to keep validation logic inside your domain classes.

If a validation error message is needed, the following API should be used:

if (!EmailAddress.TryFrom(someString, out EmailAddress? email, out string? error))
{
    Console.WriteLine($"Error occurred. {error}");
}

In this case, the validation method should also be extended and have the following signature:

public static bool IsValid(string value, out string? error)
{
    bool isValid = Regex.IsMatch(value, @"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.IgnoreCase);

    error = isValid ? null : $"Invalid email: '{value}'.";

    return isValid;
}

Scenario 3 - validation is needed, value type value object

  • Create a readonly record struct implementing ValueOf<TValue, TThis>.AsStruct interface.
  • Create a single-argument private constructor.
  • Implement the interface - create public readonly property named Value of type TValue. Unfortunately, this boilerplate can't be implemented in the interface as it requires storing instance-specific state.
  • Define a bool-returning public static method named IsValid with the signature (TValue value) or (TValue value, out string? error).
  • Alternatively, you can create an arbitrarily named method with the same signature and mark it with the [Validator] attribute.
public readonly record struct UserId : ValueOf<int, UserId>.AsStruct
{
    public int Value { get; }

    private UserId(int value)
    {
        Value = value;
    }
    public static bool IsValid(int value, out string? error)
    {
        if (value < 0)
        {
            error = "UserId cannot be a negative value.";
            return false;
        }

        error = null;
        return false;
    }
}

Due to how the TryFrom/From methods are 'mixed in' to the struct (by means of default interface implementation), they end up unavailable to be called directly, i.e. as static methods of the implementing type. Hence the API for instance creation is not as pretty as for reference-based ValueOf types:

ValueOf<int, UserId>.TryFrom(10, out UserId userId);

To slightly improve the situation a 'forwarding' method can be added to a value type:

public readonly record struct UserId : ValueOf<int, UserId>.AsStruct
{
    public int Value { get; }

    private UserId(int value)
    {
        Value = value;
    }

    public static bool TryFrom(int value, out UserId userId)
    {
        return ValueOf<int, UserId>.TryFrom(10, out userId);
    }

    public static bool IsValid(int value, out string? error)
    {
        ...
    }
}

Now the object creation syntax is much cleaner and fully matches the syntax for referece-based value objects:

UserId.TryFrom(10, out UserId userId);

The obvious downside of this approach is that it requires users to implement extra boilerplate.

A totaly different alternative would be to use .NET 6 source code generators. When/If the new API proves to be the one to go forward with 😉

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net6.0

    • No dependencies.

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
0.1.3 1,115 11/22/2021