Transactions for C# objects?
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 s开发者_开发百科imilar 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 ;-)
Microsoft is working on it. Read about Software Transactional Memory.
- STM.NET
- STM.NET Team Blog
- Channel 9 Video: STM.NET: Who. What. Why.
- Papers on STM
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();
}
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.
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
}
}
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.
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.
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.
No, this type of support does not exist today for vanilla managed objects.
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;
}
}
}
精彩评论