.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
- Add the validator library to your solution, make sure the version of the framework matches your project's framework version.
- Setup a business logic project similar to the one used in this code sample found here.
- Implement logic to perform data operations to check if a user exists, store new users.
- 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