Custom Data Annotation in ASP.NET Core MVC

How to Create Custom Data Annotation Attribute in ASP.NET Core MVC

In this article, I will discuss How to Create Custom Data Annotation Attributes in ASP.NET Core MVC Applications with Real-Time Examples. Please read our previous article discussing Data Annotation Attributes in ASP.NET Core MVC.

What is Custom Data Annotation Attribute in ASP.NET Core MVC?

In ASP.NET Core MVC, a Custom Data Annotation Attribute creates a custom validation rule that can be applied to model properties. If the Built-in Data Annotation Attribute does not fulfill your validation requirements, then we need to create a Custom Validation Attribute as per our business requirement.

Custom Data Annotation Attribute Example in ASP.NET Core MVC

Let us understand how to create a custom data annotation attribute for the “Date of Joining” property, ensuring that the data provided meets certain criteria, such as the Joining Date is not a future date. It should be a past date or today

Create the Custom Attribute Class

First, you need to create a class for the custom attribute. This class will inherit from ValidationAttribute, which is part of the System.ComponentModel.DataAnnotations namespace and override the IsValid method to provide your custom validation logic. So, create a class file named ValidJoiningDateAttribute.cs and copy and paste the following code. This class overrides the IsValid method to check whether the date is in the future and returns a ValidationResult accordingly.

using System.ComponentModel.DataAnnotations;

namespace DataAnnotationsDemo.Models
{
    public class ValidJoiningDateAttribute : ValidationAttribute
    {
        public ValidJoiningDateAttribute()
        {
            ErrorMessage = "The joining date cannot be in the future.";
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (value is DateTime)
            {
                DateTime joiningDate = (DateTime)value;
                if (joiningDate > DateTime.Now)
                {
                    return new ValidationResult(ErrorMessage);
                }
            }
            return ValidationResult.Success;
        }
    }
}
Code Explanation:

The ValidJoiningDateAttribute class extends the ValidationAttribute class provided by the .NET framework. ValidationAttribute is a base class used to create custom validation attributes that can be applied to model properties.

Constructor:

The constructor initializes the new attribute and sets the ErrorMessage property inherited from ValidationAttribute. This message is displayed when the validation fails.

IsValid Method:

This method overrides the IsValid method of the base ValidationAttribute class. It is called automatically when the framework performs model validation. The parameters are:

  • value: This is the value of the property to which the attribute is applied. 
  • validationContext: This provides context about the validation operation, such as the object being validated.
Validation Logic:
  • The validation logic first checks if the value being validated is of type DateTime. If it is, the code casts value to DateTime and assigns it to joiningDate.
  • Next, it checks if joiningDate is greater than DateTime.Now (i.e., it checks if the date is in the future). If this condition is true, the method returns a new ValidationResult containing the error message specified in the constructor. This indicates that the validation has failed.
Successful Validation:
  • If the value is not a DateTime or the joiningDate is not in the future, the method returns ValidationResult.Success indicates that the validation passed successfully.
Apply the Attribute to a Model Property

Now that you have created the custom validation attribute, you can apply it to properties in your model classes. So, create a class file named Employee.cs and copy and paste the following code. Here, you can see we have applied the Custom ValidJoiningDate Attribute to the DateOfJoining property.

using System.ComponentModel.DataAnnotations;

namespace DataAnnotationsDemo.Models
{
    public class Employee
    {
        [Required(ErrorMessage = "Date of Joining is required")]
        [DataType(DataType.Date, ErrorMessage = "Invalid Date Format")]
        [ValidJoiningDate(ErrorMessage ="Date of Joining Cannot be Future Date")]
        public DateTime DateOfJoining { get; set; }
    }
}
Modify the Employee Controller:

Next, modify the Employee Controller as follows. Please ensure the model state is checked for validity in your action methods before processing the data.

using DataAnnotationsDemo.Models;
using Microsoft.AspNetCore.Mvc;
namespace DataAnnotationsDemo.Controllers
{
    public class EmployeeController : Controller
    {
        public IActionResult Create()
        {
            return View();
        }

        [HttpPost]
        public IActionResult Create(Employee employee)
        {
            if (ModelState.IsValid)
            {
                //Save the Data into the Database
                //Redirect to a Different View
                return RedirectToAction("Successful");
            }

            return View(employee);
        }

        public string Successful()
        {
            return "Employee Added Successfully";
        }
    }
}
Create.cshtml View:

