Resolving IoC container services for Validation Attributes in ASP.NET MVC

I'm a fan of the Data Annotation Validation attributes to validate user input on both the client and server side for ASP.NET MVC applications. I'm also a fan of Inversion of Control and Dependency Injection to create loosely coupled and flexible applications. There are certain situations where one would wish to inject services into a validation attribute to use during validation; a (somewhat contrived and security cautious) example of this might be providing the user with a <select> element and ask them to choose an option from it and then validate that the option that they have chosen is one that actually exists for the type of items represented in the dropdown. Sure, the RemoteAttribute would allow you to call a server side method via AJAX to perform validation but it does not provide any server side validation, making it more of a convenience solution than a complete and robust one. What would be good is if we could

  1. provide services to a validation attribute that could be used in the process of validation
  2. Not have to explicitly resolve services from the container à la the Service Locator (Anti-)Pattern

What the framework giveth...

By default, the MVC framework uses the DataAnnotationsModelValidator to provide a ValidationContext and Model object to a Validation attribute:

/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. All rights reserved.
 *
 * This software is subject to the Microsoft Public License (Ms-PL). 
 * A copy of the license can be found in the license.htm file included 
 * in this distribution.
 *
 * You must not remove this notice, or any other, from this software.
 *
 * ***************************************************************************/
 
namespace System.Web.Mvc {
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
 
    public class DataAnnotationsModelValidator : ModelValidator {
        public DataAnnotationsModelValidator(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
            : base(metadata, context) {
 
            if (attribute == null) {
                throw new ArgumentNullException("attribute");
            }
 
            Attribute = attribute;
        }
 
        protected internal ValidationAttribute Attribute { get; private set; }
 
        protected internal string ErrorMessage {
            get {
                return Attribute.FormatErrorMessage(Metadata.GetDisplayName());
            }
        }
 
        public override bool IsRequired {
            get {
                return Attribute is RequiredAttribute;
            }
        }
 
        internal static ModelValidator Create(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute) {
            return new DataAnnotationsModelValidator(metadata, context, attribute);
        }
 
        public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() {
            IEnumerable<ModelClientValidationRule> results = base.GetClientValidationRules();
 
            IClientValidatable clientValidatable = Attribute as IClientValidatable;
            if (clientValidatable != null) {
                results = results.Concat(clientValidatable.GetClientValidationRules(Metadata, ControllerContext));
            }
 
            return results;
        }
 
        public override IEnumerable<ModelValidationResult> Validate(object container) {
            // Per the WCF RIA Services team, instance can never be null (if you have
            // no parent, you pass yourself for the "instance" parameter).
            ValidationContext context = new ValidationContext(container ?? Metadata.Model, null, null);
            context.DisplayName = Metadata.GetDisplayName();
 
            ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context);
            if (result != ValidationResult.Success) {
                yield return new ModelValidationResult {
                    Message = result.ErrorMessage
                };
            }
        }
    }
}

The Validate method creates a new ValidationContext instance and passes this, along with the model object to the Attribute via the GetValidationResult(object, ValidationContext) method on the attribute. See those other null parameter arguments passed to the ValidationContext constructor? Those are for an IServiceProvider and IDictionary<object, object>, respectively. The former is a contract for one method, GetService(Type), that takes a type and returns an object. On this basis, MVC provides a hook for being able to resolve services inside of a validation attribute, but that hook isn't used by default. Luckily, the default can be replaced with our own implementation.

A crude solution

Let's write our own ModelValidator. Using Castle Windsor IoC container, the IKernel implements IServiceProvider already, so we could go with something like this:

public class WindsorModelValidator : DataAnnotationsModelValidator
{
    public IKernel Kernel { get; set; }

    public WindsorModelValidator(
        ModelMetadata metadata,
        ControllerContext context,
        ValidationAttribute attribute)
        : base(metadata, context, attribute)
    {
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        ValidationContext context = CreateValidationContext(container);

        ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context);

        if (result != ValidationResult.Success)
        {
            yield return new ModelValidationResult
                {
                    Message = result.ErrorMessage
                };
        }
    }

    protected virtual ValidationContext CreateValidationContext(object container)
    {
        var context = new ValidationContext(container ?? Metadata.Model, Kernel, null);
        context.DisplayName = Metadata.GetDisplayName();
        return context;
    }
}

