Remote Validation in ASP.NET Core MVC

Remote Validation in ASP.NET Core MVC

In this article, I will discuss How to Implement Remote Validations using Data Annotation in ASP.NET Core MVC Applications with Real-Time Examples. Please read our previous article discussing Custom Validation Attributes in ASP.NET Core MVC Application. At the end of this article, you will understand what Remote Validation is, why we need it, and how to implement it in ASP.NET Core MVC applications.

What is Remote Validation in ASP.NET Core MVC Application?

Remote Validation in ASP.NET Core MVC is a technique that allows client-side validation to make asynchronous calls to server-side code for validation purposes. This feature ensures that the data entered by users meets specific criteria without requiring a full-page refresh, providing a smoother and more responsive user experience. It is particularly useful for scenarios such as:

  • Checking Uniqueness: Verifying if a username or email address is already in use.
  • Verifying Complex Patterns: Ensuring input adheres to complex business rules that cannot be easily validated on the client side.
  • Ensuring Data Consistency: Validating data against the latest server-side information, such as the availability of resources.

For example, sometimes, we may need to make a database call to validate a field value. A classic example is the Gmail user registration page, requiring a unique email. To check if the email has already been taken, a server call must be made to validate it against the database. Remote Validation is extremely useful in scenarios like this, ensuring user feedback is immediate without page reload.

Remote Validation Real-Time Example in ASP.NET Core MVC:

Let us understand Remote Validation in ASP.NET Core MVC with a real-time example. We are working with the following Employee Registration Form. If someone else has already taken the entered email address, we won’t get immediate feedback. Instead, upon clicking the submit button, we will receive an error like the one below. This occurs because we’ve applied a unique index to the email column, meaning no duplicates are allowed.

Remote Validation Real-Time Example in ASP.NET Core MVC

Now, instead of showing the above error message after form submission, we want to display an immediate error message as soon as the user enters an already-taken email address. We also want to provide some suggestions, as shown in the image below.

Remote Validation Real-Time Example in ASP.NET Core MVC

Let us proceed and see how we can implement this step by step.

Create a Service for Generating Unique Email Suggestions:

So, first, create a class file named GenerateEmailSuggestions.cs within the Models folder and copy and paste the following code. In the following code, the GenerateUniqueEmailsAsync method generates unique email suggestions. This functionality is useful in systems where users or employees must have unique emails and another user has already taken the proposed email.

using DataAnnotationsDemo.Data;
using Microsoft.EntityFrameworkCore;
namespace DataAnnotationsDemo.Models
{
    public class GenerateEmailSuggestions
    {
        private readonly ApplicationDbContext _context;

        public GenerateEmailSuggestions(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<string> GenerateUniqueEmailsAsync(string baseEmail, int count = 2)
        {
            var suggestions = new List<string>();
            string emailPrefix = baseEmail.Split('@')[0];
            string emailDomain = baseEmail.Split('@')[1];
            string suggestion;

            while (suggestions.Count < count)
            {
                do
                {
                    suggestion = $"{emailPrefix}{new Random().Next(100, 999)}@{emailDomain}";
                    //Use AnyAsync to asynchronously check if the email exists
                } while (await _context.Employees.AnyAsync(u => u.Email == suggestion) || suggestions.Contains(suggestion));

                suggestions.Add(suggestion);
            }

            return string.Join(", ", suggestions);
        }
    }
}
GenerateUniqueEmailsAsync Method

The GenerateUniqueEmailsAsync method generates a list of unique emails based on the provided base email, ensuring that none of the suggested emails are already registered in the database or duplicated in the list of suggestions. This method works as follows:

  • Split the Email: It divides the baseEmail into two parts: emailPrefix (before the ‘@’) and emailDomain (after the ‘@’).
  • Generate Suggestions: It iterates, creating new email addresses by appending a random number between 100 and 999 to the emailPrefix, and combines it with the original domain.
  • Check Uniqueness: For each new suggestion, it asynchronously checks if the email already exists in the database using AnyAsync(). It also checks if the suggestion is already included in the list of generated suggestions to avoid duplicates within the same batch.
  • Generating Suggestions: It continues generating suggestions until the required number (count) of unique emails is achieved. Finally, it returns these suggestions as a comma-separated string.
Registering the GenerateSuggestions Service:

Next, we need to register the GenerateEmailSuggestions service in the dependency injection container. This ensures that ASP.NET Core will inject the service object when needed. Please add the following code to the Program.cs class file:

builder.Services.AddScoped<GenerateEmailSuggestions>();

Creating a Remote Validation Controller

This controller will handle the remote validation checks. Create an Empty MVC Controller named RemoteValidationController within the Controllers folder and copy and paste the following code. The IsEmailAvailable method validates the user’s input in real-time, improving the user experience by quickly providing feedback on the availability of emails.

using DataAnnotationsDemo.Data;
using DataAnnotationsDemo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;
namespace DataAnnotationsDemo.Controllers
{
    public class RemoteValidationController : Controller
    {
        private readonly ApplicationDbContext _context;
        private readonly GenerateEmailSuggestions _generateSuggestions;

