Использование Entity Framework 4.0 и элемента управления ObjectDataSource, часть 2. Добавление уровня бизнес-логики и модульных тестов

Том Дайкстра

Эта серия руководств основана на веб-приложении Университета Contoso, созданном начало работы с серией учебников По Entity Framework 4.0. Если вы не выполнили предыдущие руководства, в качестве отправной точки для этого руководства можно скачать созданное приложение . Вы также можете скачать приложение , созданное в полной серии учебников. Если у вас есть вопросы по этим руководствам, вы можете опубликовать их на форуме ASP.NET Entity Framework.

В предыдущем руководстве вы создали n-уровневое веб-приложение с помощью Entity Framework и ObjectDataSource элемента управления . В этом руководстве показано, как добавить бизнес-логику, сохраняя уровень бизнес-логики (BLL) и уровень доступа к данным (DAL) отдельно, а также создание автоматических модульных тестов для BLL.

В этом руководстве вы выполните следующие задачи:

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

Вы будете работать со страницами Departments.aspx и DepartmentsAdd.aspx , созданными в предыдущем руководстве.

Создание интерфейса репозитория

Начните с создания интерфейса репозитория.

Image08

В папке DAL создайте файл класса, назовите его ISchoolRepository.cs и замените существующий код следующим кодом:

using System;
using System.Collections.Generic;

namespace ContosoUniversity.DAL
{
    public interface ISchoolRepository : IDisposable
    {
        IEnumerable<Department> GetDepartments();
        void InsertDepartment(Department department);
        void DeleteDepartment(Department department);
        void UpdateDepartment(Department department, Department origDepartment);
        IEnumerable<InstructorName> GetInstructorNames();
    }
}

Интерфейс определяет один метод для каждого метода CRUD (create, read, update, delete), созданных в классе репозитория.

SchoolRepository В классе SchoolRepository.cs укажите, что этот класс реализует ISchoolRepository интерфейс :

public class SchoolRepository : IDisposable, ISchoolRepository

Создание класса Business-Logic

Далее вы создадите класс бизнес-логики. Это необходимо для добавления бизнес-логики, которая будет выполняться элементом ObjectDataSource управления, хотя пока этого не будет. Пока новый класс бизнес-логики будет выполнять только те же операции CRUD, что и репозиторий.

Image09

Создайте новую папку и назовите ее BLL. (В реальном приложении уровень бизнес-логики обычно реализуется как библиотека классов — отдельный проект, но для простоты этого учебника классы BLL будут храниться в папке проекта.)

В папке BLL создайте файл класса, назовите его SchoolBL.cs и замените существующий код следующим кодом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using ContosoUniversity.DAL;

namespace ContosoUniversity.BLL
{
    public class SchoolBL : IDisposable
    {
        private ISchoolRepository schoolRepository;

        public SchoolBL()
        {
            this.schoolRepository = new SchoolRepository();
        }

        public SchoolBL(ISchoolRepository schoolRepository)
        {
            this.schoolRepository = schoolRepository;
        }

        public IEnumerable<Department> GetDepartments()
        {
            return schoolRepository.GetDepartments();
        }

        public void InsertDepartment(Department department)
        {
            try
            {
                schoolRepository.InsertDepartment(department);
            }
            catch (Exception ex)
            {
                //Include catch blocks for specific exceptions first,
                //and handle or log the error as appropriate in each.
                //Include a generic catch block like this one last.
                throw ex;
            }
        }

        public void DeleteDepartment(Department department)
        {
            try
            {
                schoolRepository.DeleteDepartment(department);
            }
            catch (Exception ex)
            {
                //Include catch blocks for specific exceptions first,
                //and handle or log the error as appropriate in each.
                //Include a generic catch block like this one last.
                throw ex;
            }
        }

        public void UpdateDepartment(Department department, Department origDepartment)
        {
            try
            {
                schoolRepository.UpdateDepartment(department, origDepartment);
            }
            catch (Exception ex)
            {
                //Include catch blocks for specific exceptions first,
                //and handle or log the error as appropriate in each.
                //Include a generic catch block like this one last.
                throw ex;
            }

        }

        public IEnumerable<InstructorName> GetInstructorNames()
        {
            return schoolRepository.GetInstructorNames();
        }

