55

I have the following class:

public class CreateJob
{
    [Required]
    public int JobTypeId { get; set; }
    public string RequestedBy { get; set; }
    public JobTask[] TaskDescriptions { get; set; }
}

I'd like to have a data annotation above TaskDescriptions so that the array must contain at least one element? Much like [Required]. Is this possible?

dove
  • 20,469
  • 14
  • 82
  • 108
CallumVass
  • 11,288
  • 26
  • 84
  • 154

8 Answers8

84

It can be done using standard Required and MinLength validation attributes, but works ONLY for arrays:

public class CreateJob
{
    [Required]
    public int JobTypeId { get; set; }
    public string RequestedBy { get; set; }
    [Required, MinLength(1)]
    public JobTask[] TaskDescriptions { get; set; }
}
Meta-Knight
  • 17,626
  • 1
  • 48
  • 58
Sat
  • 1,240
  • 1
  • 10
  • 8
  • Apparently it's only [available in .net 4.5+](http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.minlengthattribute.minlengthattribute(v=vs.110).aspx) as well. :-( – John MacIntyre Aug 29 '14 at 12:43
  • 23
    A little more info about this attribute: You should use [Required] in conjunction with [MinLength(1)] because MinLength will not trigger if the array is null (not empty, null). Also important to note, this is not supported (correct me if I'm wrong) with default client side validators. It will only trigger the ModelState.IsValid. – Pluc May 13 '15 at 13:54
  • That attribute also works for objects that implements the ICollection interface and strings. – Cat_Clan Feb 18 '16 at 22:29
  • 1
    You should add @Pluc's comment to your answer. Having the [Required] attribute makes your answer better than the accepted answer above. – Mike Devenney Feb 29 '16 at 16:02
  • Thank you this is exactly what I was looking for. – Lostaunaum Mar 27 '17 at 16:19
  • 6
    `[MinLength(1)]` works with `IEnumerable` in ASP.NET Core – whyleee Feb 02 '18 at 10:32
38

I've seen a custom validation attribute used for this before, like this:

(I've given sample with a list but could be adapted for array or you could use list)

public class MustHaveOneElementAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        var list = value as IList;
        if (list != null)
        {
            return list.Count > 0;
        }
        return false;
    }
}

[MustHaveOneElementAttribute (ErrorMessage = "At least a task is required")]
public List<Person> TaskDescriptions { get; private set; }

// as of C# 8/9 this could be more elegantly done with     
public class MustHaveOneElementAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        return value is IList {Count: > 0};
    }
}

Credit to Antonio Falcão Jr. for elegance

dove
  • 20,469
  • 14
  • 82
  • 108
7

Here is a bit improved version of @dove solution which handles different types of collections such as HashSet, List etc...

public class MustHaveOneElementAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        var collection = value as System.Collections.IEnumerable;
        if (collection != null && collection.GetEnumerator().MoveNext())
        {
            return true;
        }
        return false;
    }
}
mynkow
  • 4,408
  • 4
  • 38
  • 65
  • Shortened to: `public override bool IsValid(object value) => value is System.Collections.IEnumerable collection && collection.GetEnumerator().MoveNext();` – codeMonkey Mar 22 '23 at 23:08
6

Please allow me a side note on using MinLengthAttribute with .NET Core.

Microsoft recommends using Razor Pages starting with .NET Core 2.0.

Currently, The validation with MinLengthAttribute on a property within the PageModel does not work:

[BindProperty]
[Required]
[MinLength(1)]
public IEnumerable<int> SelectedStores { get; set; }

ModelState.IsValid returns true when SelectedStores.Count() == 0.

Tested with .NET Core 2.1 Preview 2.

Sven
  • 2,345
  • 2
  • 21
  • 43
4

You have to use 2 standard annotation attribute

public class CreateJob
{
    [MaxLength(1), MinLength(1)]
    public JobTask[] TaskDescriptions { get; set; }
}
M.R.T
  • 746
  • 8
  • 20
1

Further to mynkow's answer, I've added the ability to pass a minimum count value to the attribute and produce meaningful failure messages:

public class MinimumElementsRequiredAttribute : ValidationAttribute
{
  private readonly int _requiredElements;

  public MinimumElementsRequiredAttribute(int requiredElements)
  {
    if (requiredElements < 1)
    {
      throw new ArgumentOutOfRangeException(nameof(requiredElements), "Minimum element count of 1 is required.");
    }

    _requiredElements = requiredElements;
  }

  protected override ValidationResult IsValid(object value, ValidationContext validationContext)
  {
    if (!(value is IEnumerable enumerable))
    {
      return new ValidationResult($"The {validationContext.DisplayName} field is required.");
    }

    int elementCount = 0;
    IEnumerator enumerator = enumerable.GetEnumerator();
    while (enumerator.MoveNext())
    {
      if (enumerator.Current != null && ++elementCount >= _requiredElements)
      {
        return ValidationResult.Success;
      }
    }

    return new ValidationResult($"At least {_requiredElements} elements are required for the {validationContext.DisplayName} field.");
  }
}

Use it like this:

public class Dto
{
  [MinimumElementsRequired(2)]
  public IEnumerable<string> Values { get; set; }
}
Chris Pickford
  • 8,642
  • 5
  • 42
  • 73
1

Just updating Dove's (@dove) response to C# 9 syntax:

    public class MustHaveOneElementAttribute : ValidationAttribute
    {
        public override bool IsValid(object value)
            => value is IList {Count: > 0};
    }
-1

MinLength attribute considers the value as valid if it's null. Therefore just initialize your property in the model as an empty array and it'll work.

MinLength(1, ErrorMessageResourceName = nameof(ValidationErrors.AtLeastOneSelected), ErrorMessageResourceType = typeof(ValidationErrors))]
int[] SelectedLanguages { get; set; } = new int[0];