リポジトリパターンを使った際の後始末について

ASP.NET MVC のコントローラ・クラスは IDisposable インターフェイスを実装していることから、 リポジトリパターンを適用したときの後始末について、ちょっと動作を確かめてみました。確かめる内容は、リポジトリ・インスタンスの破棄と System.Data.Entity.DbContext クラスの Dispose メソッドの実行についてです。なお、開発環境は Visual Studio Express 2012 for Web を使用しています。

プロジェクト名は EfCodeFirstContextDispose として、プロジェクトテンプレートは「基本」、ビューエンジンは「Razor」を選択しています。確認の方法は、デバック実行で System.Diagnostics.Debug.WriteLine でメッセージを出力ウィンドウに出すことで行います。今回はコードファーストでモデルと SQL Server Compact な DB を作成しましたが、DB ファースト及びモデルファーストでも基本的には変わりありません。

以下ソースコードですが、確認用に書いたものなのでエラーハンドリングは全く行なっていないものになっています。

まずはモデルからということで、適当に本情報のモデルを作ってみます。Models フォルダに Book クラスを作成します。

using System.ComponentModel.DataAnnotations;

namespace EfCodeFirstContextDispose.Models
{
    public class Book
    {
        public int Id { get; set; }

        [Display(Name = "書籍名")]
        [Required]
        [StringLength(60)]
        public string Title { get; set; }

        [Display(Name = "著者")]
        [Required]
        [StringLength(60)]
        public string Author { get; set; }

        [Display(Name = "価格")]
        [Required]
        [Range(0, 10000)]
        public int Price { get; set; }
    }
}

次に DbContext のクラスとリポジトリのクラスを置くためにプロジェクトに DAL フォルダを作成します。作成した DAL フォルダに EfCodeFirstContext クラスを作成します。基底クラスのコンストラクタの引数で Web.Config に設定される接続文字列の名前を渡しています。

using System.Data.Entity;

using EfCodeFirstContextDispose.Models;

namespace EfCodeFirstContextDispose.DAL
{
    public class EfCodeFirstContext : DbContext
    {
        public EfCodeFirstContext() : base("EfCodeFirstSampleDb") { }

        public DbSet<Book> Books { get; set; }
    }
}

次にリポジトリということで、まずは DAL フォルダに IBookRepository インターフェイスを作成します。DbContext クラスの Dispose メソッドを確実に実行するために IDisposable インターフェイスを持つことを指定します。

using System;
using System.Linq;

using EfCodeFirstContextDispose.Models;

namespace EfCodeFirstContextDispose.DAL
{
    public interface IBookRepository : IDisposable
    {
        IQueryable<Book> FindMessage();
        Book GetBook(int id);
        Book AddBook(Book book);
        void EditBook(Book book);
        void DeleteBook(int id);
    }
}

次に DAL フォルダに BookRepository クラスを作成します。Dispose メソッドを実装し、デストラクタで Dispose(false) メソッドを呼び出すことで確実に(コントローラー側でリポジトリの Dispose メソッドを呼び出さないで一連の処理が終了しても) DbContext クラスの Dispose メソッドが実行されるようにしています。なお、public void Dispose() メソッドで GC.SuppressFinalize メソッドをコメントアウトしているのは、SuppressFinalize メソッドを呼び出すとFinalize メソッドの呼び出しが行われないためデストラクタが実行されない(デストラクタで「– Destruct Repository!」を表示させようとしている)ためです(詳しくはDispose メソッドの実装(MSDN)をご覧ください)。また、GC.SuppressFinalize メソッドのコメントアウトを外すとデストラクタが呼び出されないということで、先ほどの「デストラクタで Dispose(false) メソッドを呼び出すことで」との関連が引っかかる方がいるかもしれませんが、Dispose() メソッドが呼び出されていれば、デストラクタでの Dispose(false) メソッドの呼び出しは不要なので、問題ありません。

using System.Linq;

using EfCodeFirstContextDispose.Models;

namespace EfCodeFirstContextDispose.DAL
{
    public class BookRepository : IBookRepository
    {
        private readonly EfCodeFirstContext _context;

        public BookRepository() : this(new EfCodeFirstContext()) { }
        public BookRepository(EfCodeFirstContext context)
        {
            _context = context;

            System.Diagnostics.Debug.WriteLine("++ Create Repository!");
        }