        public RemoteValidationController(ApplicationDbContext context, GenerateEmailSuggestions generateSuggestions)
        {
            _context = context;
            _generateSuggestions = generateSuggestions;
        }

        // Checks if the provided email is available. If not, returns suggestions.
        // Email: The email to validate
        // Returns a JSON result indicating availability or suggestions
        [AcceptVerbs("GET", "POST")]
        public async Task<IActionResult> IsEmailAvailable(string email)
        {
            // Check if the email is empty
            if (string.IsNullOrEmpty(email))
            {
                return Json("Please enter a valid email address.");
            }

            // Validate the email format
            var emailAttribute = new EmailAddressAttribute();
            if (!emailAttribute.IsValid(email))
            {
                return Json("Please enter a valid email address.");
            }

            // Check if the email is already in use (case-insensitive)
            var emailExists = await _context.Employees.AnyAsync(u => u.Email.ToLower() == email.ToLower());
            if (emailExists)
            {
                //The second parameter will decide the number of unique suggestions to be generated
                var suggestedEmails = await _generateSuggestions.GenerateUniqueEmailsAsync(email, 3);
                return Json($"Email is already in use. Try anyone of these: {suggestedEmails}");
            }

            // If the email is available
            return Json(true);  // Indicates success to jQuery Unobtrusive Validation
        }
    }
}
IsEmailAvailable Method

This method asynchronously checks if an email address is already registered in the database and, if so, provides alternative suggestions. The method uses the AnyAsync method to query the database asynchronously and determine if the given email already exists in the Employees table.

  • If an Invalid Email: If the provided email format is invalid or empty, the method returns a JSON object with the message “Please enter a valid email address,” which will be displayed on the page.
  • If Email Available: If the email format is valid and does not exist in the database, the method returns a JSON object with a true value, indicating that the email is available for use.
  • If Email Not Available: If the email is valid and already exists in the Employees database table, it triggers the GenerateUniqueEmailsAsync method using the GenerateEmailSuggestions instance to generate three alternative email suggestions. It then returns a JSON object containing a message that the email is in use, along with the suggestions.
How to Use Remote Validation in ASP.NET Core MVC?

We need to use the built-in Remote Data Annotation attribute in ASP.NET Core MVC to enable server-side validation as part of the client-side validation process. This attribute makes an AJAX call to a specified server method to validate the data entered by the user, providing an asynchronous way to check for uniqueness or other custom conditions that cannot be easily validated using standard client-side validation techniques. Here’s how it works in ASP.NET Core MVC:

  • AJAX Call: When a user interacts with a form field (typically on losing focus), the Remote attribute triggers an AJAX call to a specific server-side method.
  • Server-Side Method: The server-side action method specified by the Remote attribute on the property checks the input against business rules or database constraints. It then returns a validation response (true if valid or an error message if not).
  • Feedback: If the response is an error message, it is displayed to the user without needing to submit the form. This instant feedback enhances the user experience by preventing form submissions that would otherwise fail server-side validation.
Add Model Property with Remote Validation Attribute:

Next, we need to modify the EmployeeViewModel with the Remote validation attribute on the Email property, specifying the controller and action method name, and also the optional error message. Modify the EmployeeViewModel class as follows.

using DataAnnotationsDemo.ValidationAttributes;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.ComponentModel.DataAnnotations;

namespace DataAnnotationsDemo.Models.ViewModels
{
    public class EmployeeViewModel
    {
        // Basic Information
        [Required(ErrorMessage = "First Name is required")]
        [Display(Name = "First Name")]
        [StringLength(30, MinimumLength = 2, ErrorMessage = "First name should be between 2 and 30 characters")]
        public string FirstName { get; set; }

