.NET Fiddle

CSharp · Console

/Program.cs

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

using OperationsList = System.Collections.Generic.LinkedList<OperationType>;
using OperationsResult = System.Collections.Generic.Dictionary<string, object?>;

var runner = new TransactionRunner();
runner
    .Add("Op 1", () => { Console.WriteLine(1); return 2; })
    .Add("Op 2", (res) => { Console.WriteLine(res.GetResult<int>("Op 1")); })
    .Add("Rollback", 
    () => { throw new Exception(); }, 
    () => Console.WriteLine("Exception was thrown. Rolling back...")
).Execute();

public class OperationType
{
  public required string OperationName { get; set; }
  public required Delegate Operation { get; set; } 
  public Delegate? Rollback { get; set; }
}

public enum RunnerState
{
  RolledBack = -1,
  Initialized = 0,
  Commited = 1
};

public class TransactionRunner
{
  private readonly OperationsList _operations = new OperationsList();
  internal readonly OperationsResult _results = new OperationsResult();
  private RunnerState _state = RunnerState.Initialized;
  public RunnerState State => _state;

  public TransactionRunner Add(string name, Delegate operation, Delegate? rollback = null)
  {
    var op = new OperationType { OperationName = name, Operation = operation, Rollback = rollback };

    AppendOperation(op);
    return this;
  }

  public TransactionRunner Add<TResult>(string name, Func<TResult> operation, Delegate? rollback = null)
  {
    var op = new OperationType { OperationName = name, Operation = operation, Rollback = rollback };

    AppendOperation(op);
    return this;
  }
    
  public TransactionRunner Add(string name, Func<OperationsResult, Delegate> operation, Delegate? rollback = null)
  {
    var op = new OperationType { OperationName = name, Operation = operation, Rollback = rollback };
    AppendOperation(op);

    return this;
  }

  public TransactionRunner Add(string name, Action<OperationsResult> operation, Delegate? rollback = null)
  {
    var op = new OperationType { OperationName = name, Operation = operation, Rollback = rollback };
    AppendOperation(op);

    return this;
  }

  public TransactionRunner Add<TResult>(string name, Func<OperationsResult, Func<TResult>> operation,
    Delegate? rollback = null)
  {
    var op = new OperationType { OperationName = name, Operation = operation, Rollback = rollback };
    AppendOperation(op);

    return this;
  }
    
  private void AppendOperation(OperationType operation)
  {
    _operations.AddLast(operation);
  }
  
  public ExecutionType<T> Returns<T>(string operationName)
  {
    return new ExecutionType<T>(this, operationName);
  }

  public void Execute()
  {
    new ExecutionType(this).Execute();
  }
  
  public Task ExecuteAsync()
  {
    return new ExecutionType(this).ExecuteAsync();
  }
    
  internal void Executor()
  {
    if (_state is not RunnerState.Initialized)
    {
      throw new ApplicationException("Runner can't be called more than once");
    }
    
    var currentNode = _operations.First;
    while (currentNode is not null)
    {
      var operation = currentNode.Value;
      try
      {
        if (operation.Operation is Func<OperationsResult> or Action<OperationsResult>)
        {
          _results[operation.OperationName] = operation.Operation.DynamicInvoke(_results) ?? null;
        }
        else
        {
          _results[operation.OperationName] = operation.Operation.DynamicInvoke() ?? null;
        }

        currentNode = currentNode.Next;
      }
      catch (Exception e)
      {
        Rollback(currentNode);
        throw new Exception($"Transaction have failed on operation \"{operation.OperationName}\" and was rolled back",
          e);
      }
    }
  }

  public async Task AsyncExecutor()
  {
    if (_state is not RunnerState.Initialized)
    {
      throw new ApplicationException("Runner can't be called more than once");
    }
    
    var currentNode = _operations.First;
    while (currentNode is not null)
    {
      var operation = currentNode.Value;

      try
      {
        if (operation.Operation is Func<OperationsResult> or Action<OperationsResult>)
        {
          _results[operation.OperationName] = operation.Operation.DynamicInvoke(_results) ?? null;
        }
        else if (operation.Operation.Method.ReturnType.IsSubclassOf(typeof(Task)))
        {
          dynamic invoke = operation.Operation.DynamicInvoke();
          if (invoke is not null)
          {
            _results[operation.OperationName] = await invoke ?? null;
          }
        }
        else
        {
          _results[operation.OperationName] = operation.Operation.DynamicInvoke() ?? null;
        }

        currentNode = currentNode.Next;
      }
      catch (Exception e)
      {
        await RollbackAsync(currentNode);
        throw new Exception($"Transaction have failed on operation \"{operation.OperationName}\" and was rolled back",
          e);
      }
    }
  }

  private void Rollback(LinkedListNode<OperationType>? currentNode)
  {
    while (currentNode?.Previous is not null)
    {
      var rollback = currentNode.Value.Rollback;

      if (rollback is Func<OperationsResult> or Action<OperationsResult>)
      {
        rollback.DynamicInvoke(_results);
      }
      else
      {
        rollback?.DynamicInvoke();
      }

      currentNode = currentNode.Previous;
    }

    _state = RunnerState.RolledBack;
  }

  private async Task RollbackAsync(LinkedListNode<OperationType>? currentNode)
  {
    while (currentNode is not null)
    {
      var rollback = currentNode.Value.Rollback;

      if (rollback is Func<OperationsResult> or Action<OperationsResult>)
      {
        rollback.DynamicInvoke(_results);
      }
      else if (rollback is Func<Task>)
      {
        dynamic invoke = rollback.DynamicInvoke();
        if (invoke is null) continue;
        await invoke;
      }
      else if (rollback is not null)
      {
        rollback.DynamicInvoke();
      }

      currentNode = currentNode.Previous;
    }

    _state = RunnerState.RolledBack;
  }
}
  
  public class ExecutionType
  {
    private readonly TransactionRunner _parent;

    public ExecutionType(TransactionRunner parent)
    {
      _parent = parent;
    }

    public void Execute() => _parent.Executor();
    public async Task ExecuteAsync() => await _parent.AsyncExecutor();
  }

  public class ExecutionType<T>
  {
    private readonly TransactionRunner _parent;
    private readonly string _returnKey;

    public ExecutionType(TransactionRunner parent, string returnKey)
    {
      _parent = parent;
      _returnKey = returnKey;
    }

    public T? Execute()
    {
      _parent.Executor();
      return (T?)_parent._results[_returnKey];
    }

    public async Task<T?> ExecuteAsync()
    {
      await _parent.AsyncExecutor();
      return (T?)_parent._results[_returnKey];
    }
}

public static class DictionaryExtensions
{
  public static object? GetResult(this Dictionary<string, object?> instance, string name)
  {
    var value = instance[name];

    if (value is not null)
    {
      return value;
    }

    return null;
  }

  public static T? GetResult<T>(this Dictionary<string, object?> instance, string name)
  {
    try
    {
      return (T?)GetResult(instance, name);
    }
    catch
    {
      throw new ApplicationException("Resulting value is not available");
    }
  }
}










;