        ~BookRepository()
        {
            System.Diagnostics.Debug.WriteLine("-- Destruct Repository!");
            Dispose(false);
        }

        public IQueryable<Book> FindMessage()
        {
            return _context.Books.AsQueryable();
        }

        public Book GetBook(int id)
        {
            return _context.Books.Find(id);
        }

        public Book AddBook(Book book)
        {
            var newBook = _context.Books.Add(book);
            _context.SaveChanges();

            return newBook;
        }

        public void EditBook(Book book)
        {
            _context.Entry(book).State = System.Data.EntityState.Modified;
            _context.SaveChanges();
        }

        public void DeleteBook(int id)
        {
            var item = _context.Books.Find(id);
            _context.Books.Remove(item);
            _context.SaveChanges();
        }

        #region IDisposable

        private bool _disposed = false;

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

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed) return;

            _disposed = true;

            if (disposing)
            {
                // マネージ リソースの解放処理
            }
            // アンマネージ リソースの解放処理
            _context.Dispose();

            System.Diagnostics.Debug.WriteLine("!! Disposed Repository!");
        }

        #endregion
    }
}

次にデータベース接続文字列を設定します。Web.config の connectionStrings に EfCodeFirstSampleDb という名前で設定しています(この名前の参照の設定は EfCodeFirstContext クラスで行なっています)。

<?xml version="1.0" encoding="utf-8"?>
~省略~
<configuration>
~省略~
  <connectionStrings>
    <add name="EfCodeFirstSampleDb" connectionString="Data Source=|DataDirectory|\EfCodeFirstSampleDatabase.sdf" providerName="System.Data.SqlServerCe.4.0"/>
  </connectionStrings>
~以下省略~

次にイニシャライザの設定です。Global.asax の Application_Start メソッドを変更します。設定は、モデルが変更された際には再作成し、テストデータの投入は行わないものにしています。Application_Start() メソッドの最後の行に SetInitializer の呼び出しを追加しています。

using System.Data.Entity;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

using EfCodeFirstContextDispose.DAL;

namespace EfCodeFirstContextDispose
{
    // メモ: IIS6 または IIS7 のクラシック モードの詳細については、
    // http://go.microsoft.com/?LinkId=9394801 を参照してください

    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            Database.SetInitializer(new DropCreateDatabaseIfModelChanges<EfCodeFirstContext>());
        }
    }
}

次にコントローラーですが、ここで一度プロジェクトをビルドしておきます。Controllers フォルダに HomeController を作成します。スキャフォールディングで一気に作成したいところですが、なぜか “Models.Book のメタデータを取得出来ませんでした。”というエラーが返されるので、「空の読み取り/書き込み操作のある MVC コントローラー」を選んで作成します(DB ファーストではスキャフォールディングが使えるので。。。謎 😀 )。各アクションを実装して、Dispose(bool disposing) メソッドをオーバーライドしてリポジトリの Dispose メソッドを呼び出すようにします。また、各アクションの動作状況を見るための Debug.WriteLine も付け加えています。

using System;
using System.Web.Mvc;

using EfCodeFirstContextDispose.DAL;
using EfCodeFirstContextDispose.Models;

namespace EfCodeFirstContextDispose.Controllers
{
    public class HomeController : Controller
    {
        private IBookRepository _bookRepository;

        public HomeController() : this(new BookRepository()) { }
        public HomeController(IBookRepository bookRepository)
        {
            _bookRepository = bookRepository;
            System.Diagnostics.Debug.WriteLine("++ Create Controller!");
        }

        ~HomeController()
        {
            System.Diagnostics.Debug.WriteLine("-- Destruct Controller!");
        }

        //
        // GET: /Home/

        public ActionResult Index()
        {
            System.Diagnostics.Debug.WriteLine("++ Enter Index action!");
            var message = _bookRepository.FindMessage();
            System.Diagnostics.Debug.WriteLine("-- Exit Index action!");
            return View(message);
        }

        //
        // GET: /Home/Details/5

        public ActionResult Details(int id = 0)
        {
            System.Diagnostics.Debug.WriteLine("++ Enter Details action!");
            var book = _bookRepository.GetBook(id);
            if (book == null)
            {
                return HttpNotFound();
            }
            System.Diagnostics.Debug.WriteLine("++ Exit Details action!");
            return View(book);
        }

