Genius DM

DataAnnotation > Recursive validation for collection items 본문

.NET

DataAnnotation > Recursive validation for collection items

Damon Jung 2017. 12. 29. 23:13

Attribute 로 리스트 객체 유효성 검사하기


Annotation 을 통한 Validation 은 int, string 등의 데이터 타입 단위의 유효성 검증에 유용하다고 첫 번째 포스트에서 밝혔다. 주로 MVC 에서 많이 사용하므로, 사용자 Form 에서 입력받는 데이터 검증이 그 주된 사용처이기 때문에 리스트 아이템이나 객체 단위로 검증하는 경우에는 사용되는 경우를 아직 보지 못 한 것 같다. 


개인적으로 Annotation 을 통한 Class decoration 은 장점이 많다고 생각한다. Model 에 Validation 대상을 명확하게 지정할 수 있으며 Required, Range, Email 처럼 코드 상에서 그 의미도 매우 명확하여 개발자로 하여금 Model 만 보고도 주요 데이터에 대한 파악을 쉽게 할 수 있도록 돕는다. 그리고 무엇보다 Annotation Decorated 라는 표현 처럼, Model 을 예쁘게 꾸민다.


이 장점을 그대로 살려서 Model 내부에 멤버 변수로 선언된 다른 Model 에 대한 검사, 그리고 그것이 리스트라면 리스트 아이템 갯수 만큼 모두 검사할 수 있다면 DataAnnotation 시리즈에서 목표로 하는 " 모든 요소에 대한 벨리데이션 수행 "  목표를 달성할 수 있다.


일단 ValidationResult 를 리턴 받는 이 방식에서 ValidationResult 만으로는 Graph, 즉 계층구조를 구성할 수 없다. ValidationResult 내부에 선언된 IEnumerable<ValidationResult> 형태에 멤버 선언이 없기 때문이다. 준비물은 간략하게 아래와 같다.


    1. GeniusDMValidationResult : ValidationResult 확장 클래스
      확장 클래스에서 내부 ValidationResult List 를 멤버변수로 추가한다.

    2. CheckChildrenAttribute : ValidationAttribute base 클래스 확장 Attribute
      싱글 객체와 리스트 객체를 IsValid 컨텍스트에서 계층 구조로 처리 할 클래스

    3. 샘플 모델


모델 예제

public class Company { [Required] public string Name { get; set; } [Required] public string Address { get; set; }


// CheckChildren 을 통해 내부에 선언된 리스트 객체를 검사한다. 물론 단일 객체에 대한 검사도 가능.


[CheckChildren] public List<Department> Departments { get; set; } }


public class Department { [Required] public string Name { get; set; } [Required] public int EmployeeCount { get; set; } [CheckChildren] public List<Employee> Employees { get; set; } }


public class Employee { [Required] public string Name { get; set; } public int Age { get; set; } public int Sex { get; set; } }


CheckChildrenAttribute

public class CheckChildrenAttribute : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { GeniusDMValidationResult result = new GeniusDMValidationResult(); result.ErrorMessage = string.Format(@"Error occured at {0}", validationContext.DisplayName); IEnumerable list = value as IEnumerable; if (list == null) { // Single Object List<ValidationResult> results = new List<ValidationResult>(); Validator.TryValidateObject(value, validationContext, results, true); result.NestedResults = results; return result; } else { List<ValidationResult> recursiveResultList = new List<ValidationResult>(); // List Object foreach (var item in list) { List<ValidationResult> nestedItemResult = new List<ValidationResult>(); ValidationContext context = new ValidationContext(item, validationContext.ServiceContainer, null); GeniusDMValidationResult nestedParentResult = new GeniusDMValidationResult(); nestedParentResult.ErrorMessage = string.Format(@"Error occured at {0}", validationContext.DisplayName); Validator.TryValidateObject(item, context, nestedItemResult, true); nestedParentResult.NestedResults = nestedItemResult; recursiveResultList.Add(nestedParentResult); } result.NestedResults = recursiveResultList; return result; } } }


GeniusDMValidationResult

public class GeniusDMValidationResult : ValidationResult { public GeniusDMValidationResult() : base("") { } public IList<ValidationResult> NestedResults { get; set; } }


테스트

class Program { static void Main(string[] args) { Company company = new Company(); company.Departments = new List<Department> { new Department { Employees = new List<Employee> { new Employee(), new Employee() } }, new Department { Employees = new List<Employee> { new Employee(), new Employee() } }, new Department { Employees = new List<Employee> { new Employee(), new Employee() } } }; List<ValidationResult> results = new List<ValidationResult>(); ValidationContext context = new ValidationContext(company, null, null); Validator.TryValidateObject(company, context, results, true); Console.Read(); } }


결과 Sample

위 실행 결과에서 results 를 살펴보면 다음과 같을 것이다.
    • results = count 3
      • [0] Name field is required
      • [1] Address field is required
      • [2] Departments has errors -> NestedResult = count 3
        • [0] Error occured at Departments -> NestedResult = count 2
          • [0] Name field is required
          • [1] Error occured at Employees -> NestedResult = count 2
            • [0] Name field is required
              .
              .
              .
              .
만약 두 번째 포스트와 같이 Validator 유틸리티까지 만들어서 IValidtableObject 를 생성했다면, Company 모델에 선언된 인터페이스 구현 결과가 1 Depth 인 results 배열에 나란히 들어갈 것이다. 이것까지 해주면 이제 벨리데이션 결과를 한꺼번에 수집해서 비즈니스 로직을 구성하는 것이 가능해진다. 

CodeHighlighter 도 조악하게 동작하고, 폰트도 들쭉날쭉으로 엉망인데 그래도 간신히 마무리 포스트까지 작성했다. 누군가에게는 도움이 되길 바라며...


















Validation for collection items with attribute class


I mentioned that validation via annotation attribute is great for data types such as int, string, and etc in the first post. This is most likely useful in MVC app, hence validation will take care of user input data. That's why I haven't seen an annotation attribute validating list items or objects yet, IMAO. 


I think data annotations are great for decorating a class. It clearly shows developers that what's going to be validated and what's not like Required, Range, and email. This certainly helps them to see which data is important or not by just taking a glance at the model. And annotations literally decorates a model gracefully. 


Validating all properties, even if it's an object or list items, in a model is what DataAnnotation posts are for. We can achieve this goal, taking the full advantage of annotation attribute to check nested items in a model if you implement these below.


First of all, we need Graph data structure. But ValidationResult cannot produce it because it doesn't have any list member in it like IEnumerable<ValidationResult>. You need a nested list to build a hierarchical structure. See prerequisites below.


