【问题标题】:Where to run a duplicate check for an entity在哪里为实体运行重复检查
【发布时间】:2012-03-25 01:52:56
【问题描述】:

我正在寻找有关在 MVC 应用程序中使用 Entity Framework Code-First 时放置验证逻辑的“最佳”位置的建议,例如对实体的重复检查。

举个简单的例子:

public class JobRole
{
  public int Id { get; set; }        
  public string Name { get; set; }
}

规则是“名称”字段必须是唯一的。

当我添加一个新的 JobRole 时,很容易在 Job Role Repository 中检查该名称不存在。

但是如果用户编辑一个现有的 JobRole,并且不小心将 Name 设置为一个已经存在的,我该如何检查呢?

问题是存储库上不需要“更新”方法,因为工作角色实体会自动检测更改,因此在尝试保存之前没有逻辑位置进行此检查。

到目前为止,我已经考虑了两种选择:

  1. 重写 DbContext 上的 ValidateEntry 方法,然后在使用 EntityState.Modified 保存 JobRole 实体时,然后运行重复检查。
  2. 在尝试保存之前创建某种从控制器调用的重复检查服务。

两者似乎都不理想。使用 ValidateEntry 似乎很晚(就在保存之前)并且很难测试。使用 Service 可能会导致有人忘记从 Controller 调用它,从而导致重复数据通过。

有没有更好的办法?

