Click here to Skip to main content
15,878,809 members
Articles / Web Development / HTML

Best Ways of Implementing Uniqueness or Unique Key Attribute on a Model Property in ASP.NET MVC Code First: Part 2

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
29 Sep 2016CPOL5 min read 57.8K   442   5   5
There are lots of ways to implement Uniqueness or Unique Key attribute on a model property other than primary key, but here I am going to discuss two possible best and easiest ways to do so in two parts of these series. This is Part 2.

[Latest Update Message]

Like Part 1, before this update, unfortunately there was a serious problem which went unnoticed that the method used to work fine in case of creating new entity only but did not work properly in case of Edit/Updating any existing entity. It did not allow to pass the original value of unique field while updating the others. This update eradicated this problem and made the code more simple to understand.

[Special Mention]

This is Part 2 of the series "Best ways of implementing Uniqueness or Unique Key attribute on a model property in ASP. NET MVC Code first".

Here is Part 1.

Introduction

Sometimes, we are in need of not permitting the duplicate value of a column or property in a database table, such as: for a username column or property in database table, we should not allow user to insert a value that already exists in the database table because a username is a unique value.

Disclaimer

In the last part of the article, to make the CustomRemoteValidation attribute reusable irrespective of properties and class, help has been taken from the very famous KudVenkat blog.

Let's Start

Suppose we have an Inventory where there is a Product table/ class which is used to track all products as a list of products. So it is rational to not permit the user to insert a product name that already exists in the table. Here is our product model class which will be used throughout this article:

C#
public class Product
 {       
        public int Id { get; set; }

        public string ProductName { get; set; }

        public int ProductQuantity { get; set; }

        public decimal UnitPrice { get; set; }
}

Method 2: Using Remote Validation Attribute

This is another way of implementing Uniqueness or Unique Key attribute on a model property in ASP. NET MVC Code first.

Step 1: Add the "Remote" attribute, to the ProductName property , which takes four parameters. These are: Action/Method name, Controller Name, AdditionalFields and ErrorMessage.

After adding the "Remote" attribute, Product class will look like follows:

C#
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ImplementingUniqueKey.Models
{
    public class Product
    {
        public int Id { get; set; }

        [Required]
        [StringLength(50)]
        [Remote("IsProductNameExist", "Product", AdditionalFields = "Id", 
                ErrorMessage = "Product name already exists")]       
        public string ProductName { get; set; }

        public int ProductQuantity { get; set; }

        public decimal UnitPrice { get; set; }
    }
}

Remember that "Remote" attribute does not exist in "System.ComponentModel.DataAnnotations" namespace, rather it is in "System.Web.Mvc" namespace!!

Step 2: Now implement the "IsProductNameExist()" method which will be added to the Product Controller.

"IsProductNameExist()" method will look like follows:

C#
public JsonResult IsProductNameExist(string ProductName, int ? Id)
        {
            var validateName = db.Products.FirstOrDefault
                                (x => x.ProductName == ProductName && x.Id != Id);
            if (validateName != null)
            {
                return Json(false, JsonRequestBehavior.AllowGet);
            }
            else
            {
                return Json(true, JsonRequestBehavior.AllowGet);
            }
        }

Step 3: Now Add three following JavaScript files to the Create.cshtml and Edit.cshtml or in your own cshtml file.

HTML
<script src="~/Scripts/jquery-1.10.2.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>

Now run the project and try to insert a product entity with a product name that already exists in the table. You will get the validation error message as follows:

Image 1

You can also be able to Edit/Update any field of an existing entity/record by satisfying the validation.

Till now, everything is working fine!! Now think! What will happen if the JavaScript is disabled in client browser?? Will the validation work anymore??

Absolutely no! The validation will not work anymore. Because this is a JavaScript based client side validation which makes an asynchronous AJAX call to the server side validation method. As a result, any wicked user will be able to insert duplicate values by disabling JavaScript in his browser. That's why it's always important to have server side validation along with client side validation.

To make server side validation work, when JavaScript is disabled, there are 2 ways:

  1. Adding model validation error dynamically in the controller action method
  2. Creating a custom remote attribute and override IsValid() method

a) Adding Model Validation Error Dynamically in the Controller Action Method

