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

二次信任 提交于 2019-12-03 10:10:27

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);
            }
        }
    }
}

With C# 6.0, this is greatly simplified. Now you can just do this (a little bit of a hack, but a lot better than forking Fluent Validation):

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

Pity they didn't offer a WithMessage overload that takes a lambda accepting the object, and you could just do:

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

I think it's silly they tried to duplicate string.Format themselves in the goal to achieve shorter syntax, but ultimately made it less flexible so that we can't use the new C# 6.0 syntax cleanly.

Ilya Ivanov

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);
    }
}

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.

Chuck Kasabula

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}.");
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!