2

I'm trying to build a series of attribute classes to make it easier for our development team to validate objects. The objects are POCO classes like this.

public class User
{
    public string Name { get; set; }
    public string Company { get; set; }
}

I want to decorate this model with a custom attribute.

public class User
{ 
    [MustHaveValue]
    public string Name { get; set; }
    public string Company { get; set; }
}

Then I would create my own class implementing ValidationAttribute, the base class in .NET Framework, which belongs to System.ComponentModel.DataAnnotations.

public class MustHaveValueAttribute : ValidationAttribute
{
    .
    .
    public override IsValid(object value) 
    {
        // validation logic.
    }
}

And then I can validate the User model whenever I want by making the set of instances like ValidationContext, List<ValidationResult>.

But in an enterprise environment, problems just can't be solved by a specific class. My validation scenario requires more complex and more flexible ways. Imagine that one of the required validation scenarios would something like this.

public class User
{
    public string Name { get; set; }
    public string Company { get; set; }

    // Check if an item exists in this list.
    [MustHaveMoreThanOneItem]
    public IList<Client> Clients { get; set; }
}

Then I would need to make another attribute class

public class MustHaveMoreThanOneItemAttribute : ValidationAttribute
{
    .
    .
    public override IsValid(object value) 
    {
        // Let's assume this value is List<Client> for now.
        // I know the exact type, so I'm going to cast it to List<Client> without further considerations
        List<Client> clients = value as List<Client>;
        if(clients.Count > 0) {
            return true;
        } else {
            return false;
        }
    }
}

But the problem is that there are a lot of other models that have a nested list items. Try to imagine the time when I want to reuse the MustHaveMoreThanOneItem in one of the other models like...

public class Department
{ 
    public string Name { get; set; }

    [MustHaveMoreThanOneItem]
    public IList<Employee> { get; set; }
}

You already know that it's not going to work because it was strongly typed only for List<Client>. So I decided to use Generic there to solve this problem.

But to my disappointment, the _Attribute interface doesn't support Generic. There's no additional implementation like _Attribute<T> : Attribute and therefore, no ValidationAttribute<T> alas!! I just cannot use Generic here !!

public class Department
{ 
    public string Name { get; set; }

    // No way to use this syntax.
    [MustHaveMoreThanOneItem<Employee>]
    public IList<Employee> { get; set; }
}

So I made a conclusion that Attribute must have been designed for a fixed set of validations like email format, card format, null check, and etc IMAO.

But I still want to use an attribute and give a lot of flexibilities in it to prevent the duplicated, verbose validation codes like this.

if(model.Clients.Count > 0) ...
if(model.Name != null) ...
if(model.Clients.GroupBy(x => x.Country == Country.USA).Count >= 1) ...
if(model.Clients.Where(x => x.CompanyName == Company.Google).ToList().Count > 1 ) ...
.
.
.

I want to pose two questions here.

  1. If Attirbute supports Generic, this problem will be solved?
  2. Is there any way to implement Generic Attribute? in order to use [MustHaveMoreThanOneItem<Employee>] annotation on a class member?
hina10531
  • 3,938
  • 4
  • 40
  • 59
  • 2
    Just letting you know there is already a `[Required]` attribute so `[MustHaveValueAttribute]` may not be _required_ –  Dec 07 '17 at 15:27
  • 2
    It's simple. Cast the property's value to IEnumerable. All collections implement IEnumerable. You can then simply check to see if it returns at least two items. No need for generics here. You can also use reflection to examine the object for your requirements as well. –  Dec 07 '17 at 15:27
  • 1
    In your particular case casting to `IEnumerable` will suffice, but if you ever run to a situation where you have to know the type use the fact that `Attribute` constructor can take `Type` parameter (so you can give it `typeof(Employee)`). – Dialecticus Dec 07 '17 at 15:33
  • c# language spec forbids generic attributes more by decision than technical limitations. For more info: https://stackoverflow.com/questions/294216/why-does-c-sharp-forbid-generic-attribute-types – Jason W Dec 07 '17 at 15:57

2 Answers2

2

You can generically check any object that implements IEnumerable like this:

public class MustHaveMoreThanOneItemAttribute : ValidationAttribute
{
    public override bool IsValid(object value) 
    {
        // omitted null checking
        var enumerable = value as IEnumerable;
        var enumerator = enumerable.GetEnumerator();
        if (!enumerator.MoveNext())
        {
            return false;
        }

        if (!enumerator.MoveNext())
        {
            return false;
        }

        return true;
    }
}
stuartd
  • 70,509
  • 14
  • 132
  • 163
2

C# by definition does not support generic type attributes, although this has been requested actively for a long time:

However, you can still inject a type into a validation attribute via constructor. You then can use reflection or whatever you need to define your custom validation criteria.

public class MustHaveMoreThanOneItemAttribute : ValidationAttribute
{
    public Type EnumerableType { get; }
    public MustHaveMoreThanOneItemAttribute(Type t)
        => this.EnumerableType = typeof(ICollection<>).MakeGenericType(t);
    public override bool IsValid(object value)
    {
        var count = this.EnumerableType.GetProperty("Count").GetValue(value) as int?;
        return (count ?? 0) > 1;
    }
}

Now this allows you to use something similar to your goal:

public class Department
{ 
    public string Name { get; set; }

    [MustHaveMoreThanOneItem(typeof(Employee))]
    public IList<Employee> { get; set; }
}
Jason W
  • 13,026
  • 3
  • 31
  • 62
  • Just provided this as alternative to Stuart's answer. I would use his solution to avoid reflection for use case from OP, but thought this was interesting if other use cases could benefit from needing type at runtime. – Jason W Dec 07 '17 at 16:29
  • This is good. But checking the number of list items is not enough for me. Checking how many matched items are in a given list will require a conditional search like this `model.Clients.GroupBy(x => x.Country == Country.USA).Count >= 1)`, which was specified on this post. – hina10531 Dec 08 '17 at 02:04
  • This is an edge case that Microsoft team has decided isn't worth complexity in dealing with. You can basically just pass value types, Type, or enum into constructor of an attribute - no lambda expressions. I supposed you can bypass this by creating a class containing a predicate as a Func, and then pass in the type to the custom attribute constructor to get instantiated their through activator, but that isn't very enterprise-ready and creates too many classes. See https://learn.microsoft.com/en-us/dotnet/visual-basic/misc/bc30045 – Jason W Dec 08 '17 at 18:55