在 ASP.NET MVC Code First 中实现模型属性唯一性或唯一键属性的最佳方法:第 2 部分





5.00/5 (6投票s)
除了主键之外,还有很多方法可以为模型属性实现唯一性或唯一键属性,但在本文系列的两部分中,我将讨论两种可能最好且最简单的方法。这是第 2 部分。
[最新更新消息]
与第 1 部分一样,在此更新之前,不幸的是,存在一个严重的未被注意到的问题:该方法在创建新实体时运行良好,但在编辑/更新现有实体时却无法正常工作。它不允许在更新其他字段时传递唯一字段的原始值。此更新消除了此问题,并使代码更易于理解。
[特别鸣谢]
这是“在 ASP.NET MVC Code First 中实现模型属性唯一性或唯一键属性的最佳方法”系列文章的第 2 部分。
这是 第 1 部分。
引言
有时,我们需要不允许数据库表中的列或属性出现重复值,例如:对于数据库表中的用户名列或属性,我们不应允许用户插入数据库表中已存在的名称,因为 username
是唯一值。
免责声明
在上篇文章的最后一部分中,为了使 CustomRemoteValidation
属性可重用,不受属性和类的限制,我们借鉴了非常著名的 KudVenkat 博客。
开始吧
假设我们有一个库存系统,其中有一个 Product
表/类,用于跟踪所有 product
作为 product
列表。因此,不允许用户插入表中已存在的 product
名称是合理的。这是我们将在这篇文章中使用的 product
模型类
public class Product
{
public int Id { get; set; }
public string ProductName { get; set; }
public int ProductQuantity { get; set; }
public decimal UnitPrice { get; set; }
}
方法 2:使用 Remote Validation Attribute
这是在 ASP.NET MVC Code First 中为模型属性实现唯一性或唯一键属性的另一种方法。
步骤 1:将“Remote”属性添加到 ProductName 属性,该属性接受四个参数。它们是:Action/Method 名称、Controller 名称、AdditionalFields 和 ErrorMessage。
添加 “Remote
” 属性后,Product
类将如下所示
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; }
}
}
请记住,“Remote
”属性不在“System.ComponentModel.DataAnnotations
”命名空间中,而是在“System.Web.Mvc
”命名空间中!
步骤 2:现在实现将添加到 Product Controller 的“IsProductNameExist()”方法。
“IsProductNameExist()
” 方法将如下所示
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);
}
}
步骤 3:现在将以下三个 JavaScript 文件添加到 Create.cshtml 和 Edit.cshtml 或您自己的 cshtml 文件中。
<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>
现在运行项目,并尝试插入一个 product
名称已存在于表中的 product
实体。您将收到如下验证错误消息
您还可以编辑/更新现有实体/记录的任何字段,只要满足验证条件即可。
到目前为止,一切正常!现在思考一下!如果客户端浏览器中的 JavaScript 被禁用会怎样?验证还会起作用吗?
绝对不会!验证将不再起作用。因为这是基于 JavaScript 的客户端验证,它会向服务器端验证方法进行异步 AJAX 调用。因此,任何恶意的用户都可以通过在浏览器中禁用 JavaScript 来插入重复值。因此,始终在客户端验证的同时进行服务器端验证非常重要。
要使服务器端验证在 JavaScript 被禁用时也能正常工作,有两种方法:
- 在控制器操作方法中动态添加模型验证错误
- 创建自定义远程属性并重写
IsValid()
方法
a) 在控制器操作方法中动态添加模型验证错误
通过添加 Model
验证错误来修改控制器中的 [HttpPost]
Create
和 [HttpPost]
Edit
操作方法。添加后,您的 [HttpPost] Create
和 [HttpPost] Edit
操作方法将如下所示
[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);
}
现在运行项目。在尝试插入重复值之前,请在浏览器中禁用 JavaScript。然后尝试插入重复值。您肯定无法插入重复值。由于在控制器操作方法中应用了 Model 验证,验证错误将如前所示。
虽然这可以正常工作,但控制器中存在大量重复代码。此外,将执行验证的责任委托给控制器操作方法不是一个好习惯,因为它违反了 MVC 中的关注点分离原则。理想情况下,所有验证逻辑都应放在模型中。在 MVC 模型中使用验证属性是进行验证的首选方法。
为了做到这一点,现在我们将创建一个自定义远程属性,该属性将应用于所需的模型/类属性。
b) 创建自定义远程属性并重写 IsValid() 方法
在此之前,请从 [HttpPost] Create
和 [HttpPost] Edit
操作方法中删除您为动态执行模型验证错误而添加的以下代码。
bool IsProductNameExist = db.Products.Any
(x => x.ProductName == product.ProductName && x.Id !=product.Id);
if (IsProductNameExist == true)
{
ModelState.AddModelError("ProductName", "ProductName already exists");
}
现在向项目中添加一个名为“CommonCode”的新文件夹。在此新添加的文件夹中,创建一个名为“CustomRemoteValidation.cs”的文件。现在将以下代码添加到 CustomRemoteValidation.cs 文件中。
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)
{
}
}
}
现在将新创建的“CustomRemoteValidation
” 属性应用于 Product
模型类中的 ProductName
属性。添加“CustomRemoteValidation
” 属性后,Product
模型类应如下所示
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; }
}
}
现在运行项目。在尝试插入重复值之前,请在浏览器中禁用 JavaScript。然后尝试插入重复值。由于 CustomRemoteValidation
属性,验证错误将如前所示。
尽管这是比前一种更好的选择,但 CustomRemoteValidation 控制器中的 override IsValid() 方法,它不是一个可重用方法。这是一个硬编码的方法。您无法将此“CustomRemoteValidation" attribute 应用于另一个类的属性。
为了使 CustomRemoteValidation
属性在不受属性和类限制的情况下可重用,请将 CustomRemoteValidation.cs 文件中的代码替换为以下代码
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)
{
}
}
}
现在最后一次运行项目。在尝试插入重复值之前,请在浏览器中禁用 JavaScript。然后尝试插入重复值。验证错误将如前所示。
您还可以通过满足验证条件来编辑/更新现有实体/记录的任何字段。
太棒了!第 2 部分方法 2 结束了!!
这是 第 1 部分。
结论
这是“在 ASP.NET MVC Code First 中实现模型属性唯一性或唯一键属性的最佳方法”系列文章的结尾。在 ASP.NET MVC Code First 中实现模型属性唯一性或唯一键属性有两种方法。现在由您选择使用哪种方法。
如果您有任何建议,请不要忘记评论。谢谢!!