        //
        // GET: /Home/Create

        public ActionResult Create()
        {
            System.Diagnostics.Debug.WriteLine("++ Enter Create action!");
            System.Diagnostics.Debug.WriteLine("++ Exit Create action!");
            return View();
        }

        //
        // POST: /Home/Create

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Book book)
        {
            System.Diagnostics.Debug.WriteLine("++ Enter Post Create action!");
            try
            {
                if (ModelState.IsValid)
                {
                    _bookRepository.AddBook(book);
                    System.Diagnostics.Debug.WriteLine("++ Exit Post Create action!");
                    return RedirectToAction("Index");
                }

                System.Diagnostics.Debug.WriteLine("++ Exit Post Create action!");
                return View(book);
            }
            catch
            {
                return View(book);
            }
        }

        //
        // GET: /Home/Edit/5

        public ActionResult Edit(int id = 0)
        {
            System.Diagnostics.Debug.WriteLine("++ Enter Edit action!");
            var book = _bookRepository.GetBook(id);
            if (book == null)
            {
                return HttpNotFound();
            }
            System.Diagnostics.Debug.WriteLine("++ Exit Edit action!");
            return View(book);
        }

        //
        // POST: /Home/Edit/5

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(Book book)
        {
            System.Diagnostics.Debug.WriteLine("++ Enter Post Edit action!");
            try
            {
                if (ModelState.IsValid)
                {
                    _bookRepository.EditBook(book);
                    System.Diagnostics.Debug.WriteLine("++ Exit Post Edit action!");
                    return RedirectToAction("Index");
                }

                System.Diagnostics.Debug.WriteLine("++ Exit Post Edit action!");
                return View(book);
            }
            catch
            {
                return View(book);
            }
        }

        //
        // GET: /Home/Delete/5

        public ActionResult Delete(int id = 0)
        {
            System.Diagnostics.Debug.WriteLine("++ Enter Delete action!");
            var book = _bookRepository.GetBook(id);
            if (book == null)
            {
                return HttpNotFound();
            }

            System.Diagnostics.Debug.WriteLine("++ Exit Delete action!");
            return View(book);
        }

        //
        // POST: /Home/Delete/5

        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public ActionResult DeleteConfirmed(int id)
        {
            System.Diagnostics.Debug.WriteLine("++ Enter Post Delete action!");
            try
            {
                _bookRepository.DeleteBook(id);
                System.Diagnostics.Debug.WriteLine("++ Exit Post Delete action!");
                return RedirectToAction("Index");
            }
            catch
            {
                return View();
            }
        }

        protected override void Dispose(bool disposing)
        {
            _bookRepository.Dispose();
            base.Dispose(disposing);
            if (_bookRepository != null)
            {
                System.Diagnostics.Debug.WriteLine("リポジトリへの参照をクリア!");
                _bookRepository = null;
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
            }
        }
    }
}

次にビューです。コントローラーの各メソッドを右クリックして自動生成しただけのものです 😀 (なので見た目が。。。 :mrgreen: )

Index.cshtml

@model IEnumerable<EfCodeFirstContextDispose.Models.Book>

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th>
            @Html.DisplayNameFor(model => model.Title)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Author)
        </th>
        <th>
            @Html.DisplayNameFor(model => model.Price)
        </th>
        <th></th>
    </tr>

@foreach (var item in Model) {
    <tr>
        <td>
            @Html.DisplayFor(modelItem => item.Title)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Author)
        </td>
        <td>
            @Html.DisplayFor(modelItem => item.Price)
        </td>
        <td>
            @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
            @Html.ActionLink("Details", "Details", new { id=item.Id }) |
            @Html.ActionLink("Delete", "Delete", new { id=item.Id })
        </td>
    </tr>
}

</table>

Details.cshtml

@model EfCodeFirstContextDispose.Models.Book

@{
    ViewBag.Title = "Details";
}

<h2>Details</h2>

<fieldset>
    <legend>Book</legend>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Title)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Title)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Author)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Author)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Price)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Price)
    </div>
</fieldset>
<p>
    @Html.ActionLink("Edit", "Edit", new { id=Model.Id }) |
    @Html.ActionLink("Back to List", "Index")
</p>

Create.cshtml

