4

Validation framework:

using FluentValidation;
        
namespace ASValidation.Validators.Common
{
    public static class CommonValidators
    {
        // Validates address: max 200 chars, allowed chars only
        public static IRuleBuilderOptions<T, string> ValidateAddress<T>(this IRuleBuilder<T, string> rule)
        {
            return rule
                       .MaximumLength(200).WithMessage("Address cannot exceed 200 characters.")
                       .Matches(@"^[a-zA-Z0-9\s\-,.#]+$").WithMessage("Address contains invalid characters.");
        }
        
        // Validates captcha: required and must be true
        public static IRuleBuilderOptions<T, bool> ValidateCaptcha<T>(this IRuleBuilder<T, bool> rule)
        {
            return rule
                      .NotEmpty().WithMessage("Captcha is required.")
                      .Equal(true).WithMessage("Captcha must be verified.");
        }
              
        // Validates ID: required (not empty)
        public static IRuleBuilderOptions<T, TId> ValidateId<T, TId>(this IRuleBuilder<T, TId> rule)
        {
            return rule
                        .NotEmpty()
                        .WithMessage("{PropertyName} is required.");
        }
        
        // Validates primary key ID: must not be default unless 0
        public static IRuleBuilderOptions<T, int> ValidatePrimaryKeyID<T>(this IRuleBuilder<T, int> rule)
        {
            return rule
                        .Must(id => id == 0 || id != default)
                        .WithMessage("{PropertyName} is required.");
        }
        
        // Validates custom ID: 5-20 chars, alphanumeric, no spaces, case insensitive
        public static IRuleBuilderOptions<T, string> ValidateCustomId<T>(this IRuleBuilder<T, string> rule)
        {
            return rule
                        .NotEmpty().WithMessage("ID is required.")
                        .MinimumLength(5).WithMessage("ID must be at least 5 characters long.")
                        .MaximumLength(20).WithMessage("ID must not exceed 20 characters.")
                        .Matches(@"^[a-zA-Z0-9]+$").WithMessage("ID must contain only alphanumeric characters and no spaces.")
                        .WithMessage("ID must be case insensitive.");
        }
        
        // Validates property is required (not empty)
        public static IRuleBuilderOptions<T, TProperty> IsRequired<T, TProperty>(this IRuleBuilder<T, TProperty> rule)
        {
            return rule
                        .NotEmpty().WithMessage("{PropertyName} is required.");
        }
        
        // Validates page: must be greater than 0
        public static IRuleBuilderOptions<T, int> ValidatePage<T>(this IRuleBuilder<T, int> rule)
        {
            return rule
                        .GreaterThan(0)
                        .WithMessage("Page must be greater than 0.");
        }
    }
}

Consuming validation framework rules in my API project:

using ASValidation.Validators.Common;
using ASValidation.Validators.Entity;
using FluentValidation;
using Microsoft.AspNetCore.Mvc.ViewFeatures;

namespace agentservice.Validators
{
    public class SubmitAgencyDetailsRequestDTOValidator : AbstractValidator<SubmitAgencyDetailsRequestDTO>
    {
        public SubmitAgencyDetailsRequestDTOValidator()
        {
            RuleFor(x => x.AsAgencyId).ValidateId();

            RuleFor(x => x.RegisteredEmailId).NotEmpty().ValidateEmail();

            RuleFor(x => x.WebsiteURL).ValidateAgentWebsiteUrl().When(x => !string.IsNullOrWhiteSpace(x.WebsiteURL));

            RuleFor(x => x.AccessMethods).NotEmpty().IsRequired();

            RuleFor(x => x.TypeOfBusinessID).ValidateId();
        }
    }
}

To create a validation rule that only validates when a string property has a value (not null/empty). Currently, I'm using .When(x => !string.IsNullOrEmpty(x.Property)), but this is verbose and repetitive.

Current implementation:

RuleFor(x => x.WebsiteURL)
    .ValidateAgentWebsiteUrl()
    .When(x => !string.IsNullOrEmpty(x.WebsiteURL)); // Want to simplify this

Expected implementation

RuleFor(x => x.WebsiteURL)
    .ValidateAgentWebsiteUrl()
    .ValidateWhenNotEmpty(); // Custom extension method

Question

How can I create a ValidateWhenNotEmpty() extension method that:

  • Skips validation for null/empty strings
  • Works with any validator chain (e.g., EmailAddress(), Matches())
  • Doesn't trigger "required" errors for empty fields
  • A generic version that works for all data types
3
  • I'd make that the default behavior. Implement all your custom validations so it supports null/empty without erroring out. Then you can specifically opt-in to null/empty-checking by chaining NotEmpty() before them. Commented Jun 12 at 6:27
  • ^^ That said, if the custom validation must include null/empty checking, I'd a) document that in XML-Docs and b) call those something like ValidRequiredXYZ Commented Jun 12 at 6:31
  • If you still want that ValidateWhenNotEmpty(): What would you expect this to do: RuleFor(x => x.SomeProp).ValidateWhenNotEmpty().IsRequired(); ? Commented Jun 12 at 6:38

1 Answer 1

1

You can define just another extension method for that usecase:

public static class CustomValidationExtensions
{
    public static IRuleBuilderOptions<T, string> ValidateWhenNotEmpty<T>(
        this IRuleBuilderOptions<T, string> rule)
    {
        return rule.When((instance, context) =>
        {
            var value = instance
                ?.GetType()
                ?.GetProperty(context.PropertyPath)
                ?.GetValue(instance)
                ?.ToString();
            return !string.IsNullOrEmpty(value);
        });
    }
}

THen below two validators are equivalent:

public class PersonValidatorWithCustomMethod : AbstractValidator<Person>
{
    public PersonValidatorWithCustomMethod()
    {
        RuleFor(x => x.Name).Length(2, 10)
            .ValidateWhenNotEmpty();
    }
}

public class PersonValidator : AbstractValidator<Person>
{
    public PersonValidator()
    {
        RuleFor(x => x.Name).Length(2, 10)
            .When(x => !string.IsNullOrEmpty(x.Name));
    }
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.