        [Required(ErrorMessage = "Last Name is required")]
        [StringLength(30, ErrorMessage = "Last name cannot exceed 30 characters")]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }

        [Required(ErrorMessage = "Email is required")]
        [EmailAddress(ErrorMessage = "Invalid Email Address")]
        [Remote(action: "IsEmailAvailable", controller: "RemoteValidation", ErrorMessage = "Email is already in use. Please use a different email address.")]
        public string Email { get; set; }

        [Required(ErrorMessage = "Date of Birth is required")]
        [DataType(DataType.Date)]
        [Display(Name = "Date Of Birth")]
        //Applying Custom AgeRange Validation Attribute
        [AgeRange(18, 60, ErrorMessage = "Employee must be between 18 and 60 years old.")]
        public DateTime? DateOfBirth { get; set; }

        [Required(ErrorMessage = "Joining Date is required")]
        [DataType(DataType.Date)]
        //Applying Custom DateNotInFuture Validation Attribute
        [DateNotInFuture(ErrorMessage = "Joining date cannot be in the future.")]
        [Display(Name = "Joining Date")]
        public DateTime? JoiningDate { get; set; }

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

        // Address Information
        [Required(ErrorMessage = "Street is required")]
        [StringLength(100, ErrorMessage = "Street cannot exceed 100 characters")]
        public string Street { get; set; }

        [Required(ErrorMessage = "City is required")]
        [StringLength(50, ErrorMessage = "City cannot exceed 50 characters")]
        public string City { get; set; }

        [Required(ErrorMessage = "State is required")]
        [StringLength(50, ErrorMessage = "State cannot exceed 50 characters")]
        public string State { get; set; }

        [Required(ErrorMessage = "Postal Code is required")]
        [RegularExpression(@"^\d{5}(-\d{4})?$|^\d{6}$", ErrorMessage = "Invalid Postal Code")]
        [Display(Name = "Postal or Zip Code")]
        public string PostalCode { get; set; }

        // Job Details
        [Required(ErrorMessage = "Job Title is required")]
        [Display(Name = "Job Title")]
        public int SelectedJobTitleId { get; set; }

        [Required(ErrorMessage = "Department is required")]
        [Display(Name = "Department")]
        public int DepartmentId { get; set; }

        [DataType(DataType.Currency)]
        [Range(30000, 200000, ErrorMessage = "Salary must be between 30,000 and 200,000")]
        public decimal Salary { get; set; }

        // Skills
        [Display(Name = "Skills")]
        public List<int> SkillSetIds { get; set; }

        // Account Information
        [Required(ErrorMessage = "Password is required")]
        [DataType(DataType.Password)]
        [StringLength(100, MinimumLength = 6, ErrorMessage = "Password should be at least 6 characters")]
        public string Password { get; set; }

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

        // Lists for Dropdowns and Radio Buttons
        public IEnumerable<SelectListItem>? Departments { get; set; }
        public IEnumerable<SelectListItem>? SkillSets { get; set; }
        public IEnumerable<Gender>? Genders { get; set; }
        public IEnumerable<SelectListItem>? JobTitles { get; set; }
    }
}
Understanding the Remote Validation Attribute:

If you look at the above View Model, then you will see we have applied the following Remote Attribute with the Email property:

Remote(action: “IsEmailAvailable”, controller: “RemoteValidation”, ErrorMessage = “Email is already in use. Please use a different email address.”)]

  • Action: “IsEmailAvailable” – This is the server-side action method that the AJAX call will invoke in the RemoteValidation controller.
  • Controller: “RemoteValidation” – Specifies the controller where the IsEmailAvailable action method is defined.
  • ErrorMessage: If the IsEmailAvailable method returns an error message (indicating the email is already in use), this optional error message is displayed: “Email is already in use. Please use a different email address.”
Testing the Application:

Now, run the application, navigate to the Employee/Create URL, and try to enter the Email that exists in the database. You should see the Error Message immediately, as shown in the below image, along with the suggestions:

When Should We Use Custom Data Annotation in ASP.NET Core MVC?

Limitations of Remote Validation in ASP.NET Core MVC:

It’s important to note that Remote Validation relies on client-side JavaScript to make the asynchronous calls to the server. If JavaScript is disabled in the user’s browser, the remote validation will not work, and the validation logic will not be executed. Therefore, Remote Validation primarily works on the client side and does not replace the need for server-side validation.

