shyaway

DataAnnotation > Make a custom attribute 본문

.NET

DataAnnotation > Make a custom attribute

shyaway 2017. 12. 22. 10:01

Make a custom attribute

We looked at how attributes in DataAnnotations work in the last post. And I talked about IValidatableObject to perform more complex validation and discussed its limitations. I'm going to look at some ways we can take to overcome those limitations.


Custom Attribute

the attribute [Required] has a method like this.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class RequiredAttribute : ValidationAttribute
{
    .
    . // Etc Member Variables
    .
    public override IsValid(object value)
    {
        // Validation Content
    }
    .
    .
    .
}

You can declare [Required] right above a property or an object because this class exists. Isn't it possible to make my own annotation attribute? yes it is possible.


Step1. Make a class

Generate a class. I'm going to name it GeniusDM as an example. Attribute must come after your class name, it's a strict rule. If you make a class like this below, you can use [GeniusDM] right away.
1
2
3
4
5
public class GeniusDMAttribute : ValidationAttribute
{
     
 
}


Step2. overriding base class

ValidationAttribute Base class has two method overloadings.

    • public virtual bool IsValue(object value)
      when you want to return true / false and error .

    • protected virtual ValidationResult isValue(object value, ValidationContext validationContext)
      when you want to return ValidationResult, error message, and customized property information at the same time.
      Remember if you implement all of them, this one is going to be invoked by the Validator.

You have to override one of them because it's virtual !
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class GeniusDMAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        if(value == null)
        {
            Message = "GeniusDM Validation Tutorial - Fail !";
            return false;
        }
        else
        {
            Message = "GeniusDM Validation Tutorial - Success !";
            return true;
        }
    }
}


Step3. assign an annotation

1
2
3
4
5
6
public class TestModel
{
    [GeniusDM]
 
    public string Test { get; set; }
}

Just assigned [GeniusDM] annotation on TestModel. Let's see if it works.


Step4. Run and the result

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void SomeMethod
{
    // 1. Make a model
    TestModel model = new TestModel { Name = "Test" };
    // 2. Declare a result list
    List<ValidationResult> results = new List<ValidationResult>();
    // 3. Declare a context
    ValidationContext context = new ValidationContext(model, null, null);
    // 4. Call the Validator's method
    var result = Validator.TryValidateObject(model, context, results, true);
    // 5. Final result
    // result -> true;
    // results.Count == 1
    // results[0].ErrorMessage = "GeniusDM Validation Tutorial - Success !";
}


If you want to get the originated property name and error message.

IsValid method above just checks if the value is valid or not and returns true or false, and an error message additionally. But if you need to leave a log in detail and if you need to know the details of validation errors in another context, getting an object will not suffice because you just can't get the property name and object information by reflection. ( It could raise a serious performance issue. )

As I mentioned above, IsValid has two overloadings. I'm going to implement IsValid(object value, ValidationContext validationContext) this time.
1
2
3
4
5
6
7
8
9
10
11
public class GeniusDMAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if(value == null) {
            return new ValidationResult(String.Format("GeniusDM Validation Tutorial - Fail at {0}", validationContext.DisplayName));
        } else {
            return new ValidationResult(String.Format("GeniusDM Validation Tutorial - Success at {0}", validationContext.DisplayName));
        }
    }
}
Now invoking SomeMethod again will make it possible for you to build an error message with the originated property name.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void SomeMethod
{
    // 1. Make a model
    TestModel model = new TestModel { Name = "Test" };
    // 2. Declare a result list
    List<ValidationResult> results = new List<ValidationResult>();
    // 3. Declare a context
    ValidationContext context = new ValidationContext(model, null, null);
     
 
    // 4. Call the Validator's method
    var result = Validator.TryValidateObject(model, context, results, true);
     
 
    // 5. Final result.
    // result -> true;
    // results.Count == 1
    // results[0].ErrorMessage = "GeniusDM Validation Tutorial - Success at Name!";
}

Make sure to invoke IValidatableObject implementation no matter what

I explained IValidatableObject about its usage and the limitation in the last post. There're two ways to call the interface implementation.
    1. Force to invoke IValidatableObject implementation
    2. Make a new interface.