        private bool disposedValue = false;

        protected virtual void Dispose(bool disposing)
        {
            if (!this.disposedValue)
            {
                if (disposing)
                {
                    schoolRepository.Dispose();
                }
            }
            this.disposedValue = true;
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

    }
}

Этот код создает те же методы CRUD, которые вы видели ранее в классе репозитория, но вместо прямого доступа к методам Entity Framework он вызывает методы класса репозитория.

Переменная класса, содержащая ссылку на класс репозитория, определяется как тип интерфейса, а код, создающий экземпляр класса репозитория, содержится в двух конструкторах. Конструктор без параметров будет использоваться элементом ObjectDataSource управления . Он создает экземпляр класса, созданного SchoolRepository ранее. Другой конструктор позволяет любому коду, который создает экземпляр класса бизнес-логики, передавать любой объект, реализующий интерфейс репозитория.

Методы CRUD, вызывающие класс репозитория, и два конструктора позволяют использовать класс бизнес-логики с любым выбранным внутренним хранилищем данных. Классу бизнес-логики не нужно знать, как вызываемый класс сохраняет данные. (Это часто называется незнанием сохраняемости.) Это упрощает модульное тестирование, так как вы можете подключить класс бизнес-логики к реализации репозитория, в котором для хранения данных используется нечто такое простое, как коллекции в памяти List .

Примечание

Технически объекты сущностей по-прежнему не являются невежественными, так как они создаются из классов, наследующих от класса Entity Framework EntityObject . Для полного незнания сохраняемости можно использовать обычные старые объекты CLR или POCO вместо объектов, наследующих от EntityObject класса . Использование POCOs выходит за рамки область этого руководства. Дополнительные сведения см. в статье Testability and Entity Framework 4.0 на веб-сайте MSDN.)

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

В Departments.aspx и DepartmentsAdd.aspx измените каждое вхождение на TypeName="ContosoUniversity.DAL.SchoolRepository"TypeName="ContosoUniversity.BLL.SchoolBL". (Всего существует четыре экземпляра.)

Запустите страницы Departments.aspx и DepartmentsAdd.aspx , чтобы убедиться, что они по-прежнему работают так же, как и раньше.

Изображение01

Image02

Создание проекта Unit-Test и реализации репозитория

Добавьте новый проект в решение с помощью шаблона Тестовый проект и назовите его ContosoUniversity.Tests.

В тестовом проекте добавьте ссылку на System.Data.Entity и ссылку на ContosoUniversity проект.

Теперь можно создать класс репозитория, который будет использоваться с модульными тестами. Хранилище данных для этого репозитория будет находиться в классе .

Изображение12

В тестовом проекте создайте файл класса, назовите его MockSchoolRepository.cs и замените существующий код следующим кодом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ContosoUniversity.DAL;
using ContosoUniversity.BLL;

namespace ContosoUniversity.Tests
{
    class MockSchoolRepository : ISchoolRepository, IDisposable
    {
        List<Department> departments = new List<Department>();
        List<InstructorName> instructors = new List<InstructorName>();

        public IEnumerable<Department> GetDepartments()
        {
            return departments;
        }

        public void InsertDepartment(Department department)
        {
            departments.Add(department);
        }

        public void DeleteDepartment(Department department)
        {
            departments.Remove(department);
        }

        public void UpdateDepartment(Department department, Department origDepartment)
        {
            departments.Remove(origDepartment);
            departments.Add(department);
        }

        public IEnumerable<InstructorName> GetInstructorNames()
        {
            return instructors;
        }

        public void Dispose()
        {
            
        }
    }
}

Этот класс репозитория имеет те же методы CRUD, что и класс, который обращается к Entity Framework напрямую, но они работают с коллекциями List в памяти, а не с базой данных. Это упрощает для тестового класса настройку и проверку модульных тестов для класса бизнес-логики.

Создание модульных тестов

Шаблон тестового проекта создал класс модульного теста заглушки, и следующая задача — изменить этот класс, добавив в него методы модульного теста для бизнес-логики, которую вы хотите добавить в класс бизнес-логики.

Изображение13