Modify the [HttpPost] Create and [HttpPost] Edit action methods in the controller by adding the Model validation error. After adding, your [HttpPost] Create and [HttpPost] Edit action methods will be as follows:

C#
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "Id,ProductName,ProductQuantity,UnitPrice")]
                            Product product)
{

    bool IsProductNameExist = db.Products.Any
         (x => x.ProductName == product.ProductName && x.Id != product.Id);
    if (IsProductNameExist == true)
    {
        ModelState.AddModelError("ProductName", "ProductName already exists");
    }

    if (ModelState.IsValid)
    {
        db.Products.Add(product);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(product);
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "Id,ProductName,ProductQuantity,UnitPrice")]
                          Product product)
{
    bool IsProductNameExist = db.Products.Any
                    (x => x.ProductName == product.ProductName && x.Id != product.Id);
    if (IsProductNameExist == true)
    {
        ModelState.AddModelError("ProductName", "ProductName already exists");
    }

    if (ModelState.IsValid)
    {
        db.Entry(product).State = EntityState.Modified;
        db.SaveChanges();
        return RedirectToAction("Index");
    }
    return View(product);
}

Now run the project. Before trying to insert a duplicate value, disable the JavaScript in your browser. And now try to insert a duplicate value. Surely you will not be able to insert a duplicate value. The validation error will be shown as before due to application of Model Validation in the controller action method.

Although it is working fine, there is a lot of duplicate code in the controller. Moreover, it’s not a good practice to delegate the responsibility of performing validation to a controller action method because it violates the separation of concerns principle within MVC. Ideally, all validation logic should be in the Model. Using validation attributes in MVC models is the preferred method for validation.

In order to do that, now we will create a custom remote attribute which will be applied on the desired model/class property.

b) Creating a Custom Remote Attribute and Override IsValid() Method

Before doing this, remove the following code from the [HttpPost] Create and [HttpPost] Edit action methods which you added to perform model validation error dynamically.

C#
bool IsProductNameExist = db.Products.Any
       (x => x.ProductName == product.ProductName && x.Id !=product.Id);
   if (IsProductNameExist == true)
   {
       ModelState.AddModelError("ProductName", "ProductName already exists");
   }

Now add a new folder named "CommonCode" to the project . In this newly added folder, create a class file named "CustomRemoteValidation.cs". Now add the following code to the CustomRemoteValidation.cs file.

C#
using ImplementingUniqueKey.Models;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using ImplementingUniqueKey.Models;

namespace ImplementingUniqueKey.CommonCode
{
    public class CustomRemoteValidation : RemoteAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            ProductDbContext db = new ProductDbContext();

            //Take the additional field property name and value
            PropertyInfo additionalPropertyName = 
            validationContext.ObjectInstance.GetType().GetProperty(AdditionalFields);
            object additionalPropertyValue = 
            additionalPropertyName.GetValue(validationContext.ObjectInstance, null);

            bool validateName = db.Products.Any
            (x => x.ProductName == (string)value && x.Id != (int)additionalPropertyValue);
            if (validateName == true)
            {
                return new ValidationResult
                ("The Product Name already exist", new string[] { "ProductName" });
            }
            return ValidationResult.Success;
        }

        public CustomRemoteValidation(string routeName)
            : base(routeName)
        {
        }

        public CustomRemoteValidation(string action, string controller)
            : base(action, controller)
        {
        }

        public CustomRemoteValidation(string action, string controller,
            string areaName) : base(action, controller, areaName)
        {
        }
    }
}

Now apply the newly created "CustomRemoteValidation" attribute to the ProductName property in Product model class. After adding "CustomRemoteValidation" attribute Product model class should look like follows:

C#
using ImplementingUniqueKey.CommonCode;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Web.Mvc;

namespace ImplementingUniqueKey.Models
{
    public class Product
    {
        public int Id { get; set; }

        [Required]
        [StringLength(50)]
        [CustomRemoteValidation("IsProductNameExist", "Product", 
        AdditionalFields = "Id", ErrorMessage = "Product Name already exists")]
        public string ProductName { get; set; }

        public int ProductQuantity { get; set; }

        public decimal UnitPrice { get; set; }
    }
}

