Как реализовать и протестировать сложные бизнес-правила для классов POCO

Я использую EF5 и имею сущности в классах POCO. Мой первый вопрос: где лучше всего реализовать бизнес-правила и проверку?

Мое первое предположение состоит в том, чтобы поместить его непосредственно в класс POCO в некоторую функцию Validate(), которая вызывается из DBContext при срабатывании SaveChanges(). Это работает хорошо, но некоторые правила требуют проверки для нескольких сущностей, как в этом примере для класса Invoice:

if(this.Items.Where(i=>i.Price > 100).Count() > 0)
{
    //mark invoice for review
    this.IsForReview = true;
}

Теперь модульные тесты будут проверять функцию проверки (для каждого бизнес-правила), но также должны будут заполнить класс счета-фактуры элементами (в противном случае он всегда будет пустым).

Другая идея состоит в том, чтобы создать класс InvoiceValidation с отдельными функциями проверки (или даже классом для каждого правила?), которые легче тестировать, но это увеличивает количество поддерживаемых файлов/классов.

Буду признателен за любые предложения или ссылки на существующие решения.


person Andrej Kovacik    schedule 14.01.2013    source источник
comment
Пометка для проверки не является задачей проверки. Это бизнес-логика. Подтверждением будет проверка того, что такие счета были помечены для проверки. Небольшое, но важное отличие. Проверка никогда не должна изменять состояние объекта. Хотя сути вашего вопроса это не меняет.   -  person Gert Arnold    schedule 15.01.2013


Ответы (3)


Лучший способ будет зависеть от ваших зависимостей. Зависит ли сборка POCO/core от EF? Внедряете ли вы Access to DB в свою основную сборку библиотеки? и Т. Д.

Я лично использую шаблон репозиторий/luw, где различные объекты репозитория наследуются от базового объекта репозитория, который является универсальным. DAL зависит от EF, но классы POCO в ядре не зависят.

Подкласс репозитория имеет определенный тип и выполняет бизнес-проверки ДРУГОГО ОБЪЕКТА. Бизнес-правила IE, требующие проверки других объектов, я реализую в DAL.

Классы репозитория относятся к проекту уровня доступа к данным и ДЕЙСТВИТЕЛЬНО зависят от EF и имеют внедренный контекст. Пример ниже.

Проверки, специфичные для экземпляра, который я выполняю на POCO. Проверки, требующие доступа к БД, я выполняю через интерфейс, реализованный в классе репозитория базового класса, который, в свою очередь, переопределяется по требованию. Так что теперь вызовы CheckEntity запускаются при добавлении или изменении объекта.

например... ПРИМЕЧАНИЕ. Некоторый код удален, чтобы сохранить актуальность примера...

 public class RepositoryEntityBase<T> : IRepositoryEntityBase<T>, IRepositoryEF<T> where T : BaseObject
public virtual OperationStatus Add(T entity)
    {
        var opStatus = new OperationStatus(status: true, operation: OperationType.Add);
        try
        {
            if (OnBeforeAdd != null) // registered listeners of added event?
            {
                var evtArg = PrepareEventArgs(entity, MasterEventType.Create);
                OnBeforeAdd(this, evtArg);
            }
            opStatus = CheckBeforePersist(entity);
            if (opStatus.Status)
            {
                Initialize(entity);
                EntityDbSet.Add(entity);

                if (OnAfterAdd != null) // registered listeners of added event?
                {
                    var evtArg = PrepareEventArgs(entity, MasterEventType.Create);
                    OnAfterAdd(this, evtArg);
                }
            }
        }
        catch (Exception ex)
        {
            opStatus.SetFromException("Error Adding " + typeof(T), ex);
        }
        return opStatus;
    }

//... then in a specific repository class



//... irepositorybase expects Check before persist.

  public override OperationStatus CheckBeforePersist(MasterUser entity)
    {

        // base entity rule check first
        var opStatus = new OperationStatus(true, OperationType.Check);          
        opStatus.ValidationResults  = base.CheckEntity(entity);      
        if (opStatus.ValidationResults.Count > 0)
        {
            opStatus.Status = false;
            opStatus.Message = "Validation Errors";
            return opStatus;
        }


        //now check the local memory
        var masterUser = Context.Set<MasterUser>().Local   //in context
                                              .Where(mu => mu.Id != entity.Id // not this record
                                                    &&     mu.UserName == entity.UserName ) // same name
                                              .FirstOrDefault();
        if (masterUser != null)
        {
            opStatus.Status = false;
            opStatus.Message = "Duplicate UserName :" + masterUser.UserName + " UserId:"+ masterUser.Id.ToString();
            return opStatus;
        }
        masterUser = Context.Set<MasterUser>().Local   //in context
                                             .Where(mu => mu.Id != entity.Id // not this record
                                                   && mu.Email == entity.Email) // same email
                                             .FirstOrDefault();
        if (masterUser != null)
        {
            opStatus.Status = false;
            opStatus.Message = "Duplicate Email :" + masterUser.Email + " Username:" + masterUser.UserName;
            return opStatus;
        }                                               

        // now check DB
        masterUser = Get(mu => mu.Id != entity.Id             //not this record being checked
                          && mu.UserName == entity.UserName);     // has same username
        if (masterUser != null)
        {
            opStatus.Status = false;
            opStatus.Message = "Duplicate UserName :" + masterUser.UserName + " UserId:"+ masterUser.Id.ToString();
            return opStatus;
        }
        masterUser = Get(mu => mu.Id != entity.Id    // not this record
                      && mu.Email == entity.Email);  // but same email 
        if (masterUser != null)
        {
            opStatus.Status = false;
            opStatus.Message = "Duplicate Email:" + masterUser.Email + " UserName:"+ masterUser.UserName;
            return opStatus;
        }
        return opStatus;   
    }

}
person phil soady    schedule 14.01.2013
comment
+1 спасибо за ваш вклад. Хотя это не прямой ответ (я понимаю, что их может быть несколько). Я хотел бы спросить вас, какова цель наличия различных объектов репозитория? Если у вас уже есть общий репозиторий, который обрабатывает все стандартные функции, зачем разбивать его на несколько (на самом деле я видел это и в других примерах, но не вижу преимуществ по сравнению с работой с универсальным репозиторием напрямую) - person Andrej Kovacik; 26.01.2013

Я бы предложил что-то вроде Fluent Validation (http://fluentvalidation.codeplex.com/), которое позволяет вам взять набор правил и объединить их в единый контекст, отдельный от класса POCO, который он проверяет.

person Rich    schedule 14.01.2013

Если кому-то интересно, это лучший пример, который я нашел до сих пор:

http://codeinsanity.com/archive/2008/12/02/a-framework-for-validation-and-business-rules.aspx

person Andrej Kovacik    schedule 15.03.2013