다음을 통해 공유


.NET: Defensive data programming Part 4 (a) Data Annotation

Introduction

In this article which is an extension of .NET: Defensive data programming (Part 4) Data Annotation article which discusses using provided methods for validating input entered by users in a Windows form application using System.ComponentModel.DataAnnotations Namespace standard attributes along with custom attribute for when the standard attributes do not fit business logic  a walkthrough for registering a user to a application with specific properties which include credit card data which after validating user input would perform additional (which is not covered here as credit card providers vary) validation to a validation service typically done by a bank or service such as PayPal.

Sample screen

Control selection

For this screen, all inputs utilize TextBox controls while credit card information uses a custom TextBox which only allows numeric data while retrieving the credit card information the data is returned as a string since returning as Integer the leading zero (if present) would be lost. Numeric data most likely when sent to a validation service will be transmitted as strings via XML or JSON so this is okay to validate as numeric then use as strings. If a developer has no concern for leading zeros then adjustments may be made to accommodate their requirements.

Setting up validation

In the article on data annotations the following class was used to for showing, in this case, ensuring that a password and confirm password matched which is a common requirement in many applications.

Imports System.ComponentModel.DataAnnotations
Imports BasicClassValidation.Rules
 
Namespace Classes
    Public Class  CustomerLogin
 
        <Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
        <MaxLength(12, ErrorMessage:="The {0} can not have more than {1} characters")>
        Public Property  Name() As  String
 
        ''' <summary>
        ''' Disallow date to be a weekend date
        ''' </summary>
        ''' <returns></returns>
        <WeekendDateNotPermitted>
        Public Property  EntryDate() As  Date
 
        <Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
        <StringLength(12, MinimumLength:=6)>
        Public Property  Password() As  String
        <Compare("Password", ErrorMessage:="Passwords do not match, please try again")>
        <StringLength(12, MinimumLength:=6)>
        Public Property  PasswordConfirmation() As String
 
    End Class
End Namespace

 
Usually, to protect someone for guessing a user's password a common practice is to require upper-case characters, include at least one numeric and one special character. The following class provides a place to set rules as per uppercased, numeric and special chars.

Imports System.ComponentModel.DataAnnotations
Imports System.Text.RegularExpressions
 
