次の方法で共有


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

作成者: Tom Dykstra

このチュートリアル シリーズは、Entity Framework 4.0 チュートリアル シリーズを使用してはじめにによって作成された Contoso University Web アプリケーションに基づいています。 以前のチュートリアルを完了していない場合は、このチュートリアルの開始点として、作成 したアプリケーションをダウンロード できます。 完全なチュートリアル シリーズによって作成された アプリケーションをダウンロード することもできます。 チュートリアルについて質問がある場合は、 ASP.NET Entity Framework フォーラムに投稿できます。

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

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 メソッドに直接アクセスする代わりに、リポジトリ クラス メソッドを呼び出します。

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

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

Note

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

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

Departments.aspxDepartmentsAdd.aspx で、 のTypeName="ContosoUniversity.DAL.SchoolRepository"各出現箇所を にTypeName="ContosoUniversity.BLL.SchoolBL変更します。 (全部で 4 つのインスタンスがあります)。

Departments.aspx ページと DepartmentsAdd.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 では、個々の講師は 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、[クラス] の順に選択します。

[クラス] サブメニューで [生成] が選択されていることを示すスクリーンショット。

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

テスト プロジェクトを実行します。 予想どおり、テストは失敗します。

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で検索します。 見つかった場合、コードは例外をスローします。 挿入または更新されるエンティティに値がない場合、検証チェックは必要ありません。更新中にメソッドが呼び出され、検出されたエンティティが更新対象のエンティティとDepartment一致Departmentする場合、例外はスローされませんAdministrator

メソッドと Update メソッドから新しいメソッドをInsert呼び出します。

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 、オブジェクト コンテキストには、オブジェクト状態マネージャーに History 部門エンティティが既に含まれます。 次に、ビューステートから再作成された履歴部門エンティティをアタッチすると、オブジェクト コンテキストによって という "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 例外の処理

プロジェクトで ContosoUniversityDepartments.aspx ページを実行し、ある部門の管理者を別の部門の既に管理者であるユーザーに変更します。 (データベースには無効なデータが事前に読み込まれているため、このチュートリアルで追加した部門のみを編集できることに注意してください)。次のサーバー エラー ページが表示されます。

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 作成します。

ページを実行し、もう一度 2 つの部門の管理者になるようにします。 今回は、コントロールに 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 ページをテストして、1 人のユーザーを複数の部門の管理者にする試行も正しく処理されることを確認できるようになりました。

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

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