使用 Entity Framework 4.0 和 ObjectDataSource 控制項,第 2 部分:新增商務邏輯層和單元測試

作者 :Tom Dykstra

本教學課程系列是以 Contoso University Web 應用程式為基礎,此應用程式是由使用Entity Framework 4.0教學課程系列消費者入門所建立。 如果您未完成先前的教學課程,作為本教學課程的起點,您可以下載您已建立 的應用程式 。 您也可以下載完整教學課程系列所建立 的應用程式 。 如果您有關于教學課程的問題,您可以將這些教學課程張貼到 ASP.NET Entity Framework 論壇

在上一個教學課程中,您已使用 Entity Framework 和 ObjectDataSource 控制項建立多層式 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 網站上的 Testability and Entity Framework 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()
        {
            
        }
    }
}

此存放庫類別的 CRUD 方法與直接存取 Entity Framework 的方法相同,但它們在 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 會建立您為單元測試專案建立的存放庫類別實例,然後傳遞給商務邏輯類別的新實例。 然後,方法會使用商務邏輯類別來插入三個可在測試方法中使用的部門。

如果有人嘗試插入與現有部門相同的系統管理員的新部門,或有人嘗試將它設定為已是另一個部門系統管理員的人員識別碼,測試方法會驗證商務邏輯類別是否擲回例外狀況。

您尚未建立例外狀況類別,因此此程式碼將不會編譯。 若要讓它進行編譯,請以滑鼠右鍵按一下 DuplicateAdministratorException 並選取 [產生],然後選取 [ 類別]。

顯示 [類別] 子功能表中已選取 [產生] 的螢幕擷取畫面。

這會在測試專案中建立類別,您可以在主要專案中建立例外狀況類別之後刪除。 並實作商務邏輯。

執行測試專案。 如預期般,測試會失敗。

Image03

新增商務邏輯以通過測試

接下來,您將實作商務邏輯,使其無法設定為已經是另一個部門系統管理員的部門系統管理員。 您會從商務邏輯層擲回例外狀況,如果使用者編輯部門,然後在選取已是系統管理員的人員之後按一下 [更新 ],就會在呈現層中攔截該例外狀況。 (您也可以在轉譯頁面之前,從已經是系統管理員的下拉式清單中移除講師,但這裡的用途是使用商務邏輯層。)

首先,建立例外狀況類別,當使用者嘗試讓講師成為多個部門的系統管理員時,您將會擲回的例外狀況類別。 在主要專案中,在 BLL 資料夾中建立新的類別檔案,將它命名為 DuplicateAdministratorException.cs,並以下列程式碼取代現有的程式碼:

using System;

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

現在,刪除您稍早在測試專案中建立的 Temporary DuplicateAdministratorException.cs 檔案,以便進行編譯。

在主要專案中,開啟 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 來執行專案存放庫所使用的相同資料選取 ContosoUniversity 專案LINQ to Entities。

再次執行測試專案。 測試這一次會成功。

Image04

處理 ObjectDataSource 例外狀況

ContosoUniversity 專案中,執行 Departments.aspx 頁面,並嘗試將部門的系統管理員變更為已經是另一個部門系統管理員的人員。 (請記住,您只能編輯在本教學課程期間新增的部門,因為資料庫已預先載入無效 data.) 您會收到下列伺服器錯誤頁面:

Image05

您不希望使用者看到這種錯誤頁面,因此您需要新增錯誤處理常式代碼。 開啟 Departments.aspx ,並指定 事件的處理常式 OnUpdatedDepartmentsObjectDataSource 。 開頭 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 頁面,以確認它也正確地處理嘗試讓一個人員成為多個部門的系統管理員。

這會完成實作存放庫模式的簡介,以搭配 Entity Framework 使用 ObjectDataSource 控制項。 如需存放庫模式和可測試性的詳細資訊,請參閱 MSDN 技術白皮書 Testability 和 Entity Framework 4.0

在下列教學課程中,您將瞭解如何將排序和篩選功能新增至應用程式。