65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2016 年 9 月 19 日

CPOL

5分钟阅读

viewsIcon

60241

downloadIcon

478

除了主键之外,还有很多方法可以为模型属性实现唯一性或唯一键属性,但在本文系列的两部分中,我将讨论两种可能最好且最简单的方法。这是第 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 被禁用时也能正常工作,有两种方法:

  1. 在控制器操作方法中动态添加模型验证错误
  2. 创建自定义远程属性并重写 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 中实现模型属性唯一性或唯一键属性有两种方法。现在由您选择使用哪种方法。

如果您有任何建议,请不要忘记评论。谢谢!!

© . All rights reserved.