This repository provides various code samples for validating data using data annotations and FluentValidation.
Validation is the first and most important step in securing an application. It prevents the application from processing unwanted inputs that may produce unpredictable results. Couple validation with properly transmitting data to a data source.
When validating data should there be instant feedback? This is subjective, instant feedback will be better when there are many inputs so this would be better than waiting to submit their input. In the case of instant feedback there needs to be events triggered to perform validation while on submit there is a central generic method to perform validation.
Let's take another view of validating on submit, with all types of application which can be created with Visual Studio there are methods which permit displaying messages to assist the user to correct mistakes. So back to subjective, it may come down to if code is written in a team or individual in regards to, are there team rules or it's the Wild West.
This repository contains validation code samples for user input, not validation of data coming from a database or other external sources, for working with databases and external sources that deserves it's own article.
With ASP.NET Core using data annotations the standard for validating a model is with the Validator Class which defines a helper class that can be used to validate objects, properties, and methods when it is included in their associated ValidationAttribute attributes.
The ValidationAttribute class enforces validation, based on the metadata that is associated with the data table. You can override this class to create custom validation attributes.
Example
public class Person
{
public int Id { get; set; }
[RegularExpression("^.{3,}$", ErrorMessage = "{0} Minimum 3 characters required")]
[Required(ErrorMessage = "{0} Required")]
[StringLength(30, MinimumLength = 3, ErrorMessage = "Invalid {0}")]
public string FirstName { get; set; }
[RegularExpression("^.{3,}$", ErrorMessage = "{0} Minimum 3 characters required")]
[Required(ErrorMessage = "{0} Required")]
[StringLength(30, MinimumLength = 3, ErrorMessage = "Invalid {0}")]
public string LastName { get; set; }
[ValidateYears(ErrorMessage = "Valid range for BirthDate is from {0} to {1}")]
[Required(ErrorMessage = "{0} Required")]
public DateTime BirthDate { get; set; }
}
Property | Attribute | Description |
---|---|---|
FirstName | RegularExpression | Minimum 3 characters required |
Required | Must have a value couple with above, minimum of three characters | |
StringLength | Min of three characters, max of 30 characters | |
LastName | RegularExpression | Minimum 3 characters required |
Required | Must have a value couple with above, minimum of three characters | |
StringLength | Min of three characters, max of 30 characters | |
BirthDate | ValidateYears | Custom attribute for range of years |
To validate an instance of Person
, set up a unit test method where all properties are valid.
[TestMethod]
[TestTraits(Trait.Annotations)]
public void ValidPerson()
{
// arrange
Person person = new ()
{
FirstName = "Mike",
LastName = "Flowers",
BirthDate = new DateTime(1932, Now.Month, Now.Day)
};
// act
EntityValidationResult result = Model.Validate(person);
// assert
Check.That(result.IsValid).IsTrue();
}
Next setup another test method, in this case BirthDate is out of range.
[TestMethod]
[TestTraits(Trait.Annotations)]
public void InvalidDateValidPerson()
{
// arrange
DateTime date = new (2022, 4, 27);
Person person = new ()
{
FirstName = "Mike",
LastName = "Flowers",
BirthDate = new DateTime(1931, date.Month, date.Day)
};
// act
EntityValidationResult result = Model.Validate(person);
// assert
Check.That(result.IsValid).IsFalse();
}
In both test, the following code validates an instance of person
EntityValidationResult result = Model.Validate(person);
Followed by asking if the instance of person
is valid by IsValid
method. Check is from NFluent.
Check.That(result.IsValid).IsFalse();
- All code to validate are in a class project in this solution under BaseDataValidatorLibrary.
- For a list of stock validation attributes see System.ComponentModel.DataAnnotations Namespace.
There is a simple project taken from the following repository. In the project NetCoreUnitTestProject there are unit test against the model in this project.
https://github.com/karenpayneoregon/wpf-login-annotations-cs.git
Some inputs use default validation while others have custom rules.
In markup, the following uses asp-validation-for
tag helper, see Validation Tag Helpers documentation for more information on usage..
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
<!-- <snippet_ReleaseDate> -->
<div class="form-group">
<label asp-for="Movie.ReleaseDate" class="control-label"></label>
<input asp-for="Movie.ReleaseDate" class="form-control" />
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
<!-- </snippet_ReleaseDate> -->
<div class="form-group">
<label asp-for="Movie.Description" class="control-label"></label>
<input asp-for="Movie.Description" class="form-control" />
<span asp-validation-for="Movie.Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Price" class="control-label"></label>
<input asp-for="Movie.Price" class="form-control" />
<span asp-validation-for="Movie.Price" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Genre" class="control-label"></label>
<select asp-for="Movie.Genre" asp-items="@(Html.GetEnumSelectList<Genre>())" class="form-control"></select>
<span asp-validation-for="Movie.Genre" class="text-danger"></span>
</div>
<div class="form-group">
<div class="checkbox">
<input asp-for="Movie.PreOrder" />
<label asp-for="Movie.PreOrder"></label>
</div>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
In code behind the model Movies
is setup as follows BindProperty.
BindPropertyAttribute An attribute that can specify a model name or type of IModelBinder to use for binding the associated property.
[BindProperty]
public Movie Movie { get; set; }
In the Post action, ModelState.IsValid validates or invalidates properties in the model.
Model state represents errors that come from two subsystems: model binding and model validation. Errors that originate from model binding are generally data conversion errors.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movies.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
That's pretty much the basics for ASP.NET Core.
The following sections will demonstrate using mocked data in unit test methods.
The best method to test validating models is in unit test methods, devoid of user interfaces.
Simple example, we have a Book class, wired up for
For Book
class these are how each property is setup with attributes for validation.
Property | Required | Uses custom attribute |
---|---|---|
Title | ✓ | ✗ |
ISBN | ✓ | ✗ |
BookCategory | ✓ | ✓ |
NotesList | ✓ | ✓ |
BookCategory
is an enum
public enum BookCategory
{
Select = 1,
SpaceTravel = 2,
Adventure = 3,
Romance = 4,
Sports = 5,
Automobile = 6
}
Which uses a custom custom attribute inheritied by RequiredAttribute to validate a property enum is used.
public class RequiredEnumAttribute : RequiredAttribute
{
public override bool IsValid(object sender)
{
if (sender == null)
{
return false;
}
var type = sender.GetType();
return type.IsEnum && Enum.IsDefined(type, sender); ;
}
}
Notes
uses the following class to validate an instance of a Book
has at least one note.
public class ListHasElements : ValidationAttribute
{
public override bool IsValid(object sender)
{
if (sender == null)
{
return false;
}
if (sender.IsList())
{
var result = ((IEnumerable)sender).Cast<object>().ToList();
return result.Any();
}
else
{
return false;
}
}
}
The Book class
should reside in a business class project while any custom attribute classes
should reside in a common class project suitable for many projects to utilize rather than in a single project/application.
For this Visual Studio solution
- All
validation methods
, extension methods and custom attribute classes are inBaseDataValidationLibrary
- All
Classes/models
reside in the projectBaseModelsLibrary
Let's look at testing a Book. There is a test class BooksTest marked as a partial class, the test are in the root folder of the test project while the second part is in the Base folder.
In the base folder we setup a valid Book instance for use in test methods in the root folder.
First test
Validates the model using valid data
[TestMethod]
[TestTraits(Trait.Annotations)]
public void ValidateBook_Good_Test()
{
// arrange
Book book = TheBook;
// act
EntityValidationResult validationResult = Model.Validate(book);
// assert
Check.That(validationResult.HasError).IsFalse();
}
Second test
Ensure that the model is not valid as the Category
property is null
.
[TestMethod]
[TestTraits(Trait.Annotations)]
public void ValidateBook_NoCategory_Test()
{
// arrange
Book book = TheBook;
book.Category = null;
const string expected = "Category is required";
// act
EntityValidationResult result = Model.Validate(book);
// assert
Check.That(result.Errors.Any(validationResult =>
validationResult.ErrorMessage!.Contains(expected)))
.IsTrue();
}
Third test
Ensure that the model is not valid as the ISBN
property is null
.
[TestMethod]
[TestTraits(Trait.Annotations)]
public void ValidateBook_NoIsbn_Test()
{
// arrange
Book book = TheBook;
book.ISBN = "";
const string expected = "ISBN is required";
// act
EntityValidationResult result = Model.Validate(book);
// assert
Check.That(result.Errors.Any(validationResult =>
validationResult.ErrorMessage!.Contains(expected)))
.IsTrue();
}
That is it although we do not have full coverage for all paths the idea is to give the reader an idea how to get started. Consider writing more test against the book model for practice.
FluentValidation is a validation library for .NET, used for building strongly typed validation rules for business objects.
Fluent validations use a Fluent interface and lambda expressions to build validation rules.
Customer class
public class Customer : CustomerLogin
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public decimal CreditLimit { get; set; }
public int Discount { get; set; }
public bool HasDiscount { get; set; }
public string Address { get; set; }
public string Postcode { get; set; }
public string Pin { get; set; }
public string SocialSecurity { get; set; }
public DateTime BirthDate { get; set; }
public override string ToString() => $"{FirstName} {LastName}";
}
Validator for Customer class/model
Rules
Some rules are not realistic, they are here to show what is possible.
- Id must be in the range from 1 to 10
- FirstName can not be empty and valid length is 3 to 10 characters
- LastName can not be empty and valid length is 3 to 20 characters
- Email must be a valid email address
- Discount is only valid if
HasDiscount
property is set to true - CreditLimit` must be less than $9,999
- Postcode must be in the method
HasValidPostcode
list of postal codes - Pin is converted from a string to an int and can not be greater than 8888
- SocialSecurity uses over the top validation using regular expressions in a language extension method.
- BirthDate can not be less than 01/01/1932
public class CustomerValidator : AbstractValidator<Customer>
{
public CustomerValidator()
{
RuleFor(customer => customer.Id)
.InclusiveBetween(1, 10);
RuleFor(customer => customer.FirstName)
.NotEmpty()
.Length(3, 10)
.WithMessage("Please specify a first name");
RuleFor(customer => customer.LastName)
.NotEmpty()
.Length(3, 20)
.WithMessage("Please specify a last name");
RuleFor(customer => customer.Email)
.EmailAddress();
RuleFor(customer => customer.Discount)
.NotEqual(0)
.When(customer => customer.HasDiscount);
RuleFor(customer => customer.CreditLimit)
.LessThanOrEqualTo(9999);
RuleFor(customer => customer.Address)
.MaximumLength(250);
RuleFor(customer => customer.Postcode)
.Must(HasValidPostcode)
.WithMessage("Please specify a valid postcode");
Transform(from: customer => customer.Pin, to: value =>
int.TryParse(value, out int result) ? (int?)result : null)
.GreaterThan(8888);
Transform(
from: customer => customer.SocialSecurity,
to: value => value.IsSSNValid()).Must(value => value);
RuleFor(customer => customer.BirthDate).GreaterThan(new DateTime(1932,1,1));
}
private static bool HasValidPostcode(string postcode)
{
List<string> list = new() { "97301", "97223", "97209", "97146", "97374", "97734" };
var result = list.FirstOrDefault(item => item == postcode);
return result is not null;
}
}
Validating a Customer, as in the Book example we setp a valid customer in the base class for the test class.
public partial class MainTest
{
private CustomerValidator CustomerValidator;
/// <summary>
/// Perform initialization before test runs using assertion on current test name.
/// </summary>
[TestInitialize]
public void Initialization()
{
CustomerValidator = new CustomerValidator();
}
/// <summary>
/// Perform any initialize for the class
/// </summary>
/// <param name="testContext"></param>
[ClassInitialize()]
public static void ClassInitialize(TestContext testContext)
{
TestResults = new List<TestContext>();
}
public static Customer ValidCustomer => new Customer
{
Id = 1,
FirstName = "Karen",
LastName = "Payne",
BirthDate = new DateTime(1956,9,24),
Pin = "8889",
Email = "kp@gmail.com",
CreditLimit = 9999,
Discount = 10, // TODO
SocialSecurity = "205-55-1234",
HasDiscount = true,
Address = "101 Microsoft Way",
Postcode = "97209"
};
}
Then in the main test class, unit test for customers.
There are several methods used for validation.
- To check if a model is valid o rnot
- Check.That(result.IsValid).IsTrue();
- result.ShouldHaveValidationErrorFor(customer => customer.FirstName);
- result.ShouldHaveValidationErrorFor(customer => customer.Email);
[TestClass]
public partial class MainTest : TestBase
{
[TestMethod]
[TestTraits(Trait.FluentValidation)]
public async Task CleanCustomerTest()
{
// arrange
Customer thisCustomer = ValidCustomer;
// act
var result = await CustomerValidator.ValidateAsync(thisCustomer);
// assert
Check.That(result.IsValid).IsTrue();
}
[TestMethod]
[TestTraits(Trait.FluentValidation)]
public async Task CustomerNoFirstNameNoLastNameTest()
{
// arrange
Customer thisCustomer = ValidCustomer;
thisCustomer.FirstName = "";
thisCustomer.LastName = "";
// act
var result = await CustomerValidator.TestValidateAsync(thisCustomer);
// assert
result.ShouldHaveValidationErrorFor(customer => customer.FirstName);
result.ShouldHaveValidationErrorFor(customer => customer.LastName);
}
[TestMethod]
[TestTraits(Trait.FluentValidation)]
public async Task CustomerBadEmailTest()
{
// arrange
Customer thisCustomer = ValidCustomer;
thisCustomer.Email = "karenGmail";
// act
var result = await CustomerValidator.TestValidateAsync(thisCustomer);
// assert
result.ShouldHaveValidationErrorFor(customer => customer.Email);
}
[TestMethod]
[TestTraits(Trait.FluentValidation)]
public async Task CustomerInvalidPostalCodeTest()
{
// arrange
Customer thisCustomer = ValidCustomer;
thisCustomer.Postcode = "99999";
// act
var result = await CustomerValidator.TestValidateAsync(thisCustomer);
// assert
result.ShouldHaveValidationErrorFor(customer => customer.Postcode);
}
[TestMethod]
[TestTraits(Trait.FluentValidation)]
public async Task CustomerInvalidPrimaryKeyTest()
{
// arrange
Customer thisCustomer = ValidCustomer;
thisCustomer.Id = 0;
// act
var result = await CustomerValidator.TestValidateAsync(thisCustomer);
// assert
result.ShouldHaveValidationErrorFor(customer => customer.Id);
}
[TestMethod]
[TestTraits(Trait.FluentValidation)]
public async Task CustomerInvalidPinTest()
{
// arrange
Customer thisCustomer = ValidCustomer;
thisCustomer.Pin = "5555"; // must be greater than 8888
// act
var result = await CustomerValidator.TestValidateAsync(thisCustomer);
// assert
result.ShouldHaveValidationErrorFor(customer => customer.Pin);
}
[TestMethod]
[TestTraits(Trait.FluentValidation)]
public async Task CustomerBirthDateInvalidTest()
{
// arrange
Customer thisCustomer = ValidCustomer;
thisCustomer.BirthDate = new DateTime(1930, 1, 1);
// act
var result = await CustomerValidator.TestValidateAsync(thisCustomer);
// assert
result.ShouldHaveValidationErrorFor(customer => customer.BirthDate);
}
[TestMethod]
[TestTraits(Trait.FluentValidation)]
public async Task CustomerSocialSecurityNumberInvalidTest()
{
// arrange
Customer thisCustomer = ValidCustomer;
thisCustomer.SocialSecurity = "219-09-9999";
// act
var result = await CustomerValidator.TestValidateAsync(thisCustomer);
// assert
result.ShouldHaveValidationErrorFor(customer => customer.SocialSecurity);
}
}
✋ 04/25/2022 version in branch net-core-version includes C# .NET Core versions of original work done in .NET Framework.
See master branch
Microsoft TechNet .NET: Defensive data programming (Part 4) Data Annotation