Then in Application_Start, register our WindsorModelValidator with the container and hook it up to be used as the ModelValidator for the application:

protected void Application_Start()
{
    // superfluous code here like route registration, etc...

    var container = new WindsorContainer();

    container.Register(AllTypes.FromThisAssembly()
                    .BasedOn<IController>()
                    .If(Component.IsInSameNamespaceAs<HomeController>())
                    .If(t => t.Name.EndsWith("Controller"))
                    .Configure(c => c.LifeStyle.Transient));


    container.Register(
        Component.For<ICategoryRepository>().ImplementedBy<CategoryRepository>(),
        Component.For<ModelValidator>().ImplementedBy<WindsorModelValidator>().LifeStyle.Transient);

    ControllerBuilder.Current.SetControllerFactory(new WindsorControllerFactory(container.Kernel));

    // hook up our WindsorModelValidator here
    DataAnnotationsModelValidatorProvider.RegisterDefaultAdapterFactory(
         (metadata, context, attribute) => container.Resolve<ModelValidator>(new { metadata, context, attribute }));
}

Now, inside of validation attributes we can resolve services from the IServiceProvider (provided by the Kernel) on the ValidationContext:

public class ValidCategoryAttribute : ValidationAttribute
{
    private const string DefaultErrorMessage = "'{0}' does not contain a valid category";

    public ValidCategoryAttribute() : base(DefaultErrorMessage)
    {
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var input = Convert.ToString(value, CultureInfo.CurrentCulture);

        // let the Required attribute take care of this validation
        if (string.IsNullOrWhiteSpace(input))
        {
            return null;
        }

        // resolve our ICategoryRepository using IServiceProvider on the ValidationContext
        var categories = validationContext.GetService(typeof(ISettingsRepository)) as ICategoryRepository;
 
        int categoryId;
        if (!int.TryParse(input, out categoryId))
        {
            return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
        }
      
        return categories.Where(c => c.Id == categoryId).FirstOrDefault() == null ? 
            new ValidationResult(FormatErrorMessage(validationContext.DisplayName)) : null;
    }
}

This satisfies Point 1 above, but not Point 2. We don't want to have to explicitly get the service, we want it to be provided to us (i.e. the Hollywood Principle).

A better solution

Quite a while ago, Jeremy Skinner wrote an article about Dependency Injection with Action Filters where he created his own IActionInvoker that could inject services into ActionFilter attributes. We can do something similar here for Validation attributes. Here is an improved WindsorModelValidator:

public class WindsorModelValidator : DataAnnotationsModelValidator
{
    public IKernel Kernel { get; set; }

    public WindsorModelValidator(
        ModelMetadata metadata,
        ControllerContext context,
        ValidationAttribute attribute)
        : base(metadata, context, attribute)
    {
    }

    public override IEnumerable<ModelValidationResult> Validate(object container)
    {
        // inject the services from the container that
        // the Validation attribute requires
        Kernel.InjectProperties(Attribute);

        ValidationContext context = CreateValidationContext(container);

        ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context);

        if (result != ValidationResult.Success)
        {
            yield return new ModelValidationResult
                {
                    Message = result.ErrorMessage
                };
        }
    }

    protected virtual ValidationContext CreateValidationContext(object container)
    {
        var context = new ValidationContext(container ?? Metadata.Model, Kernel, null);
        context.DisplayName = Metadata.GetDisplayName();
        return context;
    }
}

Keeping the registration in Application_Start the same as before, we can now write our ValidCategoryAttribute like so:

public class ValidCategoryAttribute : ValidationAttribute
{
    private const string DefaultErrorMessage = "{0} is not a valid category";

    public ValidCategoryAttribute() : base(DefaultErrorMessage)
    {
    }

    public ICategoryRepository Categories { get; set; }

    public override bool IsValid(object value)
    {
        var input = Convert.ToString(value, CultureInfo.CurrentCulture);

        // let the Required attribute take care of this validation
        if (string.IsNullOrWhiteSpace(input))
        {
            return true;
        }

        int categoryId;
        if (!int.TryParse(input, out categoryId))
        {
            return false;
        }
      
        return Categories.Where(c => c.Id == categoryId).FirstOrDefault() != null;
    }
}

Much cleaner, yes? The InjectProperties extension method implementation can be found on Jeremy's blog.

Comments

comments powered by Disqus