This library is constantly being developed and new scalars are being added continuosly.
ScalarKit offers a contract for types that are synonymous with a backing type, usually a primitive type. This is common to combat bad code smells, such as primitive obsession, while also making your code base more readable and maintainable.
public interface IScalar<TSelf, TPrimitive>
where TSelf : notnull, IScalar<TSelf, TPrimitive>
where TPrimitive : notnull
{
TPrimitive Value { get; }
static abstract implicit operator TSelf(TPrimitive primitive);
static abstract bool TryFrom(TPrimitive primitive, out TSelf scalar);
string? ToString()
=> Value.ToString();
}
The implicit operator is where it is recommended for you to do any validation checks, and call a private constructor for your type. This allows you to create a type that is both immutable and safe to use, but of course use the library in any way you see fit.
ScalarKit offers an alternative to error handling and the expensive throwing of exceptions, wrapping them in return objects called ErrorPrones.
There are two flavors of ErrorProne
, one with a value and a list of Exceptions...
ErrorProne<int> proneInteger = 21;
proneInteger.Errors; // List of Exceptions
...and one with a value and a list of your own custom error types.
public record Error(string Code, string Message);
ErrorProne<int, Error> proneInteger = 420;
proneInteger.Errors; // List of Error objects
Both ways allow you to create an ErrorProne
from:
- A starting value.
- A starting error.
- A list of errors.
ErrorProne<int> proneInteger = 69;
ErrorProne<int> proneIntegerWithAnError = new Exception("I don't like this number...");
ErrorProne<int> proneIntegerWithErrors = new(new[]
{
new Exception("I don't like this number..."),
new ArgumentOutOfRangeException("Try a smaller number")
});
Note The value or error type will be implicitly converted to the
ErrorProne
type, however, anyIEnumerable
of errors can only be passed in through the constructor. This is due to interfaces not being able to implicitly convert to a type.
When using ScalarKits built in exception ErrorProne
,
instantiations that will throw an exception will instead add the exception to the list of errors:
ErrorProne<byte> proneByte = 256; // Will usually throw an exception during runtime, but will instead add the exception to the Errors property.
proneByte.IsFaulty; // True
The method you will use often is Inspect
, as this is how ErrorPrones build up their container of errors with a very fluent syntax.
public record User(Guid Id, string Username, string Password);
ErrorProne<User> proneUser = new User(Guid.NewGuid(), "John Doe");
proneUser
.Inspect(
constraint: user => user.Username.Contains(" "),
error: new Exception("Username cannot contain spaces")
)
.Inspect(
constraint: user => user.Password.Any(c => char.IsDigit(c)),
error: new Exception("Password must contain at least one digit"
);
After inspecting the value, you can check if the ErrorProne is valid or not.
if (proneUser.IsFaulty)
{
// Do something with the value
}
else
{
// Do something with the errors
}
There is also Dispatch
, which allows for two functions to be utilized, one for the value and one for the errors.
// Do something with the first error...
proneUser.DispatchSingle(
onValue: user => Console.WriteLine($"User {user.Username} is valid!"),
onError: error => Console.WriteLine($"User is invalid: {error.Message}")
);
// ...or do something with all the errors
proneUser.Dispatch(
onValue: user => Console.WriteLine($"User {user.Username} is valid!"),
onError: errors => Console.WriteLine($"User is invalid, there are {user.Errors.Count} errors!")
);
Both Inspect
and Dispatch
offer asynchronous variants as well, InspectAsync
, DispatchSingleAsync
and DispatchAsync
.
ScalarKit also offers a few built in fluent inspection methods for common validation checks on primitives:
- Any Type
OneOf(IEnumerable<T> values, TError error)
NoneOf(IEnumerable<T> values, TError error)
- Numbers
GreaterThan(TNumber min, TError onOutOfBounds, bool includeMin = false)
LessThan(TNumber max, TError onOutOfBounds, bool includeMax = false)
InRange(TNumber min, TNumber max, TError onOutOfBounds, bool includeMin = false, bool includeMax = false)
- Strings
NotEmpty(TError onEmpty)
MinLength(int min, TError onOutOfBounds, bool includeMin = false)
MaxLength(int max, TError onOutOfBounds, bool includeMax = false)
BoundLength(int min, int max, TError onOutOfBounds, bool includeMin = false, bool includeMax = false)
ErrorProne
implements IErroneous<TError>
, providing a contract for types that can be faulty and contain a list of errors.
public interface IErroneous<TError>
where TError : notnull
{
bool IsFaulty { get; }
IReadOnlyCollection<TError> Errors { get; }
}
This allows your own custom types to be used in the same way as ErrorProne
. ScalarKit uses it to be able to implement the AggregateErrors
and AccumulateErrors
methods, allowing to group up ErrorProne
objects with different values, but share the same error type.
public record AuthenticationResponse(
string Username,
string Email,
string ssword
);
ErrorProne<string> proneUsername = "John Doe";
ErrorProne<string> proneEmail = "johnDoe@gmail.com";
ErrorProne<string> pronePassword = "password123";
proneUsername.Inspect(...);
proneEmail.Inspect(...);
pronePassword.Inspect(...);
// approach 1
ErrorProne<AuthenticationResponse> proneAuthResponse = new(ErrorProne.AccumulateErrors(proneUsername, proneEmail, pronePassword));
// approach 2
proneAuthResponse = ErrorProne.AggregateErrors(
proneUsername,
proneEmail,
pronePassword);
);
return proneAuthResponse.Dispatch(
onValue: authResponse => authResponse.Value,
onError: errors => authResponse
);
You can clean your instance of ErrorProne
to only contain unique errors with a given comparator, for ErrorProne<TValue>
, if a comparator for Exception
is not provided, a built in comaparator will be used that compares the type and message of the exception.
ErrorProne<bool> alwaysTrue = true;
alwaysTrue
.Inspect(
constraint: _ => false,
error: new Exception("NOT TRUE!")
)
.Inspect(
constraint: _ => false,
error: new Exception("NOT TRUE!")
)
.OnlyUniqueErrors(); // only contains one exception
The value of an ErrorProne
can be accessed through the Value
property, however, this will throw an exception if the ErrorProne
is faulty, so use the IsFaulty
property to check beforehand, or configure any of the Dispatch
methods to handle it.
ErrorProne<int> proneInteger = 69;
proneInteger.LessThan(
max: 50,
onOutOfBounds: new ArgumentOutOfRangeException("Try a smaller number"),
includeMax: true
);
Console.WriteLine(proneInteger.Value);
Unhandled exception. ScalarKit.Exceptions.FaultyValueException: The error prone Int32 can not be accessed as it is faulty.
ErrorProne
is inspired from functional railway oriented programming, as well as Rust's approach to error handling.