Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make full property path more accessible #2132

Closed
rez-gparker opened this issue Aug 5, 2023 · 1 comment
Closed

Make full property path more accessible #2132

rez-gparker opened this issue Aug 5, 2023 · 1 comment

Comments

@rez-gparker
Copy link

rez-gparker commented Aug 5, 2023

Is your feature request related to a problem? Please describe.

We need to use the full property path for the display name globally. I know that you can create an extension method WithFullName() like this guy did #1906 (comment). Doing that, however, would require me to add WithFullName() after EVERY RULE in every validator which stinks.

For example:

RuleFor(x => x.FirstName).NotNull().WithFullName();
RuleFor(x => x.LastName).NotNull().WithFullName();
RuleFor(x => x.Whatever).NotNull().WithFullName();
RuleFor(x => x.Whatever2).NotNull().WithFullName();

ValidatorOptions.Global.DisplayNameResolver would have been the perfect place to fix my problem, but it does not provide the property path. It would be super easy to add it there, but that would be a breaking change.

Describe the solution you'd like

  • Add full property path to ValidatorOptions.Global.DisplayNameResolver
  • Add full property path as a placeholder to make it easily accessible in messages. I.e., {FullPropertyName}
  • Add an extension method to get full property path so we don't have to write our own. Ie, "WithFullName"
  • Add a property to the AbstractValidator that we can set so that all of it's rules use the full property path as the display name. Call it "UsePropertyPathAsDisplayName" maybe?

Describe alternatives you've considered

I came up with solution, but it is ugly. I'm hoping I can throw this monstrosity away and that there is a simple solution that I was unaware of. Note: I am a FluentValidation newbie. I am sure this is not what the ValidatorSelectors are for, but it was the only place I could find to hook in. To use it, you just call this:

ValidatorOptions.Global.UsePropertyPathAsDisplayName();

Here is my workaround:

namespace FluentValidation;

using System.Collections.Concurrent;
using FluentValidation.Internal;

internal static class ValidatorConfigurationExtensions
{
    public static void UsePropertyPathAsDisplayName(this ValidatorConfiguration config)
    {
        ArgumentNullException.ThrowIfNull(config);
        var x = ValidatorOptions.Global.ValidatorSelectors;
        if (x.DefaultValidatorSelectorFactory is var def) x.DefaultValidatorSelectorFactory = () => new ValidatorSelectorWrapper(def());
        if (x.RulesetValidatorSelectorFactory is var ruleset) x.RulesetValidatorSelectorFactory = x => new ValidatorSelectorWrapper(ruleset(x));
        if (x.MemberNameValidatorSelectorFactory is var memberName) x.MemberNameValidatorSelectorFactory = x => new ValidatorSelectorWrapper(memberName(x));
        if (x.CompositeValidatorSelectorFactory is var composite) x.CompositeValidatorSelectorFactory = x => new ValidatorSelectorWrapper(composite(x));
    }
}

file class ValidatorSelectorWrapper : IValidatorSelector
{
    private readonly IValidatorSelector selector;
    private static readonly ConcurrentDictionary<Type, bool> cache = new();
    private const string InterfaceName = "IValidationRule`2";

    public ValidatorSelectorWrapper(IValidatorSelector selector) => this.selector = selector;

    public bool CanExecute(IValidationRule rule, string propertyPath, IValidationContext context)
    {
        if (cache.GetOrAdd(rule.GetType(), type => type.GetInterface(InterfaceName) is not null))
        {
            SetDisplayName((dynamic)rule, propertyPath);
        }

        return selector.CanExecute(rule, propertyPath, context);
    }

    private static void SetDisplayName<T, TProperty>(IValidationRule<T, TProperty> rule, string name) =>
        rule.SetDisplayName(name);
}

Additional Context

No response

@rez-gparker rez-gparker changed the title Cannot access property path from ValidatorOptions.Global.DisplayNameResolver or from Placeholders. Make full property path more easily accessible Aug 5, 2023
@rez-gparker rez-gparker changed the title Make full property path more easily accessible Make full property path more accessible Aug 5, 2023
@JeremySkinner
Copy link
Member

JeremySkinner commented Aug 6, 2023

As I mentioned in the other issue that you linked to, the display name is intended to be user-readable, which is why it doesn't contain the full property path. The full property path is made available to you as a property on each ValidationFailure instance, for you to use as you want. However if you really want to do this, then the extension method approach you've used is the correct/valid approach to take.

ValidatorOptions.Global.DisplayNameResolver would have been the perfect place to fix my problem, but it does not provide the property path. It would be super easy to add it there, but that would be a breaking change.

No, it is not super easy to add it here. The full property path is only constructed when validation occurs, as it relies on information about the object being validated (eg indexers etc) - it's dynamic. The display name resolver must support standalone execution, outside of a validation run. This is needed in order to provide metadata for UI libraries (for example, ASP.NET's clientside validation), where it's necessary to obtain the failure message without invoking the validator. This is why the DisplayName resolver only has access to information from when the validator is constructed (type, member & expression), but not runtime contextual information.

Add full property path as a placeholder to make it easily accessible in messages. I.e., {FullPropertyName}

This is doable. We can add a {PropertyPath} placeholder easily enough and will look at doing this for a future release. (Edit: created #2134)

Doing that, however, would require me to add WithFullName() after EVERY RULE in every validator which stinks.

FluentValidation encourages explicitness over automatic/magic changes to default behaviour. An explicit call for every rule is recommended, as it's then very clear you're overriding the default behaviour with something nonstandard. If you think this "stinks" then you're welcome to use a different library.

@JeremySkinner JeremySkinner closed this as not planned Won't fix, can't repro, duplicate, stale Aug 6, 2023
@FluentValidation FluentValidation locked as resolved and limited conversation to collaborators Aug 6, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants