しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

ASP.NET で DbContext を扱う際のベストプラクティス

ASP.NET MVC のスキャフォールディングを使うと、以下のように Entity Framework の DbContext をコントローラのフィールドに持つコードが生成されます。

public class CustomerController : Controller
{
    private SampleDbContext db = new SampleDbContext();

    //
    // GET: /Customer/

    public ActionResult Index()
    {
        return View(db.Customers.ToList());
    }
}

単純なアプリの場合はこれでいいかもしれないですが、リポジトリパターンを使った場合では DbContext をどうやって持つべきかと悩む人が居ると思います。

変更追跡やトランザクションの観点的に、DbContext や Unit of Work は複数作成するべきではないですよね。

public class CustomerRepository : ICustomerRepository
{
    // インスタンスはどうする?
    private SampleDbContext _db;

    public Customer FindById(int id)
    {
        return db.Customers.Find(id);
    }
}

Unity などの DI コンテナを使えば 1 リクエストごとに 1 インスタンスだけ作成されるように出来ます。*1

しかし、DI コンテナを使うのに適さない、小~中規模のケースもあると思います。そういう場合のために、HttpContext.Items プロパティを使って DbContext のインスタンスを、1 リクエストごとに 1 インスタンスだけ作成されるようにしましょう。

public class SampleDbContext : DbContext
{
    private const string CacheKey = "__SampleDbContext__";

    public static bool HasCurrent
    {
        get { return HttpContext.Current.Items[CacheKey] != null; }
    }

    public static SampleDbContext Current
    {
        get
        {
            var context = (SampleDbContext)HttpContext.Current.Items[CacheKey];

            if (context == null)
            {
                context = new SampleDbContext();

                HttpContext.Current.Items[CacheKey] = context;
            }

            return context;
        }
    }
}

これで SampleDbContext.Current プロパティを参照すると、そのリクエストで DbContext が作成されていなければ自動で作成されますし、作成されている場合はそれが使われます。

ちなみに、このままだと作成した DbContext の Dispose が呼ばれないので、Global.asax.cs に Application_EndRequest を追加して、その内部で DbContext の存在を確認して Dispose 呼び出すようにします。

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_EndRequest()
    {
        if (SampleDbContext.HasCurrent)
        {
            SampleDbContext.Current.Dispose();
        }
    }
}

これで DbContext を 1 リクエストに 1 つだけ生成されるようになりましたし、Dispose の処理も問題ないですね。複数リポジトリにも Current プロパティを参照することで簡単に対応出来るようになりました。

*1:PerRequestLifetimeManager を使ったりする