Skip to content
  • iImagine
  • Register
  • Log In

Web Development School

Learning made easy.

  • Books
    • Beginning Web Development with ASP.Net Core & Client-Side Technologies
      • TOC
      • Part 1
        • Chapter 1: Static HTML – Designing the landing page
      • Part 2
        • Chapter 2: ASP.Net Core – Let’s talk Dynamic
        • Chapter 3: Introduction to ASP.Net Core MVC
          [ASP.Net Core v9]
      • Part 4
        • Chapter 7: Using Server Side & Client Side technologies together
          [ASP.Net Core v7 & Angular 15]
  • Environment Setup
    • Installing Angular
    • Installing Visual Studio 2022
    • Installing SQL Server 2022 Express
    • Installing Postman
    • Installing Git for Windows
  • Blog
  • iImagine WebSolutions
  • Events
  • Learning Videos
  • Toggle search form

Validation

Currently, the MVC Core web application is using default server-side validation. In this module we are going to start using custom validation by applying validation attributes which will be applied to both client-side and server-side validation.

Table Of Contents
  1. Data Annotation Attributes/Annotations
    • Column annotation
      • SQL Server money Data Type
    • DataType annotation
  2. Validation Attributes
  3. Digging into Default Server-Side validation
  4. Understanding Custom Server-Side Validation
    • [Required] example
      • Update the edit form
  5. Client-Side Validation
    • Fix a failing unit test from the Vehicle model modification
    • [StringLength] example
    • [Range] example
    • [RegularExpression] example
  6. A Custom Validator example
  7. What's Next

Data Annotation Attributes/Annotations

We first looked at Data Annotation Attributes or, Annotations, back in module 12 when we applied the Column and DataType attributes to the Price column in the Vehicle model.

 [Column(TypeName = "decimal(9, 2)")]
 [DataType(DataType.Currency)]
 public decimal Price { get; set; }

Column annotation

The Column attribute is used above with its TypeName attribute to modify the database schema to use an SQL Server decimal data type with a total of nine digits with two digits after the decimal point and got rid of the error:

No store type was specified for the decimal property ‘Price’ on entity type ‘Movie’.
This will cause values to be silently truncated if they do not fit in the default precision and scale.
Explicitly specify the SQL server column type that can accommodate all the values in ‘OnModelCreating’ using ‘HasColumnType’, specify precision and scale using ‘HasPrecision’, or configure a value converter using ‘HasConversion’.

Column is in the System.ComponentModel.DataAnnotations.Schema namespace since it alters the database schema. Notice Schema as the last part of the pathway in the namespace here.

SQL Server money Data Type

We could probably keep the data type at decimal(9, 2) for the Vehicle.Price property and it would be fine. But there is actually a more accurate SQL Server data type to represent currency called money. In the next step we will change the data type and update the db schema with a migration.

Update the Vehicle class with the code shown below.

FredsCars\Models\Vehicle.cs

[Column(TypeName = "money")]
[DataType(DataType.Currency)]
[Required(ErrorMessage = "Please enter a price")]
[Range(1000, 500000, ErrorMessage = "Price must be between $1,000 and $500,000.")]
public decimal Price { get; set; }

If the application is running in a console window, stop it with Ctrl-C. In a console navigate to the FredsCars project folder and run the following command.

dotnet ef migrations add VehiclePriceToMoney

After running the command you may see an error in the console window that says:

An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.

You can ignore this error because money is actually more accurate for currency then a decimal in SQL Server.

Now run the following command to update the database.

dotnet ef database update

Now if we check the db schema we will see the Price data type is now money.

The behavior of the application remains the same after the change.

DataType annotation

The DataType annotation was used along with its DataType attribute which is an enumeration set here to currency. This tells the browser to display the decimal price as currency with the ‘$’ character in front of it.

[DataType(DataType.Currency)]

The DataType attribute specifies a datatype that is more specific then the database intrinsic type. For instance [DataType(DataType.Date)] would tell the browser to display a Date picker for a DateTime property rendered as an input control. These hints only work though for modern browsers that can understand HTML5.

There are many more interesting DataType enumeration values to choose from as you can see from Intellisense in the following screenshot.

Since DataType does not change the database schema it is in the System.ComponentModel.DataAnnotations namespace. Notice there is no schema at the end of the pathway for this namespace. now we have both namespaces in the Vehicle model for all other annotations we will look at.

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

Using the DataType.Price enumeration value of the DataType attribute also brings us back to the notion of DRY (Do Not Repeat Yourself), the software engineering principle we have looked at several times in this book starting from way back in Chapter one. We only have to specify to render this property as currency in one place, the model, for it to take affect through out the whole application. It will render as currency in the results and details pages. It will not, however render with the ‘$’ character in the create and edit pages because we wouldn’t want that character to show up in input controls for price when creating or editing a Vehicle.

Validation Attributes

The DataType and Column attributes do not provide any validation. But there are plenty out of the box Validation attributes in ASP.Net Core and you can even create custom Validation attributes.