Now run the project. Before trying to insert a duplicate value, disable the JavaScript in your browser. And now try to insert a duplicate value. Surely you will not be able to insert a duplicate value. The validation error will be shown as before due to CustomRemoteValidation attribute.

Although this is a better option than the previous but override IsValid() method in the CustomRemoteValidation controller, it is not a reusable method. This is a hardcoded method. You cannot use this "CustomRemoteValidation" attribute to a property of another class.

To make the CustomRemoteValidation attribute reusable irrespective of properties and class, replace the code in the CustomRemoteValidation.cs file with the following code:

C#
using System;
using System.Linq;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using System.Reflection;

namespace ImplementingUniqueKey.CommonCode
{
    public class CustomRemoteValidation : RemoteAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            // Get the controller using reflection
            Type controller = Assembly.GetExecutingAssembly().GetTypes()
                .FirstOrDefault(type => type.Name.ToLower() == string.Format("{0}Controller",
                    this.RouteData["controller"].ToString()).ToLower());
            if (controller != null)
            {
                // Get the action method that has validation logic
                MethodInfo action = controller.GetMethods()
                    .FirstOrDefault(method => method.Name.ToLower() ==
                        this.RouteData["action"].ToString().ToLower());
                if (action != null)
                {
                    // Create an instance of the controller class
                    object instance = Activator.CreateInstance(controller);

                    //Take the additional field property name and value
                    PropertyInfo additionalPropertyName = 
                    validationContext.ObjectInstance.GetType().GetProperty(AdditionalFields);
                    object additionalPropertyValue = 
                    additionalPropertyName.GetValue(validationContext.ObjectInstance, null);

                    // Invoke the action method that has validation logic
                    object response = action.Invoke(instance, new object[] 
                                        { value, additionalPropertyValue });
                    if (response is JsonResult)
                    {
                        object jsonData = ((JsonResult)response).Data;
                        if (jsonData is bool)
                        {
                            return (bool)jsonData ? ValidationResult.Success :
                                new ValidationResult(this.ErrorMessage);
                        }
                    }
                }
            }

            return new ValidationResult(base.ErrorMessageString);
        }

        public CustomRemoteValidation(string routeName)
            : base(routeName)
        {
        }

        public CustomRemoteValidation(string action, string controller)
            : base(action, controller)
        {
        }

        public CustomRemoteValidation(string action, string controller,
            string areaName) : base(action, controller, areaName)
        {
        }
    }
}

Now run the project for the final time. Before trying to insert a duplicate value, disable the JavaScript in your browser. And now try to insert a duplicate value. The validation error will be shown as before.

You will also be able to Edit/Update any field of an existing entity/record by satisfying the validation.

Hurray! That's the end of Part 2 Method 2!!

Here is Part 1.

Conclusion

This is the end of "Best ways of implementing Uniqueness or Unique Key attribute on a model property in ASP. NET MVC Code first" series. There are two ways to implement Uniqueness or Unique Key attribute on a model property in ASP. NET MVC Code first. Now it's your choice which method to use.

Don't forget to comment if you have any suggestions. Thank you!!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Bangladesh Bangladesh
A Software Engineer, Passionate Programmer and Technical Writer!!

Comments and Discussions

 
QuestionError in Code Pin
Mohamad Sadegh Alishahi15-Aug-19 18:25
Mohamad Sadegh Alishahi15-Aug-19 18:25 
QuestionAnother approach Pin
Member 1119372321-Sep-18 5:17
Member 1119372321-Sep-18 5:17 
QuestionActivator.CreateInstance(controller) no parameterless error Pin
Msn.afshari6-Mar-18 2:34
Msn.afshari6-Mar-18 2:34 
Hi I Use CustomRemoteValidation from viewmodel project and call action from another peoject .
error becuase web controller seprate from viewmodel . pls help me
QuestionHow To implement Remote Attribute in a Class Liberary Project Pin
Member 134769026-Nov-17 19:22
Member 134769026-Nov-17 19:22 
GeneralMy vote of 5 Pin
Md. Rashidul Hasan Masum19-Sep-16 19:00
professionalMd. Rashidul Hasan Masum19-Sep-16 19:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.