問題の解決策を見つけました。
既にデータ コンテキストにアタッチされているエンティティをアタッチしようとしたときに例外が発生しました。これは、メソッドが使用するデータ コンテキストにエンティティが既にアタッチされているかどうかを Update() メソッドに示していなかったために発生しました。
アタッチを削除したときに例外は発生しませんでしたが、エンティティが更新されないことがありました (データコンテキストが新しい場合)。
そこで、これら 2 つの相反する状況を橋渡しする方法を考えました。その解決策が TransactionScope です。
メソッドがスコープに参加および脱退できるようにする TransactionScope クラスを作成しました。
Join() メソッドは、新しいデータ コンテキストを作成し、使用カウンターを初期化するため、このメソッドを連続して呼び出すと、このカウンターが 1 増加します。
Leave() メソッドは使用カウンターをデクリメントし、ゼロに達するとデータコンテキストが破棄されます。
データ コンテキストを返すプロパティもあります (各メソッドが独自のコンテキストを取得しようとする代わりに)。
データ コンテキストを必要とするメソッドを、質問で説明したものから次のように変更しました。
_myTranscationScope.Join();
try
{
var context = _myTransactionScope.Context;
//Do some database operation for example:
context.User.GetByKey("username");
}
finally
{
_myTranscationScope.Leave();
}
さらに、データ コンテキストの Dispose メソッドをオーバーライドし、各メソッドでこれを行う代わりに、エンティティのデタッチをそこに移動しました。
私が確認する必要があるのは、正しいトランザクションスコープを持っていることと、参加するための各呼び出しにも(例外であっても)去るための呼び出しがあることを確認することだけです
これにより、私のコードはすべてのシナリオ (単一のデータベース操作、複雑なトランザクション、シリアル化されたオブジェクトの操作など) で完璧に機能するようになりました。
これが TransactionScope クラスのコードです (私が取り組んでいるプロジェクトに依存する行コードを削除しましたが、それでも完全に機能するコードです):
using System;
using CSG.Games.Data.SqlRepository.Model;
namespace CSG.Games.Data.SqlRepository
{
/// <summary>
/// Defines a transaction scope
/// </summary>
public class TransactionScope :IDisposable
{
private int _contextUsageCounter; // the number of usages in the context
private readonly object _contextLocker; // to make access to _context thread safe
private bool _disposed; // Indicates if the object is disposed
internal TransactionScope()
{
Context = null;
_contextLocker = new object();
_contextUsageCounter = 0;
_disposed = false;
}
internal MainDataContext Context { get; private set; }
internal void Join()
{
// block the access to the context
lock (_contextLocker)
{
CheckDisposed();
// Increment the context usage counter
_contextUsageCounter++;
// If there is no context, create new
if (Context == null)
Context = new MainDataContext();
}
}
internal void Leave()
{
// block the access to the context
lock (_contextLocker)
{
CheckDisposed();
// If no one using the context, leave...
if(_contextUsageCounter == 0)
return;
// Decrement the context usage counter
_contextUsageCounter--;
// If the context is in use, leave...
if (_contextUsageCounter > 0)
return;
// If the context can be disposed, dispose it
if (Context.Transaction != null)
Context.Dispose();
// Reset the context of this scope becuase the transaction scope ended
Context = null;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
lock (_contextLocker)
{
if (_disposed) return;
if (disposing)
{
if (Context != null && Context.Transaction != null)
Context.Dispose();
_disposed = true;
}
}
}
private void CheckDisposed()
{
if (_disposed)
throw new ObjectDisposedException("The TransactionScope is disposed");
}
}
}