views:

808

answers:

4

I was wondering what the best way to use transations with the entity framework.

Say I have three repositories:

 Repo1(ObjectContext context)
 Repo2(ObjectContext context)
 Repo3(ObjectContext context)

and a service object that takes the three repositories:

 Service(Repo1 repo1,Repo2 repo2, Repo3 repo3)
 Serive.CreateNewObject <- calls repo1, repo2, repo3 to do stuff.

So when I create the service I create three repositories first and pass them down, each repositry takes a object context so my code looks something like this:

MyObjectContext context = new MyObjectContext();
Repo1 repo = new Repo1(context);
// etc

Now I have a controller class that is responsible for calling different services and compants of my application, showing the right forms etc. Now what I want to be able to do is wrap everything that happens in one of the controller methods in a transaction so that if some thing goes wrong I can rollback back.

The controller takes a few different Service objects, but doesn't know anything about the object context.

My questions are:

  1. Should the context be passed in to the service layer also.
  2. How do I implement a transaction in the controller so that anything that happens in the service layers arn't commited untill everything has passed.

Sorry if it's a bit hard to understand..

A: 

You might want to implement the transaction model used by the Workflow Foundation. It basically has an interface that all "components" implement. After each does the main work successfully, then the host calls the "commit" method on each. If one failed, it calls the "rollback" method.

John Fisher
A: 

Wrap the operation in a TransactionScope.

Mark Seemann
+2  A: 

Why doesn't your controller know about the ObjectContext?

This is where I would put it. Check out - http://msdn.microsoft.com/en-us/magazine/dd882510.aspx - here the Command is what will commit/rollback the UnitOfWork(ObjectContext).

If you don't want to have your Controller know exactly about the EF (good design) then you want to abstract your ObjectContext into an interface similar to the approach in the above link.

Duncan
+1 Great article!
Albic
+1 Ditto - great link
serialhobbyist
+2  A: 

How about using a custom TransactionScope, one that commits when all of your services have committed?

public class TransactionScope : Scope<IDbTransaction>
{
    public TransactionScope()
    {
        InitialiseScope(ConnectionScope.CurrentKey);
    }

    protected override IDbTransaction CreateItem()
    {
        return ConnectionScope.Current.BeginTransaction();
    }

    public void Commit()
    {
        if (CurrentScopeItem.UserCount == 1)
        {
            TransactionScope.Current.Commit();
        }
    }
}

So the transaction is only committed when the UserCount is 1, meaning the last service has committed.

The scope classes are (shame we can't do attachements...):

public abstract class Scope<T> : IDisposable
    where T : IDisposable
{
    private bool disposed = false;

    [ThreadStatic]
    private static Stack<ScopeItem<T>> stack = null;

    public static T Current
    {
        get { return stack.Peek().Item; }
    }

    internal static string CurrentKey
    {
        get { return stack.Peek().Key; }
    }

    protected internal ScopeItem<T> CurrentScopeItem
    {
        get { return stack.Peek(); }
    }

    protected void InitialiseScope(string key)
    {
        if (stack == null)
        {
            stack = new Stack<ScopeItem<T>>();
        }

        // Only create a new item on the stack if this
        // is different to the current ambient item
        if (stack.Count == 0 || stack.Peek().Key != key)
        {
            stack.Push(new ScopeItem<T>(1, CreateItem(), key));
        }
        else
        {
            stack.Peek().UserCount++;
        }            
    }

    protected abstract T CreateItem();

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // If there are no users for the current item
                // in the stack, pop it
                if (stack.Peek().UserCount == 1)
                {
                    stack.Pop().Item.Dispose();
                }
                else
                {
                    stack.Peek().UserCount--;
                }
            }

            // There are no unmanaged resources to release, but
            // if we add them, they need to be released here.
        }

        disposed = true;
    }
}

public class ScopeItem<T> where T : IDisposable
{
    private int userCount;
    private T item;
    private string key;

    public ScopeItem(int userCount, T item, string key)
    {
        this.userCount = userCount;
        this.item = item;
        this.key = key;
    }


    public int UserCount
    {
        get { return this.userCount; }
        set { this.userCount = value; }
    }

    public T Item
    {
        get { return this.item; }
        set { this.item = value; }
    }

    public string Key
    {
        get { return this.key; }
        set { this.key = value; }
    }
}

public class ConnectionScope : Scope<IDbConnection>
{
 private readonly string connectionString = "";
 private readonly string providerName = "";

 public ConnectionScope(string connectionString, string providerName)
 {
  this.connectionString = connectionString;
  this.providerName = providerName;

  InitialiseScope(string.Format("{0}:{1}", connectionString, providerName));
 }

    public ConnectionScope(IConnectionDetailsProvider connectionDetails)
        : this(connectionDetails.ConnectionString, connectionDetails.ConnectionProvider)
    {
    }

    protected override IDbConnection CreateItem()
 {
  IDbConnection connection = DbProviderFactories.GetFactory(providerName).CreateConnection();
  connection.ConnectionString = connectionString;
  connection.Open();
  return connection;
 }
}
Simon Hughes