Next, add Create.cshtml view and copy and paste the following code. Ensure that you use HTML helpers like Html.ValidationMessageFor() or Tag Helper like asp-validation-for to display validation messages.

@model DataAnnotationsDemo.Models.Employee

@{
    ViewData["Title"] = "Create";
}

<h1>Create Employee</h1>

<div class="row">
    <form asp-controller="Employee" asp-action="Create" method="post" class="mt-3">
        
        <div class="form-group row">
            <label asp-for="DateOfJoining" class="col-sm-2 col-form-label"></label>
            <div class="col-sm-10">
                <input asp-for="DateOfJoining" class="form-control" placeholder="Please Enter Your UserName/LoginName">
                <span asp-validation-for="DateOfJoining" class="text-danger"></span>
            </div>
        </div>
        
        <div class="form-group row">
            <div class="col-sm-10">
                <button type="submit" class="btn btn-primary">Create</button>
            </div>
        </div>
    </form>
</div>
@section Scripts {
    @{
        await Html.RenderPartialAsync("_ValidationScriptsPartial");
    }
}
Test Your Application

Finally, test your application to ensure the custom validation attribute works as expected. Try submitting a form with a future date for the “Date of Joining” field and verify that the error message is displayed, as shown in the image below.

How to Create Custom Data Annotation Attribute in ASP.NET Core MVC

By following these steps, we have successfully created and applied a custom data annotation attribute in ASP.NET Core MVC, specifically for validating a “Date of Joining” property. This not only ensures data integrity but also enhances your application’s robustness. The above validation attribute only works on the server side, not on the client side. Let us proceed and see how to enable client-side validation.

How Client-Side Validation Works in ASP.NET Core MVC?

Before understanding how to enable Client-Side Validation in ASP.NET Core MVC for Custom Data Annotation Attributes, let’s first understand how client-side validation works in ASP.NET Core MVC when using Data Annotation.

When we use Razor syntax to create form fields linked to model properties decorated with data annotation attributes, the ASP.NET Core MVC Framework automatically generates HTML elements with data attributes that describe the validation rules. The jQuery Validate Unobtrusive library reads these attributes.

When we add a data annotation to a model property, the ASP.NET Core MVC tag helpers will automatically generate the necessary data-val-* attributes to represent the validation rules for that property. Let us take the following class. Here, we have applied the Required Data Annotation attribute with the DateOfJoining model property.

public class Employee
{
    [Required(ErrorMessage = "Date of Joining is required")]
    public DateTime DateOfJoining { get; set; }
}

We can then use ASP.NET Core MVC tag helpers in our Razor view to generate the HTML as follows:

<input asp-for="DateOfJoining" class="form-control">
<span asp-validation-for="DateOfJoining" class="text-danger"></span>

Once you run the application and view the Page source, then you will see the Razor Engine will generate the following HTML code behind the scenes:

<input class="form-control" type="datetime-local" data-val="true" data-val-required="Date of Joining is required" id="DateOfJoining" name="DateOfJoining" value="">
<span class="text-danger field-validation-valid" data-valmsg-for="DateOfJoining" data-valmsg-replace="true"></span>

Understanding the data-* Attributes:

  • data-val=”true”: This attribute signifies that the input element has validation rules to be enforced. Its presence enables jQuery Unobtrusive Validation to handle this input for validation purposes.
  • data-val-required=”Date of Joining is required”: This attribute is used for the ‘required’ validation rule. It indicates that the input field cannot be left empty. The string “Date of Joining is required” is the error message displayed when the validation fails (i.e., if the field is submitted without a value).
  • data-valmsg-for=”DateOfJoining”: This attribute links the span element to the DateOfJoining input field as its validation message container. When the input field’s validation state changes, this span is updated with the appropriate validation message.
  • data-valmsg-replace=”true”: This attribute tells jQuery Validation Unobtrusive to replace the entire content of the span with the validation error message if there is one. If set to false, the error message would be appended to the content of the span, preserving any existing content.

The jQuery Validate Unobtrusive library parses these data attributes and sets up the jQuery Validate plugin accordingly. This plugin then performs client-side validation based on these rules. If an input fails validation, the form will display an error message and prevent the form from being submitted.

Enabling Client-Side Validation with Custom Data Annotation Attribute:

Enabling client-side validation for a custom data annotation attribute in ASP.NET Core MVC involves several more steps than server-side validation. Let us see how to modify the ValidJoiningDateAttribute to include client-side validation:

