There are multiple ways to support validation within .NET. Most notable are
Qowaiv.Validation aims to provide extensions on top of those that work well when using Qowaiv SVO's, and prevent vendor lock-in.
To prevent a vendor lock-in, Qowaiv.Validation.Abstractions
has been introduced.
To achieve that the following is added:
The main interface. Validates a model and returns the validation result.
namespace Qowaiv.Validation.Abstractions
{
public interface IValidator<TModel>
{
Result<TModel> Validate(TModel model);
}
}
A (validation) result, containing validation messages. Creation is only via factory methods.
namespace Qowaiv.Validation.Abstractions
{
public class Result
{
public IReadOnlyList<IValidationMessage> Messages { get; }
public bool IsValid => !Errors.Any();
public IEnumerable<IValidationMessage> Errors => Messages.GetErrors();
public IEnumerable<IValidationMessage> Warnings => Messages.GetWarnings();
public IEnumerable<IValidationMessage> Infos => Messages.GetInfos();
public static Result OK => new Result(Enumerable.Empty<IValidationMessage>());
public static Result<T> Null<T>(params IValidationMessage[] messages) => new Result<T>(messages);
public static Result<T> For<T>(T value, params IValidationMessage[] messages) => new Result<T>(value, messages);
public static Result WithMessages(params IValidationMessage[] messages) => new Result(messages);
public static Result<T> WithMessages<T>(params IValidationMessage[] messages) => new Result<T>(default, messages);
}
}
The generic result contains also the value/validated model. This Value
is
only accessible when the model is valid, otherwise, an InvalidModelException
is thrown. This exception contains the validation messages. The result is considered
invalid if the value is null
, unless explicitly created with Result.Null<T>()
.
namespace Qowaiv.Validation.Abstractions
{
public sealed class Result<T> : Result
{
public T Value => IsValid
? _value
: throw InvalidModelException.For<T>(Errors);
}
}
Typical use cases are:
Result<DataType> result = Result.For(value);
Result<DataType> resultWithMessages = Result.For(value, messages);
Task<Result<DataType>> asyncResult = Result.For(value).AsTask();
A Composed Action can be created by method chaining of multiple smaller
actions/functions. Subsequent actions are executed while the Result<TModel>
is valid:
Result<DataType> result = GetModel()
.Act(m => m.Action1())
.Act(m => m.Action2());
Or with the |
pipe operator:
Result<DataType> result = GetModel()
| (m => m.Action1())
| (m => m.Action2());
This is short for:
Result<DataType> result = GetModel()
if (result.Isvalid)
{
result = result.Value.Action1();
}
if (result.Isvalid)
{
result = result.Value.Action2();
}
It is also possible to have multiple acts that update a shared context:
Result<Context> context = NewContext()
.Act(c => Service.GetValue(), (c, value) => c.Value = value)
.Act(c => Service.GetOther(), (c, other) => c.Value = other);
Or, with a (shared) immutable context:
Result<Context> context = NewContext()
.Act(c => Service.GetValue(), (c, value) => /* return Context */ c.Update(value))
.Act(c => Service.GetOther(), (c, other) => /* return Context */ c.Update(other));
The following castings are supported:
Result<T> implicit = new T();
T explicit = Result.For<T>(new T());
Result<TOut> casted = Result.For<T>(new T()).Cast<TOut>();
The explicit casts fails if the result was not valid. The Cast<TOut>()
fails
when TOut
is not a subclass of T
.
The common ground of validation messages.
namespace Qowaiv.Validation.Abstractions
{
public interface IValidationMessage
{
ValidationSeverity Severity { get; }
string PropertyName { get; }
string Message { get; }
}
}
There are implementations available in Qowaiv.Validation.Abstraction
,
Qowaiv.Validation.Fluent
and Qowaiv.Validation.DataAnnotation
. You can pick
your implementation of choice based on your scenario.
Qowaiv.Validation.Messages
contains a specific implementation of IValidationMessage
for communicating insufficient rights. A use case for this can be to
communicate a 403 - Forbidden
response.
Qowaiv.Validation.Messages
contains a specific implementation of IValidationMessage
for communicating that a concurrency issue. A use case for this can be to
communicate a 409 - Conflict
response.
Qowaiv.Validation.Messages
contains a specific implementation of IValidationMessage
for communicating that an entity could not be found. A use case for this can be to
communicate a 404 - Not Found
response.
Qowaiv.Validation.Messages
contains a specific implementation of IValidationMessage
for communicating that a service was unavailable. A use case for this can be to
communicate a 503 - Service Unavailable
response.
Qowaiv exensions on *Fluent Validation
Provides a Fluent Validation based implementation of the Qowaiv.Validation.Abstractions.IValidator
and custom validation extensions (..).
Provides an data annotations based implementation of the Qowaiv.Validation.Abstractions.IValidator
and data annotation attributes (..).
Validating XML documents via XSD schema's is a common scenario. To benefit from
Result<T>
the following scenario is supported:
var document = XDocument.Parse("<some xml />");
Result<XDocument> result = document.Validate(schema);
Where the schema can be System.IO.Stream
, System.Xml.Schema.XmlSchema
, or
System.Xml.Schema.XmlSchemaSet
.
A schema can also be the source of a model validator:
var validator = new SchemaValidator<MyModel>(schema);
Result<MyModel> result = validator.Validate(model);
// or
Result<MyModel> deserialized = validator.Deserialize(stream);
So validation can be triggered on an existing model, or when deserializing.
Note that TModel (obviously) has to be XML serializable.
To guard pre-conditions, the fluent syntax .Must()
guards conditions using a
Result<T>
to communicate the outcome. So:
game.Must().Be(game.Phase == GamePhase.Started, "Game has started");
will return a valid Result<Game>
if the game is the required state,
otherwise an invalid Result<Game>
with the specified error message
is returned.
Out-of-the-box, Be
, NotBe
, and Exist
are provided, but it can easily be
extended by writing custom extension methods on Must<TSubject>
based on
what guarding your (domain) logic requires.
Qowaiv.Valdation comes with a separate Test Tools package. Details about that package can be found here.