20

Just curious, is there any support for transactions on plain C# objects? Like

using (var transaction = new ObjectTransaction(obj))
{
  try
  {
    obj.Prop1 = value;
    obj.Prop2 = value;
    obj.Recalculate(); // may fire exception
    transaction.Commit(); // now obj is saved
  }
  except
  {
     transaction.Rollback(); // now obj properties are restored
  }
}

Just to make answers more useful ;-) is there anything similar in other languages?

Update on STM: here's what it claims:

atomic {
  x++;
  y--;
  throw;
}

will leave x/y unchanged, including chained methods calls. Looks like what I ask for. At least it's very interesting. I think that's close enough. Also, there're similar things in other languages, for example Haskell STM. Notice I don't say that it should be used for production ;-)

queen3
  • 15,333
  • 8
  • 64
  • 119

8 Answers8

14

Microsoft is working on it. Read about Software Transactional Memory.

They use a few different syntaxes:

// For those who like arrows
Atomic.Do(() => { 
    obj.Prop1 = value;
    obj.Prop2 = value;
    obj.Recalculate();
});

// For others who prefer exceptions
try { 
    obj.Prop1 = value;
    obj.Prop2 = value;
    obj.Recalculate();
}
catch (AtomicMarker) {
}

// we may get this in C#:
atomic { 
    obj.Prop1 = value;
    obj.Prop2 = value;
    obj.Recalculate();
}
Frank Krueger
  • 69,552
  • 46
  • 163
  • 208
  • 2
    Doesn't STM have more to do with shared state and concurrency than it does the traditional concept of a transaction? – Robert Harvey Nov 19 '09 at 18:55
  • 1
    @Robert It handles both quite well. You can use it single-threaded if you just want to safeguard against exceptions (you get rollback), or you can run it on multiple threads. Single-threading is just a degenerate case of multithreading. :-) – Frank Krueger Nov 19 '09 at 19:00
5

For what its worth, a full-blown STM is a little ways out, and I would strongly recommend against rolling your own.

Fortunately, you can get the functionality you want by carefully designing your classes. In particular, immutable classes support transaction-like behavior out of the box. Since immutable objects return a new copy of themselves each time a property is set, you always have a full-history changes to rollback on if necessary.

Juliet
  • 80,494
  • 45
  • 196
  • 228
4

Juval Lowy has written about this. Here is a link to his original MSDN article (I first heard about the idea in his excellent WCF book). Here's a code example from MSDN, which looks like what you want to achieve:

public class MyClass
{
   Transactional<int> m_Number = new Transactional<int>(3);


public void MyMethod()
   {
      Transactional<string> city = new Transactional<string>("New York");

      using(TransactionScope scope = new TransactionScope())
      {
         city.Value = "London";
         m_Number.Value = 4;
         m_Number.Value++;
         Debug.Assert(m_Number.Value == 5);

         //No call to scope.Complete(), transaction will abort
      }
}
RichardOD
  • 28,883
  • 9
  • 61
  • 81
  • The code looks great, but in practice has some nasty bugs. The TransactionalLock class suffers from deadlocks. Exceptions thrown during a transaction will cause mayhem. They prevent the transaction committing, but run outside of the transaction so the locking system collapses. Objects are left in undefined states if multiple threads are working on the same objects simultaneously if any of them throws an exception. – Ant Dec 02 '09 at 10:42
  • @Ant. interesting- have you encountered these issues yourself? I've not used the classes myself so can't really comment on how well they work. – RichardOD Dec 02 '09 at 11:01
  • Yep, figure out if it would work in my current project. I take back the second prob listed above - was caused by me trying to work around the first prob. Problems are all caused by interactions between code run within the scope of a transaction and code that runs outside it. For example, transaction rollback does not run within the transaction; nor do exceptions. However, the transaction keeps the lock until the rollback is complete. I'm getting deadlocks all over the place. It could be my fault for trying to be too clever, but this solution looks horribly specific and horribly fragile. – Ant Dec 02 '09 at 12:10
  • @Ant- maybe you should try posting some sample code in a question on here? You might be doing something wrong. – RichardOD Dec 02 '09 at 12:26
3

You can make a copy of the object prior to executing methods and setting properties. Then, if you don't like the result, you can just "roll back" to the copy. Assuming, of course, that you have no side-effects to contend with.

Robert Harvey
  • 178,213
  • 47
  • 333
  • 501
3

No, there isn't currently anything like this built into .net or C#.

However depending on your usage requirements you could implement something that did the job for you.

Your ObjectTransaction class would serialise (or just duplicate) the object and hold the copy during the transaction. If you called commit the copy could just be deleted, but if you called rollback you could restore all of the properties on the original object from the copy.

There are lots of caveats to this suggestion.

  • if you have to use reflection to get the properties (because you want it to handle any object type) it will be quite slow. Equally for serialisation.
  • If you have a tree of objects and not just a simple object with a few properties, handling something like that generically for all object types could be pretty tricky.
  • Properties that are lists of data are also tricky.
  • If any properties get/set methods trigger changes (or events) that have effects this could cause real issues elsewhere in your app.
  • It will only really work for public properties.

All that said, a project I worked on a few years ago did do something exactly like this. Under very strict restrictions it can work really nicely. We only supported our internal business layer data objects. And they all had to inherit from a base interface that provided some additional meta data on property types, and there were rules on what events could be triggered from property setters. We would start the transaction, then bind the object to the GUI. If the user hit ok, the transaction was just closed, but if they hit cancel, the transaction manager unbound it from the GUI and rolled back all the changes on the object.

Simon P Stevens
  • 27,303
  • 5
  • 81
  • 107
  • Nice description of many of the related problems. There're more problems, though; for example, OR/Ms tend to use identity to track POCOs, so it's risky to re-create objects. But it's nice to know that someone somewhere really _did_ it ;-) – queen3 Nov 19 '09 at 19:17
  • Does this implementation look similar? http://www.codeproject.com/KB/vb/Transaction.aspx – queen3 Nov 19 '09 at 19:24
  • Hm, no, this seems to be too simple. Sorry for many comments ;-) – queen3 Nov 19 '09 at 19:27
  • Yeah, you have to be very careful with this, it can only really work if you restrict the circumstances in which the transactions can be used, you certainly would have to be careful with ORMs. But yes, we really did do it and it worked very well for handling the GUI based editing with ok/apply/cancel style semantics. We did actually have a bug where when some objects were passed via web service calls, the deserialisation on the receiving end would trigger a transaction rollback and mess up the object, so that's another example of where you have to be very careful with this kind of approach. – Simon P Stevens Nov 19 '09 at 23:00