Modify the Custom Attribute Class

First, we need to ensure our Custom Data Annotation Attribute can output the appropriate HTML5 data attributes that jQuery validation can use. To do this, our Custom Data Annotation Attribute will implement the IClientModelValidator interface. So, modify the ValidJoiningDateAttribute class as follows.

The following ValidJoiningDateAttribute class defines an enhanced custom validation attribute in ASP.NET Core MVC. This attribute not only checks server-side validation constraints but also enables client-side validation. It uses the ValidationAttribute for server-side validation and the IClientModelValidator interface to add client-side validation.

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace DataAnnotationsDemo.Models
{
    public class ValidJoiningDateAttribute : ValidationAttribute, IClientModelValidator
    {
        public ValidJoiningDateAttribute()
        {
            ErrorMessage = "The joining date cannot be in the future.";
        }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (value is DateTime)
            {
                DateTime joiningDate = (DateTime)value;
                if (joiningDate > DateTime.Now)
                {
                    return new ValidationResult(ErrorMessage);
                }
            }
            return ValidationResult.Success;
        }

        public void AddValidation(ClientModelValidationContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            MergeAttribute(context.Attributes, "data-val", "true");
            MergeAttribute(context.Attributes, "data-val-ValidJoiningDate", ErrorMessage);
        }

        private bool MergeAttribute(IDictionary<string, string> attributes, string key, string value)
        {
            if (!attributes.ContainsKey(key))
            {
                attributes.Add(key, value);
                return true;
            }
            return false;
        }
    }
}
Code Explanation:

IsValid Method: Enable Server-Side Validation. We have already explained this method in our previous example.

AddValidation Method:
  • Parameter Check: Initially, the method checks if the context is null, throwing an exception if it is. This check ensures the method has the necessary context to operate.
  • Adding Validation Attributes: The method uses the MergeAttribute to add HTML5 data attributes to the form input element that will be used for client-side validation:
  • data-val=”true” signals to the jQuery validation framework that the field has validation rules.
  • data-val-ValidJoiningDate=”ErrorMessage” adds a custom attribute with the validation error message. The key data-val-ValidJoiningDate is a unique identifier for this particular validation rule.
MergeAttribute Method:

This helper method checks if an attribute already exists in the dictionary. If it doesn’t, the attribute is added with the specified key and value. The method returns true if the attribute was added and false if it was already present. This functionality prevents overwriting of any existing attributes that may have been set elsewhere.

Client-Side Validation Logic

The client-side part of the validation logic is not explicitly detailed in this code but generally involves:

  • JavaScript Integration: You would need to write custom JavaScript or jQuery that uses the data attributes (data-val-ValidJoiningDate) to perform validation on the client side.
  • Validation Framework: ASP.NET Core typically uses jQuery Unobtrusive Validation for client-side validation. Your JavaScript would integrate with this framework, ensuring that the client validates that the joining date is not in the future before the form data is submitted to the server.
Add Client-Side JavaScript

Using jQuery and the jQuery Validation library, we can implement the client-side validation logic. Create a JavaScript file that will extend jQuery validation to handle your custom attribute. Add a new JavaScript file named JoiningDateValidate.js into the wwwroot folder as shown below:

How to Create Custom Data Annotation Attributes in ASP.NET Core MVC Applications with Real-Time Examples

Then, copy and paste the following code into the JoiningDateValidate.js file. The following code is self-explained, so please go through the comment lines for a better understanding.

// Define a custom method for jQuery validation to check the "Date of Joining".
jQuery.validator.addMethod('ValidJoiningDate', function (value, element) {
    // Directly return true for empty values, which allows the field to be optional.
    if (!value) {
        return true;
    }

    // Compare input date with the current date, ignoring time of day.
    var inputDate = new Date(value);
    var today = new Date();
    today.setHours(0, 0, 0, 0); // Normalize today's date by removing time components.

    // Validate that the input date is not in the future.
    return inputDate <= today;
}, 'The joining date cannot be in the future.'); // Default error message.

// Integrate the custom method with jQuery unobtrusive validation.
jQuery.validator.unobtrusive.adapters.addBool('ValidJoiningDate');
Include the JavaScript File in Your View

Make sure to reference the JavaScript file you created in the views that use the validation. So, modify the Create.cshtml file as follows:

@model DataAnnotationsDemo.Models.Employee

@{
    ViewData["Title"] = "Create";
}

<script>
    console.log('jQuery version:', $.fn.jquery);
