[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 product
s as a list of product
s. 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:
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:
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:
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.
<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:
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:
- Adding model validation error dynamically in the controller action method
- 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:
[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.
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.
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();
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:
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:
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)
{
Type controller = Assembly.GetExecutingAssembly().GetTypes()
.FirstOrDefault(type => type.Name.ToLower() == string.Format("{0}Controller",
this.RouteData["controller"].ToString()).ToLower());
if (controller != null)
{
MethodInfo action = controller.GetMethods()
.FirstOrDefault(method => method.Name.ToLower() ==
this.RouteData["action"].ToString().ToLower());
if (action != null)
{
object instance = Activator.CreateInstance(controller);
PropertyInfo additionalPropertyName =
validationContext.ObjectInstance.GetType().GetProperty(AdditionalFields);
object additionalPropertyValue =
additionalPropertyName.GetValue(validationContext.ObjectInstance, null);
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!!