Implementing Server-Side Validation:

To ensure that validation occurs even if JavaScript is disabled, we need to implement server-side validation. There are two approaches to implementing server-side validation:

  • Validating in the Controller Action Method: Manually checking the validation logic in the controller’s action method.
  • Creating a Custom Validation Attribute: Encapsulating the validation logic within a custom validation attribute applied to the model.

Let’s explore both methods.

Approach 1: Validating in the Controller Action Method

One way to implement server-side validation is to check the validation in the controller action method manually. For better understanding, modify the EmployeeController as follows:

using DataAnnotationsDemo.Data;
using DataAnnotationsDemo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using DataAnnotationsDemo.Models.ViewModels;

namespace DataAnnotationsDemo.Controllers
{
    public class EmployeeController : Controller
    {
        private readonly ApplicationDbContext _context;
        private readonly GenerateEmailSuggestions _generateSuggestions;

        public EmployeeController(ApplicationDbContext context, GenerateEmailSuggestions generateSuggestions)
        {
            _context = context;
            _generateSuggestions = generateSuggestions;
        }

        // GET: Employees/Create
        public async Task<IActionResult> Create()
        {
            var viewModel = new EmployeeViewModel();
            await PopulateViewModelAsync(viewModel);
            return View(viewModel);
        }

        // POST: Employees/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(EmployeeViewModel model)
        {
            //Manually validating the Email Uniques
            if (_context.Employees.Any(u => u.Email == model.Email))
            {
                //The second parameter will decide the number of unique suggestions to be generated
                var suggestedEmails = await _generateSuggestions.GenerateUniqueEmailsAsync(model.Email, 2);

                //Dynamically Adding Model Validation Error incase JavaScript is Disabled
                ModelState.AddModelError("Email", $"Email is already in use. Try anyone of these: {suggestedEmails}");
            }

            if (ModelState.IsValid)
            {
                try
                {
                    // Map ViewModel to Model
                    var employee = new Employee
                    {
                        FirstName = model.FirstName,
                        LastName = model.LastName,
                        Email = model.Email,
                        DateOfBirth = model.DateOfBirth,
                        JoiningDate = model.JoiningDate,
                        Gender = model.Gender.Value,
                        Password = model.Password,
                        
                        // Job Details
                        JobDetail = new JobDetail
                        {
                            JobTitleId = model.SelectedJobTitleId,
                            DepartmentId = model.DepartmentId,
                            Salary = model.Salary
                        },

                        // Address
                        Address = new Address
                        {
                            Street = model.Street,
                            City = model.City,
                            State = model.State,
                            PostalCode = model.PostalCode
                        },

                        // Skills
                        SkillSets = new List<SkillSet>()
                    };

                    // Adding Skills
                    if (model.SkillSetIds != null)
                    {
                        foreach (var skillId in model.SkillSetIds)
                        {
                            employee.SkillSets.Add(new SkillSet
                            {
                                SkillSetId = skillId
                            });
                        }
                    }

                    // Attach selected skills
                    if (model.SkillSetIds != null && model.SkillSetIds.Any())
                    {
                        // Fetch the selected skills from the database
                        var selectedSkills = await _context.SkillSets
                            .Where(s => model.SkillSetIds.Contains(s.SkillSetId))
                            .ToListAsync();

                        // Assign to the Employee's SkillSets
                        employee.SkillSets = selectedSkills;
                    }

                    // Add to DbContext
                    _context.Employees.Add(employee);
                    await _context.SaveChangesAsync();

                    // Redirect to Success page with EmployeeId
                    return RedirectToAction(nameof(Successful), new { id = employee.EmployeeId });
                }
                catch (DbUpdateException ex)
                {
                    // Log the error (uncomment ex variable name and write a log.)
                    ModelState.AddModelError("", "Unable to save changes. " +
                        "Try again, and if the problem persists see your system administrator.");
                }
                catch (Exception ex)
                {
                    // Handle other exceptions
                    ModelState.AddModelError("", $"An error occurred: {ex.Message}");
                }
            }

            // If we reach here, something failed; re-populate dropdowns
            await PopulateViewModelAsync(model);
            return View(model);
        }