</script>


<h1>Create Employee</h1>

<div class="row">
    <form asp-controller="Employee" asp-action="Create" method="post" class="mt-3">
        
        <div class="form-group row">
            <label asp-for="DateOfJoining" class="col-sm-2 col-form-label"></label>
            <div class="col-sm-10">
                <input asp-for="DateOfJoining" class="form-control">
                <span asp-validation-for="DateOfJoining" class="text-danger"></span>
            </div>
        </div>

        <div class="form-group row">
            <div class="col-sm-10">
                <button type="submit" class="btn btn-primary">Create</button>
            </div>
        </div>
    </form>
</div>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
<script src="~/js/joiningdatevalidate.js"></script>
Applying Custom Data Annotation Attribute:

Please make sure to apply the Custom Data Annotation Attribute with DateOfJoining Property of Employee Model as follows:

using System.ComponentModel.DataAnnotations;

namespace DataAnnotationsDemo.Models
{
    public class Employee
    {
        [Required(ErrorMessage = "Date of Joining is required")]
        [DataType(DataType.Date, ErrorMessage = "Invalid Date Format")]
        [ValidJoiningDate(ErrorMessage ="Date of Joining Cannot be Future Date")]
        public DateTime DateOfJoining { get; set; }
    }
}
Test the Validation

Now that both client-side and server-side validations are in place, test the application, and it should work as expected:

  • Client-side: Try entering a future date in your form and see if the form prevents submission without needing to go to the server.
  • Server-side: Disable JavaScript in your browser and try the same to ensure the server-side validation is still working as expected.

Now, if you view the page source code, then you will see the following HTML code:

<input class="form-control" type="date" data-val="true" data-val-required="Date of Joining is required" data-val-ValidJoiningDate="Date of Joining Cannot be Future Date" id="DateOfJoining" name="DateOfJoining" value="">
<span class="text-danger field-validation-valid" data-valmsg-for="DateOfJoining" data-valmsg-replace="true"></span>

Here,

  • type=”date”: This specifies that the input field is meant to enter a date. Browsers that support HTML5 will render a date picker to facilitate the input of date values.
  • data-val=”true”: This attribute signals validation rules are applied to this field. JQuery Validate Unobtrusive or similar libraries use it to initialize validation on this element.
  • data-val-required=”Date of Joining is required”: This is a validation attribute indicating that this field must be filled out. If left empty, the specified error message “Date of Joining is required” will be displayed.
  • data-val-ValidJoiningDate=”Date of Joining Cannot be Future Date”: This is a custom validation attribute, presumably defined in the server-side code (ASP.NET Core MVC). It checks that the entered date is not in the future. If the validation fails, it displays the message “Date of Joining Cannot be Future Date.”
  • data-valmsg-for=”DateOfJoining”: This attribute links the <span> to the <input> with id=”DateOfJoining”. It tells the validation framework to display any error messages related to the “DateOfJoining” input field in this <span>.
  • data-valmsg-replace=”true”: This attribute tells the validation framework to replace the content of this <span> with the validation message when an error occurs. If false, the error message will be appended to any existing content inside the <span>.
Why Custom Data Annotation Attribute in ASP.NET Core MVC?

Custom data annotation attributes in ASP.NET Core MVC are used for several reasons, primarily to enhance the functionality of model validation, provide meaningful error messages, and ensure that data is valid as per the business rules before it reaches the underlying database. The following are the reasons why we might use custom data annotation attributes in ASP.NET Core Applications:

  • Custom Validation Rules: While ASP.NET Core MVC provides a set of standard validation attributes (like Required, Range, StringLength, etc.), these may not cover all scenarios specific to your business logic. Custom attributes allow us to define these rules explicitly. 
  • Reusable Validation Logic: Once defined, custom attributes can be reused across different models within your application. This avoids duplication of validation code and ensures consistency in how data validation is applied throughout your application.
  • Client and Server Validation: Custom data annotations can support server-side and client-side validation. This means you can prevent the form from being submitted until the input meets all specified criteria, enhancing the user experience by providing immediate feedback.

In the next article, I will discuss Custom Data Annotation Attribute Real-Time Examples in ASP.NET Core MVC Applications. In this article, I explain Custom Data Annotation in an ASP.NET Core MVC Application with Examples. I hope you enjoy this Custom Data Annotation in ASP.NET Core MVC article.

Leave a Reply

Your email address will not be published. Required fields are marked *