“Is One Of” and “Is Not One Of” validation attributes

A collection of red buttons that can be used for clothing. A collection of red buttons that can be used for clothing.

I love attribute validation! They can be used for a myriad of things. In .NET Core MVC we use them to validate models that come into our controllers. In one of our projects, we kept running into the same thing: we need to validate a value against an array of pre-defined values. So we wrote some base validation attributes.

  1. Intro
  2. Validation for the win!
  3. A base class for "Is One Of"
  4. Validating against IOptions
  5. How about "Not One Of"?
  6. Validation against a service
  7. Validating objects with attributes
  8. Conclusion
  9. Improvements
  10. Comments

Validation for the win!

Here we see a model with multiple validation attributes. Required, EmailAddress and StringLengthare well known. But now I want to validate that the Label is one a list of preset values from my config. I also would like to validate that the Name is a new repository name.

using System;
using System.ComponentModel.DataAnnotations;

public class GitHubProvisioningRequest
{
    [Required, Label]
    public string Label { get; set; }

    [Required]
    public string Description { get; set; }

    [Required, TeamIdShouldExist]
    public int TeamId { get; set; }

    [Required]
    public string Tag { get; set; }

    [Required, StringLength(100, MinimumLength = 5), NewRepositoryName]
    public string Name { get; set; }

    [Required, EmailAddress]
    public string EmailAddress { get; set; }
}

A base class for "Is One Of"

We have an array of values and the validation value must be one of those values. If the value is not present in the array, we must return a validation result. Let's capture this behavior in a base validation attribute:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;

public abstract class IsOneOfValidationAttribute : ValidationAttribute
{
    protected abstract object[] GetValues(ValidationContext validationContext);

    protected abstract string GetInvalidValueMessage(
        object invalidValue, 
        object[] validValues);

    protected TOption GetOption<TOption>(ValidationContext validationContext)
        where TOption: class, new()
    {
        return validationContext.GetRequiredService<IOptions<TOption>>().Value;
    }

    protected override ValidationResult IsValid(
        object value, 
        ValidationContext validationContext)
    {
        var values = GetValues(validationContext);
        var exists = values.Contains(value);

        if (exists)
        {
            return ValidationResult.Success;
        }

        var msg = GetInvalidValueMessage(value, values);
        return new ValidationResult(msg);
    }
}

Implementations of this class need to provide an array of valid values (GetValues) and a validation error message (GetInvalidValueMessage).

Note: I wanted to make this a generic class to support strong typing, but C# seems to forbid it.

Validating against IOptions

I need to validate a lot against my application configuration, that's why I added the GetOption in the base class. It uses dependency injection to get the option. Check this example in which the label attribute:

public class LabelAttribute : IsOneOfValidationAttribute
{
    protected override object[] GetValues(ValidationContext validationContext)
    {
        var option = GetOption<ProvisioningConfiguration>(validationContext);
        return option.GitHub.Labels;
    }

    protected override string GetInvalidValueMessage(
        object invalidValue,
        object[] validValues)
    {
        var valid = String.Join(", ", validValues);
        return $"{invalidValue} is not a valid or allowed. Options are: [{valid}]";
    }
}

How about "Not One Of"?

This case is slightly different. I decided not to list all the invalid options, as in most use cases we only need to show that the value itself is invalid.

public abstract class IsNotOneOfValidationAttribute : ValidationAttribute
{
    protected abstract object[] GetValues(ValidationContext validationContext);

    protected abstract string GetInvalidValueMessage(object invalidValue);

    protected TOption GetOption<TOption>(ValidationContext validationContext)
        where TOption: class, new()
    {
        return validationContext.GetRequiredService<IOptions<TOption>>().Value;
    }

    protected override ValidationResult IsValid(
        object value,
        ValidationContext validationContext)
    {
        var values = GetValues(validationContext);
        var exists = values.Contains(value);

        if (!exists)
        {
            return ValidationResult.Success;
        }

        var msg = GetInvalidValueMessage(value);
        return new ValidationResult(msg);
    }
}

Validation against a service

Now, let's implement the NewRepositoryNameAttribute, by inheriting from the IsNotOneOfValidationAttribute base. We'll use dependency injection to get the IGitHubProvisioningService which we need to validate the repository name.

using Blaze.PlatformProvisioning.Resources.Base.Validators;
using Microsoft.Extensions.DependencyInjection;
using System.ComponentModel.DataAnnotations;

public class NewRepositoryNameAttribute : IsNotOneOfValidationAttribute
{
    protected override object[] GetValues(ValidationContext validationContext)
    {
        var git = validationContext.GetRequiredService<IGitHubProvisioningService>();
        return git.GetAllRepositoryNames().Result;
    }

    protected override string GetInvalidValueMessage(object invalidValue)
    {
        return $"'{invalidValue}' is already in use.";
    }
}

Note: I would not recommend the "not one of" validator for all your "value exists in database" calls.

Validating objects with attributes

Validation is easy when you are using ASP.NET MVC and models, because validation is done by ASP.NET. But what if you need to do validation yourself? I created some extension methods that make validation easier:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;

public static class AttributeValidationExtension
{
    public static bool TryValidate<T>(this T obj, bool throwOnInvalid)
    {
        return TryValidate<T>(obj, throwOnInvalid, out _);
    }

    public static bool TryValidate<T>(this T obj, bool throwOnInvalid, out IList<ValidationResult> validationErrors)
    {
        if (obj == null)
        {
            throw new ArgumentNullException(nameof(obj));
        }

        validationErrors = new List<ValidationResult>();
        var context = new ValidationContext(obj);

        var valid = Validator.TryValidateObject(obj, context, validationErrors, true);
        if (valid) return true;

        if (throwOnInvalid)
        {
            var msg = validationErrors.GetErrorMessage(obj.GetType());
            throw new ValidationException(msg);
        }

        return false;
    }

    public static string GetErrorMessage(this IList<ValidationResult> validationErrors, Type objectType)
    {
        if(validationErrors == null)
            throw new ArgumentNullException(nameof(validationErrors));

        if (objectType == null)
            throw new ArgumentNullException(nameof(objectType));

        return @$"Input invalid for '{objectType.Name}':\n" + 
            String.Join(
                @"\n", 
                validationErrors.Select(r => 
                    String.Join(", ", r.MemberNames) + 
                    ": " + 
                    r.ErrorMessage
                )
            );
    }
}

The main advantage of extension methods is that they can be executed from the object itself, like this:

var obj = new GitHubProvisioningRequest();
var valid = obj.TryValidate(false, out var errors);
var msg = errors.GetErrorMessage(obj.GetType());

Conclusion

With validation attributes .NET provides a powerful and extendable mechanism to influence the way objects are validated. Having some base classes will cut your development time in halve.

Improvements

2020-07-17: Added a section on how to validate objects with validation attributes using extension methods.

expand_less