Где запустить повторную проверку для объекта - PullRequest
8 голосов
/ 07 марта 2012

Я ищу совет о "лучшем" месте для размещения логики проверки, например, при проверке дубликата объекта при использовании Entity Framework Code-First, в приложении MVC.

Чтобы использовать простой пример:

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

Правило состоит в том, что поле «Имя» должно быть уникальным.

Когда я добавляю новую JobRole, легко запустить проверку в репозитории ролей заданий на то, что имя еще не существует.

Но если пользователь редактирует существующего JobRole и случайно устанавливает имя на то, которое уже существует, как я могу проверить это?

Проблема заключается в том, что в репозитории не требуется метод «обновления», поскольку объект роли роли автоматически обнаружит изменения, поэтому нет логического места для выполнения этой проверки перед попыткой сохранения.

Я рассмотрел два варианта:

  1. Переопределите метод ValidateEntry для DbContext, а затем, когда сущность JobRole сохраняется с EntityState.Modified, запустите проверку дубликатов.
  2. Создайте какую-либо службу проверки дубликатов, вызываемую из контроллера, перед попыткой сохранения.

Ни один из них не кажется идеальным. Использование ValidateEntry выглядит довольно поздно (непосредственно перед сохранением) и его сложно протестировать. Использование службы оставляет возможность того, что кто-то забудет вызвать ее с контроллера, пропуская дубликаты данных.

Есть ли лучший способ?

Ответы [ 3 ]

5 голосов
/ 20 мая 2013

Ваша проблема с 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. Вставить 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);
    }
    

Мой базовый класс репозитория реализует 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 дает множество контроля над тем, как и когда происходит проверка.

5 голосов
/ 07 марта 2012

Самым надежным местом для выполнения этой логики была бы сама база данных, объявив ограничение уникального поля в столбце имени. Когда кто-то пытается вставить или обновить существующую сущность и попытаться установить для ее имени существующее имя, будет выдано исключение нарушения ограничения, которое вы можете перехватить и интерпретировать на уровне доступа к данным.

1 голос
/ 07 марта 2012

Первый , ваш первый вариант выше приемлемый . Тот факт, что другой dev может не заманить в ловушку для отказа, не является огромным недостатком. Это всегда имеет место с проверкой: вы ловите ее как можно раньше и элегантно, но главное - сохранить целостность ваших данных. Проще просто иметь ограничение в базе данных и ловить его, правда? Но да, вы хотите поймать это как можно раньше.

Если у вас есть область действия и время, лучше было бы иметь более умный объект, который бы справлялся с экономией , и, возможно, другие вещи, которые вам нужно обрабатывать последовательно. Есть много способов сделать это. Это может быть обертка для вашей сущности. (См. Шаблон декоратора , хотя я предпочитаю, чтобы у моего объекта всегда было свойство Data для доступа к сущности). Для создания экземпляра может потребоваться соответствующая сущность. Ваш контроллер передаст объект этому смарт-объекту для сохранения (опять же, возможно, создание экземпляра этого смарт-объекта с помощью вашей сущности). Этот смарт-объект будет знать всю необходимую логику проверки и быть уверенным, что это произойдет.

Например, вы можете создать бизнес-объект JobRole. ("busJobRole", префикс шины для "business".) Может иметь коллекцию DataExceptions. Контроллер принимает сущность 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). Первые все наследуются от общего базового класса, чтобы обеспечить согласованность (и, конечно, уменьшить код). Согласованность допускает истинную инкапсуляцию, возможность обнаружения кода, для правильного кода, находящегося в правильном (единственном) месте.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...