使用 Entity Framework 4.0 和 ObjectDataSource 控件,第 2 部分:添加业务逻辑层和单元测试

作者 :Tom Dykstra

本教程系列基于 Contoso University Web 应用程序,该应用程序由入门使用 Entity Framework 4.0 教程系列创建。 如果未完成前面的教程,作为本教程的起点,可以下载已创建 的应用程序 。 还可以下载由完整教程系列创建 的应用程序 。 如果对教程有疑问,可以将其发布到 ASP.NET 实体框架论坛

在上一教程中,你使用 Entity Framework 和 ObjectDataSource 控件创建了 n 层 Web 应用程序。 本教程演示如何添加业务逻辑,同时将业务逻辑层 (BLL) 和数据访问层 (DAL) 分开,并演示如何为 BLL 创建自动化单元测试。

在本教程中,你将完成以下任务:

  • 创建存储库接口,用于声明所需的数据访问方法。
  • 在存储库类中实现存储库接口。
  • 创建一个业务逻辑类,该类调用存储库类以执行数据访问函数。
  • ObjectDataSource 控件连接到业务逻辑类,而不是存储库类。
  • 创建单元测试项目和存储库类,该类使用内存中集合进行数据存储。
  • 为要添加到业务逻辑类的业务逻辑创建单元测试,然后运行测试并看到它失败。
  • 在业务逻辑类中实现业务逻辑,然后重新运行单元测试并查看它通过。

你将使用在上一教程中创建的 Departments.aspxDepartmentsAdd.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 定义一个方法, (在存储库类中创建、读取、更新、删除) 方法。

SchoolRepositorySchoolRepository.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 方法使业务逻辑类可用于所选的任何后端数据存储。 业务逻辑类不需要知道它所调用的类如何持久保存数据。 (这通常称为 持久性 ignorance.) 这有利于单元测试,因为可以将业务逻辑类连接到存储库实现,该实现使用内存 List 中集合等简单内容来存储数据。

注意

从技术上讲,实体对象仍然不是持久性的,因为它们是从从 Entity Framework 的类继承的 EntityObject 类实例化的。 为了完全保留无知,可以使用 普通的旧 CLR 对象POCO)来代替从 EntityObject 类继承的对象。 使用 POCO 超出了本教程的范围。 有关详细信息,请参阅 MSDN 网站上的 可测试性和实体框架 4.0 。)

现在,可以将控件连接到 ObjectDataSource 业务逻辑类而不是存储库,并验证一切是否都像以前一样工作。

Departments.aspxDepartmentsAdd.aspx 中,将 每个匹配项 TypeName="ContosoUniversity.DAL.SchoolRepository" 更改为 TypeName="ContosoUniversity.BLL.SchoolBL“。 (all.)

运行 Departments.aspxDepartmentsAdd.aspx 页面,验证它们是否仍然像以前一样工作。

Image01

Image02

创建Unit-Test项目和存储库实现

使用测试项目模板将新 项目 添加到解决方案,并将其命名为 ContosoUniversity.Tests

在测试项目中,添加对 System.Data.Entity 的引用,并添加对 ContosoUniversity 项目的项目引用。

现在可以创建将用于单元测试的存储库类。 此存储库的数据存储将位于 类中。

Image12

在测试项目中,创建新的类文件,将其命名为 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()
        {
            
        }
    }
}

此存储库类与直接访问 Entity Framework 的 CRUD 方法相同,但它们使用 List 内存中的集合,而不是数据库。 这使测试类可以更轻松地设置和验证业务逻辑类的单元测试。

创建单元测试

测试项目模板为你创建了一个存根单元测试类,你的下一个任务是通过为要添加到业务逻辑类的业务逻辑添加单元测试方法来修改此类。

Image13

在 Contoso University,任何单个讲师只能是单个部门的管理员,并且你需要添加业务逻辑来强制实施此规则。 首先,添加测试并运行测试以查看它们失败。 然后,你将添加代码并重新运行测试,期望它们通过。

打开 UnitTest1.cs 文件,并为在 ContosoUniversity 项目中创建的业务逻辑和数据访问层添加 using 语句:

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 创建为单元测试项目创建的存储库类的实例,然后将其传递给业务逻辑类的新实例。 然后, 方法使用业务逻辑类插入三个可在测试方法中使用的部门。

测试方法验证如果有人尝试插入与现有部门相同的管理员的新部门,或者有人尝试通过将部门管理员设置为已是另一个部门的管理员的 ID 来更新部门管理员,则业务逻辑类是否会引发异常。

尚未创建异常类,因此此代码不会编译。 若要使其进行编译,请右键单击并选择“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值,则无需验证检查;如果在更新期间调用方法并且找到的实体与要更新的DepartmentDepartment实体匹配,则不会引发异常。

InsertUpdate 方法中调用新方法:

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 执行项目存储库使用 LINQ to Entities 的相同数据选择 ContosoUniversity

再次运行测试项目。 这次测试通过了。

Image04

处理 ObjectDataSource 异常

ContosoUniversity 项目中,运行 Departments.aspx 页,并尝试将某个部门的管理员更改为已经是另一个部门的管理员的人员。 (请记住,只能编辑在本教程中添加的部门,因为数据库预加载了无效的数据。) 你会收到以下服务器错误页:

Image05

你不希望用户看到此类错误页,因此需要添加错误处理代码。 打开 Departments.aspx 并为 的 DepartmentsObjectDataSource事件指定处理程序OnUpdated。 开始 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 中,为 OnInsertedDepartmentsObjectDataSource事件指定处理程序。 生成的标记将类似于以下示例。

<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 控件与实体框架配合使用。 有关存储库模式和可测试性的详细信息,请参阅 MSDN 白皮书 可测试性和实体框架 4.0

在以下教程中,你将了解如何向应用程序添加排序和筛选功能。