    1. GeniusDMValidationResult : inherits the original ValidationResult class.
      In this derived class, declare a ValidationResult list as a member variable.

    2. CheckChildrenAttribute : inherits ValidationAttribute base class.
      This is going to process a single object and list object and return it as a graph.

    3. Sample model for you to test.


Sample models

public class Company { [Required] public string Name { get; set; } [Required] public string Address { get; set; }


// CheckChildren : checks all items if it is an object, checks an item.


[CheckChildren] public List<Department> Departments { get; set; } }


public class Department { [Required] public string Name { get; set; } [Required] public int EmployeeCount { get; set; } [CheckChildren] public List<Employee> Employees { get; set; } }


public class Employee { [Required] public string Name { get; set; } public int Age { get; set; } public int Sex { get; set; } }


CheckChildrenAttribute

public class CheckChildrenAttribute : ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { GeniusDMValidationResult result = new GeniusDMValidationResult(); result.ErrorMessage = string.Format(@"Error occured at {0}", validationContext.DisplayName); IEnumerable list = value as IEnumerable; if (list == null) { // Single Object List<ValidationResult> results = new List<ValidationResult>(); Validator.TryValidateObject(value, validationContext, results, true); result.NestedResults = results; return result; } else { List<ValidationResult> recursiveResultList = new List<ValidationResult>(); // List Object foreach (var item in list) { List<ValidationResult> nestedItemResult = new List<ValidationResult>(); ValidationContext context = new ValidationContext(item, validationContext.ServiceContainer, null); GeniusDMValidationResult nestedParentResult = new GeniusDMValidationResult(); nestedParentResult.ErrorMessage = string.Format(@"Error occured at {0}", validationContext.DisplayName); Validator.TryValidateObject(item, context, nestedItemResult, true); nestedParentResult.NestedResults = nestedItemResult; recursiveResultList.Add(nestedParentResult); } result.NestedResults = recursiveResultList; return result; } } }


GeniusDMValidationResult

public class GeniusDMValidationResult : ValidationResult { public GeniusDMValidationResult() : base("") { } public IList<ValidationResult> NestedResults { get; set; } }


Test

class Program { static void Main(string[] args) { Company company = new Company(); company.Departments = new List<Department> { new Department { Employees = new List<Employee> { new Employee(), new Employee() } }, new Department { Employees = new List<Employee> { new Employee(), new Employee() } }, new Department { Employees = new List<Employee> { new Employee(), new Employee() } } }; List<ValidationResult> results = new List<ValidationResult>(); ValidationContext context = new ValidationContext(company, null, null); Validator.TryValidateObject(company, context, results, true); Console.Read(); } }


Result Sample

If you inspect the result above, it's going to look like this.
    • results = count 3
      • [0] Name field is required
      • [1] Address field is required
      • [2] Departments has errors -> NestedResult = count 3
        • [0] Error occured at Departments -> NestedResult = count 2
          • [0] Name field is required
          • [1] Error occured at Employees -> NestedResult = count 2
            • [0] Name field is required
              .
              .
              .
              .
If you saw my second post of [DataAnnotation] and made a utility and implemented IValidatableObject, the additional results will be come after those results above on 1 depth position. If you've followed everything, now you can see all validation results and build your own business logics accordingly. 

It doesn't look seamless because the Codehighlighter didn't work well and different fonts are here and there... but hope somebody loves it. Thanks.




















Comments