ValueExtensions.ValueOf
0.1.3
dotnet add package ValueExtensions.ValueOf --version 0.1.3
NuGet\Install-Package ValueExtensions.ValueOf -Version 0.1.3
<PackageReference Include="ValueExtensions.ValueOf" Version="0.1.3" />
paket add ValueExtensions.ValueOf --version 0.1.3
#r "nuget: ValueExtensions.ValueOf, 0.1.3"
// 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 | Versions 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. |
-
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,128 | 11/22/2021 |