Search code examples
c#asp.net-core-webapifluentvalidation

How to set up validation for nested objects using FluentValidation


I'm using FluentValidation in my API, and I've created four validators. The first one is for order level validation, the second one is for order detail level validation, and the other two are for withdraw and transfer operations on order details.

I can call the OrderValidator from the application as shown below:

var validator = new OrderValidator();
foreach (var order in orders)
{
    var vResult = validator.Validate(order);
}

The code above works for both the OrderValidator and the OrderDetailValidator. However, it doesn't work for the WithdrawInfoValidator and TransferInfoValidator because I haven't declared the WithdrawInfoValidator yet.

The question is how to set up the WithdrawInfoValidator within the OrderDetailValidator.

This is my model

public class Order
{
   
    public string AdvisorCode { get; set; }
    public string AccountNo { get; set; }
    public string AccountType { get; set; }
    public List<OrderDetail> Details { get; set; }
}

public class OrderDetail
{
    public int GroupNo { get; set; }
    public int SubOrderNo { get; set; }
    public string OrderType { get; set; }
    public string FundSource { get; set; }
    public string ProductCode { get; set; }
    public string Currency { get; set; }
    public decimal Amount { get; set; }
    public decimal Units { get; set; }
    public string DivOption { get; set; }
    public int? SellAll { get; set; }
    public WithdrawInfo WithdrawInfo { get; set; }
}

public class WithdrawInfo
{
    public string WithdrawMode { get; set; }
    public string WDCurrency { get; set; }
    public decimal WDAmount { get; set; }
    public TransferInfo TransferInfo { get; set; }
}

public class TransferInfo
{
    public string BankName { get; set; }
    public string AccountNo { get; set; }
    public string Branch { get; set; }
    public string AccHolderName { get; set; }
    public string SwiftCode { get; set; }
    public string IntBankName { get; set; }
    public string IntSwiftCode { get; set; }
    public string OtherInfoOrAddress { get; set; }
}

public class OrderErrorReference
{
    public string OrderType { get; set; }
    public int GroupNo { get; set; }
    public int SubOrderNo { get; set; }
}

This is my validator

public class OrderValidator : AbstractValidator<Order>
{

    public OrderValidator()
    {
        RuleFor(m => m.AdvisorCode)
            .NotEmpty()
            .WithMessage(
                "AdvisorCode field cannot be empty."
                );

       
        RuleForEach(m => m.Details)
            .SetValidator(new OrderDetailValidator())
            .When(m => m.Details != null);

    }
}

public class OrderDetailValidator : AbstractValidator<OrderDetail>
{
    public OrderDetailValidator()
    {
        RuleFor(m => m.GroupNo)
            .Cascade(CascadeMode.Stop)
            .NotEmpty().WithMessage(
                "GroupNo field cannot be empty."
            ).WithState(m => new OrderErrorReference
            {
                GroupNo = m.GroupNo,
                SubOrderNo = m.SubOrderNo,
                OrderType = m.OrderType
            })
            .Must(m => m > 0)
            .WithMessage(
                "GroupNo must be greater than zero."
                ).WithState(m => new OrderErrorReference
                {
                    GroupNo = m.GroupNo,
                    SubOrderNo = m.SubOrderNo,
                    OrderType = m.OrderType
                });          

    }
}

public class WithdrawInfoValidator : AbstractValidator<WithdrawInfo>
{
    public WithdrawInfoValidator(OrderDetail orderDetail)
    {
        RuleFor(m => m.WithdrawMode)
            .Cascade(CascadeMode.Stop)
            .Must(mode => mode == "TT" || mode == "QC")
            .When(m => m.WithdrawMode != null)
            .WithMessage(
                "Withdraw Mode must be either \"NA\"=No Withdrawal, \"TT\"=Telegraphic Transfer or \"QC\"=Quick Check Deposit."
                ).WithState(m => new OrderErrorReference
                {
                    GroupNo = orderDetail.GroupNo,
                    SubOrderNo = orderDetail.SubOrderNo,
                    OrderType = orderDetail.OrderType
                })
            .NotEmpty()
            .WithMessage(
                "The WithdrawMode field cannot be empty."
                ).WithState(m => new OrderErrorReference
                {
                    GroupNo = orderDetail.GroupNo,
                    SubOrderNo = orderDetail.SubOrderNo,
                    OrderType = orderDetail.OrderType
                });

        

        RuleFor(m => m.TransferInfo)
            .Must(info => info != null)
            .When(m => m.WithdrawMode == "TT" || m.WithdrawMode == "QC")
            .WithMessage(
                "TransferInfo cannot be empty if WithdrawMode is \"TT\" or \"QC\""
                ).WithState(m => new OrderErrorReference
                {
                    GroupNo = orderDetail.GroupNo,
                    SubOrderNo = orderDetail.SubOrderNo,
                    OrderType = orderDetail.OrderType
                })
            .SetValidator(m => new TransferInfoValidator(orderDetail))
            .When(m => m.WithdrawMode == "TT" || m.WithdrawMode == "QC");
    }
}

public class TransferInfoValidator : AbstractValidator<TransferInfo>
{
    public TransferInfoValidator(OrderDetail orderDetail)
    {

        RuleFor(m => m.OtherInfoOrAddress)
              .NotEmpty()
              .When(m => orderDetail.WithdrawInfo.WithdrawMode == "TT")
              .WithMessage(
                  "OtherInfoOrAddress field cannot be empty if WithdrawMode is TT."
                  ).WithState(m => new OrderErrorReference
                  {
                      GroupNo = orderDetail.GroupNo,
                      SubOrderNo = orderDetail.SubOrderNo,
                      OrderType = orderDetail.OrderType
                  });
    }
}

I already tried many ways but it didn't work.


Solution

  • You should declare the validation rule for the WithdrawInfo property by providing the WithdrawInfoValidator instance with the orderDetail object.

    public class OrderDetailValidator : AbstractValidator<OrderDetail>
    {
        public OrderDetailValidator()
        {
            ...        
                    
            RuleFor(m => m.WithdrawInfo)
                .SetValidator(m => new WithdrawInfoValidator(m));
    
        }
    }