Namespace Rules
    ''' <summary>
    ''' Specialized class to validate a password
    ''' </summary>
    Public Class  PasswordCheck
        Inherits ValidationAttribute
 
        Public Overrides  Function IsValid(value As Object) As  Boolean
            Dim validPassword = False
            Dim reason = String.Empty
            Dim password As String  = If(value Is  Nothing, String.Empty, value.ToString())
 
            If String.IsNullOrWhiteSpace(password) OrElse  password.Length < 6 Then
                reason = "new password must be at least 6 characters long. "
            Else
                Dim pattern As New  Regex("((?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%]).{6,20})")
                If Not  pattern.IsMatch(password) Then
                    reason &= "Your new password must contain at least 1 symbol character and number."
                Else
                    validPassword = True
                End If
            End If
 
            If validPassword Then
                Return True
            Else
                Return False
            End If
 
        End Function
 
    End Class
End Namespace

To implement the above on the password properties.

''' <summary>
''' User password
''' </summary>
''' <returns></returns>
<Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
<StringLength(20, MinimumLength:=6)>
<PasswordCheck(ErrorMessage:="Must include a number and symbol in {0}")>
Public Property  Password() As  String
 
''' <summary>
''' Confirmation of user password
''' </summary>
''' <returns></returns>
<Compare("Password", ErrorMessage:="Passwords do not match, please try again"),
    DataType(DataType.CreditCard)>
<StringLength(20, MinimumLength:=6)>
Public Property  PasswordConfirmation() As String

For email addresses, there is a standard under EmailAddressAttribute class which as with passwords may not suit a developer's requirements which like password validation a developer can discard the standard DataTypeAttribute.EmailAddressAttribute  to using in this case regular expressions.

<Required(ErrorMessage:="The Email address is required")>
<RegularExpression("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}",
                   ErrorMessage:="Invalid Email address")>
Public Property  EmailAddress() As  String

Note on email validation
There are many regular expressions for performing email validation, the one presented above is simply one of them. Another option is to tell the user to look for an email which the application would send then when the user response validates this manually (which is old school) or create a service which will perform validation if the user responded within a specific time period.

For working with other types such as credit card the standard CreditCardAttribute Class may not be enough so this code sample includes a special class for performing validation against the card number but is only the first step as once the credit card information, card number, secret code, expire date are obtained a call would need to be made to a credit card company or bank to fully validate the card.

Here are the properties to collect and first step validation to send to service to validate.

<ValidatorLibrary.Rules.CreditCard(
    AcceptedCardTypes:=ValidatorLibrary.Rules.CreditCardAttribute.CardType.Visa Or
                       ValidatorLibrary.Rules.CreditCardAttribute.CardType.MasterCard)>
Public Property  CreditCardNumber() As String
 
<Required(ErrorMessage:="Credit card expire month required")>
<Range(1, 12, ErrorMessage:="{0} is required")>
Public Property  CreditCardExpireMonth() As Integer
 
<Required(ErrorMessage:="Credit card expire year required")>
<Range(2019, 2022, ErrorMessage:="{0} is not valid {1} to {2} are valid")>
Public Property  CreditCardExpireYear() As Integer
 
<Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
<StringLength(3, MinimumLength:=3)>
Public Property  CreditCardCode() As  String

Here is the full class with all required properties that are required mark as required and with special validation rules.

Imports System.ComponentModel.DataAnnotations
Imports ValidatorLibrary.Rules
 
Namespace Entities
    Public Class  CustomerLogin
 
        ''' <summary>
        ''' User name
        ''' </summary>
        ''' <returns></returns>
        <Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
        <StringLength(20, MinimumLength:=6)>
        Public Property  UserName() As  String
 
        <Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
        Public Property  FirstName() As  String
 
        <Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
        Public Property  LastName() As  String
 
        Public ReadOnly  Property FullName() As String
            Get
                Return $"{FirstName} {LastName}"
            End Get
        End Property
 
        ''' <summary>
        ''' User password
        ''' </summary>
        ''' <returns></returns>
        <Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
        <StringLength(20, MinimumLength:=6)>
        <PasswordCheck(ErrorMessage:="Must include a number and symbol in {0}")>
        Public Property  Password() As  String
 
        ''' <summary>
        ''' Confirmation of user password
        ''' </summary>
        ''' <returns></returns>
        <Compare("Password", ErrorMessage:="Passwords do not match, please try again"),
            DataType(DataType.CreditCard)>
        <StringLength(20, MinimumLength:=6)>
        Public Property  PasswordConfirmation() As String
 
        ''' <summary>
        ''' Validate email address
        ''' </summary>
        ''' <returns></returns>
        ''' <remarks>
        ''' We use regular expressions rather than using DataType(DataType.EmailAddress)
        ''' for more control.
        ''' </remarks>
        <Required(ErrorMessage:="The Email address is required")>
        <RegularExpression("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}",
                           ErrorMessage:="Invalid Email address")>
        Public Property  EmailAddress() As  String
 
        <ValidatorLibrary.Rules.CreditCard(
            AcceptedCardTypes:=ValidatorLibrary.Rules.CreditCardAttribute.CardType.Visa Or
                               ValidatorLibrary.Rules.CreditCardAttribute.CardType.MasterCard)>
        Public Property  CreditCardNumber() As String
 
        <Required(ErrorMessage:="Credit card expire month required")>
        <Range(1, 12, ErrorMessage:="{0} is required")>
        Public Property  CreditCardExpireMonth() As Integer
 
        <Required(ErrorMessage:="Credit card expire year required")>
        <Range(2019, 2022, ErrorMessage:="{0} is not valid {1} to {2} are valid")>
        Public Property  CreditCardExpireYear() As Integer
 
        <Required(ErrorMessage:="{0} is required"), DataType(DataType.Text)>
        <StringLength(3, MinimumLength:=3)>
        Public Property  CreditCardCode() As  String
    End Class
End Namespace

Implementing capturing data from a user in a form using TextBox controls. Note each import statement points to a class project used in the Visual Studio solution included in the source code.

DataLibraryMocked is used to provide sample data in a text file rather than from a database as using a database is only required for a production application. The data is read from the text file when the registration is done the only true check is to see if the user name is in the system if so they are prompted for a different name, otherwise, all field is validated using the class above.
 

Imports WindowsFormsLibrary
Imports BusinessLibrary.Entities
Imports DataLibraryMocked
Imports ValidatorLibrary.LanguageExtensions
Imports ValidatorLibrary.Validators
 
Public Class  LoginForm
    Private _retryCount As Integer  = 0
 
    Private dataOperations As New  DataOperations
 
    Private Sub  LoginButton_Click(sender As Object, e As  EventArgs) _
        Handles LoginButton.Click
 
        '
        ' Create an instance of CustomerLogin
        '
        Dim login As New  CustomerLogin With
                {
                    .UserName = UserNameTextBox.Text,
                    .Password = PasswordTextBox.Text,
                    .PasswordConfirmation = PasswordConfirmTextBox.Text,
                    .EmailAddress = EmailTextBox.Text,
                    .FirstName = FirstNameTextBox.Text,
                    .LastName = LastNameTextBox.Text,
                    .CreditCardNumber = CreditCardTextBox.Text,
                    .CreditCardExpireMonth = ExpireMonthTextBox.AsInteger,
                    .CreditCardExpireYear = ExpireYearTextBox.AsInteger,
                    .CreditCardCode = CreditCardCode.Text
                }
 
        '
        ' Perform all required validation
        '
        Dim validationResult As EntityValidationResult = ValidationHelper.ValidateEntity(login)
 
        If validationResult.HasError Then
 
            '
            ' After three tries deny access, in real life there
            ' may be a method to contact the owner of the app.
            '
            If _retryCount >= 3 Then
                MessageBox.Show("Guards toss them out!")
                Close()
            End If
 
            '
            ' Show the validation issues
            '
            MessageBox.Show(validationResult.ErrorMessageList())
            _retryCount += 1
 
        Else
            '
            ' Here current users are read from a plain text file, in a real app this
            ' information would come from a database e.g. SLQ-Server, MS-Access, Oracle etc
            '
            dataOperations.ReadUsers()
 
            '
            ' Check if user name exists
            '
            Dim testIfUserNameExist = dataOperations.Dictionary.ContainsValue(UserNameTextBox.Text)
            If testIfUserNameExist Then
                MessageBox.Show("User name already exist, please select a different user name")
                Exit Sub
            End If
 
            '
            ' User name is available, add them to the mocked data in text file
            '
            dataOperations.Dictionary.Add(dataOperations.Dictionary.Keys.Max() + 1, UserNameTextBox.Text)
            dataOperations.Save()
 
            '
            ' Show what would be the main form of an application
            '
            Dim f As New  MainForm(login.FullName)
            f.Show()
            Hide()
        End If
 
    End Sub
End Class

Implementation in your project

  1. Add the validator library to your solution, make sure the version of the framework matches your project's framework version.
  2. Setup a business logic project similar to the one used in this code sample found here.
  3. Implement logic to perform data operations to check if a user exists, store new users.
  4. For the custom numeric TextBox use, the one provided in this code sample found here and note it is a basic custom numeric TextBox which also prevents pasting data from the windows clipboard.

Summary

This article has expanded on the prior article which taught the basics of working with data annotations for validating data inputted by users rather than using assertion which is usually performed in a form which means if this logic is needed in another form or project the code must be copied to the other form(s) while this method for validation is portable, usable in one or more projects. Another benefit is if and when a developer moves to web solutions this knowledge can be used in these solutions such as ASP.NET MVC and ASP.NET Core which have even more functionality which has been slightly gone over in the prior article

Now that this has been learned it will be easy to use as is or adapt to your specific business logic needs.

See also

.NET: Defensive data programming (Part 4) Data Annotation 
.NET: Defensive data programming (Part 3) 
.NET: Defensive data programming (Part 2)   
.NET: Defensive data programming (part 1) 
Adding Validation to the Model (VB) 
VB.NET Writing better code Part 2

Source code

https://github.com/karenpayneoregon/ClassValidationVisualBasic1