在哪里运行重复检查实体
在MVC应用程序中使用entity frameworkCode-First时,我正在寻找“最好的”地方的build议来放置validation逻辑,例如对实体进行重复检查。
使用一个简单的例子:
public class JobRole { public int Id { get; set; } public string Name { get; set; } }
规则是“名称”字段必须是唯一的。
当我添加一个新的JobRole时,很容易在Job Role Repository中运行一个名称不存在的检查。
但是,如果用户编辑现有的JobRole,并意外地将名称设置为已存在的名称,我该如何检查?
问题是不需要在存储库上使用“更新”方法,因为作业angular色实体会自动检测到更改,因此在尝试保存之前没有合理的位置来执行此检查。
到目前为止我已经考虑了两个select:
- 重写DbContext上的ValidateEntry方法,然后在使用EntityState.Modified保存JobRole实体时,运行重复检查。
- 在尝试保存之前,创build一些从Controller调用的重复检查服务。
也不是真的很理想。 使用ValidateEntry看起来比较晚(就在保存之前),很难testing。 使用服务使人们忘记从控制器调用它,让重复的数据通过。
有没有更好的办法?
您的ValidateEntity问题似乎是validation发生在SaveChanges上,这对您来说已经太迟了。 但在Entity Framework 5.0中,如果您希望使用DbContext.GetValidationErrors,则可以先调用validation。 当然你也可以直接调用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中embedded
Context.SaveChanges
并创build一个访问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()
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); }
我的基础知识库类像这样实现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; } }
我仍然build议在数据库中添加一个唯一的约束条件,因为这将绝对保证您的数据完整性,并提供一个可以提高效率的索引,但是重写ValidateEntry可以控制何时以及何时进行validation。
执行这个逻辑最可靠的地方是数据库本身,通过在名称列上声明一个唯一的字段约束。 当有人尝试插入或更新现有实体并尝试将其名称设置为现有名称时,会抛出一个约束违例exception,您可以在数据访问层中捕获并解释这个exception。
首先 ,你的第一个选项是可以接受的 。 另一个开发者可能不会陷入失败的事实并不是一个巨大的缺陷。 validation总是如此:尽可能早地捕捉到它,但最重要的是保持数据的完整性。 更简单的是在数据库中只有一个约束,陷阱虽然,对吧? 但是,是的,你想尽早赶上它。
如果你有范围和时间,最好是有一个更聪明的对象来处理保存 ,也许还有其他一些你需要处理的东西。 有很多方法可以做到这一点。 它可能是你的实体的包装。 (请参阅装饰模式 ,但我更喜欢让我的对象始终有一个Data
属性访问实体)。 它可能需要一个相关的实体才能被实例化。 你的控制器会给实体这个智能对象来保存(再次,或许用你的实体实例化这个智能对象)。这个智能对象将知道所有需要的validation逻辑,并确保它发生。
作为一个例子,你可以创buildJobRole业务对象。 (“busJobRole”,“业务”的总线前缀)。它可以有一个集合DataExceptions
。 您的Controller将JobRole实体发回,实例化一个busJobRole,并调用一个方法SaveIfValid
,如果项目已成功保存,将返回true ,如果存在validation问题,则返回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
这允许将exception(由业务对象确定)快速,优雅地添加到ModelState中:
ModelState.AddModelErrorsFromNameValueCollection(NewApp.RuleExceptions, "TrainingRequest")
你担心其他的开发者可能不会遵循你所设定的计划,这是非常有效和好的想法。 这就是为什么你的计划需要保持一致 。 例如,在我目前的项目中,我有两类类,就像我所描述的那样。 如果他们非常轻,只处理caching和validation,他们是“数据pipe理器”类(例如: BureauDataManager )。 有些是真正的业务领域对象,非常全面,我用“公交车”(例如:busTrainingRequest)作为前缀。 前者全部从一个普通的基类inheritance,保证了一致性(当然也减less了代码)。 一致性允许真正的封装,代码可发现性,正确的代码在正确(单一)的地方。