3

And here again simple solution is not to allow your objects to get into invalid state in the first place. Then you don't need to roll anything back, you don't need to call "Validate" etc. If you remove your setters and start thinking about sending messages to objects to do something on internal data, rather then setting properties, you'll uncover subtle things about your domain, that otherwise you would not.

epitka
  • 17,275
  • 20
  • 88
  • 141
2

No, this type of support does not exist today for vanilla managed objects.

JaredPar
  • 733,204
  • 149
  • 1,241
  • 1,454
1

Here is my solution that I just wrote:) Should work also with arrays and any reference types.

public sealed class ObjectTransaction:IDisposable
{
    bool m_isDisposed;

    Dictionary<object,object> sourceObjRefHolder;
    object m_backup;
    object m_original;

    public ObjectTransaction(object obj)
    {
        sourceObjRefHolder = new Dictionary<object,object>();
        m_backup = processRecursive(obj,sourceObjRefHolder,new CreateNewInstanceResolver());
        m_original = obj;
    }

    public void Dispose()
    {
        Rollback();
    }

    public void Rollback()
    {
        if (m_isDisposed)
            return;

        var processRefHolder = new Dictionary<object,object>();
        var targetObjRefHolder = sourceObjRefHolder.ToDictionary(x=>x.Value,x=>x.Key);
        var originalRefResolver = new DictionaryRefResolver(targetObjRefHolder);
        processRecursive(m_backup, processRefHolder, originalRefResolver);

        dispose();
    }

    public void Commit()
    {
        if (m_isDisposed)
            return;

        //do nothing
        dispose();
    }

    void dispose()
    {
        sourceObjRefHolder = null;
        m_backup = null;
        m_original = null;
        m_isDisposed = true;
    }

    object processRecursive(object objSource, Dictionary<object,object> processRefHolder, ITargetObjectResolver targetResolver)
    {
        if (objSource == null) return null;
        if (objSource.GetType()==typeof(string) || objSource.GetType().IsClass == false) return objSource;
        if (processRefHolder.ContainsKey(objSource)) return processRefHolder[objSource];

        Type type = objSource.GetType();
        object objTarget = targetResolver.Resolve(objSource);
        processRefHolder.Add(objSource, objTarget);

        if (type.IsArray)
        {
            Array objSourceArray = (Array)objSource;
            Array objTargetArray = (Array)objTarget;
            for(int i=0;i<objSourceArray.Length;++i)
            {
                object arrayItemTarget = processRecursive(objSourceArray.GetValue(i), processRefHolder, targetResolver);
                objTargetArray.SetValue(arrayItemTarget,i);
            }
        }
        else
        {
            IEnumerable<FieldInfo> fieldsInfo = FieldInfoEnumerable.Create(type);

            foreach(FieldInfo f in fieldsInfo)
            {
                if (f.FieldType==typeof(ObjectTransaction)) continue;

                object objSourceField = f.GetValue(objSource);
                object objTargetField = processRecursive(objSourceField, processRefHolder, targetResolver);

                f.SetValue(objTarget,objTargetField);                    
            }
        }

        return objTarget;
    }

    interface ITargetObjectResolver
    {
        object Resolve(object objSource);
    }

    class CreateNewInstanceResolver:ITargetObjectResolver
    {
        public object Resolve(object sourceObj)
        {
            object newObject=null;
            if (sourceObj.GetType().IsArray)
            {
                var length = ((Array)sourceObj).Length;
                newObject = Activator.CreateInstance(sourceObj.GetType(),length);
            }
            else
            {
                //no constructor calling, so no side effects during instantiation
                newObject = System.Runtime.Serialization.FormatterServices.GetUninitializedObject(sourceObj.GetType());

                //newObject = Activator.CreateInstance(sourceObj.GetType());
            }
            return newObject;
        }
    }

    class DictionaryRefResolver:ITargetObjectResolver
    {
        readonly Dictionary<object,object> m_refHolder;

        public DictionaryRefResolver(Dictionary<object,object> refHolder)
        {
            m_refHolder = refHolder;
        }

        public object Resolve(object sourceObj)
        {
            if (!m_refHolder.ContainsKey(sourceObj))
                throw new Exception("Unknown object reference");

            return m_refHolder[sourceObj];
        }
    }
}

class FieldInfoEnumerable
{
    public static IEnumerable<FieldInfo> Create(Type type)
    {
        while(type!=null)
        {
            var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);

            foreach(FieldInfo fi in fields)
            {
                yield return fi; 
            }

            type = type.BaseType;
        }            
    }
}
nicolas2008
  • 945
  • 9
  • 11
  • I have escalated your post in here http://stackoverflow.com/questions/17280550/single-threaded-object-rollback-in-c-sharp Could you please look at there? – user1121956 Jun 26 '13 at 11:34