As a data/backend engineers that extract/filter dirty data to clean data would be necessary for some scenario. The first thing was to write some rule based validations for the purpose. And the most common way would be combined lots of IF/ELSE block even the code became within nested IF structures (for some complex situation).
The article would give you another aspect to get rid of mixed the validations behaviors all together but instead using the library Fluentvalidation.
The sample gonna express how to design a chain of validations that across the collection list for validating data among “instance” by “instance” and the property of inside of “instance" as shown below:
public class ShipmentValidator : AbstractValidator<Model>
{
public ShipmentValidator()
{
// PalletId should have value if packing type belong to "Pallet"
RuleFor(s => s.PalletId)
.NotEmpty()
.When(s => s.PackagingType.ToLower() == "pallet")
.WithMessage(s => $"{{PropertyName}} is requried.");
}
}
public class ShipmentServiceValidator : AbstractValidator<List<Model>>
{
public ShipmentServiceValidator()
{
var errors = new List<string>();
RuleFor(sources => sources).Custom((shipments, context) =>
{
// chain validations from extension methods
shipments
.ValidateValidSites(errors)
.ValidateUniqueInvoice(errors);
if (errors.Any())
{
context.AddFailure(string.Join("<br>", errors));
}
});
RuleForEach(source => source).SetValidator(new ShipmentValidator());
}
}
Here were sample extension methods:
public static class VaildationExtensionMethods
{
// check if site of value should be in the range of Constants.SITES ["JP", "AU", "TW", "US"]
public static List<Mode> ValidateValidSites(this List<Model> shipments, List<string> errors)
{
shipments.ForEach(s =>
{
var findSite = Constants.SITES.Any(site => site.ToLower() == s.Site.ToLower());
if(!findSite)
{
errors.Add($"{nameof(ValidateValidSites)} failed.<br> Shipment NO. {s.ShipmentNo}, Site: {s.Site} not belong to Sites: \"{string.Join(",", Constants.SITES)}\"");
}
});
return shipments;
}
// check if no more duplicated invoice NO for the same shipment NO.
public static List<Mode> ValidateUniqueInvoice(this List<Model> shipments, List<string> errors)
{
var query = shipments.GroupBy(
s => s.ShipmentNo,
s => s.InvoiceNo,
(shipmentNo, invoiceNo) => new
{
ShipmentNo = shipmentNo,
InvoiceCount = invoiceNo.Distinct().Count()
});
foreach (var result in query)
{
if (result.InvoiceCount > 1)
{
errors.Add($"{nameof(ValidateUniqueInvoice)} failed.");
}
}
}
}
We could easily apply the “ShipmentServiceValidator” into our application just like below:
// code of sinppets
// -------------------------------
var shipments = GetShipment(); // might get the shipments data via API or...
var validator = new ShipmentServiceValidator();
var validationResult = validator.Validate(shipments);
if(!validationResult.IsValid)
{
// validation not pass
// logic for handling failure cases here
// .....
}
As you could compare this aspect to see if the style was more maintainable and more clean to understand. Also the Fluentvalidation provides some extensions while help you writing your unit test:)