Validation of Complex Data Structures in FluentValidation

In the world of programming, especially when dealing with applications that handle complex data, we often encounter the challenge of verifying the correctness of these data. FluentValidation, with its flexibility, is a powerful tool that allows us to elegantly manage the validation process, not just for simple data types, but also for complex structures.

Complex data structures in the context of FluentValidation refer to those parts of the data model that are objects themselves. This is not uncommon—models often consist of other models, creating a layer of complexity that can be difficult to manage without the right tools. For example, an Order model might contain a list of Product objects, and each Product may have its own specific validation rules.

Consider a situation where we need to verify an order that contains a list of products. Each product has a price and quantity, and the order contains information about the customer and the order date.

public class Order
{
    public Customer Customer { get; set; }
    public DateTime OrderDate { get; set; }
    public List<Product> Products { get; set; }
}

public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int Quantity { get; set; }
}

We want to ensure that each order contains a date and a list of products, and every product has a name, a price greater than zero, and a quantity.

We create a validator for Order and Product, using FluentValidation to ensure data compliance with our expectations.

public class OrderValidator : AbstractValidator<Order>
{
    public OrderValidator()
    {
        RuleFor(order => order.OrderDate).NotEmpty();
        RuleForEach(order => order.Products).SetValidator(new ProductValidator());
    }
}

public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(product => product.Name).NotEmpty();
        RuleFor(product => product.Price).GreaterThan(0);
        RuleFor(product => product.Quantity).GreaterThan(0);
    }
}

In OrderValidator, we check that the order date is not empty and set a validator for each product in the order. ProductValidator ensures each product meets specified criteria—having a name, a price greater than zero, and an adequate quantity. By using the SetValidator method in OrderValidator, we introduce complex validation rules for collection elements, allowing the application of specific validators to individual order components. More information on collection validation can be found in the text that follows.

By using FluentValidation to check the correctness of complex data structures, we are able to efficiently manage even the most intricate structures while maintaining clarity and ease of code maintenance.

Collection Validation

Collection validation in FluentValidation is a key component in ensuring data correctness in applications that operate on complex data structures. FluentValidation provides mechanisms that allow for efficient and flexible validation of collection elements.

Suppose we have a collection of objects that also require validation. A direct approach could involve iterating through the collection and validating each element individually. However, FluentValidation offers a sophisticated and efficient solution through the RuleForEach() method. This method allows for the application of specific validation rules to each element in a collection, making the process not only more efficient but also more organized and easier to maintain. This is particularly useful when we want to ensure all collection elements meet specific criteria. Here's an example of applying RuleForEach():

public class Customer
{
    public List<Order> Orders { get; set; } = new List<Order>();
}

public class Order
{
    public decimal TotalAmount { get; set; }
}


public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleForEach(customer => customer.Orders).Must(order => order.TotalAmount >= 100)
            .WithMessage("Each order should have a value of at least 100.");
    }
}

In this example, each order in the customer's Orders collection is individually validated to ensure its TotalAmount is at least 100.

ChildRules is another advanced feature of FluentValidation that allows for defining validation rules for nested objects. Instead of creating separate validators for each nested type and manually managing their calls, you can take advantage of the integrated approach that FluentValidation offers through ChildRules. This approach not only simplifies the validation process but also ensures that all validation rules for a given model are contained in one place, significantly easing code management and maintenance.

Suppose we have a Customer class containing a collection of Addresses, and each address has its own validation rules.

public class Customer
{
    public List<Address> Addresses { get; set; } = new List<Address>();
}

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }


    public string PostalCode { get; set; }
}

public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleForEach(customer => customer.Addresses).ChildRules(address =>
        {
            address.RuleFor(a => a.Street).NotEmpty().WithMessage("The street cannot be empty.");
            address.RuleFor(a => a.City).NotEmpty().WithMessage("The city cannot be empty.");
            address.RuleFor(a => a.PostalCode).Matches(@"^\d{2}-\d{3}$")
                .WithMessage("The postal code must be in the format 00-000.");
        });
    }
}

In this example, each address in the Addresses collection is validated using ChildRules. This ensures each address must have a specified street, city, and postal code that matches a specific pattern. Using ChildRules makes the validation more modular and easier to maintain, especially when data structures are complex and contain many levels of nesting.

In summary, in our series dedicated to FluentValidation, we discussed two key mechanisms for efficient collection validation: RuleForEach() and ChildRules. Both tools are extremely useful in ensuring data integrity in applications managing complex data structures.

RuleForEach() is perfect for applying uniform validation rules to each element in a collection. As shown in the example with CustomerValidator, this method allows for easy and effective validation of all elements in a collection, ensuring they meet specified requirements, such as a minimum order value.

On the other hand, ChildRules allows for advanced validation of nested objects within a collection. The example with CustomerValidator illustrates how each address in the Addresses collection can be validated by applying individual rules to different address attributes. This approach not only enhances the precision and effectiveness of validation but also makes the code more organized and easier to maintain.

In conclusion, RuleForEach() and ChildRules in FluentValidation offer developers powerful tools for creating clean, modular, and efficient validations for applications managing complex data. Their application significantly improves code quality and ensures a solid defense against invalid data, which is crucial for the stability and reliability of any application.

See you in the next posts!