В Университете Contoso любой отдельный преподаватель может быть администратором только одного отдела, и для применения этого правила необходимо добавить бизнес-логику. Для начала вы добавите тесты и выполните тесты, чтобы увидеть их сбой. Затем вы добавите код и повторно выполните тесты, ожидая, что они будут пройдены.

Откройте файл UnitTest1.cs и добавьте using инструкции для уровней бизнес-логики и доступа к данным, созданных в проекте ContosoUniversity:

using ContosoUniversity.BLL;
using ContosoUniversity.DAL;

Замените TestMethod1 метод следующими методами:

private SchoolBL CreateSchoolBL()
{
    var schoolRepository = new MockSchoolRepository();
    var schoolBL = new SchoolBL(schoolRepository);
    schoolBL.InsertDepartment(new Department() { Name = "First Department", DepartmentID = 0, Administrator = 1, Person = new Instructor () { FirstMidName = "Admin", LastName = "One" } });
    schoolBL.InsertDepartment(new Department() { Name = "Second Department", DepartmentID = 1, Administrator = 2, Person = new Instructor() { FirstMidName = "Admin", LastName = "Two" } });
    schoolBL.InsertDepartment(new Department() { Name = "Third Department", DepartmentID = 2, Administrator = 3, Person = new Instructor() { FirstMidName = "Admin", LastName = "Three" } });
    return schoolBL;
}

[TestMethod]
[ExpectedException(typeof(DuplicateAdministratorException))]
public void AdministratorAssignmentRestrictionOnInsert()
{
    var schoolBL = CreateSchoolBL();
    schoolBL.InsertDepartment(new Department() { Name = "Fourth Department", DepartmentID = 3, Administrator = 2, Person = new Instructor() { FirstMidName = "Admin", LastName = "Two" } });
}

[TestMethod]
[ExpectedException(typeof(DuplicateAdministratorException))]
public void AdministratorAssignmentRestrictionOnUpdate()
{
    var schoolBL = CreateSchoolBL();
    var origDepartment = (from d in schoolBL.GetDepartments()
                          where d.Name == "Second Department"
                          select d).First();
    var department = (from d in schoolBL.GetDepartments()
                          where d.Name == "Second Department"
                          select d).First();
    department.Administrator = 1;
    schoolBL.UpdateDepartment(department, origDepartment);
}

Метод CreateSchoolBL создает экземпляр класса репозитория, созданного для проекта модульного теста, который затем передается в новый экземпляр класса бизнес-логики. Затем метод использует класс бизнес-логики для вставки трех отделов, которые можно использовать в методах тестирования.

Методы тестирования проверяют, что класс бизнес-логики создает исключение, если кто-то пытается вставить новый отдел с тем же администратором, что и существующий отдел, или если кто-то пытается обновить администратора отдела, задав для него идентификатор человека, который уже является администратором другого отдела.

Вы еще не создали класс исключения, поэтому этот код не будет компилироваться. Чтобы выполнить компиляцию, щелкните правой кнопкой мыши DuplicateAdministratorException и выберите Создать, а затем — Класс.

Снимок экрана: элемент

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

Запустите тестовый проект. Как и ожидалось, тесты завершаются сбоем.

Image03

Добавление бизнес-логики для прохождения теста

Далее вы реализуете бизнес-логику, которая не позволяет задать в качестве администратора отдела пользователя, который уже является администратором другого отдела. Вы создадите исключение из уровня бизнес-логики, а затем перехватите его на уровне презентации, если пользователь изменяет отдел и нажимает кнопку Обновить после выбора пользователя, который уже является администратором. (Вы также можете удалить из раскрывающегося списка преподавателей, которые уже являются администраторами, прежде чем вы отрисовываете страницу, но здесь нужно работать со слоем бизнес-логики.)

Начните с создания класса исключения, который будет создаваться, когда пользователь пытается сделать преподавателя администратором нескольких отделов. В проекте main создайте файл класса в папке BLL, назовите его DuplicateAdministratorException.cs и замените существующий код следующим кодом:

using System;

namespace ContosoUniversity.BLL
{
    public class DuplicateAdministratorException : Exception
    {
        public DuplicateAdministratorException(string message)
            : base(message)
        {
        }
    }
}

Теперь удалите временный файл DuplicateAdministratorException.cs , созданный ранее в тестовом проекте, чтобы иметь возможность компиляции.

