Where to run a duplicate check for an entity

前端 未结 3 1960
灰色年华
灰色年华 2020-11-27 21:13

I\'m looking for advice on the \"best\" place to put validation logic, such as a duplicate check for an entity, when using Entity Framework Code-First, in an MVC application

3条回答
  •  醉梦人生
    2020-11-27 21:44

    Your problem with ValidateEntity appears to be that the validation occurs on SaveChanges and this is too late for you. But in Entity Framework 5.0 you can call the validation earlier if you wish using DbContext.GetValidationErrors. And of course you could also just call DbContext.ValidateEntity directly. This is how I do it:

    1. Override the ValidateEntity method on the DbContext:

      protected override DbEntityValidationResult 
                         ValidateEntity(DbEntityEntry entityEntry,
                         IDictionary 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. Embed Context.SaveChanges in a try catch and create a method to access Context.GetValidationErrors(). This is in my UnitOfWork class:

      public Dictionary 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. In my controller, call GetValidationErrors() after adding the entity to the context but before 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);
      }
      

    My base repository class implements InsertOrUpdate like this:

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

    I still recommend adding a unique constraint to the database because that will absolutely guarantee your data integrity and provide an index that can improve the efficiency, but overriding ValidateEntry gives loads of control over how and when validation occurs.

提交回复
热议问题