【问题讨论】:

    标签: .net asp.net-mvc entity-framework validation design-patterns


    【解决方案1】:

    执行此逻辑的最可靠位置是数据库本身,方法是在 name 列上声明唯一字段约束。当有人尝试插入或更新现有实体并尝试将其名称设置为现有名称时,将引发违反约束的异常,您可以在数据访问层中捕获和解释该异常。

    【讨论】:

    • 谢谢达林。对于独特的约束,这将是最安全的方法。尽管我使用了唯一字段的示例,但我还有其他需要强制执行的约束(例如,如果其父项设置为只读,则无法保存行项)并且我正在尝试找到用于验证的最佳通用方法处于修改状态的实体(特别是必须在被验证实体之外查看的验证)。我还想在尝试保存之前进行验证。
    • 只是为了扩展一点想法,保持实体的上下文应该负责检查唯一性。在这种情况下是数据库,在其他情况下可能是聚合根(用于值对象)或内存存储库中的一些。
    • 我同意这个限制(今天确实发送了关于它的电子邮件),但我们可能仍然需要一种验证方法,而且首先进行检查更容易。
    【解决方案2】:

    首先,您上面的第一个选项可以接受。另一个开发者可能不会陷入失败的事实并不是一个巨大的缺点。验证总是如此:您尽可能早且优雅地捕捉到它,但最重要的是保持数据的完整性。更简单的是,在数据库中设置一个约束并为此设置陷阱,对吧?但是,是的,您想尽早抓住它。

    如果您有足够的范围和时间,最好有一个可以处理保存的更智能的对象,也许您需要始终如一地处理其他事情。有很多方法可以做到这一点。它可能是您实体的包装器。 (请参阅Decorator pattern,尽管我更喜欢让我的对象始终具有Data 属性以访问实体)。它可能需要相关实体才能被实例化。您的控制器会将实体提供给此智能对象以进行保存(同样,也许用您的实体实例化此智能对象。)此智能对象将知道所有必要的验证逻辑并确保它发生。

    例如,您可以创建 JobRole 业务对象。 (“busJobRole”,“business”的总线前缀。)它可以有一个集合DataExceptions。您的控制器获取回传的 JobRole 实体,实例化一个 busJobRole,并调用一个方法 SaveIfValid,如果项目保存成功,则返回 true,如果有则返回 false验证问题。然后您检查 busJobRoles DataExceptions 属性的确切问题,并填写您的模型状态等。可能像这样:

    // Check ModelState first for more basic errors, like email invalid format, etc., and react accordingly.
    var jr = new busJobRole(modelJobRole);
    if (jr.SaveIfValid == false) {
         ModelState.AddModelError(jr.DataExceptions.First.GetKey(0), jr.DataExceptions.First.Get(0))
    }
    

    我们一直遵循这个模型,我为 ModelState 做了一个扩展方法来接受一个 NameValue 集合(由业务对象返回)(vb.net 版本):

    <Extension()> _
    Public Sub AddModelErrorsFromNameValueCollection(
                            ByVal theModelState As ModelStateDictionary,
                            ByVal collectionOfIssues As NameValueCollection,
                            Optional ByRef prefix As String = "")
        If String.IsNullOrEmpty(prefix) Then
            prefix = ""
        Else
            prefix = prefix & "."
        End If
        For i = 0 To CollectionOfIssues.Count - 1
            theModelState.AddModelError(prefix & CollectionOfIssues.GetKey(i), 
                            CollectionOfIssues.Get(i))
        Next
    End Sub
    

    这允许快速、优雅地将异常(由业务对象确定)添加到 ModelState:

    ModelState.AddModelErrorsFromNameValueCollection(NewApp.RuleExceptions, "TrainingRequest")
    

    您担心其他开发人员可能不会遵循您设置的计划是非常有效和好的想法。 这就是为什么您的方案需要保持一致。例如,在我当前的项目中,我有两类类,它们的作用与我所描述的一样。如果它们非常轻量级,并且只处理缓存和验证,它们就是“数据管理器”类(例如:BureauDataManager)。有些是真正的业务领域对象,非常全面,我用“总线”作为前缀(例如:busTrainingRequest)。前者都继承自一个通用的基类,以确保一致性(当然也减少了代码)。 一致性允许真正的封装、代码的可发现性、正确的代码位于正确的(单个)位置。

    【讨论】:

    • 感谢帕特里克深思熟虑的回复。我实际上已经实现了一些与您的方法模糊相似的东西,除了当我在 DbContext 上调用 SaveChanges 时,如果有任何验证错误,EF 会抛出一个 DbEntityValidationException,它封装了它发现的所有验证错误(包括我在我的 ValidateEntry 方法中添加的任何错误) .所以我只是抓住了这一点,然后将所有错误转换为 ModelState,这些错误完美地流回 UI。但是这一切都是在保存时发生的,如果可能的话,我想早点做。在将一篇帖子标记为答案之前,我会等待任何其他想法。
    • @史蒂夫。 MVC 和 EF 结合得非常好,不是吗?有很多方法不仅可以实现您的目标,而且可以简单且可维护地实现它们。 OOP/D,从上到下。我玩得很开心。随着我当前应用程序的需求和功能的增长,我仍然能够将代码库 更小 尽可能多的天。
    【解决方案3】:

    您对 ValidateEntity 的问题似乎是验证发生在 SaveChanges 上,这对您来说为时已晚。但在 Entity Framework 5.0 中,如果您希望使用 DbContext.GetValidationErrors,可以提前调用验证。当然,您也可以直接致电DbContext.ValidateEntity。我就是这样做的:

    1. 覆盖DbContext 上的ValidateEntity 方法:

      protected override DbEntityValidationResult 
                         ValidateEntity(DbEntityEntry entityEntry,
                         IDictionary<object, object> items)
      {
          //base validation for Data Annotations, IValidatableObject
          var result = base.ValidateEntity(entityEntry, items);
      
          //You can choose to bail out before custom validation
          //if (result.IsValid)
          //    return result;
      
          CustomValidate(result);
          return result;
      }
      
      private void CustomValidate(DbEntityValidationResult result)
      {
          ValidateOrganisation(result);
          ValidateUserProfile(result);
      }
      
      private void ValidateOrganisation(DbEntityValidationResult result)
      {
          var organisation = result.Entry.Entity as Organisation;
          if (organisation == null)
              return;
      
          if (Organisations.Any(o => o.Name == organisation.Name 
                                     && o.ID != organisation.ID))
              result.ValidationErrors
                    .Add(new DbValidationError("Name", "Name already exists"));
      }
      
      private void ValidateUserProfile(DbEntityValidationResult result)
      {
          var userProfile = result.Entry.Entity as UserProfile;
          if (userProfile == null)
              return;
      
          if (UserProfiles.Any(a => a.UserName == userProfile.UserName 
                                    && a.ID != userProfile.ID))
              result.ValidationErrors.Add(new DbValidationError("UserName", 
                                    "Username already exists"));
      }
      
    2. 在 try catch 中嵌入Context.SaveChanges 并创建访问Context.GetValidationErrors( 的方法)。这是我的UnitOfWork 班级:

      public Dictionary<string, string> GetValidationErrors()
      {
          return _context.GetValidationErrors()
                         .SelectMany(x => x.ValidationErrors)
                         .ToDictionary(x => x.PropertyName, x => x.ErrorMessage);
      }
      
      public int Save()
      {
          try
          {
              return _context.SaveChanges();
          }
          catch (DbEntityValidationException e)
          {
              //http://blogs.infosupport.com/improving-dbentityvalidationexception/
              var errors = e.EntityValidationErrors
                .SelectMany(x => x.ValidationErrors)
                .Select(x => x.ErrorMessage);
      
              string message = String.Join("; ", errors);
      
              throw new DataException(message);
          }
      }
      
    3. 在我的控制器中,在将实体添加到上下文之后但在SaveChanges() 之前调用GetValidationErrors()

      [HttpPost]
      public ActionResult Create(Organisation organisation, string returnUrl = null)
      {
          _uow.OrganisationRepository.InsertOrUpdate(organisation);
      
          foreach (var error in _uow.GetValidationErrors())
              ModelState.AddModelError(error.Key, error.Value);
      
          if (!ModelState.IsValid)
              return View();
      
          _uow.Save();
      
          if (string.IsNullOrEmpty(returnUrl))
              return RedirectToAction("Index");
      
          return Redirect(returnUrl);
      }
      

    我的基础存储库类像这样实现InsertOrUpdate

        protected virtual void InsertOrUpdate(T e, int id)
        {
            if (id == default(int))
            {
                // New entity
                context.Set<T>().Add(e);
            }
            else
            {
                // Existing entity
                context.Entry(e).State = EntityState.Modified;
            }      
        }
    

    我仍然建议向数据库添加唯一约束,因为这绝对可以保证您的数据完整性并提供可以提高效率的索引,但是覆盖 ValidateEntry 可以控制验证发生的方式和时间。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多