Entity Framework 4.0 と ObjectDataSource コントロールの使用、パート 2: ビジネス ロジック層と単体テストの追加

著者: Tom Dykstra

このチュートリアル シリーズは、Entity Framework 4.0 の概要チュートリアル シリーズを使用して作成された Contoso University Web アプリケーションをベースに構築されています。 前のチュートリアルを終わらせていない場合は、このチュートリアルの始めに、前のチュートリアルで作成するはずであったアプリケーションをダウンロードできます。 完了したチュートリアル シリーズで作成されたアプリケーションをダウンロードすることもできます。 チュートリアルに関する質問がある場合は、ASP.NET Entity Framework フォーラムに質問を投稿できます。

前のチュートリアルでは、Entity Framework と ObjectDataSource コントロールを使用して n 層 Web アプリケーションを作成しました。 このチュートリアルでは、ビジネスロジック層 (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 (作成、読み取り、更新、削除) メソッドごとに 1 つのメソッドを定義します。

SchoolRepository.csSchoolRepository クラスで、このクラスが ISchoolRepository インターフェイスを実装していることを示します。

public class SchoolRepository : IDisposable, ISchoolRepository

ビジネスロジック クラスの作成

次に、ビジネスロジック クラスを作成します。 これを行うことで、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 のメソッドに直接アクセスする代わりに、リポジトリ クラスのメソッドを呼び出します。

リポジトリ クラスへの参照を保持するクラス変数はインターフェイス型として定義され、リポジトリ クラスをインスタンス化するコードは 2 つのコンストラクターに含まれています。 パラメーターなしのコンストラクターは、ObjectDataSource コントロールによって使用されます。 前に作成した SchoolRepository クラスのインスタンスが作成されます。 もう 1 つのコンストラクターでは、ビジネスロジック クラスをインスタンス化するすべてのコードが、リポジトリ インターフェイスを実装する任意のオブジェクトを渡すことを許可します。

リポジトリ クラスと 2 つのコンストラクターを呼び出す CRUD メソッドにより、任意のバックエンド データ ストアでビジネスロジック クラスを使用できます。 ビジネスロジック クラスが、呼び出しているクラスがデータを保持する方法を認識している必要はありません (これは多くの場合、"永続化非依存" と呼ばれます)。これにより、ビジネスロジック クラスを、データを格納するために List メモリ内コレクションと同じくらい単純なものを使用するリポジトリ実装に接続できるため、単体テストが容易になります。

Note

技術的には、エンティティ オブジェクトは Entity Framework の EntityObject クラスから継承するクラスからインスタンス化されるため、まだ永続化に依存します。 永続化への依存を完全になくすために、EntityObject クラスから継承するオブジェクトの代わりに、"単純な従来の CLR オブジェクト" (POCO) を使用できます。 POCO の使用については、このチュートリアルの範囲を超えています。 詳細については、MSDN Web サイトの「テストの容易性と Entity Framework 4.0」を参照してください)。

これで、リポジトリの代わりにビジネスロジック クラスに ObjectDataSource コントロールを接続し、すべてが以前と同じように動作することを確認できます。

Departments.aspxDepartmentsAdd.aspx に出現する TypeName="ContosoUniversity.DAL.SchoolRepository"TypeName="ContosoUniversity.BLL.SchoolBL に変更します (全部で 4 回出現します)。

Departments.aspx ページと DepartmentsAdd.aspx ページを実行して、以前と同じように動作することを確認します。

Image01

Image02

単体テスト プロジェクトとリポジトリ実装の作成

テスト プロジェクト テンプレートを使用してソリューションに新しいプロジェクトを追加し、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 では、個々の講師が管理者になることができるのは 1 つの学科のみであり、この規則を適用するためにビジネス ロジックを追加する必要があります。 まずはテストを追加し、そのテストを実行して、不合格になるのを確認します。 その後、コードを追加し、再度テストを実行すると、テストが合格になると想定します。

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 メソッドは、単体テスト プロジェクト用に作成したリポジトリ クラスのインスタンスを作成し、ビジネスロジック クラスの新しいインスタンスに渡します。 次に、このメソッドはビジネスロジック クラスを使用して、テスト メソッドで使用できる 3 つの学科を挿入します。

