次の方法で共有


ASP.NET MVC アプリケーションでの リポジトリ パターンおよび Unit of Work パターンの実装 (9/10)

著者: Tom Dykstra

Contoso University のサンプル Web アプリケーションでは、Entity Framework 5 Code First と Visual Studio 2012 を使用して ASP.NET MVC 4 アプリケーションを作成する方法を示します。 チュートリアル シリーズについては、シリーズの最初のチュートリアルを参照してください。

Note

解決できない問題が発生した場合は、完了した章をダウンロードして、問題を再現してみてください。 通常、完成したコードと自分のコードを比較することで、問題の解決策を見つけることができます。 一般的なエラーとその解決方法については、「エラーと回避策」をご覧ください。

前のチュートリアルでは、継承を使用して、Student エンティティ クラスと Instructor エンティティ クラスの冗長コードを減らしました。 このチュートリアルでは、CRUD 操作に リポジトリ パターンおよび Unit of Work パターンを使用するいくつかの方法について説明します。 前のチュートリアルと同様に、このチュートリアルでは、新しいページを作成するのではなく、既に作成したページでのコードの動作方法を変更します。

リポジトリ パターンおよび Unit of Work パターン

リポジトリ パターンおよび Unit of Work パターンは、アプリケーションのデータ アクセス層とビジネス ロジック層の間に抽象化レイヤーを作成するためのものです。 これらのパターンを実装すると、データ ストアの変更からアプリケーションを隔離でき、自動化された単体テストやテスト駆動開発 (TDD) を円滑化できます。

このチュートリアルでは、エンティティ型ごとにリポジトリ クラスを実装します。 Student エンティティ型の場合は、リポジトリ インターフェイスとリポジトリ クラスを作成します。 コントローラーでリポジトリをインスタンス化するときは、コントローラーがリポジトリ インターフェイスを実装する任意のオブジェクトへの参照を受け入れるようにするために、インターフェイスを使用します。 コントローラーが Web サーバーで実行されると、Entity Framework で動作するリポジトリを受け取ります。 コントローラーが単体テスト クラスで実行されると、メモリ内コレクションなど、テスト用に簡単に操作できる方法で格納されたデータを操作するリポジトリを受け取ります。

チュートリアルの後半では、Course コントローラー内の Course エンティティ型と Department エンティティ型に対して、複数の リポジトリ クラスおよび Unit of Work クラスを使用します。 Unit of Work クラスは、すべてのリポジトリが共有する単一のデータベース コンテキスト クラスを作成することで、複数のリポジトリの作業を調整します。 自動単体テストを実行できるようにする場合は、Student リポジトリの場合と同じ方法で、これらのクラスのインターフェイスを作成して使用します。 ただし、チュートリアルをシンプルにするために、ここではインターフェイスなしでこれらのクラスを作成して使用します。

次の図は、リポジトリまたは Unit of Work パターンをまったく使用しない場合と比較して、コントローラーとコンテキスト クラスの関係を概念化する 1 つの方法を示しています。

Repository_pattern_diagram

このチュートリアル シリーズでは単体テストを作成しません。 リポジトリ パターンを使用する MVC アプリケーションでの TDD の概要については、「チュートリアル: ASP.NET MVC での TDD の使用」を参照してください。 リポジトリ パターンの詳細については、次のリソースを参照してください:

Note

リポジトリ パターンと Unit of Work パターンを実装するには、さまざまな方法があります。 リポジトリ クラスは、Unit of Work クラスの有無にかかわらず使用できます。 すべてのエンティティ型に対して 1 つのリポジトリを実装することも、型ごとにそれぞれ実装することもできます。 型ごとにそれぞれ実装する場合は、個別のクラス、ジェネリック 基底クラスと派生クラス、または抽象基底クラスと派生クラスを使用できます。 ビジネス ロジックをリポジトリに含めたり、データ アクセス ロジックに制限したりできます。 エンティティ セットで DbSet 型の代わりに IDbSet インターフェイスを使用して、抽象化レイヤーをデータベース コンテキスト クラスに構築することもできます。 このチュートリアルで示す抽象化レイヤーを実装する方法は、すべてのシナリオと環境の推奨事項ではなく、検討すべき 1 つのオプションです。

