Транзакции для объектов С#?
Любопытно, есть ли поддержка транзакций на простых объектах С#? Как
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
}
}
Просто чтобы сделать ответы более полезными;-) есть ли что-то подобное на других языках?
Обновление STM: вот что он утверждает:
atomic {
x++;
y--;
throw;
}
оставит x/y неизменным, включая вызовы с цепными методами. Похоже, что я прошу. По крайней мере, это очень интересно. Я думаю, что это достаточно близко. Кроме того, на других языках есть похожие вещи, например Haskell STM. Заметьте, я не говорю, что он должен использоваться для производства; -)
Ответы
Ответ 1
Microsoft работает над этим. Читайте о транзакционной памяти программного обеспечения.
Они используют несколько разных синтаксисов:
// 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();
}
Ответ 2
Для чего стоит, полномасштабный STM - это небольшой выход, и я настоятельно рекомендую не кататься самостоятельно.
К счастью, вы можете получить необходимую функциональность, тщательно разработав свои классы. В частности, неизменяемые классы поддерживают транзакционное поведение из коробки. Поскольку неизменяемые объекты возвращают новую копию себя каждый раз, когда свойство задано, у вас всегда есть полные истории изменений при откате, если это необходимо.
Ответ 3
Об этом написал Джувал Лоуи. Вот ссылка на его оригинальную статью MSDN (я впервые услышал об этой идее в своей отличной книге WCF). Вот пример кода из MSDN, который выглядит так, как вы хотите достичь:
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
}
}
Ответ 4
Вы можете сделать копию объекта перед выполнением методов и настройкой свойств. Затем, если вам не нравится результат, вы можете просто "откат" к копии. Предполагая, конечно, что у вас нет побочных эффектов, с которыми можно бороться.
Ответ 5
Нет, в настоящее время нет ничего подобного встроенному в .net или С#.
Однако в зависимости от ваших требований к использованию вы можете реализовать что-то, что сделало эту работу для вас.
Ваш класс ObjectTransaction
будет сериализовать (или просто дублировать) объект и сохранить копию во время транзакции. Если вы вызвали commit
, копия могла быть просто удалена, но если вы вызываете откат, вы можете восстановить все свойства исходного объекта из копии.
В этом предложении есть много предостережений.
- Если вам нужно использовать отражение, чтобы получить свойства (потому что вы хотите, чтобы он обрабатывал любой тип объекта), он будет довольно медленным. Аналогично для сериализации.
- Если у вас есть дерево объектов, а не просто простой объект с несколькими свойствами, обработка чего-то подобного в общем случае для всех типов объектов может быть довольно сложной.
- Свойства, являющиеся списками данных, также сложны.
- Если какие-либо свойства get/set методы вызывают изменения (или события), которые имеют эффекты, это может вызвать реальные проблемы в другом месте вашего приложения.
- Он действительно будет работать только для публичных свойств.
Все, что сказал, проект, над которым я работал несколько лет назад, сделал что-то в этом роде. При очень жестких ограничениях он может работать действительно красиво. Мы поддерживали только внутренние объекты данных бизнес-уровня. И все они должны были унаследовать от базового интерфейса, который предоставил некоторые дополнительные метаданные по типам свойств, и были правила о том, какие события могут быть вызваны из средств определения свойств. Мы начнем транзакцию, а затем привяжем объект к графическому интерфейсу. Если пользователь нажимал "ОК", транзакция была только что закрыта, но если они отменили отмену, диспетчер транзакций отключил его от графического интерфейса пользователя и отбросил все изменения на объекте.
Ответ 6
Нет, такого типа поддержки сегодня не существует для управляемых ванили объектов.
Ответ 7
И здесь снова простое решение не должно позволять вашим объектам попадать в недействительное состояние в первую очередь. Тогда вам не нужно ничего откатывать, вам не нужно называть "Validate" и т.д. Если вы удалите сеттеры и начнете думать о том, чтобы отправлять сообщения объектам, чтобы что-то делать во внутренних данных, а не устанавливать свойства, я обнаружил бы тонкие вещи о вашем домене, иначе вы бы этого не сделали.
Ответ 8
Вот мое решение, которое я только что написал:)
Должен работать также с массивами и любыми ссылочными типами.
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;
}
}
}