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

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 situation: we need to validate a value against an array of pre-defined values. That how our IsOneOfValidationAttribute was born.

Table of contents
  1. Intro
  2. Validation for the win!
  3. A base class for "Is One Of"
  4. How about "Not One Of"?
  5. Conclusion
  6. Changelog
  7. Comments

Validation for the win!

Here we see a model with multiple validation attributes. Required, EmailAddress and StringLength are well known (I hope). 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), 
     ApplicationNameAvailable]
    public string Name { get; set; }

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

A base class for "Is One Of"

Our configuration has a string array with valid Label values. What if we could use that to validate our property? First we need to create a base class that is the bridge between the validation and the array of valid values. Then we only need to implement the retrieval of said array.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = true)]
public abstract class IsOneOfValidationAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(
        object? value,
        ValidationContext validationContext)
    {
        var values = GetValidValues(validationContext);
        var exists = values.Contains(value);

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

        var msg = GetInvalidValueMessage(value, values);
        string[] members = validationContext.MemberName is not null
            ? [validationContext.MemberName]
            : [];

        return new ValidationResult(msg, members);
    }

    protected abstract object[] GetValidValues(ValidationContext validationContext);

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

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

Let's implement it for our label to get the values from our configuration:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = true)]
public class LabelAttribute : IsOneOfValidationAttribute
{
    protected override object[] GetValidValues(ValidationContext validationContext) =>
        GetOption<ProvisioningOptions>(validationContext).Labels;
}

How about "Not One Of"?

What if you need something not to be on a list? You could build the inverse of the IsOneOfValidationAttribute, but would that make sense? Only if you have a small list of items, it may work, and I'm not even sure if you want to show the list of items that are not allowed. Most of my use cases involve a query to see if something does not exist.

Let's make our ValidationAttribute class a little nicer to work with:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = true)]
public abstract class SimpleValidationAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(
        object? value,
        ValidationContext validationContext)
    {
        var valid = IsValidValue(value, validationContext);

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

        var msg = GetInvalidValueMessage(value);
        string[] members = validationContext.MemberName is not null
        ? [validationContext.MemberName]
        : [];

        return new ValidationResult(msg, members);
    }

    protected abstract bool IsValidValue(object? value, ValidationContext validationContext);

    protected virtual string GetInvalidValueMessage(object? invalidValue)
    {
        return $"{invalidValue} is not valid or allowed.";
    }
}

Now we can easily implement the validator, without having to worry about messages and members:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, Inherited = true)]
public class ApplicationNameAvailableAttribute : SimpleValidationAttribute
{
    protected override bool IsValidValue(object? value, ValidationContext validationContext)
    {
        if (value is not string name || string.IsNullOrWhiteSpace(name))
        {
            return false;
        }

        var exists = validationContext
            .GetRequiredService<ProvisionerService>()
            .Exists(name)
            .Result;

        return !exists;
    }
}

Conclusion

With data annotation validations, .NET provides a powerful and extendable mechanism to influence the way objects are validated. But what if you want to use the validation in your business service? I've got you covered, check: Data Annotation validation with dependency injection in a business service.

The code is on GitHub, so check it out: code gallery / 14. validation

Changelog

expand_less brightness_auto