Show / Hide Table of Contents

Importing rules from another builder

Validator builders are used to specify which rules should be applied to an object and its directly-accessible members/values. They can be used as a self-contained piece of validation configuration. Validator builders may also consume the rules and configuration from another builder; there are a few scenarios where this technique is useful.

  • De-duplicating validation configuration for base types
  • Creating alternative validation scenarios
  • Validating referenced objects
  • Polymorphic validation

Using a validator builder, importing another builder is accomplished via the AddRules<TBuilder>() method from either an instance of IConfiguresValidator<TValidated> or (for validating referenced objects) from an instance of IConfiguresValueAccessor<TValidated, TValue>.

Example: De-duplicating base type validation

This technique is useful when you want to validate members of a class hierarchy. Consider this very simple example of an object model for validation:

public abstract class Pet
{
    public string Name { get; set; }
}

public class PetCat : Pet
{
    public string FurColour { get; set; }
}

public class PetFish : Pet
{
    public int MinimumTankSizeCubicCm { get; set; }
}

Now, we wish to validate instances of PetCat and PetFish but we don't want to duplicate any validation rules for Pet. Here is how this could be laid out using some validator builders.

public class PetValidatorBuilder : IBuildsValidator<Pet>
{
    public void ConfigureValidator(IConfiguresValidator<Pet> config)
    {
        config.ForMember(x => x.Name, m =>
        {
            m.AddRule<NotNullOrEmpty>();
        });
    }
}

public class PetCatValidatorBuilder : IBuildsValidator<PetCat>
{
    public void ConfigureValidator(IConfiguresValidator<PetCat> config)
    {
        config.AddRules<PetValidatorBuilder>();

        config.ForMember(x => x.FurColour, m =>
        {
            m.AddRule<NotNullOrEmpty>();
        });
    }
}

Notice how the the PetCatValidatorBuilder imports the rules from the PetValidatorBuilder. This has the same effect as consuming any and all configuration in the PetValidatorBuilder as if it were a part of PetCatValidatorBuilder.

Example: Creating alternate validation scenarios

Imagine that a single object type needs to be validated in more than one conceptual manner. Imagine you run a library that loans books and members are allowed to take a loan to a maximum initial duration of 12 weeks but they are also allowed to extend a loan but only for 4 additional weeks at a time. A request for a loan might look like this:

public class BookLoanRequest
{
    public long BookId { get; set; }
    public long MemberId { get; set; }
    public int LoanDurationWeeks { get; set; }
}

Now we want to validate this model, but under two different scenarios: For an initial loan and for a loan extension. We do not want to duplicate common rules across those two validator builders though.

public class BookLoanRequestCommonBuilder : IBuildsValidator<BookLoanRequest>
{
    public void ConfigureValidator(IConfiguresValidator<BookLoanRequest> config)
    {
        // This example uses two fictitious rules,
        // their implementation is unimportant.
        config.ForMember(x => x.BookId, m =>
        {
            m.AddRule<MustExistInDatabase>();
        });

        config.ForMember(x => x.MemberId, m =>
        {
            m.AddRule<MustBeActiveMember>();
        });
    }
}

public class InitialBookLoanRequestBuilder : IBuildsValidator<BookLoanRequest>
{
    public void ConfigureValidator(IConfiguresValidator<BookLoanRequest> config)
    {
        config.AddRules<BookLoanRequestCommonBuilder>();

        config.ForMember(x => x.LoanDurationWeeks, m =>
        {
            m.AddRule<IntegerInRange>(c =>
            {
                c.ConfigureRule(r =>
                {
                    r.Min = 1;
                    r.Max = 12;
                })
            });
        });
    }
}

public class ExtensionBookLoanRequestBuilder : IBuildsValidator<BookLoanRequest>
{
    public void ConfigureValidator(IConfiguresValidator<BookLoanRequest> config)
    {
        config.AddRules<BookLoanRequestCommonBuilder>();

        config.ForMember(x => x.LoanDurationWeeks, m =>
        {
            m.AddRule<IntegerInRange>(c =>
            {
                c.ConfigureRule(r =>
                {
                    r.Min = 1;
                    r.Max = 4;
                })
            });
        });
    }
}

This produces a validator for each of the Initial & Extension loans, both of which consume the rules configured within the common validator builder.

Example: Validating referenced objects

When validating object graphs, each nontrivial object-type included in that graph should have its own validator builder. Then, when validating across references, import the rules from the appropriate builder within a ForMember or ForValue or ForMemberItems or ForValues declaration.

Consider these models

public class Vehicle
{
    public DateTime? ManufacturedDate { get; set; }
    public ICollection<Wheel> Wheels { get; set; }
}

public class Wheel
{
    public decimal? DiameterCm { get; set; }
}

The validator builders for these might look something like the following:

public class VehicleBuilder : IBuildsValidator<Vehicle>
{
    public void ConfigureValidator(IConfiguresValidator<Vehicle> config)
    {
        config.ForMember(x => x.ManufacturedDate, m =>
        {
            m.AddRule<NotNull>();
        });

        config.ForMemberItems(x => x.Wheels, m =>
        {
            m.AddRules<WheelBuilder>();
        });
    }
}

public class WheelBuilder : IBuildsValidator<Wheel>
{
    public void ConfigureValidator(IConfiguresValidator<Wheel> config)
    {
        config.ForMember(x => x.DiameterCm, m =>
        {
            m.AddRule<NotNull>();
        });
    }
}

By creating a validator from the VehicleBuilder, every one of its wheels will be validated using the rules within WheelBuilder. Such parent/child consumption of validator builders can continue through unlimited levels of relationships. Incidentally this also demonstrates the validation of collection items, via the usage of ForMemberItems.

The important thing to avoid is circular consumption, which will cause an error. In the example above, the WheelBuilder must not import the rules from VehicleBuilder.

  • Improve this Doc
In This Article
Back to top Generated by DocFX