Student リポジトリ クラスの作成

DAL フォルダーで、IStudentRepository.cs という名前のクラス ファイルを作成し、既存のコードを次のコードに置き換えます:

using System;
using System.Collections.Generic;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public interface IStudentRepository : IDisposable
    {
        IEnumerable<Student> GetStudents();
        Student GetStudentByID(int studentId);
        void InsertStudent(Student student);
        void DeleteStudent(int studentID);
        void UpdateStudent(Student student);
        void Save();
    }
}

このコードでは、2 つの読み取りメソッド (すべての Student エンティティを返すメソッドと ID で 1 つの Student エンティティを検索するメソッド) を含む、一般的な CRUD メソッドのセットを宣言します。

DAL フォルダーに、StudentRepository.cs ファイルという名前のクラス ファイル作成します。 既存のコードを、IStudentRepository インターフェイスを実装する次のコードに置き換えます:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public class StudentRepository : IStudentRepository, IDisposable
    {
        private SchoolContext context;

        public StudentRepository(SchoolContext context)
        {
            this.context = context;
        }

        public IEnumerable<Student> GetStudents()
        {
            return context.Students.ToList();
        }

        public Student GetStudentByID(int id)
        {
            return context.Students.Find(id);
        }

        public void InsertStudent(Student student)
        {
            context.Students.Add(student);
        }

        public void DeleteStudent(int studentID)
        {
            Student student = context.Students.Find(studentID);
            context.Students.Remove(student);
        }

        public void UpdateStudent(Student student)
        {
            context.Entry(student).State = EntityState.Modified;
        }

        public void Save()
        {
            context.SaveChanges();
        }

        private bool disposed = false;

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

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

データベース コンテキストはクラス変数で定義され、コンストラクターは呼び出し元オブジェクトがコンテキストのインスタンスで渡される必要があります:

private SchoolContext context;

public StudentRepository(SchoolContext context)
{
    this.context = context;
}

リポジトリで新しいコンテキストをインスタンス化することもできますが、1 つのコントローラーで複数のリポジトリを使用した場合、それぞれが個別のコンテキストになります。 後で Course コントローラーで複数のリポジトリを使用します。また、Unit of Work クラスで、すべてのリポジトリが同じコンテキストを確実に使用できるようにする方法を確認します。

リポジトリは IDisposable を実装し、コントローラーで前に確認したようにデータベース コンテキストを破棄します。また CRUD メソッドは、前に見たのと同じ方法でデータベース コンテキストを呼び出します。

リポジトリを使用するように Student コントローラーを変更する

StudentController.cs で、クラス内の現在のコードを次のコードに置き換えます。 変更が強調表示されます。

using System;
using System.Data;
using System.Linq;
using System.Web.Mvc;
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
using PagedList;

namespace ContosoUniversity.Controllers
{
   public class StudentController : Controller
   {
      private IStudentRepository studentRepository;

      public StudentController()
      {
         this.studentRepository = new StudentRepository(new SchoolContext());
      }

      public StudentController(IStudentRepository studentRepository)
      {
         this.studentRepository = studentRepository;
      }

      //
      // GET: /Student/

      public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
      {
         ViewBag.CurrentSort = sortOrder;
         ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
         ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";

         if (searchString != null)
         {
            page = 1;
         }
         else
         {
            searchString = currentFilter;
         }
         ViewBag.CurrentFilter = searchString;

         var students = from s in studentRepository.GetStudents()
                        select s;
         if (!String.IsNullOrEmpty(searchString))
         {
            students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                                   || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
         }
         switch (sortOrder)
         {
            case "name_desc":
               students = students.OrderByDescending(s => s.LastName);
               break;
            case "Date":
               students = students.OrderBy(s => s.EnrollmentDate);
               break;
            case "date_desc":
               students = students.OrderByDescending(s => s.EnrollmentDate);
               break;
            default:  // Name ascending 
               students = students.OrderBy(s => s.LastName);
               break;
         }

         int pageSize = 3;
         int pageNumber = (page ?? 1);
         return View(students.ToPagedList(pageNumber, pageSize));
      }

      //
      // GET: /Student/Details/5

      public ViewResult Details(int id)
      {
         Student student = studentRepository.GetStudentByID(id);
         return View(student);
      }

      //
      // GET: /Student/Create

      public ActionResult Create()
      {
         return View();
      }

      //
      // POST: /Student/Create

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Create(
         [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
           Student student)
      {
         try
         {
            if (ModelState.IsValid)
            {
               studentRepository.InsertStudent(student);
               studentRepository.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
            ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
         }
         return View(student);
      }

      //
      // GET: /Student/Edit/5

      public ActionResult Edit(int id)
      {
         Student student = studentRepository.GetStudentByID(id);
         return View(student);
      }

      //
      // POST: /Student/Edit/5

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Edit(
         [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
         Student student)
      {
         try
         {
            if (ModelState.IsValid)
            {
               studentRepository.UpdateStudent(student);
               studentRepository.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
            ModelState.AddModelError(string.Empty, "Unable to save changes. Try again, and if the problem persists contact your system administrator.");
         }
         return View(student);
      }

      //
      // GET: /Student/Delete/5

      public ActionResult Delete(bool? saveChangesError = false, int id = 0)
      {
         if (saveChangesError.GetValueOrDefault())
         {
            ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
         }
         Student student = studentRepository.GetStudentByID(id);
         return View(student);
      }

      //
      // POST: /Student/Delete/5

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Delete(int id)
      {
         try
         {
            Student student = studentRepository.GetStudentByID(id);
            studentRepository.DeleteStudent(id);
            studentRepository.Save();
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
            return RedirectToAction("Delete", new { id = id, saveChangesError = true });
         }
         return RedirectToAction("Index");
      }

      protected override void Dispose(bool disposing)
      {
         studentRepository.Dispose();
         base.Dispose(disposing);
      }
   }
}

コントローラーは、コンテキスト クラスの代わりに IStudentRepository インターフェイスを実装するオブジェクトのクラス変数を宣言するようになりました:

private IStudentRepository studentRepository;

既定の (パラメーターなしの) コンストラクターは新しいコンテキスト インスタンスを作成し、省略可能なコンストラクターを使用すると、呼び出し元はコンテキスト インスタンスを渡すことができます。

public StudentController()
{
    this.studentRepository = new StudentRepository(new SchoolContext());
}

public StudentController(IStudentRepository studentRepository)
{
    this.studentRepository = studentRepository;
}

(依存関係の挿入 または DI を使用していた場合は、DI ソフトウェアによって正しいリポジトリ オブジェクトが常に提供されるため、既定のコンストラクターは必要ありません。)

CRUD メソッドでは、コンテキストの代わりにリポジトリが呼び出されるようになりました:

var students = from s in studentRepository.GetStudents()
               select s;
Student student = studentRepository.GetStudentByID(id);
studentRepository.InsertStudent(student);
studentRepository.Save();
studentRepository.UpdateStudent(student);
studentRepository.Save();
studentRepository.DeleteStudent(id);
studentRepository.Save();

そして、Dispose メソッドはコンテキストの代わりにリポジトリを破棄するようになりました:

studentRepository.Dispose();

サイトを実行し、[Student] タブをクリックします。

Students_Index_page

ページはリポジトリを使用するようにコードを変更する前と同じように表示され、動作し、他の Student ページも同じように動作します。 ただし、コントローラーの Index メソッドがフィルター処理と順序付けを行う方法には重要な違いがあります。 このメソッドの元のバージョンには、次のコードが含まれていました:

var students = from s in context.Students
               select s;
if (!String.IsNullOrEmpty(searchString))
{
    students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                           || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}

更新された Index メソッドには、次のコードが含まれています:

var students = from s in studentRepository.GetStudents()
                select s;
if (!String.IsNullOrEmpty(searchString))
{
    students = students.Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())
                        || s.FirstMidName.ToUpper().Contains(searchString.ToUpper()));
}

強調表示されているコードのみが変更されています。

元のバージョンのコードでは、 studentsIQueryable オブジェクトとして型指定されています。 クエリは、ToList などのメソッドを使用してコレクションに変換されるまで、データベースに送信されません。これはインデックス ビューが Student モデルにアクセスするまで発生しません。 上記の元のコードの Where メソッドは、データベースに送信される SQL クエリの WHERE 句になります。 つまり、選択したエンティティのみがデータベースによって返されます。 ただし、context.StudentsstudentRepository.GetStudents() に変更した結果として、このステートメントの後の students 変数は、データベース内のすべての Student を含む IEnumerable コレクションです。 Where メソッドを適用した結果は同じですが、データベースではなく、Web サーバー上のメモリで作業が行われます。 大量のデータを返すクエリの場合、これは非効率的な場合があります。

ヒント

IQueryable vs.IEnumerable

ここに示すようにリポジトリを実装した後、[検索] ボックスに何かを入力した場合でも、SQL Server に送信されたクエリでは検索条件が含まれていないため、すべての Student 行が返されます:

Screenshot of the code that shows the new student repository implemented and highlighted.

SELECT 
'0X0X' AS [C1], 
[Extent1].[PersonID] AS [PersonID], 
[Extent1].[LastName] AS [LastName], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[EnrollmentDate] AS [EnrollmentDate]
FROM [dbo].[Person] AS [Extent1]
WHERE [Extent1].[Discriminator] = N'Student'

リポジトリが検索条件を知らずにクエリを実行したため、このクエリはすべての Student データを返します。 並べ替え、検索条件の適用、ページング用のデータのサブセットの選択 (この場合は 3 行のみ表示) のプロセスは、後でメソッドがコレクションで呼び出されたときに ToPagedList メモリ内で IEnumerable 実行されます。

以前のバージョンのコード (リポジトリを実装する前) では、IQueryable オブジェクトに対して ToPagedList が呼び出されたときに、検索条件を適用するまで、クエリはデータベースに送信されません。

Screenshot that shows the Student Controller code. A search string row of code and the To Paged List row of code are highlighted.

ToPagedList が IQueryable オブジェクトで呼び出されると、SQL Server に送信されるクエリによって検索文字列が指定され、その結果、検索条件を満たす行のみが返され、メモリ内でフィルター処理を実行する必要はありません。

exec sp_executesql N'SELECT TOP (3) 
[Project1].[StudentID] AS [StudentID], 
[Project1].[LastName] AS [LastName], 
[Project1].[FirstName] AS [FirstName], 
[Project1].[EnrollmentDate] AS [EnrollmentDate]
FROM ( SELECT [Project1].[StudentID] AS [StudentID], [Project1].[LastName] AS [LastName], [Project1].[FirstName] AS [FirstName], [Project1].[EnrollmentDate] AS [EnrollmentDate], row_number() OVER (ORDER BY [Project1].[LastName] ASC) AS [row_number]
FROM ( SELECT 
    [Extent1].[StudentID] AS [StudentID], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[EnrollmentDate] AS [EnrollmentDate]
    FROM [dbo].[Student] AS [Extent1]
    WHERE (( CAST(CHARINDEX(UPPER(@p__linq__0), UPPER([Extent1].[LastName])) AS int)) > 0) OR (( CAST(CHARINDEX(UPPER(@p__linq__1), UPPER([Extent1].[FirstName])) AS int)) > 0)
)  AS [Project1]
)  AS [Project1]
WHERE [Project1].[row_number] > 0
ORDER BY [Project1].[LastName] ASC',N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)',@p__linq__0=N'Alex',@p__linq__1=N'Alex'

(次のチュートリアルでは、SQL Server に送信されたクエリを調べる方法について説明します。)

次のセクションでは、データベースが作業を行う必要があることを指定できるようにするリポジトリ メソッドを実装する方法を示します。

これで、コントローラーと Entity Framework データベース コンテキストの間に抽象化レイヤーが作成されました。 このアプリケーションで自動単体テストを実行する場合は、IStudentRepository を実装する単体テスト プロジェクトに代替リポジトリ クラスを作成できます。このモック リポジトリ クラスは、コンテキストを呼び出してデータの読み取りと書き込みを行う代わりに、コントローラー関数をテストするためにメモリ内コレクションを操作できます。

汎用リポジトリと Unit of Work クラスを実装する

エンティティの種類ごとにリポジトリ クラスを作成すると、多くの冗長なコードが生成され、部分的な更新が発生する可能性があります。 たとえば、同じトランザクションの一部として、2 つの異なるエンティティの種類を更新する必要があるとします。 それぞれが個別のデータベース コンテキスト インスタンスを使用している場合、1 つは成功し、もう 1 つは失敗する可能性があります。 冗長なコードを最小限に抑える方法の 1 つは、汎用リポジトリを使用することです。また、すべてのリポジトリが同じデータベース コンテキストを使用 (したがって、すべての更新を調整する) ようにする 1 つの方法は、Unit of Work クラスを使用することです。

チュートリアルのこのセクションでは、GenericRepository クラスと UnitOfWork クラスを作成し、Course コントローラーでそれらを使用して、Department エンティティ セットと Course エンティティ セットの両方にアクセスします。 前に説明したように、チュートリアルのこの部分をシンプルにするために、これらのクラスのインターフェイスは作成しません。 ただし、TDD を支援するためにそれらを使用する場合は、通常、Student リポジトリと同じ方法でインターフェイスを使用して実装します。

汎用リポジトリを作成する

DAL フォルダーで、GenericRepository.cs を作成し、既存のコードを次のコードに置き換えます:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data;
using System.Data.Entity;
using ContosoUniversity.Models;
using System.Linq.Expressions;

namespace ContosoUniversity.DAL
{
    public class GenericRepository<TEntity> where TEntity : class
    {
        internal SchoolContext context;
        internal DbSet<TEntity> dbSet;

        public GenericRepository(SchoolContext context)
        {
            this.context = context;
            this.dbSet = context.Set<TEntity>();
        }

        public virtual IEnumerable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            string includeProperties = "")
        {
            IQueryable<TEntity> query = dbSet;

            if (filter != null)
            {
                query = query.Where(filter);
            }

            foreach (var includeProperty in includeProperties.Split
                (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
            {
                query = query.Include(includeProperty);
            }

            if (orderBy != null)
            {
                return orderBy(query).ToList();
            }
            else
            {
                return query.ToList();
            }
        }

        public virtual TEntity GetByID(object id)
        {
            return dbSet.Find(id);
        }

        public virtual void Insert(TEntity entity)
        {
            dbSet.Add(entity);
        }

        public virtual void Delete(object id)
        {
            TEntity entityToDelete = dbSet.Find(id);
            Delete(entityToDelete);
        }

        public virtual void Delete(TEntity entityToDelete)
        {
            if (context.Entry(entityToDelete).State == EntityState.Detached)
            {
                dbSet.Attach(entityToDelete);
            }
            dbSet.Remove(entityToDelete);
        }

        public virtual void Update(TEntity entityToUpdate)
        {
            dbSet.Attach(entityToUpdate);
            context.Entry(entityToUpdate).State = EntityState.Modified;
        }
    }
}

クラス変数は、データベース コンテキストと、リポジトリがインスタンス化されるエンティティ セットに対して宣言されます:

internal SchoolContext context;
internal DbSet dbSet;

コンストラクターは、データベース コンテキスト インスタンスを受け入れ、エンティティ セット変数を初期化します:

public GenericRepository(SchoolContext context)
{
    this.context = context;
    this.dbSet = context.Set<TEntity>();
}

Get メソッドでは、ラムダ式を使用して、結果を並べる呼び出し元のコードでフィルター条件と列を指定できます。文字列パラメーターを使用すると、呼び出し元は一括読み込み用のナビゲーション プロパティのコンマ区切りリストを提供できます:

public virtual IEnumerable<TEntity> Get(
    Expression<Func<TEntity, bool>> filter = null,
    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
    string includeProperties = "")

このコード Expression<Func<TEntity, bool>> filter は、呼び出し元が TEntity 型に基づいてラムダ式を提供することを意味し、この式はブール値を返します。 たとえば、Student エンティティ型に対してリポジトリがインスタンス化されている場合、呼び出し元のメソッドのコードで filter パラメーターに student => student.LastName == "Smith" を指定できます。

また、このコード Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy は、呼び出し元がラムダ式を提供することを意味します。 ただし、この場合、式への入力は TEntity 型の IQueryable オブジェクトです。 式は、IQueryable オブジェクトの順序付けられたバージョンを返します。 たとえば、Student エンティティ型に対してリポジトリがインスタンス化されている場合、呼び出し元のメソッドのコードで orderBy パラメーターに q => q.OrderBy(s => s.LastName) を指定できます。

Get メソッド内のコードは IQueryable オブジェクトを作成し、フィルター式が存在する場合はそれを適用します:

IQueryable<TEntity> query = dbSet;

if (filter != null)
{
    query = query.Where(filter);
}

次に、コンマ区切りリストを解析した後に、一括読み込み式を適用します:

foreach (var includeProperty in includeProperties.Split
    (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)) 
{ 
    query = query.Include(includeProperty); 
}

最後に、orderBy 式がある場合はそれを適用し、結果を返します。それ以外の場合は、順序付けられていないクエリから結果を返します:

if (orderBy != null)
{
    return orderBy(query).ToList();
}
else
{
    return query.ToList();
}

Get メソッドを呼び出すときは、これらの関数のパラメーターを指定する代わりに、メソッドによって返される IEnumerable コレクションに対してフィルター処理と並べ替えを行うことができます。 ただし、並べ替えとフィルター処理は、Web サーバー上のメモリ内で実行されます。 これらのパラメーターを使用すると、Web サーバーではなくデータベースによって作業が行われるようにすることができます。 別の方法は、特定のエンティティ型の派生クラスを作成し、特殊な Get メソッド (例: GetStudentsInNameOrderGetStudentsByName) を追加することです。 ただし、複雑なアプリケーションでは、このような派生クラスや特殊なメソッドが多数発生する可能性があり、これを維持するにはさらに多くの作業が必要になる可能性があります。

GetByIDInsertUpdate メソッド内のコードは、非ジェネリック リポジトリで見たものと似ています。 (Find メソッドを使用して一括読み込みを行うことはできませんので、GetByID シグネチャに一括読み込みパラメーターを指定していません。)

Delete メソッドには、次の 2 つのオーバーロードが用意されています:

public virtual void Delete(object id)
{
    TEntity entityToDelete = dbSet.Find(id);
    dbSet.Remove(entityToDelete);
}

public virtual void Delete(TEntity entityToDelete)
{
    if (context.Entry(entityToDelete).State == EntityState.Detached)
    {
        dbSet.Attach(entityToDelete);
    }
    dbSet.Remove(entityToDelete);
}

そのうちの 1 つは、削除するエンティティの ID のみを渡し、1 つはエンティティ インスタンスを受け取ります。 「コンカレンシーの処理」に関するチュートリアルで説明したように、コンカレンシー処理には、追跡プロパティの元の値を含むエンティティ インスタンスを受け取る Delete メソッドが必要です。

この汎用リポジトリは、一般的な CRUD 要件を処理します。 特定のエンティティ型に、より複雑なフィルター処理や順序付けなどの特別な要件がある場合は、その型に追加のメソッドを持つ派生クラスを作成できます。

Unit of Work クラスの作成

Unit of Work クラスは、複数のリポジトリを使用するときに、1 つのデータベース コンテキストを共有するということが唯一の目的です。 そうすれば、Unit of Work が完了したら、コンテキストのそのインスタンスで SaveChanges メソッドを呼び出し、関連するすべての変更が調整されることを保証できます。 クラスに必要なのは、各リポジトリの Save メソッドとプロパティだけです。 各リポジトリ プロパティは、他のリポジトリ インスタンスと同じデータベース コンテキスト インスタンスを使用してインスタンス化されたリポジトリ インスタンスを返します。

[DAL] フォルダーで、UnitOfWork.cs という名前のクラス ファイルを作成し、テンプレート コードを次のコードに変更します:

using System;
using ContosoUniversity.Models;

namespace ContosoUniversity.DAL
{
    public class UnitOfWork : IDisposable
    {
        private SchoolContext context = new SchoolContext();
        private GenericRepository<Department> departmentRepository;
        private GenericRepository<Course> courseRepository;

        public GenericRepository<Department> DepartmentRepository
        {
            get
            {

                if (this.departmentRepository == null)
                {
                    this.departmentRepository = new GenericRepository<Department>(context);
                }
                return departmentRepository;
            }
        }

        public GenericRepository<Course> CourseRepository
        {
            get
            {

                if (this.courseRepository == null)
                {
                    this.courseRepository = new GenericRepository<Course>(context);
                }
                return courseRepository;
            }
        }

        public void Save()
        {
            context.SaveChanges();
        }

        private bool disposed = false;

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

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

このコードは、データベース コンテキストと各リポジトリのクラス変数を作成します。 context 変数の場合、新しいコンテキストがインスタンス化されます:

private SchoolContext context = new SchoolContext();
private GenericRepository<Department> departmentRepository;
private GenericRepository<Course> courseRepository;

各リポジトリ プロパティは、リポジトリが既に存在するかどうかを確認します。 そうでない場合は、コンテキスト インスタンスを渡してリポジトリをインスタンス化します。 その結果、すべてのリポジトリが同じコンテキスト インスタンスを共有します。

public GenericRepository<Department> DepartmentRepository
{
    get
    {

        if (this.departmentRepository == null)
        {
            this.departmentRepository = new GenericRepository<Department>(context);
        }
        return departmentRepository;
    }
}

Save メソッドは、データベース コンテキストで SaveChanges を呼び出します。

クラス変数でデータベース コンテキストをインスタンス化するクラスと同様に、UnitOfWork クラスは IDisposable を実装してコンテキストを破棄します。

UnitOfWork クラスとリポジトリを使用するようにコース コントローラーを変更する

CourseController.cs で現在使用しているコードを次のコードに置き換えます:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using ContosoUniversity.Models;
using ContosoUniversity.DAL;

namespace ContosoUniversity.Controllers
{
   public class CourseController : Controller
   {
      private UnitOfWork unitOfWork = new UnitOfWork();

      //
      // GET: /Course/

      public ViewResult Index()
      {
         var courses = unitOfWork.CourseRepository.Get(includeProperties: "Department");
         return View(courses.ToList());
      }

      //
      // GET: /Course/Details/5

      public ViewResult Details(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         return View(course);
      }

      //
      // GET: /Course/Create

      public ActionResult Create()
      {
         PopulateDepartmentsDropDownList();
         return View();
      }

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Create(
          [Bind(Include = "CourseID,Title,Credits,DepartmentID")]
         Course course)
      {
         try
         {
            if (ModelState.IsValid)
            {
               unitOfWork.CourseRepository.Insert(course);
               unitOfWork.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
         }
         PopulateDepartmentsDropDownList(course.DepartmentID);
         return View(course);
      }

      public ActionResult Edit(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         PopulateDepartmentsDropDownList(course.DepartmentID);
         return View(course);
      }

      [HttpPost]
      [ValidateAntiForgeryToken]
      public ActionResult Edit(
           [Bind(Include = "CourseID,Title,Credits,DepartmentID")]
         Course course)
      {
         try
         {
            if (ModelState.IsValid)
            {
               unitOfWork.CourseRepository.Update(course);
               unitOfWork.Save();
               return RedirectToAction("Index");
            }
         }
         catch (DataException /* dex */)
         {
            //Log the error (uncomment dex variable name after DataException and add a line here to write a log.)
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
         }
         PopulateDepartmentsDropDownList(course.DepartmentID);
         return View(course);
      }

      private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
      {
         var departmentsQuery = unitOfWork.DepartmentRepository.Get(
             orderBy: q => q.OrderBy(d => d.Name));
         ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment);
      }

      //
      // GET: /Course/Delete/5

      public ActionResult Delete(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         return View(course);
      }

      //
      // POST: /Course/Delete/5

      [HttpPost, ActionName("Delete")]
      [ValidateAntiForgeryToken]
      public ActionResult DeleteConfirmed(int id)
      {
         Course course = unitOfWork.CourseRepository.GetByID(id);
         unitOfWork.CourseRepository.Delete(id);
         unitOfWork.Save();
         return RedirectToAction("Index");
      }

      protected override void Dispose(bool disposing)
      {
         unitOfWork.Dispose();
         base.Dispose(disposing);
      }
   }
}

このコードは、UnitOfWork クラスのクラス変数を追加します。 (ここでインターフェイスを使用していた場合は、ここで変数を初期化しません。代わりに、Student リポジトリの場合と同様に、2 つのコンストラクターのパターンを実装します。)

private UnitOfWork unitOfWork = new UnitOfWork();

クラスの残りの部分では、データベース コンテキストへのすべての参照が適切なリポジトリへの参照に置き換えられ、UnitOfWork プロパティを使用してリポジトリにアクセスします。 Dispose メソッドは、UnitOfWork インスタンスを破棄します。

var courses = unitOfWork.CourseRepository.Get(includeProperties: "Department");
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Insert(course);
unitOfWork.Save();
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Update(course);
unitOfWork.Save();
// ...
var departmentsQuery = unitOfWork.DepartmentRepository.Get(
    orderBy: q => q.OrderBy(d => d.Name));
// ...
Course course = unitOfWork.CourseRepository.GetByID(id);
// ...
unitOfWork.CourseRepository.Delete(id);
unitOfWork.Save();
// ...
unitOfWork.Dispose();

サイトを実行し、[コース] タブをクリックします。

Courses_Index_page

ページの外観と動作は変更前と同じです。また、他のコース ページも同じように動作します。

まとめ

これで、リポジトリ パターンと Unit of Work パターンの両方が実装されました。 ジェネリック リポジトリのメソッド パラメーターとしてラムダ式を使用しました。 IQueryable オブジェクトでこれらの式を使用する方法の詳細については、MSDN ライブラリの IQueryable(T) インターフェイス (System.Linq) を参照してください。 次のチュートリアルでは、いくつかの高度なシナリオを処理する方法について説明します。

他の Entity Framework リソースへのリンクは、ASP.NET データ アクセス コンテンツ マップに関するページにあります。