@model EfCodeFirstContextDispose.Models.Book

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Book</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Title)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Title)
            @Html.ValidationMessageFor(model => model.Title)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Author)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Author)
            @Html.ValidationMessageFor(model => model.Author)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Price)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Price)
            @Html.ValidationMessageFor(model => model.Price)
        </div>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Edit.cshtml

@model EfCodeFirstContextDispose.Models.Book

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Book</legend>

        @Html.HiddenFor(model => model.Id)

        <div class="editor-label">
            @Html.LabelFor(model => model.Title)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Title)
            @Html.ValidationMessageFor(model => model.Title)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Author)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Author)
            @Html.ValidationMessageFor(model => model.Author)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Price)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Price)
            @Html.ValidationMessageFor(model => model.Price)
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Delete.cshtml

@model EfCodeFirstContextDispose.Models.Book

@{
    ViewBag.Title = "Delete";
}

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<fieldset>
    <legend>Book</legend>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Title)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Title)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Author)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Author)
    </div>

    <div class="display-label">
         @Html.DisplayNameFor(model => model.Price)
    </div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Price)
    </div>
</fieldset>
@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    <p>
        <input type="submit" value="Delete" /> |
        @Html.ActionLink("Back to List", "Index")
    </p>
}

そして、デバック実行を行なって表示されたメッセージ内容は次のとおりです。

【Indexを表示】
++ Create Repository!
++ Create Controller!
++ Enter Index action!
— Exit Index action!
!! Disposed Repository!
リポジトリへの参照をクリア!
— Destruct Repository!

【Create を表示】
++ Create Repository!
++ Create Controller!
++ Enter Create action!
++ Exit Create action!
!! Disposed Repository!
リポジトリへの参照をクリア!
— Destruct Repository!

【Create でデータをポスト】
++ Create Repository!
++ Create Controller!
++ Enter Post Create action!
++ Exit Post Create action!
!! Disposed Repository!
リポジトリへの参照をクリア!
— Destruct Repository!

【Index をリダイレクトで表示】
++ Create Repository!
++ Create Controller!
++ Enter Index action!
— Exit Index action!
!! Disposed Repository!
リポジトリへの参照をクリア!
— Destruct Repository!

【Delete を表示】
++ Create Repository!
++ Create Controller!
++ Enter Delete action!
++ Exit Delete action!
!! Disposed Repository!
リポジトリへの参照をクリア!
— Destruct Repository!

【Delete で確認実行をポスト】
++ Create Repository!
++ Create Controller!
++ Enter Post Delete action!
++ Exit Post Delete action!
!! Disposed Repository!
リポジトリへの参照をクリア!
— Destruct Repository!

【Index をリダイレクトで表示】
++ Create Repository!
++ Create Controller!
++ Enter Index action!
— Exit Index action!
!! Disposed Repository!
リポジトリへの参照をクリア!
— Destruct Repository!

メッセージ内容を見ると、コントローラーはクライアントからのリクエストごとに生成されています。リダイレクト時には一度クライアントにリダイレクト要求が返されて、再度クライアントから指定されたリダイレクト先へリクエストが来るという流れになるので、やはりリクエストごとに生成されています。

確認したかった事項は「リポジトリ・インスタンスの破棄」「System.Data.Entity.DbContext クラスの Dispose メソッドの実行」の二点ですが、このコーディングで両者とも行われることが確認できました 🙂 (「System.Data.Entity.DbContext クラスの Dispose メソッドの実行」についてはリポジトリクラスの Dispose メソッドで _context.Dispose() を呼び出した後に「!! Disposed Repository!」を表示しているという間接確認ですが)

なお、コントローラーの「– Destruct Controller!」が表示されていませんが、これは Controller クラスの Dispose メソッドが SuppressFinalize メソッドを呼び出していることからデストラクタが実行されないためです(CodePlex に登録されているソースを参照して確認しました)。

また、コントローラーのオーバーライドした Dispose(bool disposing) メソッド中で _bookRepository を null でクリアしていますが、これを行わないと次にリクエストが来るまでレジストリのインスタンスが解放されません。このことから、おそらくコントローラーのインスタンスが次のリクエストが来るまで掴まれ続けていて解放されないため、明示的に null クリアしないとすぐにリポジトリが解放されないのだと思われます(まぁ、次のリクエストが来るまでの間の話ですが 😀 )。


リポジトリパターンを使った際の後始末について」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です