Fluent validation: set custom message on custom validation

Alberto De Caro picture Alberto De Caro · Aug 5, 2014 · Viewed 19.9k times · Source

Scenario

I have a custom rule to validate the shipping cost of an order:

public class OrderValidator : BaseValidator<Order>
{

    private string CustomInfo { get; set; }

    public OrderValidator()
    {
        //here I call the custom validation method and I try to add the CustomInfo string in the message
        RuleFor(order => order.ShippingCost).Cascade(CascadeMode.StopOnFirstFailure).NotNull().Must(
            (order, shippingCost) => CheckOrderShippingCost(order, shippingCost)
        ).WithMessage("{PropertyName} not set or not correct: {PropertyValue}." + (String.IsNullOrEmpty(CustomInfo) ? "" : " " + CustomInfo));
    }

    //this is the custom validation method
    private bool CheckOrderShippingCost(Order o, decimal shippingCost)
    {
        bool res = false;

        try
        {
            /*
             * check the actual shippingCost and set the res value
             */
        }
        catch (Exception ex)
        {
            CustomInfo = ex.ToString();
            res = false;
        }

        return res;
    }
}

In case of exception, I store the exception info into the CustomInfo private member and I add it to the validation message.

Then I run the validator:

OrderValidator oVal = new OrderValidator();
oVal.Results = oVal.Validate(order);
if (!oVal.Results.IsValid)
    oVal.Results.Errors.ForEach(delegate(ValidationFailure error) {
        Console.WriteLine(error.ErrorMessage);
    });

Issue

Everything works right, in case of exception the CustomInfo is properly set to the ex.ToString() value. But eventually the error message displayed in the console does NOT show the CustomInfo, but only the first part of the message:

      "Shipping Cost not set or not correct: 5.9"

Question

Why the custom message does not contains the CustomInfo string? Is it possible to add the exception info the the custom message in another way?

Answer

Icepickle picture Icepickle · Aug 6, 2014

According to this https://fluentvalidation.codeplex.com/wikipage?title=Customising&referringTitle=Documentation&ANCHOR#CustomError

you should rather use

.WithMessage("{PropertyName} not set or not correct: {PropertyValue}. {0}", order => order.CustomInfo);

which would require that your CustomInfo on the level of the Order class, rather than your validator class

EDIT

You could use:

public static class OrderExtensions
{
    private static IDictionary<Order,string> customErrorMessages;

    public static void SetError(this Order order, string message) {
        if (customErrorMessages == null) {
            customErrorMessages = new Dictionary<Order,string>();
        }
        if (customErrorMessages.ContainsKey(order)) {
            customErrorMessages[order] = message;
            return;
        }
        customErrorMessages.Add(order, message);
    }

    public static string GetError(this Order order) {
        if (customErrorMessages == null || !customErrorMessages.ContainsKey(order)) {
            return string.Empty;
        }
        return customErrorMessages[order];
    }
}

with some small changes to your validator

public class OrderValidator : BaseValidator<Order>
{
    public OrderValidator()
    {
        //here I call the custom validation method and I try to add the CustomInfo string in the message
        RuleFor(order =>     order.ShippingCost).Cascade(CascadeMode.StopOnFirstFailure).NotNull().Must(
            (order, shippingCost) => CheckOrderShippingCost(order, shippingCost)
        ).WithMessage("{PropertyName} not set or not correct: {PropertyValue}. {0}", order => order.GetError()));
    }

    //this is the custom validation method
    private bool CheckOrderShippingCost(Order o, decimal shippingCost)
    {
        bool res = false;

        try
        {
            /*
             * check the actual shippingCost and set the res value
             */
        }
        catch (Exception ex)
        {
            order.SetError(ex.ToString());
            res = false;
        }
        return res;
    }
}