在哪里运行实体的重复检查

时间:2012-03-07 14:19:20

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

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

使用一个简单的例子:

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

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

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

但是如果用户编辑现有的JobRole,并且意外地将名称设置为已存在的名称,我该如何检查?

问题在于,存储库中不需要“更新”方法,因为作业角色实体将自动检测更改,因此在尝试保存之前没有合理的位置来执行此检查。

到目前为止,我考虑了两个选项:

  1. 覆盖DbContext上的ValidateEntry方法,然后在使用EntityState.Modified保存JobRole实体时,运行重复检查。
  2. 在尝试保存之前,创建一些从Controller调用的重复检查服务。
  3. 两者都不是真的很理想。使用ValidateEntry似乎相当晚(在保存之前)并且难以测试。使用服务可能会有人忘记从控制器调用它,让重复数据通过。

    有更好的方法吗?

3 个答案:

答案 0 :(得分:5)

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

答案 1 :(得分:5)

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

  1. 覆盖ValidateEntity上的DbContext方法:

    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. 在我的控制器中,在将实体添加到上下文之后但GetValidationErrors()之前调用SaveChanges()

    [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);
    }
    
  4. 我的基础知识库类实现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可以控制验证的发生方式和时间。

答案 2 :(得分:1)

首先,您上面的第一个选项是可接受的。另一个dev 可能没有陷入失败的事实并不是一个巨大的缺点。验证始终如此:尽可能早地和优雅地捕获它,但主要是保持数据的完整性。更简单的是在数据库中有一个约束并为此陷阱,对吧?但是,是的,你想尽早发现它。

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

例如,您可以创建JobRole业务对象。 (“busJobRole”,“business”的总线前缀。)它可以有一个集合DataExceptions。您的Controller将回发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 )。有些是真正的业务领域对象,非常全面,我用“bus”前缀(例如:busTrainingRequest)。前者都继承自公共基类,以确保一致性(当然也减少代码)。 一致性允许对代码可发现性进行真正的封装,以使正确的代码位于正确的(单个)位置。