base.validation.overview - grecosoft/NetFusion GitHub Wiki
NetFusion implements a simple validation pattern, based on Microsoft's Component Model Data Annotations, that can be replaced with other open-source alternatives. This is by design so the default implementation does not require any additional references. It should be noted that this base implementation is used exclusively when validating application settings, and not the alternative implementation provided by the host application during bootstrap. This is done so all application settings are validated consistently.
nuget | NetFusion.Base |
---|---|
types | IValidationService, IObjectValidator, IValidatableType |
If the host application doesn't want to use an alternative validation implementation, no additional bootstrap configuration is necessary. A default implementation of IValidationService is registered by the CompositeContainerBuilder. How to provide a custom implementation of IValidationService will be discussed below.
Regardless of the implementation, classes can specify custom validations by implementing the IValidatableType interface. This interface has a single method named Validate passed an instance of the IObjectValidator associated with the object's instance. The following will provide examples using the default implementation based on Microsoft Data Annotations. As with the other examples, the documentation will use code examples to illustrate different concepts.
Create the following two class examples implementing the IObjectValidator interface:
touch ./src/Components/Demo.Domain/Entities/Contact.cs
nano ./src/Components/Demo.Domain/Entities/Contact.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using NetFusion.Base.Validation;
namespace Demo.Domain.Entities
{
public class Contact : IValidatableType
{
[Required, MaxLength(20)]
public string FirstName { get; set; }
[Required, MaxLength(20)]
public string LastName {get; set; }
public IList<ContactAddress> Addresses { get; set; }
public Contact()
{
Addresses = new List<ContactAddress>();
}
private void AddAddress(ContactAddress address)
{
Addresses.Add(address);
}
public void Validate(IObjectValidator validator)
{
validator.Verify(FirstName != LastName, "First Name cannot Equal Last Name!");
validator.Verify(Addresses.Any(), "Must have at least one address.");
validator.AddChildren(Addresses);
}
}
}
touch ./src/Components/Demo.Domain/Entities/ContactAddress.cs
nano ./src/Components/Demo.Domain/Entities/ContactAddress.cs
using System.ComponentModel.DataAnnotations;
using NetFusion.Base.Validation;
namespace Demo.Domain.Entities
{
public class ContactAddress : IValidatableType
{
[Required, MaxLength(30)]
public string Street { get; set; }
[Required, MaxLength(40)]
public string City { get; set; }
[Required, StringLength(2, MinimumLength = 2)]
public string State { get; set; }
public string ZipCode { get; set; }
public void Validate(IObjectValidator validator)
{
validator.Verify(State != "NC", "Can't move here. State if Full!!!");
}
}
}
These examples show specifying validation attributes on the properties and also providing custom validation logic within the Validate methods. Objects such as these can be validated by injecting the singleton IValidationService and passing the object to the validate method.
The validation returns an instance of the ValidationResultSet class containing the results of the validation. The ObjectValidator class, providing the default implementation of IObjectValidator and used by the ValidationService, is implemented as follows:
- The associated object is first validated using Microsoft's Validation Attributes.
- If the object does not pass attribute validation, the validation stops and the results returned.
- However, if the object does pass attribute validation and implements the IValidatableType interface, the Validate method is called and passed an instance of the object's associated IObjectValidator instance.
- The Validate method can provide custom validations in code by passing a predicate condition to the Verify method of the passed validator. In addition, the Validate method can validate one or more related child objects by passing a collection to the validator's AddChildren method. This will repeat the same validation steps on the passed children objects.
- The above process recursively repeats until an invalid object is encountered having a validation item with an Error validation type. Objects with Information and Warning validation items are considered valid.
Add the following controller to invoke the validation code:
touch ./src/Demo.WebApi/Controllers/ValidationController.cs
nano ./src/Demo.WebApi/Controllers/ValidationController.cs
using Demo.Domain.Entities;
using Microsoft.AspNetCore.Mvc;
using NetFusion.Base.Validation;
namespace Demo.WebApi.Controllers
{
[Route("api/[controller]")]
public class ValidationController : Controller
{
private readonly IValidationService _validationSrv;
public ValidationController(IValidationService validationSrv)
{
_validationSrv = validationSrv;
}
[HttpPost]
public ValidationResultSet ValidateCustomer([FromBody]Contact customer) {
return _validationSrv.Validate(customer);
}
}
}
The Validate method of IValidationService returns an instance of the ValidationResultSet class. This class contains a flat list of all objects containing validation items. This class also has properties to determine if the result is invalid. The result is considered invalid if any of the validated objects contain items having an Error type. The ValidationResultSet class also has a ValidationType property containing the maximum validation level. If there are objects having both Information and Warning validations, the validation result will be of type Warning since it is considered of higher importance.
Enter the URL and the data shown in the following images to test a valid entity, an entity with invalid properties, and an entity with an invalid custom validation. After running the WebApi service the following examples can be executed:
cd ./src/Demo.WebApi
dotnet run
This section shows how to implement the IObjectValidator interface for use by a custom IValidationService registered with the composite-container. Below is a very simplistic example, but this is the same approach that would be taken to use an open-source validation library. The implementation would delegate to the open-source library and utilize its conventions for specifying validations.
# The following assumes Demo is the current working directory.
touch ./src/Components/Demo.Infra/CustomObjectValidator.cs
nano ./src/Components/Demo.Infra/CustomObjectValidator.cs
using System.Collections.Generic;
using NetFusion.Base.Validation;
using System.Linq;
namespace Demo.Infra
{
public class CustomObjectValidator : IObjectValidator
{
public object Object { get; }
public bool IsValid { get; private set; }
private readonly List<ValidationItem> _validations = new List<ValidationItem>();
private readonly List<IObjectValidator> _children = new List<IObjectValidator>();
public CustomObjectValidator(object obj)
{
Object = obj;
}
public IEnumerable<ValidationItem> Validations => _validations;
public IEnumerable<IObjectValidator> Children => _children;
public IObjectValidator AddChild(object childObject)
{
var childValidator = new CustomObjectValidator(childObject);
_children.Add(childValidator);
return childValidator;
}
public void AddChildren(IEnumerable<object> childObjects)
{
foreach (var childObj in childObjects)
{
AddChild(childObj);
}
}
public void AddChildren(params object[] childObjects)
{
throw new System.NotImplementedException();
}
public ValidationResultSet Validate()
{
var objProperties = Object.GetType().GetProperties();
IsValid = objProperties.Any(p1 => p1.Name == "FirstName") &&
objProperties.Any(p2 => p2.Name == "LastName");
if (IsValid)
{
string firstName = Object.GetType().GetProperty("FirstName")?.GetValue(Object) as string;
string lastName = Object.GetType().GetProperty("LastName")?.GetValue(Object) as string;
IsValid = firstName == "Mark" && lastName == "Twain";
}
if (!IsValid)
{
_validations.Add(new ValidationItem(
"You are not Mark Twain.",
new [] {"FirstName", "LastName"},
ValidationTypes.Error));
return new ValidationResultSet(Object, this);
}
return ValidationResultSet.ValidResult(Object);
}
public bool Verify(bool predicate, string message, ValidationTypes level = ValidationTypes.Error, params string[] propertyNames)
{
if (!predicate)
{
_validations.Add(new ValidationItem(message, level));
}
return predicate;
}
}
}
Next, an implementation of the IValidationService will be registered to preform the validation by delegating to the above IObjectValidator.
touch ./src/Components/Demo.Infra/CustomValidationService.cs
nano ./src/Components/Demo.Infra/CustomValidationService.cs
using System;
using NetFusion.Base.Validation;
namespace Demo.Infra
{
public class CustomValidationService : IValidationService
{
public ValidationResultSet Validate(object obj)
{
if (obj == null) throw new ArgumentNullException(nameof(obj),
"Object to validate cannot be null.");
IObjectValidator validator = new CustomObjectValidator(obj);
return validator.Validate();
}
}
}
Lastly, register the CustomValidationService as the IValidationService interface overriding the default implementation provided by NetFusion.
nano ./src/Demo.WebApi/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.CompositeContainer(_configuration, new NullExtendedLogger())
.AddSettings()
.AddPlugin<InfraPlugin>()
.AddPlugin<AppPlugin>()
.AddPlugin<DomainPlugin>()
.AddPlugin<WebApiPlugin>()
.Compose(config =>
{
config.AddSingleton<IValidationService, CustomValidationService>();
});
services.AddControllers();
}
The following shows posting data that will result in the custom validation reporting errors: