1

I have a validator that looks like this

public class ImageValidator : AbstractValidator<Image>
{
    public ImageValidator()
    {
        RuleFor(e => e.Name).NotEmpty().Length(1, 255).WithMessage("Name must be between 1 and 255 chars");

        RuleFor(e => e.Data).NotNull().WithMessage("Image must have data").When(e => e.Id == 0);

        RuleFor(e => e.Height).GreaterThan(0).WithMessage("Image must have a height");

        RuleFor(e => e.Width).GreaterThan(0).WithMessage("Image must have a width");
    }
}

Now my unit tests are failing because Height and Width are populated based on the value in Data.

Data holds a byte array which creates a bitmap image. If Data is not null (and Id equals 0, so its a new image), i can create a Bitmap image and obtain the Height and Width values. Upon updating, the Height and Width would already be populated and Data could be null, because it would be stored in the database already.

Is there anyway i can populate the values of Height and Width if the validation rule for Data is true within my validator?

Before i had a check like

if (myObject.Data == null) throw new ApplicationException(...

But i think this is a validation rule, and should be in the validator, or do i just need to split the rules up?

Gillardo
  • 9,518
  • 18
  • 73
  • 141
  • Here's a suggestion. I'm big on Fluent which means I make static methods all the time. In this case you want to know the Bool value return of the e.Data check. If RuleFor is a static method and it returns a RuleFor instance you could chain it like this. – JWP Feb 17 '15 at 18:48
  • @JohnPeters RuleFor is an instance method defined in the generic AbstractValidator class, which is part of the FluentValidation package. I'm not sure what your 'big on Fluent' means. – Maarten Feb 17 '15 at 20:29
  • I was trying to show a way for you. Given this code : RuleFor(e => e.Data).NotNull() You would want to replace the NotNullCheck with something similar to what is shown in code below using a CallBack. I just guessed at the time the RuleFor was a type that had extension methods but you corrected me on your design. So the question here is in your current code why does the NotNULL check work as a Fluent interface. You can simply add another extension method like "CheckIIfNull(p=>{ //callback here}); You can insert the height and width rules on same line as data line now. – JWP Feb 17 '15 at 21:33
  • Just updated answer to reflect what I was trying to show before. – JWP Feb 17 '15 at 21:43

2 Answers2

2

The current code looks like this, note that the .NotNull() fluent interface acts on the results of RuleFor.

  RuleFor(e => e.Data)
 .NotNull()
 .WithMessage("Image must have data")
 .When(e => e.Id == 0);

Assume you could do this:

var rf = RuleFor(e => e.Data)
.CheckNull(p=>{
   if(p){ 
       // this is the true branch when data is null don't set height/width}
       rf.WithMessage("Image must have data");
       .When(e=>e.id==0); 
   else{  
       //data is not null set the height and with. 
        rf(e => e.Height).GreaterThan(0).WithMessage("Image must have a height");
        rf(e => e.Width).GreaterThan(0).WithMessage("Image must have a width");
   });

In order for you to be able to do this the new CheckNull routine must be able to take the same type as the .NotNull() method does today.

JWP
  • 6,672
  • 3
  • 50
  • 74
2

Thanks to John Peters that pointed me in the right direction, but here is what i did in the end as suggested.

First i created a new validator that tests if the byte array is valid. This validator accepts an action which takes the object and a bitmap as input. This action is only called if the validator is valid.

public class IsImageValidator<T> : PropertyValidator
{
    private Action<T, Bitmap> _action;

    public IsImageValidator(Action<T, Bitmap> action) : base("Not valid image data")
    {
        _action = action;
    }

    protected override bool IsValid(PropertyValidatorContext context)
    {
        var isNull = context.PropertyValue == null;

        if (isNull)
        {
            return false;
        }

        try
        {
            // we need to determine the height and width of the image
            using (var ms = new System.IO.MemoryStream((byte[])context.PropertyValue))
            {
                using (var bitmap = new System.Drawing.Bitmap(ms))
                {
                    // valid image, so call action
                    _action((T)context.Instance, bitmap);                        
                }
            }

        }
        catch (Exception e)
        {
            return false;
        }

        return true;
    }
}

To call this validator easier, i created a simple extension, like so

public static class SimpleValidationExtensions
{
    public static IRuleBuilderOptions<T, TProperty> IsImage<T, TProperty>(this IRuleBuilder<T, TProperty> ruleBuilder, Action<T, Bitmap> action)
    {
        return ruleBuilder.SetValidator(new IsImageValidator<T>(action));
    }
}

Now i can call the validator method within my ImageValidator, so my ImageValidator now looks like this. As you can see i now can set the height and width properties.

public ImageValidator()
{
    RuleFor(e => e.Name).NotEmpty().Length(1, 255).WithMessage("Name must be between 1 and 255 chars");

    RuleFor(e => e.Data).IsImage((e, bitmap) =>
    {
        e.Height = bitmap.Height;
        e.Width = bitmap.Width;
    }).WithMessage("Image data is not valid").When(e => e.Id == 0);

    RuleFor(e => e.Height).GreaterThan(0).WithMessage("Image must have a height");

    RuleFor(e => e.Width).GreaterThan(0).WithMessage("Image must have a width");
}
Gillardo
  • 9,518
  • 18
  • 73
  • 141