我正在寻找关于在MVC应用程序中使用实体框架代码优先时放置验证逻辑的"最佳"位置的建议,例如实体的重复检查。
举一个简单的例子:
public class JobRole
{
public int Id { get; set; }
public string Name { get; set; }
}
规则是"名称"字段必须是唯一的。
当我添加一个新的JobRole时,很容易在Job Role Repository中检查该名称是否已经存在。
但是,如果用户编辑了现有的JobRole,并意外地将Name设置为已存在的名称,我该如何检查?
问题是,存储库上不需要"更新"方法,因为作业角色实体将自动检测更改,因此在尝试保存之前没有进行此检查的逻辑位置。
到目前为止,我已经考虑了两种选择:
- 重写DbContext上的ValidateEntry方法,然后在使用EntityState保存JobRole实体时。Modified,然后运行重复检查
- 在尝试保存之前,创建某种从控制器调用的重复检查服务
两者似乎都不太理想。使用ValidateEntry似乎很晚(就在保存之前),而且很难测试。使用服务可能会导致有人忘记从控制器调用服务,从而使重复数据通过。
有更好的方法吗?
ValidateEntity的问题似乎是验证发生在SaveChanges上,这对您来说太晚了。但在EntityFramework5.0中,如果您希望使用DbContext.GetValidationErrors,您可以更早地调用验证。当然,您也可以直接调用DbContext.ValidateEntity。我就是这样做的:
-
覆盖
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")); }
-
在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); } }
-
在我的控制器中,在将实体添加到上下文之后但在
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可以对验证的方式和时间进行大量控制。
执行此逻辑最可靠的地方是通过在名称列上声明唯一字段约束的数据库本身。当有人试图插入或更新现有实体并试图将其名称设置为现有名称时,将引发一个约束冲突异常,您可以在数据访问层中捕获并解释该异常。
首先,上面的第一个选项是可接受。事实上,另一个dev可能不会陷入失败陷阱,这并不是一个巨大的缺点。验证总是这样:您可以尽早优雅地捕获它,但最重要的是保持数据的完整性。只是在数据库中有一个约束并设置陷阱更简单,对吧?但是,是的,你想尽早抓住它。
如果你有足够的范围和时间,最好有一个更智能的对象来处理保存,也许还有其他你需要一致处理的事情。有很多方法可以做到这一点。它可能是实体的包装器。(请参阅Decorator模式,不过我更希望我的对象始终具有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)。前者都继承自一个公共基类,以确保一致性(当然也减少代码)一致性允许真正的封装,允许代码可发现性,允许正确的代码位于正确的(单个)位置。