ASP.NET MVC アプリケーションでのリポジトリと作業単位のパターンの実装 (9/10)

作成者: Tom Dykstra

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

Note

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

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

リポジトリと作業単位のパターン

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

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

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

次の図は、リポジトリまたは作業単位パターンをまったく使用しない場合と比較して、コントローラーとコンテキスト クラスの間のリレーションシップを概念化する 1 つの方法を示しています。

Repository_pattern_diagram

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

Note

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

Student Repository クラスの作成

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 使用し、作業クラスのユニットですべてのリポジトリが同じコンテキストを確実に使用できるようにする方法を確認します。

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

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

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();

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

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()));
}

強調表示されたコードのみが変更されました。

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

ヒント

IQueryable/IEnumerable

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

実装され強調表示された新しい学生リポジトリを示すコードのスクリーンショット。

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'

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

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

Student Controller コードを示すスクリーンショット。コードの検索文字列行と、コードの [ページ一覧へ] 行が強調表示されています。

オブジェクトにIQueryable対して ToPagedList が呼び出されると、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する単体テスト プロジェクトに別のリポジトリ クラスを作成できますこのモック リポジトリ クラスは、コンテキストを呼び出してデータの読み取りと書き込みを行う代わりに、コントローラー関数をテストするためにメモリ内コレクションを操作できます。

汎用リポジトリと作業単位クラスを実装する

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

チュートリアルのこのセクションでは、クラスとクラスをGenericRepository作成し、コントローラーでそれらをCourse使用して、 と エンティティ セットの両方DepartmentCourseアクセスUnitOfWorkします。 前に説明したように、チュートリアルのこの部分をシンプルにするために、これらのクラスのインターフェイスは作成しません。 ただし、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メソッドを追加しますGetStudentsByNameGetStudentsInNameOrder ただし、複雑なアプリケーションでは、このような派生クラスや特殊なメソッドが多数発生する可能性があり、維持する作業が増える可能性があります。

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

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

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 要件を処理します。 特定のエンティティ型に、より複雑なフィルター処理や順序付けなどの特別な要件がある場合は、その型の追加のメソッドを持つ派生クラスを作成できます。

作業単位クラスの作成

作業単位クラスは、複数のリポジトリを使用するときに、1 つのデータベース コンテキストを共有することを確認するという 1 つの目的に役立ちます。 このようにして、作業単位が完了したら、コンテキストのそのインスタンスで メソッドを呼び出 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 追加します。 (ここでインターフェイスを使用していた場合は、ここで変数を初期化しません。代わりに、リポジトリの場合と同様に、2 つのコンストラクターのパターンを Student 実装します)。

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

ページは変更前と同じように表示され、動作し、他のコース ページも同じように機能します。

まとめ

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

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