The five core validation attributes are:

  1. [Required] : Ensures that a field is not null or empty.
    Example:

    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; }

    —————————————–
  2. [StringLength] : Specifies the minimum and maximum length of a string.
    Example:

    [StringLength(100, MinimumLength = 5,
    ErrorMessage = "Length must be between 5 and 100")]
    public string Description { get; set; }

    —————————————–
  3. [Range] : Specifies a numeric range constraint.
    Example:

    [Range(1, 100, ErrorMessage = "Age must be between 1 and 100")]
    public int Age { get; set; }

    —————————————–
  4. [Compare] : Validates that two properties match (commonly used for passwords).
    Example:

    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Compare("Password", ErrorMessage = "Passwords do not match")]
    [DataType(DataType.Password)]
    public string ConfirmPassword { get; set; }

    —————————————–
  5. [RegularExpression] : Validates that the value matches a specified regular expression. pattern.
    Example:

    [RegularExpression(@"^[A-Z][a-z]*$",
    ErrorMessage = "Name must start with a capital letter")]
    public string FirstName { get; set; }

ASP.Net Core and modern .Net have also added new Validation attributes like EmailAddress and Phone. Here is a list of the newer validation attributes:

6. [EmailAddress] : Validates that the value is in a valid email format.
Example:

[EmailAddress(ErrorMessage = "Invalid email address format.")]
public string Email { get; set; }

—————————————–

7. [Phone] : Validates that the value is a valid phone number format.
Example:

[Phone(ErrorMessage = "Invalid Phone format.")]
public string Phone { get; set; }

—————————————–

8. [Url] : Validates that the value is a valid Url.
Example:

[Url(ErrorMessage = "Invalid Url format.")]
public string Url { get; set; }

—————————————–

9. [CreditCard] : Validates that the value is in a valid credit card number format.
Example:

[CreditCard(ErrorMessage = "Invalid CreditCard format.")]
public string CreditCard { get; set; }

Digging into Default Server-Side validation

As stated earlier, the MVC web app is currently using default server-side validation. Let’s take a closer look at this.

Navigate to https://localhost:40443/Vehicles/Create, do not enter any values, and click the Create button. Your browser should look similar to the following.

Let’s trace through what happens with status to get an idea of what goes on behind the scenes with default server-side validation.

If your application is running in a console window stop it now by typing Ctrl+C at the command prompt. You might have to do it twice sometimes.

Now, go to the VehiclesController file and put a break point on the following line in the POST Create action method:

if (ModelState.IsValid)

Hit the F5 key or click the Green Arrow button at the top of the VS IDE to run the project in debug mode.

A new console window will open and run the application in debug mode and a new browser window will open up at https://localhost:40443. Click the Create New link to navigate to the Create page.

Again click the Create button.

Back in Visual Studio, execution will stop at the line we put the break point on.

We have halted execution at the beginning of an if-block which checks it the state of the model, an incoming Vehicle, is valid.

Put your curser on ModelState in the if/then statement and type Shift+F9 to open the quick watch window.

In the QuickWatch window for ModelState, you can see that the validation state for Status is Invalid, as well as most of the other properties.

Click the arrow next to Status and then expand all the way down to Value->Errors->[0].

In the above screen shot we can see that the attempted value is “<--Select One -->” and the error message is “The value '<--Select One-->' is not valid for Status“.

Why is this happening? We have not even dressed the Status property in the Vehicle model with any validation attributes yet. This behavior actually surprised me as having programmed MVC since 2010. This default behavior did not used to exist until more recent versions of MVC Core.

What is happening here is that Status is an enum in our model and can only have the values “New”, or “Used”. The value, “<--Select One -->“, for our default selection of the DropDown Select control in the view is not a valid option. So, ASP.Net Core sees this as invalid for that property.

public enum Status
{
    New,
    Used
}