このテスト メソッドは、あるユーザーが既存の学科と同じ管理者が指定された新しい学科の挿入を試行した、またはある学科の管理者を既に別の学科の管理者であるユーザーの ID に設定して管理者の更新を試行した場合に、ビジネスロジック クラスが例外をスローすることを確認します。

例外クラスはまだ作成していないため、このコードはコンパイルされません。 コンパイルするには、DuplicateAdministratorException を右クリックして [生成] を選択し、[クラス] を選択します。

Screenshot that shows Generate selected in the Class submenu.

これにより、メイン プロジェクトで例外クラスを作成し、ビジネス ロジックを実装した後に削除できるクラスが テスト プロジェクトに作成されます。

テスト プロジェクトを実行します。 予想どおり、テストは不合格になります。

Image03

テスト合格に向けたビジネス ロジックの追加

次に、既に別の学科の管理者となっているユーザーを学科の管理者として指定できないようにするビジネス ロジックを実装します。 ユーザーが学科を編集し、既に管理者となっているユーザーを選択した後に [更新] をクリックした場合に、ビジネスロジック層から例外をスローし、それをプレゼンテーション層でキャッチします (ページをレンダリングする前に既に管理者になっている講師をドロップダウン リストから削除することもできますが、ここでの目的はビジネスロジック層を操作することです)。

まず、ユーザーがある講師を複数の学科の管理者に指定するとスローされる例外クラスを作成します。 メイン プロジェクトで、BLL フォルダーに DuplicateAdministratorException.cs という名前の新しいクラス ファイルを作成し、既存のコードを次のコードに置き換えます。

using System;

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

ここで、コンパイルできるように、前にテスト プロジェクトで作成した一時ファイル 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 エンティティを挿入または更新するときに、このメソッドを呼び出して、既に別の学科に同じ管理者がいないかどうかをチェックします。

このコードは、挿入または更新されるエンティティと同じ Administrator プロパティ値を持つ Department エンティティをデータベースで検索するメソッドを呼び出します。 見つかった場合、コードから例外がスローされます。 挿入または更新されるエンティティに 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 エンティティを取得します。 1 つの学科のみが見つかるはずです (存在する場合)。 ただし、データベースに制約が組み込まれていないため、複数の学科が見つかった場合の戻り値の型はコレクションです。

既定では、オブジェクト コンテキストがデータベースからエンティティを取得すると、そのオブジェクト状態マネージャーでそのエンティティが追跡されます。 この 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 を開き、DepartmentsObjectDataSourceOnUpdated イベントのハンドラーを指定します。 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 コントロールが表示するエラー メッセージが含まれる検証コントロールを作成します。

ページを実行し、もう一度誰かを 2 つの学科の管理者にしてみます。 今回は、ValidationSummary コントロールにエラー メッセージが表示されます。

Image06

DepartmentsAdd.aspx ページに同様の変更を加えます。 DepartmentsAdd.aspx に、DepartmentsObjectDataSourceOnInserted イベントのハンドラーを指定します。 結果のマークアップは、次の例のようになります。

<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 ページをテストして、1 人のユーザーを複数の学科の管理者にする試行も正しく処理されることを確認できるようになりました。

これで、Entity Framework で ObjectDataSource コントロールを使用するためのリポジトリ パターンの実装の概要は終わりです。 リポジトリ パターンとテストの容易性の詳細については、MSDN ホワイト ペーパー「テストの容易性と Entity Framework 4.0」を参照してください。

次のチュートリアルでは、並べ替えとフィルター機能をアプリケーションに追加する方法について説明します。