1

I have two worlds of validation mechanisms in my ASP.NET Core 3.1 application:

  1. In services.AddControllersWithViews I add a custom IValidationMetadataProvider to options.ModelMetadataDetailsProviders. It changes the ErrorMessage of the default ValidationAttributes that are defined in my model classes to something that can be localised. This happens for all models that are validated through MVC model-binding. The results are in the usual place of ModelState.

    Here's some code for that:

    // In Startup.ConfigureServices:
    services.AddControllersWithViews(options =>
    {
        options.ModelMetadataDetailsProviders.Add(new LocalizedValidationMetadataProvider());
    });
    
    internal class LocalizedValidationMetadataProvider : IValidationMetadataProvider
    {
        public void CreateValidationMetadata(ValidationMetadataProviderContext context)
        {
            foreach (object attribute in context.ValidationMetadata.ValidatorMetadata)
            {
                if (attribute is ValidationAttribute valAttr)
                {
                    if (valAttr is RequiredAttribute reqAttr)
                        reqAttr.ErrorMessage = "@input_required";
                    if (valAttr is MaxLengthAttribute maxLenAttr)
                        maxLenAttr.ErrorMessage = $"@max_length {maxLenAttr.Length}";
                }
            }
        }
    }
    
  2. When reading and validating data from other sources (like importing from a JSON file), I use the Validator object to validate the models using their standard ValidationAttributes and other mechanisms (IValidatableObject). The results are in a local list of ValidationResults.

    Here's what I've done so far:

    var vc = new ValidationContext(importedModel);
    var results = new List<ValidationResult>();
    bool isValid = Validator.TryValidateObject(importedModel, vc, results, true);
    if (!isValid)
    {
        // Here I need results with the localisable messages like "@input_required"
        // Instead, I get the default "The (Name) field is required."
    }
    

Now the problem is that in the second case, the default messages for the ValidationAttributes are generated and in the ValidationResults I have no reference to the source of these messages. I need to integrate the first case somehow into the second scenario.

The IValidationMetadataProvider implementation works on validation metadata found in a ValidationMetadataProviderContext instance. I tried to trace down where that comes from and how MVC model-binding uses it but I am a bit lost in the aspnetcore source code with all its interfaces.

How would I bring the ValidationContext of Validator together with the ValidationMetadataProviderContext of MVC?

Edit: Having read the relevant source code a bit further, I have the impression that it's not possible to use Validator like this. Alternatively, how could I use MVC's ModelState-related mechanisms for objects that did not come from a web form request but another arbitrary source? Can I tell one of MVC's classes to process an object as if it came from the request and through model binding?

ygoe
  • 18,655
  • 23
  • 113
  • 210

2 Answers2

0

Maybe you have problem with deserialization?

Try add AddNewtonsoftJson:

services.AddControllers().AddNewtonsoftJson();
services.AddControllersWithViews().AddNewtonsoftJson();
services.AddRazorPages().AddNewtonsoftJson();
lails
  • 105
  • 11
  • No, there is no problem with deserialisation. I get the model object correctly. But its contents can still be invalid, like strings being empty that must not be empty, which is what the `ValidationAttribute`s in the model class should check. It's the messages that *they* produce that are the problem. – ygoe Jul 16 '20 at 11:27
0

Seems to be pretty easy once you dig through the ASP.NET Core source code. So Validator has no possibility to alter the ValidationAttributes that are defined in the model classes. I can't use that for my purpose. But MVC's classes are suitable for that. In fact all established extensions are still there.

All I need is to fetch an IObjectModelValidator from dependency injection. It can validate an object. It requires some more objects that can simply be created. Here's the full solution:

IObjectModelValidator objectModelValidator;   // from DI

// In an MVC action method:
var actionContext = new ActionContext();
actionContext.HttpContext = HttpContext;
var validationStateDict = new ValidationStateDictionary();
objectModelValidator.Validate(actionContext, validationStateDict, "", model);
if (actionContext.ModelState.IsValid)
{
    // The usual methods...
    // Can go through the ModelState errors and localise the placeholder messages there
}
ygoe
  • 18,655
  • 23
  • 113
  • 210