Where to run a duplicate check for an entity

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<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. Embed Context.SaveChanges in a try catch and create a method to access Context.GetValidationErrors(). This is in my UnitOfWork class:

    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. 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<T>().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.

Leave a Comment