        // GET: Employees/Successful
        public async Task<IActionResult> Successful(int id)
        {
            var employee = await _context.Employees
                .Include(e => e.Address)
                .Include(e => e.JobDetail)
                    .ThenInclude(jd => jd.JobTitle)
                .Include(e => e.JobDetail)
                    .ThenInclude(jd => jd.Department)
                .Include(e => e.SkillSets)
                .FirstOrDefaultAsync(e => e.EmployeeId == id);

            if (employee == null)
            {
                return NotFound();
            }

            return View(employee);
        }

        // Private helper method to populate ViewModel lists
        private async Task PopulateViewModelAsync(EmployeeViewModel viewModel)
        {
            viewModel.Departments = await _context.Departments
                .AsNoTracking()
                .Select(d => new SelectListItem
                {
                    Value = d.DepartmentId.ToString(),
                    Text = d.Name
                })
                .ToListAsync();

            viewModel.SkillSets = await _context.SkillSets
                .AsNoTracking()
                .Select(s => new SelectListItem
                {
                    Value = s.SkillSetId.ToString(),
                    Text = s.SkillName
                })
                .ToListAsync();

            viewModel.JobTitles = await _context.JobTitles
                .AsNoTracking()
                .Select(j => new SelectListItem
                {
                    Value = j.JobTitleId.ToString(),
                    Text = j.TitleName
                })
                .ToListAsync();

            viewModel.Genders = Enum.GetValues(typeof(Gender))
                .Cast<Gender>()
                .ToList();
        }
    }
}

In the above code, the POST version of the Create action method handles the form submission. However, the client may disable JavaScript in their browser, in which case the Remote Validation will not work. Therefore, for safety, we are also handling the Email validation manually inside the POST version of the Create action method.

Note: While this approach works, delegating the responsibility of performing validation to a controller action method violates the separation of concerns within the MVC architecture. Ideally, all validation logic should reside in the Model. Using validation attributes in models should be the preferred method for validation.

Approach 2: Creating a Custom Unique Email Validation Attribute

We can create a custom validation attribute for the email property to adhere to the separation of concerns and promote reusability.

Creating a Custom Unique Email Validation Attribute:

Create a class file named UniqueEmailAttribute.cs within the ValidationAttributes folder and copy and paste the following code. The class inherits from the ValidationAttribute class and overrides the IsValid method.

using DataAnnotationsDemo.Data;
using DataAnnotationsDemo.Models;
using System.ComponentModel.DataAnnotations;

namespace DataAnnotationsDemo.ValidationAttributes
{
    public class UniqueEmailAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            //Get the ApplicationDbContext Instance from the DI Container
            var _context = validationContext.GetService<ApplicationDbContext>();

            var email = value as string;
            if (_context.Employees.Any(u => u.Email == email))
            {
                //Get the GenerateEmailSuggestions Instance from the DI Container
                var _generateSuggestions = validationContext.GetService<GenerateEmailSuggestions>();

                //The second parameter will decide the number of unique suggestions to be generated
                var suggestedEmails = _generateSuggestions?.GenerateUniqueEmailsAsync(email, 3).Result;

                return new ValidationResult($"Email is already in use. Try anyone of these: {suggestedEmails}");
            }

            return ValidationResult.Success;
        }
    }
}
Apply Custom Validation Attribute to Email Property

Next, apply the custom UniqueEmail validation attribute to the Email property of the EmployeeViewModel. Modify the EmployeeViewModel class as follows.

using DataAnnotationsDemo.ValidationAttributes;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.ComponentModel.DataAnnotations;

namespace DataAnnotationsDemo.Models.ViewModels
{
    public class EmployeeViewModel
    {
        // Basic Information
        [Required(ErrorMessage = "First Name is required")]
        [Display(Name = "First Name")]
        [StringLength(30, MinimumLength = 2, ErrorMessage = "First name should be between 2 and 30 characters")]
        public string FirstName { get; set; }

        [Required(ErrorMessage = "Last Name is required")]
        [StringLength(30, ErrorMessage = "Last name cannot exceed 30 characters")]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }

        [Required(ErrorMessage = "Email is required")]
        [EmailAddress(ErrorMessage = "Invalid Email Address")]
        [Remote(action: "IsEmailAvailable", controller: "RemoteValidation", ErrorMessage = "Email is already in use. Please use a different email address.")]
        [UniqueEmail]
        public string Email { get; set; }