В проекте main откройте файл SchoolBL.cs и добавьте следующий метод, содержащий логику проверки. (Код ссылается на метод, который вы создадите позже.)

private void ValidateOneAdministratorAssignmentPerInstructor(Department department)
{
    if (department.Administrator != null)
    {
        var duplicateDepartment = schoolRepository.GetDepartmentsByAdministrator(department.Administrator.GetValueOrDefault()).FirstOrDefault();
        if (duplicateDepartment != null && duplicateDepartment.DepartmentID != department.DepartmentID)
        {
            throw new DuplicateAdministratorException(String.Format(
                "Instructor {0} {1} is already administrator of the {2} department.", 
                duplicateDepartment.Person.FirstMidName, 
                duplicateDepartment.Person.LastName, 
                duplicateDepartment.Name));
        }
    }
}

Этот метод будет вызываться при вставке или обновлении Department сущностей, чтобы проверка, есть ли у другого отдела тот же администратор.

Код вызывает метод для поиска в базе данных сущности Department , которая имеет то же Administrator значение свойства, что и вставленная или обновляемая сущность. Если он найден, код создает исключение. Проверка проверка не требуется, если вставляемая или обновляемая сущность не Administrator имеет значения, и исключение не возникает, если метод вызывается во время обновления и Department найденная сущность соответствует обновляемой Department сущности.

Вызовите новый метод из Insert методов и Update :

public void InsertDepartment(Department department)
{
    ValidateOneAdministratorAssignmentPerInstructor(department);
    try
    ...

public void UpdateDepartment(Department department, Department origDepartment)
{
    ValidateOneAdministratorAssignmentPerInstructor(department);
    try
    ...

В файле ISchoolRepository.cs добавьте следующее объявление для нового метода доступа к данным:

IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator);

В файле SchoolRepository.cs добавьте следующую using инструкцию:

using System.Data.Objects;

В файле SchoolRepository.cs добавьте следующий новый метод доступа к данным:

public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
    return new ObjectQuery<Department>("SELECT VALUE d FROM Departments as d", context, MergeOption.NoTracking).Include("Person").Where(d => d.Administrator == administrator).ToList();
}

Этот код извлекает сущности Department с указанным администратором. Должен быть найден только один отдел (если таковой есть). Однако, так как в базу данных не встроено ограничение, тип возвращаемого значения является коллекцией на случай обнаружения нескольких отделов.

По умолчанию, когда контекст объекта извлекает сущности из базы данных, он отслеживает их в диспетчере состояний объектов. Параметр MergeOption.NoTracking указывает, что это отслеживание не будет выполняться для этого запроса. Это необходимо, так как запрос может вернуть точную сущность, которую вы пытаетесь обновить, а затем вы не сможете присоединить эту сущность. Например, если изменить отдел журнала на странице Departments.aspx и оставить администратора без изменений, этот запрос вернет отдел журнала. Если NoTracking параметр не задан, контекст объекта уже будет содержать сущность Отдела журнала в диспетчере состояний объектов. Затем при присоединении сущности отдела журнала, созданной из состояния представления, контекст объекта вызовет исключение с "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key"надписью .

(В качестве альтернативы указанию MergeOption.NoTrackingможно создать новый контекст объекта только для этого запроса. Так как новый контекст объекта будет иметь собственный диспетчер состояний объектов, при вызове Attach метода не возникнет конфликта. Новый контекст объекта будет совместно использовать метаданные и подключение к базе данных с исходным контекстом объекта, поэтому снижение производительности этого альтернативного подхода будет минимальным. Однако в приведенном здесь подходе NoTracking представлен параметр , который будет полезен в других контекстах. Этот NoTracking параметр рассматривается далее в следующем руководстве этой серии.)

В тестовом проекте добавьте новый метод доступа к данным в файл MockSchoolRepository.cs:

public IEnumerable<Department> GetDepartmentsByAdministrator(Int32 administrator)
{
    return (from d in departments
            where d.Administrator == administrator
            select d);
}

Этот код использует LINQ для выбора данных, которые репозиторий ContosoUniversity проекта использует LINQ to Entities.

Запустите тестовый проект еще раз. Результат на этот раз положительный.