The number one method looks like this below, based on the sample in the last post.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Company : IValidatableObject
{
    [Required]
    public string CompanyName { get; set; }
 
    [Range(1, int.MaxValue)]
    public int Employees { get; set; }
     
    public List<Employee> EmployeeList { get; set; }
 
    public List<Department> DepartmentList { get; set; }
 
    public IEnumerable<ValidationResult> Validate(object value)
    {
        Company company = value as company;
        if(company != null) {
        if(company.EmployeeList.Count <= 0 && company.DepartmentList.Count <= 0)
        {
          //               ValidationResult(" Error Message ", " MemberNames " )
          yield return new ValidationResult("No department and employee info", new string[] { "DepartmentList & EmployeeList" });
        }
        if(company.DepartmentList.Count > 10)
        {
          yield return new ValidationResult("This company doesn't have more than 10 departments.", new string[] { "DepartmentList" });
 
            }        
      }        
}

1
2
3
4
5
6
7
8
9
10
11
12
public void SomeMethod
{
    Company company = new Company();
    List<ValidationResult> results = new List<ValidationResult>();
    ValidationContext context = new ValidationContext(company, null, null);
    Validator.TryValidateObject(company, context, results, true);
     
 
    // IValidatableObject casting
    IValidatableObject validatable = (IValidatableObject)company;
    validatable.Validate(context);
}
Now it's guaranteed to be called even if there was an error during the initial validation process. It's nice when there wasn't any errors on property level validation, but when there was, this code results in calling the same method twice. Checking if there was property errors or not is difficult. Because there will be two types of error. One could be from the attribute on a property and another could be from the interface implementation unless you strictly specified some kind of identifiers in IValidatableObject validation context.


Just making an interface will simply solve this problem.

What's itching is the fact that .NET Validator automatically manages the property level validation and the Validate method implementation. Getting out of it is the exact solution !

1
2
3
4
public interface IGeniusDMValidatableObject
{
    IEnumerable<ValidationResult> Validate(ValidationContext context);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public Company : IGeniusDMValidatableObject
{
    [Required]
    public string CompanyName { get; set; }
 
    [Range(1, int.MaxValue)]
    public int Employees { get; set; }
     
    public List<Employee> EmployeeList { get; set; }
 
    public List<Department> DepartmentList { get; set; }
 
    public IEnumerable<ValidationResult> Validate(ValidationContext context)
    {
        Company company = value as company;
        if(company != null) {
        if(company.EmployeeList.Count <= 0 && company.DepartmentList.Count <= 0)
        {
          //               ValidationResult(" Error Message ", " MemberNames " )
          yield return new ValidationResult("No department and employee info", new string[] { "DepartmentList & EmployeeList" });
        }
        if(company.DepartmentList.Count > 10)
        {
          yield return new ValidationResult("This company doesn't have more than 10 departments.", new string[] { "DepartmentList" });
 
            }        
      }        
}
1
2
3
4
5
6
7
8
9
10
11
public void SomeMethod
{
    Company company = new Company();
    List<ValidationResult> results = new List<ValidationResult>();
    ValidationContext context = new ValidationContext(company, null, null);
    Validator.TryValidateObject(company, context, results, true);
     
 
    IGeniusDMValidatableObject validatable = (IGeniusDMValidatableObject)company;
    validatable.Validate(context);
}

Now you can seamlessly call the additional validation method in the model no matter what. To collect a series of ValidationResult into a single list, you have to do something like this.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void SomeMethod
{
    Company company = new Company();
    List<ValidationResult> results = new List<ValidationResult>();
    ValidationContext context = new ValidationContext(company, null, null);
    Validator.TryValidateObject(company, context, results, true);
     
 
    IGeniusDMValidatableObject validatable = (IGeniusDMValidatableObject)company;
     
 
    // Collecting IValidatableObject results.
    List<ValidationResult> voResults = validatable.Validate(context).ToList();
    foreach(var r in voResults)
    {
        results.Add(r);
    }
}
This is completely a boilerplate. You gotta prepare an utility class or method that wraps the .NET Validator for a simple method call.

1
2
3
4
5
public void SomeMethod
{
    Company company = new Company();
    List<ValidationResult> results = GeniusDMValidator.TryValidateObject(company);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class GeniusDMValidator
{
    public static List<ValidationResult> TryValidateObject(object target)
    {
        List<ValidationResult> results = new List<ValidationResult>();
        ValidationContext context = new ValidationContext(company, null, null);
 
        Validator.TryValidateObject(company, context, results, true);
         
 
        IGeniusDMValidatableObject validatable = (IGeniusDMValidatableObject)company;
 
        // Collecting IValidatableObject results.
        List<ValidationResult> voResults = validatable.Validate(context).ToList();
        foreach(var r in voResults)
        {
            results.Add(r);
        }
         
 
        return results;
    }
}

Now we learned how to make a custom validation attribute, how to use IValidatableObject, and how to work around the limitations. I'm going to post how to make a custom attribute for list items in a model in the next post. It will be the last post for validation attribute.











Comments