public class Vehicle
{
    public int Id { get; set; }
    public Status Status { get; set; }

Also notice that Year, Make, Color, Model, and VIN all give Validation messages that they are required fields.

This has to do with a feature introduced in C# 8 called nullable references.

Starting with C# 2.0 we already had nullable value types (int?, bool?) and default server-side validation did exist for these types. Honestly I never noticed this behavior until writing this module because I always applied all of my validation explicitly.

But, C# 8.0 also introduced nullable reference types (string?). And since Year, Make, Color, Model, and VIN are all string types, they trigger the default validation for an implicit [Required] attribute due to not being nullable. They are all string types, not string? (or nullable string).

In ASP.NET MVC (pre-Core), validation was usually attribute-driven only. If you didn’t add [Required], the framework didn’t assume anything. ASP.NET Core is more opinionated and tries to help prevent invalid states by default.

Meanwhile, back in the View, our Status form group has a Validation tag helper.

<div class="mb-3">
    <label asp-for="Status" class="form-label fw-bold"></label>
    <Select asp-for="Status" class="form-select">
        <option id=""><-- Select One --></></option>
        <option id="0">New</option>
        <option id="1">Used</option>
    </Select>
    <span asp-validation-for="Status" class="text-danger"></span>
</div>

The validation tag helper here knows what message to display during server-side validation from model state errors contained in the model for the view when we re-render the form in the POST Create action method due to an invalid model state. This is different then client-side validation, which we will look at next, where validation tag helpers understand what message to display from data- (data dash) attributes embedded in the HTML of the control they validate.

Understanding Custom Server-Side Validation

We can take control over the validation error messages being displayed by using validation attributes like [Required] and [StringLength] to customize both validation rules and their error messages.

For instance, rather then The value ‘<– Select One –>’ is not valid for Status for Status and The Year field is required for Year, I’d rather see Please select a Status for Status and Year required for Year.

[Required] example

Let’s apply our first validation attribute to the Vehicle model. But first we need to make a small change to the Select tag helper html in the View. Make the modification shown below.

FredsCars\FredsCars\Models\Vehicle.cs

... existing code ...
<div class="mb-3">
    <label asp-for="Status" class="form-label fw-bold"></label>
    <Select asp-for="Status" class="form-select">
        <option value =""><-- Select One --></></option>
        <option value ="0">New</option>
        <option value ="1">Used</option>
    </Select>
    <span asp-validation-for="Status" class="text-danger"></span>
</div>
... existing code ...

The value attribute will make sure that "" or null gets sent as the value to the model binder in the controller. Otherwise the text node’s value, “<– Select One –>” will get sent as the value and fire the invalid value error message short circuiting the required validation error.

Now, make the following modification in the Vehicle model.

FredsCars\Models\Vehicle.cs

... existing code ...
public class Vehicle
{
    public int Id { get; set; }
    [Required(ErrorMessage = "Please select a Status")]
    public Status? Status { get; set; }
... existing code ...

In the code above we have dressed the Status property with the Required validation attribute. Required is an ASP.Net Core attribute but it is also a class. And that class itself has an attribute called ErrorMessage. And we use the ErrorMessage attribute to set the error message for a Required validation error to a custom string, “Please select a Status”.

We also need to make the Status property nullable with the "?" character following the type in order for the model binder to accept the null option value from the Select tag helper.

Now if you restart the application, go to the create page, leave Status set to the default option of “<– Select One –>, and click the Create button, you will see the validation error message, “Please select a Status”.

Let’s apply the same fix to Category.

FredsCars\Views\Vehicles\Create.cshtml

... existing code ...
<div class="mb-3">
    <label asp-for="VehicleTypeId" class="form-label fw-bold"></label>
    <select asp-for="VehicleTypeId"
            asp-items="@ViewBag.VehicleTypeList"
            class="form-select">
        <option value =""><-- Select One --></></option>
    </Select>
    <span asp-validation-for="VehicleTypeId" class="text-danger"></span>
</div>
... existing code ...

FredsCars\Models\Vehicle.cs

... existing code ...
// Foriegn Key to VehicleType entity/table row
[Display(Name = "Category")]
[Required(ErrorMessage = "Please select a Category")]
public int? VehicleTypeId { get; set; }
... existing code ...

Update the edit form

Let’s apply the same fix to the edit form to update our validation messages in that piece of UI.

FredsCars\Views\Vehicles\Edit.cshtml

... existing code ... 
<div class="mb-3">
     <label asp-for="Status" class="form-label fw-bold"></label>
     <Select asp-for="Status" class="form-select">
         <option value =""><-- Select One --></></option>
         <option value="0">New</option>
         <option value="1">Used</option>
     </Select>
     <span asp-validation-for="Status" class="text-danger"></span>
 </div>
... existing code ...
<div class="mb-3">
    <label asp-for="VehicleTypeId" class="form-label fw-bold"></label>
    <Select asp-for="VehicleTypeId"
            asp-items="@ViewBag.VehicleTypeList"
            class="form-select">
        <option value=""><-- Select One --></></option>
    </Select>
    <span asp-validation-for="VehicleTypeId" class="text-danger"></span>
</div>
... existing code ...

Client-Side Validation

In this section we are going to wire up our client-side validation.

When we first created our project using the Empty Web project template ASP.Net Core placed a partial view with the filename _ValidationScriptsPartial.cshtml in the Views/Shared folder. If it doesn’t exist go ahead and create it now and fill it with the code below.

FredsCars\Views\Shared_ValidationScriptsPartial.cshtml

<script src="~/lib/jquery/jquery.min.js"></script>
<script src="~/lib/jquery-validation/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

These are the three files we need for the client-side validation to take affect.

