Practical .NET

Separating Validation Code from Business Objects Using DataAnnotations

While you can create classes that contain their own validation code, there are scenarios where it makes sense to separate validation code from the properties it validates using DataAnnotations.

In a recent Practical .NET column, I covered the three interfaces in the .NET Framework that support creating self-validating components --classes that contained their own validation code and integrated with the .NET Framework UI tools.

However, you might prefer not to put your validation code into your business object's class file. There are at least three good reasons for segregating your validation code: It would allow you to test your validation code independently of your class, allow other developers to incorporate your validation logic into other places in their code (rather than where they use your class), and would support UI developers incrementally adding validation to whatever core validation you incorporate into your object (this makes sense if there is some validation related to your class that only applies in some scenarios).

Separating validation code from business classes also reflects actual programming practice. In many cases, your business objects aren't what's used by your UI; instead, developers create a Data Transfer Object (DTO) or ViewModel that represents data from several different business classes and use those classes to drive their application's UI. Separating your validation code from your business class allows UI developers to apply your validation code to their DTOs and ViewModels.

Data Annotations support this separation either by creating your own custom Data Annotation or using the CustomDataAnnotation.

Creating a Custom DataAnnotation
To create your own DataAnnotation object, you add a class that inherits from ValidationAttribute (you'll need a reference to the System.ComponentModel.DataAnnotations library to do this). After that, it's just a matter of overriding the IsValid method with your validation logic; the .NET Framework will take care of passing the value the property is being set to in the method's first parameter (cleverly called value). Your method must return a ValidationResult object containing the error message if an error is found; if no error is found, ValidationResult.Success is returned.

Error messages returned as a ValidationResult from the DataAnnotation are automatically routed to the validation display control in the UI for the property that the Data Annotation is decorating, and to any summary display (e.g. in ASP.NET MVC to an HTML.ValidationMessageFor or an HTML.ValidationSummary).

In theory, if the message applies to multiple items (i.e., for validation tests that compare one property value to another), you can pass the ValidationResult a list of property names and the message will be routed to all of those properties' validation display. In practice, I haven't been able to get this to work, but I haven't really tried, either; repeating the same message in two places on the UI strikes me as a bad idea.

As an example, this DataAnnotation provides a structure that would support all the validation logic required for a CustomerID property on a Customer object:

Public Class CustomerID
    Inherits ComponentModel.DataAnnotations.ValidationAttribute

    Protected Overrides Function IsValid(value As Object, 
         validationContext As
           System.ComponentModel.DataAnnotations.ValidationContext) As
                  System.ComponentModel.DataAnnotations.ValidationResult
	  Dim ErrorMessage As New Text.StringBuilder

          '…validation code that appends error messages to ErrorMessage

        If ErrorMessage.Length > 0 Then
            Dim vr As New ComponentModel.DataAnnotations.
                              ValidationResult(ErrorMessage.ToString)
            Return vr
        Else
            Return ComponentModel.DataAnnotations.
                        ValidationResult.Success
        End If
 
   End Function

There are actually two versions of the IsValid method in the base class you can override: one version that's passed the ValidationContext object and one that's not. The version missing the ValidationContext supports backward compatibility with earlier versions of ASP.NET MVC and can be safely ignored.

The ValidationContext contains several properties that .NET uses to pass information to the method, but the only one I've found useful is the ObjectInstance, which holds a reference to the object that the DataAnnotation is being used in (i.e. the Customer object). You can use the ObjectInstance property to access other members of the class to support validation that involves several properties on the class.

To incorporate your validation logic into your class, you would just decorate your class' property with your DataAnnotation, like this:

<CustomerID()>
Public Property CustomerID() As String

UI developers can now also apply your DataAnnotation to properties in their DTOs or ViewModels.

Complete Separation with CustomValidation
To completely separate your validation logic from any of the frameworks, you can use the DataAnnotations CustomValidation attribute. In this scenario, you simply provide a class with a method that performs the validation and you (or a developer using your code) tie your method to a property using the CustomValidation attribute. When using the CustomValidation attribute, you specify the class and the name of the method containing your validation code, like this:

<ComponentModel.DataAnnotations.CustomValidation(
GetType(CustomerIDValidator), "IsValidForCustomerID")>
Public Property CustomerID() As String

Using the CustomValidation attribute makes sense if the validation code might be used elsewhere than just in your DataAnnotation. However, the method with your validation code has to meet three criteria for it to be used with the CustomValidation attribute. The method must be declared as Static/shared and accept a single parameter -- the value to which the property is being set. The method must also return a ValidationResult object containing the error message if an error is found; or if no error is found, ValidationResult.Success. Here's an example:

Public Class CustomerIDValidator
    Public Shared Function IsValidForCustomerID(value As Object) 
          As ComponentModel.DataAnnotations.ValidationResult
      Dim ErrorMessage As New Text.StringBuilder

       '…validation code

      If ErrorMessage.Length > 0 Then
        Dim vres As New 
          ComponentModel.DataAnnotations.
                      ValidationResult(ErrorMessage.ToString)
        Return vres
      Else
        Return ComponentModel.DataAnnotations.ValidationResult.Success
      End If
    End Function
End Class

Both of these options allow you to separate your validation logic from your class, while still providing the developer using your class with the validation code you expect to be used with your business entity. Even if you expect a developer to use your Data Annotation on the developer's DTO, it's probably still a good idea to decorate your class' property with your Data Annotations. While it means that validation may be done twice (once on when the DTO is updated and again when the developer updates your object from the DTO), it also ensures that your validation is performed. And the point of writing your own validation code for your class is that you can't be too careful.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube