Using FluentValidation's WithMessage method with a list of named parameters
Asked Answered
H

5

30

I am using FluentValidation and I want to format a message with some of the object's properties value. The problem is I have very little experience with expressions and delegates in C#.

FluentValidation already provides a way to do this with format arguments.

RuleFor(x => x.Name).NotEmpty()
    .WithMessage("The name {1} is not valid for Id {0}", x => x.Id, x => x.Name);

I would like to do something like this to avoid having to change the message string if I change the order of the parameters.

RuleFor(x => x.Name).NotEmpty()
    .WithMessage("The name {Name} is not valid for Id {Id}", 
    x => new
        {
            Id = x.Id,
            Name = x.Name
        });

The original method signature looks like this:

public static IRuleBuilderOptions<T, TProperty> WithMessage<T, TProperty>(
    this IRuleBuilderOptions<T, TProperty> rule, string errorMessage, 
    params Func<T, object>[] funcs)

I was thinking of providing this method with a list of Func.

Anyone can help me with this?

Hudgens answered 5/1, 2013 at 0:40 Comment(2)
It seems like the issue has more to do with string formatting than anything else. This may help you out: #159517Savoy
I think it is a little different since the expression provided to FluentValidation is not executed immediately. I think it's why the existing method wants a delegate.Hudgens
D
13

You can't do that with the WithMessage in FluentValidation but you can high-jack the CustomState property and inject your message there. Here is a working example; Your other option is to fork FluentValidation and make an additional overload for the WithMethod.

This is a console application with references to FluentValidation from Nuget and the JamesFormater from this blog post:

http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx

The Best answer. Took inspiration from Ilya and realized you can just piggyback off the extension method nature of fluent validation. So The below works with no need to modify anything in the library.

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using FluentValidation;

namespace stackoverflow.fv
{
    class Program
    {
        static void Main(string[] args)
        {
            var target = new My() { Id = "1", Name = "" };
            var validator = new MyValidator();
            var result = validator.Validate(target);

            foreach (var error in result.Errors)
                Console.WriteLine(error.ErrorMessage);

            Console.ReadLine();
        }
    }

    public class MyValidator : AbstractValidator<My>
    {
        public MyValidator()
        {
            RuleFor(x => x.Name).NotEmpty().WithNamedMessage("The name {Name} is not valid for Id {Id}");
        }
    }

    public static class NamedMessageExtensions
    {
        public static IRuleBuilderOptions<T, TProperty> WithNamedMessage<T, TProperty>(
            this IRuleBuilderOptions<T, TProperty> rule, string format)
        {
            return rule.WithMessage("{0}", x => format.JamesFormat(x));
        }
    }

    public class My
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }

    public static class JamesFormatter
    {
        public static string JamesFormat(this string format, object source)
        {
            return FormatWith(format, null, source);
        }

        public static string FormatWith(this string format
            , IFormatProvider provider, object source)
        {
            if (format == null)
                throw new ArgumentNullException("format");

            List<object> values = new List<object>();
            string rewrittenFormat = Regex.Replace(format,
              @"(?<start>\{)+(?<property>[\w\.\[\]]+)(?<format>:[^}]+)?(?<end>\})+",
              delegate(Match m)
              {
                  Group startGroup = m.Groups["start"];
                  Group propertyGroup = m.Groups["property"];
                  Group formatGroup = m.Groups["format"];
                  Group endGroup = m.Groups["end"];

                  values.Add((propertyGroup.Value == "0")
                    ? source
                    : Eval(source, propertyGroup.Value));

                  int openings = startGroup.Captures.Count;
                  int closings = endGroup.Captures.Count;

                  return openings > closings || openings % 2 == 0
                     ? m.Value
                     : new string('{', openings) + (values.Count - 1)
                       + formatGroup.Value
                       + new string('}', closings);
              },
              RegexOptions.Compiled
              | RegexOptions.CultureInvariant
              | RegexOptions.IgnoreCase);

            return string.Format(provider, rewrittenFormat, values.ToArray());
        }

        private static object Eval(object source, string expression)
        {
            try
            {
                return DataBinder.Eval(source, expression);
            }
            catch (HttpException e)
            {
                throw new FormatException(null, e);
            }
        }
    }
}
Dysphagia answered 8/1, 2013 at 16:3 Comment(4)
Thank you for your example. I would must prefer fork the code or add an extension method to be able to use the method the way I want, but I can't figure how to handle the new Expression (x => new { Name = ...}). Any tips or ideas?Hudgens
I would use the named formatter that I have above in the example. To be honest though, I like Ilya's code below. Maybe you can inherit from AbstractValidator and create a WitNamedMessage(T target) method. That way you can tweak it and you don't have to wait for Jeremy Skinner to do a new push.Dysphagia
This seems very promissing. Do you think I would be able to retrieve child objects and properties with different name? For example, what if I wanted to replace a {Test} placeholder with a something like objectUnderValidator.ChildArray[0].Name?Hudgens
I'm not sure you could do that. I know in the haacked post, some of the named formatters let you do something like {Order.Name} as a format parameter, but you'd have to look at it. You might also be able to parse it out and compile it into an action and get the behavior you want. But I wouldn't get too crazy with this cause it just might be a headache to maintain.Dysphagia
C
39