  • jquery.min.js
  • jquery.validate.min.js
  • jquery.validate.unobtrusive.min.js

Make sure jquery.min.js comes first in the partial view. It must be loaded first for jquery.validate and jquery.validate.unobtrusive to reference.

But these files currently do not exist in the wwwroot directory so we need to install the packages using libman like we did earlier on for Bootstrap.

Open a command prompt and point it to the FredsCars project directory and run the following commands.

libman install jquery --destination wwwroot/lib/jquery
libman install jquery-validate --destination wwwroot/lib/jquery-validation
libman install jquery-validation-unobtrusive --destination wwwroot/lib/jquery-validation-unobtrusive

We should now see all of our packages installed under the www/lib directory.

The last thing we need to do is call the partial view from the create and edit pages to load the scripts.

Add this line to the bottom of the Create and Edit views.

FredsCars\Views\Vehicles\Create.cshtml
and
FredsCars\Views\Vehicles\Edit.cshtml

... existing code ...
</form>

<partial name="_ValidationScriptsPartial" />

Now if you run the application in debug mode, go to the Create page, do not fill in any data, and press the Create button, you will see all of the error messages. But the breakpoint will not be hit. So we know we now have client-side validation configured correctly.

But, if a user disables JavaScript in their browser, server-side validation will kick in. Otherwise client-side validation will prevent a wasted call to the server.

Also, now client-side and JavaScript validation will use the data-dash attributes embedded in all the form control’s HTML.

For instance, the Status Select DropDown control has the following HTML.

<select class="form-select"
	data-val="true"
	data-val-required="Please select a Status" id="Status"
	name="Status">
	<option value="">&lt;-- Select One --&gt;</option>
	<option value="0">New</option>
	<option value="1">Used</option>
</select>

The data-val attribute is set to true notifying jQuery-validation that this form control should have its validation rules enforced. And, the data-val-required attribute notifies jQuery-validation what message to display if the Required validation rule is broken.

Since the Year, Price, Make, Model, and Color properties are all non-nullable, they already give validation required error messages like, “The Year field is required.” I would prefer to be explicit and apply [Required(ErrorMessage="custom message"] to these properties and we will in later sections of this module.

Fix a failing unit test from the Vehicle model modification

After modifying the Vehicle model and making the Status and VehicleTypeId required fields with the Required data annotations, if we do not supply both a Status and VehicleTypeId value in the test data, the model binder will fail and cause the Can_Update_Vehicle unit test in the VehiclesControllerTests class to also fail.

We were already supplying values for VehicleTypeId in our unit test’s test data. But, we were not supplying a Status. So modify the test with the following.

FredsCars.Tests\Controllers\VehiclesControllerTests.cs

... existing code ...
{
    public class VehiclesControllerTests 
    {
    
        ... existing code ...

        [Fact]
        public async Task Can_Update_Vehicle()
        {
            // Arrange - create in-memory database options for DbContext
            string dbName = $"FredCars-{Guid.NewGuid().ToString()}";
            var options = new DbContextOptionsBuilder<FredsCarsDbContext>()
                    .UseInMemoryDatabase(dbName)
                    .Options;

            // Arrange - create DbContext
            using (var context = new FredsCarsDbContext(options))
            {
                // Arrange - add test data to vehicle types repo
                await context.VehicleTypes.AddRangeAsync(
                    new VehicleType
                    {
                        Id = 1,
                        Name = "Cars"
                    },
                    new VehicleType
                    {
                        Id = 2,
                        Name = "Trucks"
                    },
                    new VehicleType
                    {
                        Id = 3,
                        Name = "Jeeps"
                    }
                );
                
                // Arrange - add test data to vehicle repo
                await context.Vehicles.AddRangeAsync(
                    new Vehicle
                    {
                        Id = 1,
                        Status = Status.Used,
                        Make = "Make1",
                        Model = "Model1",
                        VehicleTypeId = 1
                    },
                    new Vehicle
                    {
                        Id = 2,
                        Status = Status.Used,
                        Make = "Make2",
                        Model = "Model2",
                        VehicleTypeId = 2
                    },
                    new Vehicle
                    {
                        Id = 3,
                        Status = Status.New,
                        Make = "Make3",
                        Model = "Model3",
                        VehicleTypeId = 3
                    }
                );
                await context.SaveChangesAsync();
            }

        ... existing code ...
    }
}

The test should now be in a passing state.

[StringLength] example

The StringLength attribute sets the maximum length in the database. It can also be used to optionally set a minimum length however this setting has no affect on the database schema. Keep in mind that StringLength does not prevent a user from entering white space.

Say we want to limit the amount of characters a user can enter for Make and Model to 50 characters.

Let’s take a look at the current schema. In the Visual Studio IDE go to SSOX and right click on the Vehicle table in the FredsCarsMvc database and select View Designer. If SSOX is not open select View-> SQL Server Object Explorer from the top menu in the VS IDE.

In the Design window for the Vehicle Table, we can see that the string type for the Make and Model properties in the Vehicle model map to the SQL Server datatype NVarChar(Max) and do not allow nulls. The N in NVarChar means it will except all Unicode characters rather than just from the ASCII character set. And Max means it will accept any number of chars up to the Max a NVarChar will hold, which in SQL Server is up to 2 GB of storage.


  • Since nvarchar uses 2 bytes per character (for most Unicode characters), nvarchar(max) can store approximately 1 billion characters.
  • However, some Unicode characters (Supplementary Characters) use two byte-pairs, which means they take up 4 bytes of storage. In such cases, the number of characters that can be stored will be less than 1 billion. 

Modify the Vehicle model with the code shown below.

FredsCars\Models\Vehicle.cs

... existing code ...
public class Vehicle
    {
        public int Id { get; set; }
        [Required(ErrorMessage = "Please select a Status")]
        public Status? Status { get; set; }
        public string Year { get; set; } = string.Empty;
        [StringLength(50, MinimumLength = 3,
            ErrorMessage = "Make must be between 5 and 100 characters")]
        [Required(ErrorMessage = "Please enter a Make")]
        public string Make { get; set; } = string.Empty;
        [StringLength(50, MinimumLength = 3,
            ErrorMessage = "Model must be between 5 and 100 characters")]
        [Required(ErrorMessage = "Please enter a Model")]
        public string Model { get; set; } = string.Empty;
        public string Color {  get; set; } = string.Empty;
        [Column(TypeName = "decimal(9, 2)")]
        [DataType(DataType.Currency)]
        public decimal Price { get; set; }
        public string VIN { get; set; } = string.Empty;
        public string? ImagePath { get; set; }

        // Foriegn Key to VehicleType entity/table row
        [Display(Name = "Category")]
        [Required(ErrorMessage = "Please select a Category")]
        public int? VehicleTypeId { get; set; }
        // Entity Framework Navigation Property
        [Display(Name = "Category")]
        //public VehicleType VehicleType { get; set; } = null!;
        public VehicleType? VehicleType { get; set; }
    }
... existing code ...

In the code above we have applied the StringLength and Required validation attributes to the Make and Model properties.

For StringLength we have set the max amount of characters allowed to 50 and the optional min amount of chars setting to 3. We have also supplied a nice custom message stating these rules if violated.

We already know how Required works and we applied it here again for Make and Model to take control over the error message.


The next thing we need to do is create and run a migration since StringLength needs to alter the db schema. It needs to alter the SQL Server datatype for Make and Model from NVarChar(Max) to NVarChar(50).

If the application is running from a console window stop it by entering Ctrl+C and then type in and run the following command.

dotnet ef migrations add MakeAndModelStringLengths

This is our forth migration so lets dig a little deeper into the process for this one. We named this migration MakeAndModelStringLengths. So a file gets created in the Migrations folder named
[DateTime]_MakeAndModelStringLengths.cs.
For example, mine is named:
20250724113028_MakeAndModelStringLengths.cs
where the date is 2025-07-24, or June 24th, 2025. And the Time is 11:30:28 or eleven thirty and 28 seconds. This format is based on a standard .Net DateTime format.

Open the migration file and you see a partial class named MakeAndModelStringLengths that inherits from the Migration class. This class has two methods; an Up method that will apply the migration to the database schema, and a Down method that can be used to remove the changes the Up method applies.

The Up method receives a MigrationBuilder as a parameter to alter the db schema.

protected override void Up(MigrationBuilder migrationBuilder)
{
    ...
}

The MigrationBuilder is used to make the alterations to the db schema via its AlterColumn method.

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterColumn<string>(
        name: "Model",
        table: "Vehicle",
        type: "nvarchar(50)",
        maxLength: 50,
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)");

    migrationBuilder.AlterColumn<string>(
        name: "Make",
        table: "Vehicle",
        type: "nvarchar(50)",
        maxLength: 50,
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)");
}

In the code above there are two AlterColumn statements. One for Model and one for Make. Let’s look at the one for model. The AlterColumn method here takes in seven named paramters.

  • name: "Model" -> The name of the column to modify.
  • table: "Vehicle" -> The name of the table with the column to modify.
  • type: "nvarchar(50)" -> The datatype to change the column to.
  • maxLength: 50 -> The max length of characters the NVarChar datatype will be able to hold.
  • nullable: false -> Specifies the column will remain not nullable.
  • oldClrType: typeof(string) -> Specifies the C# type of the model property before the change takes affect.
  • oldType: "nvarchar(max) -> Specifies the SQL Server datatype of the column before the change takes affect.

Now run the following command at the command prompt pointing to the FredsCars project folder to update the database.

dotnet ef database update

Reopen the Design window from SSOX for the Vehicle Table and you will see the change has been applied.

The Make and Model columns of the Vehicle table are now both SQL Server datatypes of NVarChar(50).

Restart the application and navigate to the Create page at https://localhost:40443/Vehicles/Create. Do not enter any data or make any selections and click the Create button.

The Make and Model validation show our custom Required error messages:
Please enter a [Make | Model].

Now enter less than 3 chars, our minlength, and you will see the StringLength validation error messages.

Now try to enter more then 50 chars, our maxlength, for Make and Model and you will see the browser prevents entering any characters once a length of 50 is reached.

Let’s inspect the HTML of the Model input control after having applied the StringLength validation on the Model property in the data model. Right click on the Model control and select Inspect.

You will be brought to the HTML for the Model input form control in the web development tools.

Here we can see the HTML of the Model input control.

<input class="form-control input-validation-error"
 type="text"
 data-val="true"
 data-val-length="Model must be between 5 and 100 characters"
 data-val-length-max="50"
 data-val-length-min="3"
 data-val-required="Please enter a Model"
 id="Model"
 maxlength="50"
 name="Model"
 value=""
 aria-describedby="Model-error"
 aria-invalid="true">

Now the server renders the data-val-* attributes in the HTML of the input control needed for client-side validation of the StringLength attribute by jQuery.

  • data-val-length: shows the error message displayed when minimum or maximum character length rules are violated.
  • data-val-length-max: the max amount of characters allowed in order to submit the form.
  • data-val-length-min: the min amount of characters allowed in order to submit the form.

The maxlength attribute is also now rendered from the server and matches data-val-length-max, in this case 50, again our max amount of chars allowed.

maxlength is an HTML5 attribute that prevents a user from entering more than the specified value, again 50 in our case, of characters into the input form control. So the only way to see the dat-val-length error message in a browser is to enter below the min amount of chars like we saw in one of the above example screen shots.

If we did not set the optional MinimumLength setting for StringLength to 3 we would never see the custom error message for StringLength from a browser. maxLength prevents the user from entering more than 50 chars and there would be no minimum.

Keep in mind however that maxLength is not foolproof. Someone such as a hacker determined to enter malicious code or data could bypass maxlength by sending POST requests via Postman or Fiddler. So server-side validation should always be there incase client-side validation fails or is absent.

[Range] example

In this section we are going to set a range of values a user can enter for the price of a Vehicle.

Make the modifications to the Vehicle class shown below.

FredsCars\Models\Vehicle.cs

... existing code ... 
[Column(TypeName = "decimal(9, 2)")]
[DataType(DataType.Currency)]
[Required(ErrorMessage = "Please enter a price")]
[Range(1000, 500000, ErrorMessage = "Price must be between $1,000 and $500,000.")]
public decimal Price { get; set; }
... existing code ...

We have added a Required attribute to price so now the error message when no price is entered is, “Please enter a price”.

Also, if we try to enter non-numeric characters we get the message, “The field Price must be a number”.

The new Range validator sets the range of price between 1,000 and 500,000 with a custom error message. So if we enter a value outside of that scope, we get the error message, “Price must be between $1,000 and $500,000”.

Let’s look at the HTML rendered by the Range validation attribute from the server.

<input class="form-control"
	type="text"
	data-val="true"
	data-val-number="The field Price must be a number."
	data-val-range="Price must be between $1,000 and $500,000."
	data-val-range-max="500000"
	data-val-range-min="1000"
	data-val-required="Please enter a price"
	id="Price"
	name="Price"
	value="">

The new data-val-* html attributes are:

  • data-val-range: the custom message to be displayed for a range error.
  • data-val-range-max: the maximum value a user is allowed to enter.
  • data-val-range-min: the minimum value a user is allowed to enter.

[RegularExpression] example

In this section we will use a RegularExpression validation attribute to set the boundaries of allowed years between 1900 and 2100.

Make the following changes to the Vehicle class.

FredsCars\Models\Vehicle.cs

... existing code ...
[RegularExpression(@"^(19[0-9]{2}|20[0-9]{2}|2100)$",
    ErrorMessage = "Please enter a four digit year between 1900 and 2100.")]
[Required(ErrorMessage = "Please enter a Year")]
public string Year { get; set; } = string.Empty;
... existing code ...

In the regular expression above: ^(19[0-9]{2}|20[0-9]{2}|2100)$,
‘^’ marks the beginning of the expression and ‘$’ marks the end.
The allowed values in parenthesis are:

  • 19[0-9]{2}: 19 followed by any 2 digits from 0 to 9.
  • ‘|‘ OR
  • 20[0-9]{2}: 20 followed by any 2 digits from 0 to 9.
  • ‘|‘ OR
  • 2100: the literal string 2100.

Restart the application and navigate to the Create page at https://localhost:40443/Vehicles/Create.

Enter into the Year control characters instead of numbers like “abv” or a four digit year outside of the allowed boundaries like 1879. You should see our custom error message for the RegularExpression attribute.

Notice we also added a Required attribute to the Year field as well as the RegularExpression annotation.

Let’s look at the HTML rendered by the ReqularExpression attribute.

<input class="form-control"
	type="text"
	data-val="true"
	data-val-regex="Please enter a four digit year between 1900 and 2100."
	data-val-regex-pattern="^(19[0-9]{2}|20[0-9]{2}|2100)$"
	data-val-required="Please enter a Year"
	id="Year"
	name="Year"
	value="">

Here, the new data-val-* html attributes are:

  • data-val-regex: the custom message to be displayed for a RegularExpression error.
  • data-val-regex-pattern: the regular expression used to verify the users input for year as valid.

A Custom Validator example

In the last several sections we have looked at examples using some of the built-in validation attribute annotations; specifically Required, StringLength, Range, and RegularExpression. But there will be times when none of the built-in options will quite do the job or be exactly what we need. In these cases we can build our own custom validation attribute.

In the last section we applied a RegularExpression attribute to the Year property of the Vehicle model. In this section we are going to replace the built-in validation attribute with our own custom validation attribute.


Create a folder in the root of the FredsCars project named Infrastructure. In the new Infrastructure folder, create a subfolder named Attributes. And in Attributes create another subfolder named Validation. In the new Validation folder create a new class file named YearRangeAttribute.cs and fill it with the code shown below.

FredsCars\Infrastructure\Attributes\Validation\YearRangeAttribute.cs

using System.ComponentModel.DataAnnotations;

namespace FredsCars.Infrastructure.Attributes.Validation
{
    public class YearRangeAttribute : ValidationAttribute
    {
        private readonly int _minYear;
        private readonly int _maxYear;

        public YearRangeAttribute(int minYear = 1900, int maxYear = 2100)
        {
            _minYear = minYear;
            _maxYear = maxYear;
            ErrorMessage = $"Please enter a four digit year between {_minYear} and {_maxYear}.";
        }

        public override bool IsValid(object? value)
        {
            var str = value as string;

            // let [Required] handle nulls
            if (string.IsNullOrWhiteSpace(str)) return true;

            if (int.TryParse(str, out int year))
            {
                return year >= _minYear && year <= _maxYear;
            }

            return false;
        }
    }
}

In the code above we have a class named YarRangeAttribute which inherits from ValidationAttribute.

public class YearRangeAttribute : ValidationAttribute
{
    ...
}

The naming convention for an attribute class in C# is [class name]Attribute. But when we use it in code we do not include Attribute in the class name as we shall see.

Next we declare two class level private fields of type int named _minYear and _maxYear. These field’s values get set in the constructor to the values of two incoming parameters named minYear and maxYear respectively. minYear and maxYear are optional parameters and have default values of 1900 and 2100 also respectively.

private readonly int _minYear;
private readonly int _maxYear;

public YearRangeAttribute(int minYear = 1900, int maxYear = 2100)
{
    _minYear = minYear;
    _maxYear = maxYear;
    ErrorMessage = $"Please enter a four digit year between {_minYear} and {_maxYear}.";
}

Notice in the constructor we also set the value of the ErrorMessage string property of the base ValidationAttribute class to a custom error message.

ErrorMessage = $"Please enter a four digit year between {_minYear} and {_maxYear}.";

Next we override the IsValid method of the base ValidationAttribute class.

public override bool IsValid(object? value)
{
    ...
}

The isValid method takes in one parameter of type nullable object (object?) and returns a value of type bool.

The first line in isValid delcares a variable named str and assigns it the value of the incoming object parameter named value after converting it to a string with the ‘as’ C# keyword.

var str = value as string;

If the value of the incoming object parameter named value is null (or just white space) we just return true and let the Required attribute handle it.

// let [Required] handle nulls
if (string.IsNullOrWhiteSpace(str)) return true;

If the incoming object parameter’s value is not null, we continue on and try to parse the string via the int.TryParse method into an inline out variable of type int named year.

if (int.TryParse(str, out int year))
{
    return year >= _minYear && year <= _maxYear;
}

If the TryParse method fails we return false.


Now we just need to apply the new custom YearRange validation attribute to the Year property in the Vehicle class.

Make the following change to the Vehicle class in the code shown below.

FredsCars\Models\Vehicle.cs

//[RegularExpression(@"^(19[0-9]{2}|20[0-9]{2}|2100)$",
//    ErrorMessage = "Please enter a four digit year between 1900 and 2100.")]
[Required(ErrorMessage = "Please enter a Year")]
[YearRange(1900, 2100)]
public string Year { get; set; } = string.Empty;

In the code above we commented out the RegularExpression attribute and applied the new Custom YearRange attribute.

Now restart the application and navigate to https://localhost:40443/Vehicles/Create.

Fill in the create form with valid data for all of the fields except for Year. In the Year field enter either alpha-characters instead of a four digit year or a four digit year outside the boundaries of 1900 and 2200 and click the Create button.

The Create form will be re-rendered with the custom validator’s custom error message.

Custom validators are only caught on the Server side when they make the model invalid. It is possible to make the custom validation attribute work on the client-side as well but this is not a trivial process so I have always just made custom validation server-side since they are rarely needed in my experience.

Creating a custom validation attribute for the Year property is not the greatest example as the RegularExpression Validator would do the job. But I wanted to take you through the process of creating one so you would be aware of custom validation. I mostly use custom validation for corner cases where on a form if a user selects an option in one Select DropDown, another Select DropDown‘s value must be a specific value out of its own set of options.


Let’s finish up this module by slapping on some Required attributes to Color and VIN to make their error message match more closely the rest of the required form fields.

Make the modifications to the Vehicle class shown below.

FredsCars\Models\Vehicle.cs

... existing code ...
[Required(ErrorMessage = "Please enter a Color")]
public string Color {  get; set; } = string.Empty;

[Column(TypeName = "decimal(9, 2)")]
[DataType(DataType.Currency)]
[Required(ErrorMessage = "Please enter a price")]
[Range(1000, 500000, ErrorMessage = "Price must be between $1,000 and $500,000.")]
public decimal Price { get; set; }

[Required(ErrorMessage = "Please enter a VIN#")]
public string VIN { get; set; } = string.Empty;
... existing code ...

Again, restart the application, navigate to https://localhost:40443/Vehicles/Create, leave the Color and VIN form fields blank, and click the Create button.

The new custom error messages appear for Color and VIN and match the other required fields with a message like:

Please enter a [form label name]


The following is the complete code for the Vehicle model with all of the changes in this module.

FredsCars\Models\Vehicle.cs

using FredsCars.Infrastructure.Attributes.Validation;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace FredsCars.Models
{
    public enum Status
    {
        New,
        Used
    }
    
    public class Vehicle
    {
        public int Id { get; set; }
        
        [Required(ErrorMessage = "Please select a Status")]
        public Status? Status { get; set; }

        //[RegularExpression(@"^(19[0-9]{2}|20[0-9]{2}|2100)$",
        //    ErrorMessage = "Please enter a four digit year between 1900 and 2100.")]
        [Required(ErrorMessage = "Please enter a Year")]
        [YearRange(1900, 2100)]
        public string Year { get; set; } = string.Empty;
        
        [StringLength(50, MinimumLength = 3,
            ErrorMessage = "Make must be between 5 and 100 characters")]
        [Required(ErrorMessage = "Please enter a Make")]
        public string Make { get; set; } = string.Empty;
        
        [StringLength(50, MinimumLength = 3,
            ErrorMessage = "Model must be between 5 and 100 characters")]
        [Required(ErrorMessage = "Please enter a Model")]
        public string Model { get; set; } = string.Empty;

        [Required(ErrorMessage = "Please enter a Color")]
        public string Color {  get; set; } = string.Empty;
        
        [Column(TypeName = "money")]
        [DataType(DataType.Currency)]
        [Required(ErrorMessage = "Please enter a price")]
        [Range(1000, 500000, ErrorMessage = "Price must be between $1,000 and $500,000.")]
        public decimal Price { get; set; }

        [Required(ErrorMessage = "Please enter a VIN#")]
        public string VIN { get; set; } = string.Empty;

        public string? ImagePath { get; set; }

        // Foriegn Key to VehicleType entity/table row
        [Display(Name = "Category")]
        [Required(ErrorMessage = "Please select a Category")]
        public int? VehicleTypeId { get; set; }
        // Entity Framework Navigation Property
        [Display(Name = "Category")]
        //public VehicleType VehicleType { get; set; } = null!;
        public VehicleType? VehicleType { get; set; }
    }
}

You can read more about validation at https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-9.0

What’s Next

In this module we learned about validation in ASP.Net Core MVC which will be largely the same in the next chapter on ASP.Net Core Razor Pages.

We looked at how the default Server-Side validation works, how to take more control of validation rules and error messages with custom Server-Side validation, and how to apply Client-Side validation while still maintaining Server-Side validation just in case a user disables JavaScript in their browser or a hacker tries to insert bad data into our system.

In the next module we will set up a logging system to catch any application errors once in production. That way we will have some clues to go on when users report errors and glitches.

< Prev
Next >

Leave a ReplyCancel reply

Chapter 1: Static HTML – Designing the landing page.

  • Static HTML – Designing the landing page.
  • Let’s get started!
  • Mock your site with HTML
  • Make CSS easy with Bootstrap
  • Mock your content
  • Introducing JavaScript
  • JavaScript Code Improvements
  • Results Data
  • Images and the HTML Image Element.
  • Revisiting Reusability for CSS and JavaScript
  • Reuse for HTML: PART 1
  • Reuse for HTML: PART 2
  • Details Page – Using a Bootstrap Component
  • Creating Links
  • Chapter One Conclusion

Chapter 2: ASP.Net Core – Let’s talk Dynamic

  • Introduction to ASP.Net Core
  • What is .Net?
  • What is ASP.Net
  • Introduction to Entity Framework Core

Chapter 3: ASP.Net MVC Core – Models, Views, and Controllers [ASP.Net Core v9]

  • Introduction to ASP.Net Core MVC
  • Create the project: ASP.Net Core MVC
  • Explore the ASP.Net Core Empty Web Project Template
  • Configure the Application for MVC
  • Create a Controller: Home Controller
  • Create a View: Index View for the Home Controller
  • Install Bootstrap using Libman
  • Create the Layout template
  • Create the Model
  • Install EF Core & Create the Database
  • Seed the Database: Loading test data
  • DI (Dependency Injection): Display a List of Vehicles
  • Repository Pattern: The Vehicles Repo
  • Unit Test 1: Home Controller Can Use Vehicle Repository
  • Unit Test 2: Vehicle Repository Can Return List
  • Add the ImagePath Migration and Thumbnail images to results
  • Pagination: Create a Custom Tag Helper
  • Sorting
  • Category Filter
  • Partial View: Break out the vehicle results
  • View Component: Create dynamic category buttons
  • Create the Details page
  • Create the Create Page
  • Create the Update Page
  • Create the Delete Page
  • Validation
  • Logging & Configuration
  • Storing Secrets
  • Error Handling
  • Security & Administration

Chapter 7: Using Server Side & Client Side technologies together. [ASP.Net Core v7 & Angular v15]

  • Intro to Full Stack Development
  • Fred’s Cars – Full Stack Development
  • Prepare the environment
  • Create the Visual Studio Solution
  • Add the ASP.Net Core Web API project
  • Add the Angular Project
  • Wire it up!
  • WeatherForecast: Understanding the basics
  • Vehicles API Controller: Mock Data
  • Vehicles Angular Component: Consuming Data
  • Routing and Navigation
  • Using a Component Library: Angular Material
  • Our first Angular Material Component: MatToolbar
  • Configuring for Saas: CSS with superpowers
  • Create the Header & Footer components
  • Displaying Results with MatTable
  • Loading: Using a Progress Spinner
  • MatTable: Client-Side Paging and Sorting
  • MatSidenav: Create a Search Sidebar
  • MatCheckbox: Category Search UI
  • Adding an image to the welcome page
  • Create the database with Entity Framework Core migrations
  • MatPaginator & PageEvent: Custom Server-Side Paging
  • Unit Testing: Custom Server-Side Paging
  • Repository Pattern: VehicleRepository
  • Unit Test: Paging in the Vehicles controller
  • Server-Side Sorting
  • Unit Tests: Sorting
  • Filter (Quick Search)
  • Unit Tests: Filter feature
  • Advanced Search: Categories
  • Unit Tests: Search by category
  • Progress Spinner: Final Fix

TOC

  • What were WebForms?
  • Enter MVC
    • Understanding MVC
    • Advantages of MVC
  • ASP.Net Core MVC – A total rewrite
  • ASP.Net Core 2 MVC – Here come Razor Pages
    • Understanding Razor Pages
  • ASP.Net Core 3 – Dropping the MVC reference
    • Understanding Blazor
  • Dropping the MVC reference
  • Hello .Net 5!
  • What’s Next? – Here comes .Net 6.

Recent Posts

  • Angular Commands Cheat Sheet
  • Installing Git for Windows
  • Installing Postman
  • Installing SQL Server 2022 Express
  • Installing Visual Studio 2022

Recent Comments

No comments to show.

Archives

  • November 2023
  • October 2023
  • June 2023
  • October 2021

Categories

  • Angular
  • ASP.Net
  • Environment Setup
  • See All
  • SQL Server
  • Visual Studio
  • Web API & Rest Services

WordPress Theme Editor

Copyright © 2025 Web Development School.

Powered by PressBook Blog WordPress theme