Search code examples
c#asp.net-core-mvcunobtrusive-validationvalidationattribute

ValidationAttribute injecting services for unobtrusive client validation


I have a relatively simple ValidationAttribute which requires some services in order to complete the IsValid method - basically just retrieving a list of IDs to compare the selected value against. As suggested by this post here, I am using the service locator pattern to get at these on the server side, and that is working fine.

However, I would also like to perform the same validation on the client, but try as I might I cannot find a way to get the same data added in via the AttributeAdapter, or for that matter set it up on the Attribute itself early enough - such that the AttributeAdapter can get at it - so that I can add the Ids to the MergeAttribute call and thus access them with some JavaScript on the client side.

Is this even possible? any ideas would be most useful

bit of simplified code for context...

ValidationAttribute:

public class InvalidIdAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var configService = validationContext.GetService<IProductService>();
        var productIds = configService.GetIds()

        var selectedId = (int)value;

        var invalidId = productIds.SingleOrDefault(p => p.Invalid && p.Id == selectedId);

        if (invalidId != null)
            return new ValidationResult(FormatErrorMessage(invalidId.Name));

        return ValidationResult.Success;
    }
}

and the AttributeAdapter

public class InvalidIdAttributeAdapter : AttributeAdapterBase<InvalidIdAttribute>
{
    private readonly InvalidIdAttribute _attribute;

    public InvalidIdAttributeAdapter(InvalidIdAttribute attribute, IStringLocalizer stringLocalizer)
        : base(attribute, stringLocalizer)
    {
        _attribute = attribute;
    }

    public override void AddValidation(ClientModelValidationContext context)
    {
        //how do I get the productService in here?
        var invalidIds = productService.GetIds().Where(p=>p.Invalid==true).Select(p=>p.Id);
        var pipedIds = string.Join("|", invalidIds);

        MergeAttribute(context.Attributes, "data-val", "true");
        MergeAttribute(context.Attributes, "data-val-invalidid", GetErrorMessage(context));
        MergeAttribute(context.Attributes, "data-val-invalidid-props", pipedIds); 
    }

    public override string GetErrorMessage(ModelValidationContextBase validationContext)
    {
        return _attribute.FormatErrorMessage(validationContext.ModelMetadata.GetDisplayName());
    }
}

Solution

  • The proper way would be to just depend on that service as part of your attribute adapter. So just add the additional dependency in its constructor:

    public class InvalidIdAttributeAdapter : AttributeAdapterBase<InvalidIdAttribute>
    {
        private readonly InvalidIdAttribute _attribute;
        private readonly IProductService _productService;
    
        public InvalidIdAttributeAdapter(InvalidIdAttribute attribute, IStringLocalizer stringLocalizer, IProductService _productService)
            : base(attribute, stringLocalizer)
        {
            _attribute = attribute;
            _productService = productService;
        }
    
        public override void AddValidation(ClientModelValidationContext context)
        {
            var invalidIds = _productService.GetIds()…;
    
            // …
        }
    }
    

    If you are constructing the attribute adapter through a custom attribute adapter provider, then you will have to make that depend on the service and pass it down to the attribute adapter. Since the attribute adapter provider is registered and resolved through the dependency injection container, you can just add additional dependencies in its constructor.

    Note that if your IProductService depends on a scoped dependency, e.g. a database context, then you will have to register the attributed adapter provider as a scoped dependency itself instead of a singleton:

    services.AddScoped<IValidationAttributeAdapterProvider, InvalidIdAttributeAdapterProvider>();