[Entity Framework] トランザクションのスコープ制御（EF6：Model/Database First）

Entity Framework のコンテキストにおいて、トランザクションは、既定では SaveChanges() を実行したときに暗黙的に使用されます。
要件によっては、トランザクションのスコープを明示的に制御したいケースも出てくるでしょう。

EF6 ではトランザクション操作のために `DbContext.Database.BeginTransaction`/`UseTransaction` メソッドが導入され、`TransactionScope` よりも推奨されるようになりました。（[Microsoft Docs 解説](https://docs.microsoft.com/ja-jp/ef/ef6/saving/transactions)）
ここでは、トランザクションの明示的なスコープ制御を EF6 の Model/Database First で行う例を示します。

※EF4.1 以降で `TransactionScope` クラスや `DbTransaction.BeginTransaction` メソッドを使用する例については、「[トランザクションのスコープ制御（EF4.1～：Model/Database First）](https://qiita.com/CodeOne/items/ea2ed5a37aa0537eaeef)」をご覧ください。
※Code First での制御については「[トランザクションのスコープ制御（EF6：Code First）](https://qiita.com/CodeOne/items/cde232063bcc795b89f4)」をご覧ください。

## 複数回の SaveChanges をまたぐトランザクション

`Database.BeginTransaction` でトランザクションを開始し、その中で `SaveChanges` した変更をまとめて `Commit` します。

```c#
// コンテキスト
using (var context = new NorthwindEntities())
{
    // トランザクション開始
    using (var transaction = context.Database.BeginTransaction())
    {
        // 1つめの SaveChanges()
        var product = await context.Products.SingleAsync(p => p.ProductID == 1).ConfigureAwait(false);
        product.ProductName = "New Product Name";
        await context.SaveChangesAsync().ConfigureAwait(false);
 
        // 2つめの SaveChanges()
        var employee = await context.Employees.SingleAsync(e => e.EmployeeID == 1).ConfigureAwait(false);
        employee.Title = "New Title";
        await context.SaveChangesAsync().ConfigureAwait(false);
 
        // まとめてコミット
        transaction.Commit();
    }
}
```

## 複数のコンテキストをまたぐトランザクション

あらかじめ接続を開いておいて `BeginTransaction` メソッドでトランザクションを開始し、その中で複数のコンテキストを操作、`SaveChanges` した後にまとめて `Commit` します。

Database/Model First では、コンテキストのコンストラクタには `EntityConnection` を渡す必要があります。
コンテキストを `Dispose` しても接続が破棄されないよう、`contextOwnsConnection` 引数には `false` を指定します。

トランザクションは `Database.UseTransaction` メソッドでコンテキストに渡して共用します。

```c#
// 接続準備
var workspace = NorthwindEntities.GetMetadataWorkspace();
using (var entityConnection1 = new EntityConnection("name=NorthwindEntities"))
using (var sqlConnection = entityConnection1.StoreConnection)
using (var entityConnection2 = new EntityConnection(workspace, sqlConnection, false))
{
    // あらかじめ接続を開いておく。
    sqlConnection.Open();
 
    // トランザクション開始
    using (var transaction = sqlConnection.BeginTransaction())
    {
        // 1つ目のコンテキストを操作する。
        using (var context = new NorthwindEntities(entityConnection1, false))
        {
            context.Database.UseTransaction(transaction);
 
            var product = await context.Products.SingleAsync(p => p.ProductID == 1).ConfigureAwait(false);
            product.ProductName = "New Product Name";
            await context.SaveChangesAsync().ConfigureAwait(false);
        }
 
        // 別の EntityConnection を使って2つ目のコンテキストを操作する。
        // ※同じ EntityConnection を使用すると InvalidOperationException が発生する。
        using (var context = new NorthwindEntities(entityConnection2, false))
        {
            context.Database.UseTransaction(transaction);
 
            var employee = await context.Employees.SingleAsync(e => e.EmployeeID == 1).ConfigureAwait(false);
            employee.Title = "New Title";
            await context.SaveChangesAsync().ConfigureAwait(false);
        }
 
        // まとめてコミット
        transaction.Commit();
    }
}
 
// コンテキストの部分クラス
public partial class NorthwindEntities : DbContext
{
    /// <summary>
    /// コンストラクタ。
    /// </summary>
    /// <param name="existingConnection">コンテキストで使用する接続。</param>
    /// <param name="contextOwnsConnection">false を指定すると、コンテキストが Dispose されたときに接続を Dispose しない。</param>
    public NorthwindEntities(DbConnection existingConnection, bool contextOwnsConnection)
        : base(existingConnection, contextOwnsConnection)
    {
    }
   
    /// <summary>
    /// メタデータワークスペースを取得する。
    /// </summary>
    /// <returns></returns>
    public static MetadataWorkspace GetMetadataWorkspace()
    {
        using (var context = new NorthwindEntities())
        {
            var objectContext = ((IObjectContextAdapter)context).ObjectContext;
            return objectContext.MetadataWorkspace;
        }
    }
}
```

それぞれのコンテキストには別々の `EntityConnection` を渡す必要があります。
2つ目以降の `EntityConnection` を作成するときに必要となる `MetadataWorkspace` オブジェクトは、コンテキストが実装する `IObjectContextAdapter` インターフェイスの  `ObjectContext` プロパティから取得できます。