4

I am following this example: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation and trying to implement my own custom attribute for validation.

Now, the viewmodel has two fields that I wish to access from inside this method, so that they can be rendered with the "data-val" attributes. My question is, how can I get say a property called "Myprop" from context here? When I debug I can se the information under context.ActionContext.ViewData.Model but I have no way of getting that info other then during the debug when I use Visual Studio "quick watch" feature. The custom attributes are on properties that are on the viewmodel.

public void AddValidation(ClientModelValidationContext context)
{
    if (context == null)
    {
       throw new ArgumentNullException(nameof(context));
    }

    MergeAttribute(context.Attributes, "data-val", "true");
    MergeAttribute(context.Attributes, "data-val-classicmovie", GetErrorMessage());

    var year = _year.ToString(CultureInfo.InvariantCulture);
    MergeAttribute(context.Attributes, "data-val-classicmovie-year", year);
}

1 Answer 1

3

I ran into a similar problem. The short answer is that you can't from there. You only have access to the metadata from there, not the actual model. The reason for this is that your model metadata and validation based on it are done on first time use, and then cached. So you'll never be able to change what validation rules to return based on the model via an attribute decorator.

If you need to dynamically decide which client side data-val-* attributes to render based off the instance/content of your model, you would need to inherit from DefaultValidationHtmlAttributeProvider, instead of using attributes, and override the AddValidationAttributes method. It's the only way I've found to do this so far. This is because inside of this method, you have access to the ModelExplorer

public class CustomValidationHtmlAttributeProvider : DefaultValidationHtmlAttributeProvider
{
    private readonly IModelMetadataProvider metadataProvider;

    public CustomValidationHtmlAttributeProvider(IOptions<MvcViewOptions> optionsAccessor, IModelMetadataProvider metadataProvider, ClientValidatorCache clientValidatorCache)
        : base(optionsAccessor, metadataProvider, clientValidatorCache)
    {
        this.metadataProvider = metadataProvider;
    }

    public override void AddValidationAttributes(ViewContext viewContext, ModelExplorer modelExplorer, IDictionary<string, string> attributes)
    {
        //base implimentation
        base.AddValidationAttributes(viewContext, modelExplorer, attributes);

        //re-create the validation context (since it's encapsulated inside of the base implimentation)
        var context = new ClientModelValidationContext(viewContext, modelExplorer.Metadata, metadataProvider, attributes);

        //Only proceed if it's the model you need to do custom logic for
        if (!(modelExplorer.Container.Model is MyViewModelClass model) || !modelExplorer.Metadata.PropertyName == "Myprop") return;

        //Do stuff!
        var validationAttributeAdapterProvider = viewContext.HttpContext.RequestServices.GetRequiredService<IValidationAttributeAdapterProvider>();

        if (model.Myprop)
        {
            var validationAdapter = (RequiredAttributeAdapter)validationAttributeAdapterProvider.GetAttributeAdapter(new RequiredAttribute(), null);
            validationAdapter.Attribute.ErrorMessage = "You not enter right stuff!";
            validationAdapter.AddValidation(context);
        }
    }
}

And then register this class in the ConfigureServices() of your Startup

public void ConfigureServices(IServiceCollection services)
{
    //All your other DI stuff here

    //register the new ValidationHtmlAttributeProvider
    services.AddSingleton<ValidationHtmlAttributeProvider, PseudoAttributeValidationHtmlAttributeProvider>();
}

The downside, is that if you have multiple models you need to do this for, it gets really ugly really fast. If anyone has found a better method, I'd love to hear it :-)

Sign up to request clarification or add additional context in comments.

3 Comments

Awesome! Will try this out in a bit and get back to you before I accept as answer
May I ask how the rest of your implementation looked? When making a custom attribute, you need to inherit from ValidationAttribute, how then could you inherit from DefaultValidationHtmlAttributeProvider and customize it and override with custom logic?
You can't do it from an attribute. You don't have access to the Model, and the logic that triggers validation on decorator attributes doesn't pass the model in. This is by design because the result of iterating over your attributes to build up the metadata and validation rules are only fired once then cached. So it can't depend on the model. I'll flesh out my example in the answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.