If you are using C# 6.0 or later, here's an improved syntax.

With version 8.0.100 or later of Fluent Validation, there is a WithMessage overload that takes a lambda accepting the object, and you can just do:

RuleFor(x => x.Name)
   .NotEmpty()
   .WithMessage(x => $"The name {x.Name} is not valid for Id {x.Id}.");

However, with earlier versions of Fluent Validation this somewhat hacky way is still pretty clean, and a lot better than forking its older versions:

RuleFor(x => x.Name)
   .NotEmpty()
   .WithMessage("{0}", x => $"The name {x.Name} is not valid for Id {x.Id}.");
Conveyor answered 20/3, 2016 at 23:24 Comment(2)
Agreed, i was trying to get it to work just like you showed (how i would have expected it to with with 6.0). +1Woolgrower
Note that this suggestion is currently supported (as of v8.0.100)Hus
D
13

You can't do that with the WithMessage in FluentValidation but you can high-jack the CustomState property and inject your message there. Here is a working example; Your other option is to fork FluentValidation and make an additional overload for the WithMethod.

This is a console application with references to FluentValidation from Nuget and the JamesFormater from this blog post:

http://haacked.com/archive/2009/01/04/fun-with-named-formats-string-parsing-and-edge-cases.aspx

The Best answer. Took inspiration from Ilya and realized you can just piggyback off the extension method nature of fluent validation. So The below works with no need to modify anything in the library.

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.UI;
using FluentValidation;

namespace stackoverflow.fv
{
    class Program
    {
        static void Main(string[] args)
        {
            var target = new My() { Id = "1", Name = "" };
            var validator = new MyValidator();
            var result = validator.Validate(target);

            foreach (var error in result.Errors)
                Console.WriteLine(error.ErrorMessage);

            Console.ReadLine();
        }
    }

    public class MyValidator : AbstractValidator<My>
    {
        public MyValidator()
        {
            RuleFor(x => x.Name).NotEmpty().WithNamedMessage("The name {Name} is not valid for Id {Id}");
        }
    }

    public static class NamedMessageExtensions
    {
        public static IRuleBuilderOptions<T, TProperty> WithNamedMessage<T, TProperty>(
            this IRuleBuilderOptions<T, TProperty> rule, string format)
        {
            return rule.WithMessage("{0}", x => format.JamesFormat(x));
        }
    }

    public class My
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }

    public static class JamesFormatter
    {
        public static string JamesFormat(this string format, object source)
        {
            return FormatWith(format, null, source);
        }

        public static string FormatWith(this string format
            , IFormatProvider provider, object source)
        {
            if (format == null)
                throw new ArgumentNullException("format");

            List<object> values = new List<object>();
            string rewrittenFormat = Regex.Replace(format,
              @"(?<start>\{)+(?<property>[\w\.\[\]]+)(?<format>:[^}]+)?(?<end>\})+",
              delegate(Match m)
              {
                  Group startGroup = m.Groups["start"];
                  Group propertyGroup = m.Groups["property"];
                  Group formatGroup = m.Groups["format"];
                  Group endGroup = m.Groups["end"];

                  values.Add((propertyGroup.Value == "0")
                    ? source
                    : Eval(source, propertyGroup.Value));

                  int openings = startGroup.Captures.Count;
                  int closings = endGroup.Captures.Count;

                  return openings > closings || openings % 2 == 0
                     ? m.Value
                     : new string('{', openings) + (values.Count - 1)
                       + formatGroup.Value
                       + new string('}', closings);
              },
              RegexOptions.Compiled
              | RegexOptions.CultureInvariant
              | RegexOptions.IgnoreCase);

            return string.Format(provider, rewrittenFormat, values.ToArray());
        }

        private static object Eval(object source, string expression)
        {
            try
            {
                return DataBinder.Eval(source, expression);
            }
            catch (HttpException e)
            {
                throw new FormatException(null, e);
            }
        }
    }
}
Dysphagia answered 8/1, 2013 at 16:3 Comment(4)
Thank you for your example. I would must prefer fork the code or add an extension method to be able to use the method the way I want, but I can't figure how to handle the new Expression (x => new { Name = ...}). Any tips or ideas?Hudgens
I would use the named formatter that I have above in the example. To be honest though, I like Ilya's code below. Maybe you can inherit from AbstractValidator and create a WitNamedMessage(T target) method. That way you can tweak it and you don't have to wait for Jeremy Skinner to do a new push.Dysphagia
This seems very promissing. Do you think I would be able to retrieve child objects and properties with different name? For example, what if I wanted to replace a {Test} placeholder with a something like objectUnderValidator.ChildArray[0].Name?Hudgens
I'm not sure you could do that. I know in the haacked post, some of the named formatters let you do something like {Order.Name} as a format parameter, but you'd have to look at it. You might also be able to parse it out and compile it into an action and get the behavior you want. But I wouldn't get too crazy with this cause it just might be a headache to maintain.Dysphagia
T
11

While KhalidAbuhakmeh's answer is very good and deep, I just want to share a simple solution to this problem. If you afraid of positional arguments, why not to encapsulate error creation mechanism with concatenation operator + and to take advantage of WithMessage overload, that takes Func<T, object>. This CustomerValudator

public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleFor(customer => customer.Name).NotEmpty().WithMessage("{0}", CreateErrorMessage);
    }

    private string CreateErrorMessage(Customer c)
    {
        return "The name " + c.Name + " is not valid for Id " + c.Id;
    }
}

Prints correct original error message in next code snippet:

var customer = new Customer() {Id = 1, Name = ""};
var result = new CustomerValidator().Validate(customer);

Console.WriteLine(result.Errors.First().ErrorMessage);

Alternatively, use an inline lambda:

public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleFor(customer => customer.Name)
            .NotEmpty()
            .WithMessage("{0}", c => "The name " + c.Name + " is not valid for Id " + c.Id);
    }
}
Timeworn answered 8/1, 2013 at 21:39 Comment(0)
H
9

For anyone looking into this now - current FluentValidation (v8.0.100) allows you use a lamda in WithMessage (As ErikE suggested above) so you can use:

RuleFor(x => x.Name).NotEmpty()
   .WithMessage(x => $"The name {x.Name} is not valid for Id {x.Id}.");

Hope this helps someone.

Hus answered 14/11, 2018 at 11:24 Comment(0)
E
1

Extension methods based on ErikE's answer.

public static class RuleBuilderOptionsExtensions
{
    public static IRuleBuilderOptions<T, TProperty> WithMessage<T, TProperty>(this IRuleBuilderOptions<T, TProperty> rule, Func<T, object> func)
        => DefaultValidatorOptions.WithMessage(rule, "{0}", func);
    public static IRuleBuilderOptions<T, TProperty> WithMessage<T, TProperty>(this IRuleBuilderOptions<T, TProperty> rule, Func<T, TProperty, object> func)
        => DefaultValidatorOptions.WithMessage(rule, "{0}", func);
}

Usage examples:

RuleFor(_ => _.Name).NotEmpty()
.WithMessage(_ => $"The name {_.Name} is not valid for Id {_.Id}.");

RuleFor(_ => _.Value).GreaterThan(0)
.WithMessage((_, p) => $"The value {p} is not valid for Id {_.Id}.");
Elwoodelwyn answered 14/6, 2016 at 3:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.