        [Required(ErrorMessage = "Date of Birth is required")]
        [DataType(DataType.Date)]
        [Display(Name = "Date Of Birth")]
        //Applying Custom AgeRange Validation Attribute
        [AgeRange(18, 60, ErrorMessage = "Employee must be between 18 and 60 years old.")]
        public DateTime? DateOfBirth { get; set; }

        [Required(ErrorMessage = "Joining Date is required")]
        [DataType(DataType.Date)]
        //Applying Custom DateNotInFuture Validation Attribute
        [DateNotInFuture(ErrorMessage = "Joining date cannot be in the future.")]
        [Display(Name = "Joining Date")]
        public DateTime? JoiningDate { get; set; }

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

        // Address Information
        [Required(ErrorMessage = "Street is required")]
        [StringLength(100, ErrorMessage = "Street cannot exceed 100 characters")]
        public string Street { get; set; }

        [Required(ErrorMessage = "City is required")]
        [StringLength(50, ErrorMessage = "City cannot exceed 50 characters")]
        public string City { get; set; }

        [Required(ErrorMessage = "State is required")]
        [StringLength(50, ErrorMessage = "State cannot exceed 50 characters")]
        public string State { get; set; }

        [Required(ErrorMessage = "Postal Code is required")]
        [RegularExpression(@"^\d{5}(-\d{4})?$|^\d{6}$", ErrorMessage = "Invalid Postal Code")]
        [Display(Name = "Postal or Zip Code")]
        public string PostalCode { get; set; }

        // Job Details
        [Required(ErrorMessage = "Job Title is required")]
        [Display(Name = "Job Title")]
        public int SelectedJobTitleId { get; set; }

        [Required(ErrorMessage = "Department is required")]
        [Display(Name = "Department")]
        public int DepartmentId { get; set; }

        [DataType(DataType.Currency)]
        [Range(30000, 200000, ErrorMessage = "Salary must be between 30,000 and 200,000")]
        public decimal Salary { get; set; }

        // Skills
        [Display(Name = "Skills")]
        public List<int> SkillSetIds { get; set; }

        // Account Information
        [Required(ErrorMessage = "Password is required")]
        [DataType(DataType.Password)]
        [StringLength(100, MinimumLength = 6, ErrorMessage = "Password should be at least 6 characters")]
        public string Password { get; set; }

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

        // Lists for Dropdowns and Radio Buttons
        public IEnumerable<SelectListItem>? Departments { get; set; }
        public IEnumerable<SelectListItem>? SkillSets { get; set; }
        public IEnumerable<Gender>? Genders { get; set; }
        public IEnumerable<SelectListItem>? JobTitles { get; set; }
    }
}
Modifying Employee Controller:

Since we have applied the custom UniqueEmail validation attribute to the Email property of the EmployeeViewModel, which handles the server-side validation, we can safely remove the validation logic from the EmployeeController POST version of the Create action method. Modify the EmployeeController as follows:

using DataAnnotationsDemo.Data;
using DataAnnotationsDemo.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using DataAnnotationsDemo.Models.ViewModels;

namespace DataAnnotationsDemo.Controllers
{
    public class EmployeeController : Controller
    {
        private readonly ApplicationDbContext _context;
        private readonly GenerateEmailSuggestions _generateSuggestions;

        public EmployeeController(ApplicationDbContext context, GenerateEmailSuggestions generateSuggestions)
        {
            _context = context;
            _generateSuggestions = generateSuggestions;
        }

        // GET: Employees/Create
        public async Task<IActionResult> Create()
        {
            var viewModel = new EmployeeViewModel();
            await PopulateViewModelAsync(viewModel);
            return View(viewModel);
        }

        // POST: Employees/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(EmployeeViewModel model)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    // Map ViewModel to Model
                    var employee = new Employee
                    {
                        FirstName = model.FirstName,
                        LastName = model.LastName,
                        Email = model.Email,
                        DateOfBirth = model.DateOfBirth,
                        JoiningDate = model.JoiningDate,
                        Gender = model.Gender.Value,
                        Password = model.Password,
                        
                        // Job Details
                        JobDetail = new JobDetail
                        {
                            JobTitleId = model.SelectedJobTitleId,
                            DepartmentId = model.DepartmentId,
                            Salary = model.Salary
                        },

                        // Address
                        Address = new Address
                        {
                            Street = model.Street,
                            City = model.City,
                            State = model.State,
                            PostalCode = model.PostalCode
                        },

                        // Skills
                        SkillSets = new List<SkillSet>()
                    };