Image04

Обработка исключений ObjectDataSource

ContosoUniversity В проекте запустите страницу Departments.aspx и попробуйте изменить администратора отдела на пользователя, который уже является администратором другого отдела. (Помните, что вы можете изменять только отделы, добавленные в этом руководстве, так как база данных поставляется с предварительно загруженными недопустимыми данными.) Вы получите следующую страницу ошибки сервера:

Image05

Вы не хотите, чтобы пользователи видели такого рода страницы ошибок, поэтому необходимо добавить код обработки ошибок. Откройте Файл Departments.aspx и укажите обработчик для OnUpdated события DepartmentsObjectDataSource. Открывающий ObjectDataSource тег теперь похож на следующий пример.

<asp:ObjectDataSource ID="DepartmentsObjectDataSource" runat="server" 
        TypeName="ContosoUniversity.BLL.SchoolBL"
        DataObjectTypeName="ContosoUniversity.DAL.Department" 
        SelectMethod="GetDepartments" 
        DeleteMethod="DeleteDepartment" 
        UpdateMethod="UpdateDepartment"
        ConflictDetection="CompareAllValues"
        OldValuesParameterFormatString="orig{0}" 
        OnUpdated="DepartmentsObjectDataSource_Updated" >

В файле Departments.aspx.cs добавьте следующую using инструкцию:

using ContosoUniversity.BLL;

Добавьте следующий обработчик для Updated события:

protected void DepartmentsObjectDataSource_Updated(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        if (e.Exception.InnerException is DuplicateAdministratorException)
        {
            var duplicateAdministratorValidator = new CustomValidator();
            duplicateAdministratorValidator.IsValid = false;
            duplicateAdministratorValidator.ErrorMessage = "Update failed: " + e.Exception.InnerException.Message;
            Page.Validators.Add(duplicateAdministratorValidator);
            e.ExceptionHandled = true;
        }
    }
}

ObjectDataSource Если элемент управления перехватывает исключение при попытке выполнить обновление, он передает исключение в аргументе события (e) этому обработчику. Код в обработчике проверяет, является ли исключение повторяющимся исключением администратора. Если это так, код создает проверяющий элемент управления, содержащий сообщение об ошибке ValidationSummary для отображения элемента управления.

Запустите страницу и попытайтесь снова сделать пользователя администратором двух отделов. На этот ValidationSummary раз элемент управления отображает сообщение об ошибке.

Image06

Внесите аналогичные изменения на страницу DepartmentsAdd.aspx . В DepartmentsAdd.aspx укажите обработчик для OnInserted события DepartmentsObjectDataSource. Полученная разметка будет похожа на следующий пример.

<asp:ObjectDataSource ID="DepartmentsObjectDataSource" runat="server" 
        TypeName="ContosoUniversity.BLL.SchoolBL" DataObjectTypeName="ContosoUniversity.DAL.Department" 
        InsertMethod="InsertDepartment"  
        OnInserted="DepartmentsObjectDataSource_Inserted">

В DepartmentsAdd.aspx.cs добавьте ту же using инструкцию:

using ContosoUniversity.BLL;

Добавьте следующий обработчик событий:

protected void DepartmentsObjectDataSource_Inserted(object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.Exception != null)
    {
        if (e.Exception.InnerException is DuplicateAdministratorException)
        {
            var duplicateAdministratorValidator = new CustomValidator();
            duplicateAdministratorValidator.IsValid = false;
            duplicateAdministratorValidator.ErrorMessage = "Insert failed: " + e.Exception.InnerException.Message;
            Page.Validators.Add(duplicateAdministratorValidator);
            e.ExceptionHandled = true;
        }
    }
}

Теперь вы можете протестировать страницу DepartmentsAdd.aspx.cs , чтобы убедиться, что она также правильно обрабатывает попытки сделать одного человека администратором нескольких отделов.

На этом завершается введение в реализацию шаблона репозитория для использования ObjectDataSource элемента управления с Entity Framework. Дополнительные сведения о шаблоне репозитория и возможности тестирования см. в техническом документе MSDN Testability and Entity Framework 4.0.

В следующем руководстве вы узнаете, как добавить в приложение функции сортировки и фильтрации.