                    // Adding Skills
                    if (model.SkillSetIds != null)
                    {
                        foreach (var skillId in model.SkillSetIds)
                        {
                            employee.SkillSets.Add(new SkillSet
                            {
                                SkillSetId = skillId
                            });
                        }
                    }

                    // Attach selected skills
                    if (model.SkillSetIds != null && model.SkillSetIds.Any())
                    {
                        // Fetch the selected skills from the database
                        var selectedSkills = await _context.SkillSets
                            .Where(s => model.SkillSetIds.Contains(s.SkillSetId))
                            .ToListAsync();

                        // Assign to the Employee's SkillSets
                        employee.SkillSets = selectedSkills;
                    }

                    // Add to DbContext
                    _context.Employees.Add(employee);
                    await _context.SaveChangesAsync();

                    // Redirect to Success page with EmployeeId
                    return RedirectToAction(nameof(Successful), new { id = employee.EmployeeId });
                }
                catch (DbUpdateException ex)
                {
                    // Log the error (uncomment ex variable name and write a log.)
                    ModelState.AddModelError("", "Unable to save changes. " +
                        "Try again, and if the problem persists see your system administrator.");
                }
                catch (Exception ex)
                {
                    // Handle other exceptions
                    ModelState.AddModelError("", $"An error occurred: {ex.Message}");
                }
            }

            // If we reach here, something failed; re-populate dropdowns
            await PopulateViewModelAsync(model);
            return View(model);
        }

        // GET: Employees/Successful
        public async Task<IActionResult> Successful(int id)
        {
            var employee = await _context.Employees
                .Include(e => e.Address)
                .Include(e => e.JobDetail)
                    .ThenInclude(jd => jd.JobTitle)
                .Include(e => e.JobDetail)
                    .ThenInclude(jd => jd.Department)
                .Include(e => e.SkillSets)
                .FirstOrDefaultAsync(e => e.EmployeeId == id);

            if (employee == null)
            {
                return NotFound();
            }

            return View(employee);
        }

        // Private helper method to populate ViewModel lists
        private async Task PopulateViewModelAsync(EmployeeViewModel viewModel)
        {
            viewModel.Departments = await _context.Departments
                .AsNoTracking()
                .Select(d => new SelectListItem
                {
                    Value = d.DepartmentId.ToString(),
                    Text = d.Name
                })
                .ToListAsync();

            viewModel.SkillSets = await _context.SkillSets
                .AsNoTracking()
                .Select(s => new SelectListItem
                {
                    Value = s.SkillSetId.ToString(),
                    Text = s.SkillName
                })
                .ToListAsync();

            viewModel.JobTitles = await _context.JobTitles
                .AsNoTracking()
                .Select(j => new SelectListItem
                {
                    Value = j.JobTitleId.ToString(),
                    Text = j.TitleName
                })
                .ToListAsync();

            viewModel.Genders = Enum.GetValues(typeof(Gender))
                .Cast<Gender>()
                .ToList();
        }
    }
}

With the above changes, run the application to verify that the server-side validation works for Email uniqueness, even when JavaScript is disabled.

Real-Time Examples of Remote Validations in ASP.NET Core MVC

Remote validation in ASP.NET Core MVC is a technique that allows server-side validation of field values in a form before the entire form is submitted. This is especially useful in cases where validation cannot be handled using client-side techniques alone. The following are some typical scenarios where you might consider using remote validation:

  • Unique Field Validation: When you need to ensure that a field’s value is unique, such as a username or email address. Remote validation can be used to check the existing database and confirm that the entered value has not already been taken.
  • Complex Business Rules: Remote validation can be applied if a field’s validity depends on complex logic that requires database access or server-side resources.
  • Dynamic Data Verification: For validating data that is frequently updated and must be verified against the latest server data. For example, checking the availability of a product or a booking slot in real-time.

Always perform server-side validation in addition to client-side validation to protect against malicious inputs and ensure data integrity. Remote validation involves additional server calls, which can impact performance, especially if the validation logic is complex or the server is under heavy load.

In the next article, I will discuss Displaying and Formatting Attributes in ASP.NET Core MVC Applications. In this article, I explain Remote Validation using Data Annotation in the ASP.NET Core MVC Application with examples. I hope you enjoy this Remote Validation in ASP.NET Core MVC article.

Leave a Reply

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