【问题标题】:Run C# code in Transaction在事务中运行 C# 代码
【发布时间】:2014-01-07 21:18:48
【问题描述】:

我在asp.net中调用了三个按钮点击方法

  1. 第一种方法是在应用程序上保存一个文本文件
  2. 第二种方法是创建并保存PdF文件。
  3. 第三种方法是在asp.net中发送邮件

我想要那个,如果上述任何一个方法发生错误,那么所有之前调用的方法都应该回滚。

这怎么可能。??

【问题讨论】:

  • @Maarten 为什么不搜索谷歌并发布结果作为答案,你会得到一些支持。
  • C# 没有任何“STM”(软件事务内存)支持。一般来说,.NET 中的“事务”适用于支持此类的源 - 例如数据库。如果 副作用 / 操作需要“回滚”,则必须手动解决这些问题,例如在 catch 或由 using 保护的 Dispose 中。上述场景中的某些操作(例如创建和保存 PDF)可能是单独的原子操作(例如创建文件),但超出 .NET 环境本身的范围(并且必须通过反向操作回滚或以其他方式说明在设计中)。
  • 也就是说,如果第二种方法会进入,我们无法回滚我在第一种方法中创建的文件??

标签: c# transactions


【解决方案1】:

在如此简单的过程中,您不需要事务,因为简单的 Try/Catch/Finally 应该可以完成工作。

FileInfo localFile;
FileInfo pdfFile;

try{
    SaveTextFile(localFile);
    SavePDFFile(pdfFile);

    SendEmail();
}catch{
   // something went wrong...
   // you can remove extra try catch
   // but you might get security related
   // exceptions
   try{
      if(localFile.Exists) localFile.Delete();
      if(pdfFile.Exists) pdfFile.Delete();
   }catch{}
}

这里是详细的事务实现。

这是一个有点长的过程,但这是一个简单的实现(单线程方法,没有锁定等)。请记住,这是最简单的事务形式,没有双重锁定,没有多版本并发。

using (TransactionScope scope = new TransactionScope(TransactionScopeOption.Required))
{

    FileInfo localFile = new FileInfo("localFile.txt");
    FileInfo pdfFile = new FileInfo("localFile.pdf");

    SimpleTransaction.EnlistTransaction(

        // prepare
        () =>
        {
            CreateTextFile(localFile);
            CreatePDFFile(pdfFile);

            // prepare mail should throw an error
            // if something is missing as sending email
            // is network operation, it cannot be rolled back
            // so email should be sent in commit
            PrepareMail();
        },

        // commit
        () =>
        {
            SendEmail();
        },

        // rollback
        () =>
        {
            try
            {
                if (localFile.Exists)
                    localFile.Delete();
                if (pdfFile.Exists)
                    pdfFile.Delete();
            }
            catch { }
        },

        // in doubt...
        () => { }
    );

}

public class SimpleTransaction : IEnlistmentNotification
{

    public static void EnlistTransaction(Action prepare, Action commit, Action rollback, Action inDoubt)
    {

        var st = new SimpleTransaction(prepare, commit, rollback, inDoubt);
        Transaction.Current.EnlistVolatile(st, EnlistmentOptions.None);

    }


    Action CommitAction;
    Action PrepareAction;
    Action RollbackAction;
    Action InDoubtAction;

    private SimpleTransaction(Action prepare, Action commit, Action rollback, Action inDoubt)
    {
        this.CommitAction = commit;
        this.PrepareAction = prepare;
        this.RollbackAction = rollback;
        this.InDoubtAction = inDoubt  ?? (Action)(() => {});
    }

    public void Prepare(PreparingEnlistment preparingEnlistment)
    {
        try
        {
            PrepareAction();
            preparingEnlistment.Prepared();
        }
        catch
        {
            preparingEnlistment.ForceRollback();
        }

    }

    public void Commit(Enlistment enlistment)
    {
        CommitAction();
        enlistment.Done();
    }

    public void Rollback(Enlistment enlistment)
    {
        RollbackAction();
        enlistment.Done();
    }

    public void InDoubt(Enlistment enlistment)
    {
        InDoubtAction();
        enlistment.Done();
    }
}

这与 Try Catch 不同的原因是其他一些代码可以回滚事务而不是引发异常。

【讨论】:

    【解决方案2】:

    无论操作是否成功,您都应该始终清理您创建的文件。如果您可以绕过文件系统,并使用MemoryStream 来存储数据并将其包含在电子邮件中,那当然既可以解决您的问题,也可以更快。

    正如其他人所提到的,没有什么神奇的方法可以自动回滚您单击该按钮后创建的任何内容 - 您必须自己考虑解决方案。

    很可能不是最好的解决方案,但一个简单的解决方案是创建一个包含您已成功写入的文件的List<string>,然后在catch 中删除所有文件列表。

    还有很多其他解决方案,例如 TemporaryFile 类,它在其 Dispose() 方法中删除文件。试一试,当您在尝试遇到问题时再次询问。

    【讨论】:

      【解决方案3】:

      这是使用 IEnlistmentNotification 实现 OP 想要的另一种方法。 但不是在一个实现类中编写所有操作(保存文本、保存 pdf 和发送电子邮件),而是使用单独的 IEnlistmentNotification 实现并支持在发送电子邮件操作失败的情况下回滚。

      var textPath = "somefile.txt";
      var pdfPath = "somefile.pdf";
      
      try {
        using (var scope = new TransactionScope()) {
          var textFileSave = new TextFileSave(textPath);
          var pdfFileSave = new PDFFileSave(pdfPath);
      
          Transaction.Current.TransactionCompleted += (sender, eventArgs) => {
            try {
              var sendEmail = new SendEmail();
              sendEmail.Send();
            }
            catch (Exception ex) {
              // Console.WriteLine(ex);
              textFileSave.CleanUp();
              pdfFileSave.CleanUp();
            }
          };
      
          Transaction.Current.EnlistVolatile(textFileSave, EnlistmentOptions.None);
          Transaction.Current.EnlistVolatile(pdfFileSave, EnlistmentOptions.None);
      
          scope.Complete();
        }
      }
      catch (Exception ex) {
        // Console.WriteLine(ex);
      }
      catch {
        // Console.WriteLine("Cannot complete transaction");
      }
      

      以下是实现细节:

      发送电子邮件

      public class SendEmail {
        public void Send() {
          // uncomment to simulate error in sending email
          // throw new Exception();
      
          // write email sending operation here
          // Console.WriteLine("Email Sent");
        }
      }
      

      文本文件保存

      public class TextFileSave : AbstractFileSave {
         public TextFileSave(string filePath) : base(filePath) { }
      
         protected override bool OnSaveFile(string filePath) {        
           // write save text file operation here
           File.WriteAllText(filePath, "Some TXT contents");                
      
           return File.Exists(filePath);
         }
      }
      

      PDFFileSave

      public class PDFFileSave : AbstractFileSave {
        public PDFFileSave(string filePath) : base(filePath) {}
      
        protected override bool OnSaveFile(string filePath) {
          // for simulating a long running process
          // Thread.Sleep(5000);
      
          // write save pdf file operation here
          File.WriteAllText(filePath, "Some PDF contents");
      
          // try returning false instead to simulate an error in saving file
          // return false;
          return File.Exists(filePath);
        }
      }
      

      AbstractFileSave

      public abstract class AbstractFileSave : IEnlistmentNotification {
        protected AbstractFileSave(string filePath) {
          FilePath = filePath;
        }
      
        public string FilePath { get; private set; }
      
        public void Prepare(PreparingEnlistment preparingEnlistment) {
          try {
            var success = OnSaveFile(FilePath);
            if (success) {
              // Console.WriteLine("[Prepared] {0}", FilePath);
              preparingEnlistment.Prepared();
            }
            else {
              throw new Exception("Error saving file");
            }
          }
          catch (Exception ex) {
            // we vote to rollback, so clean-up must be done manually here
            OnDeleteFile(FilePath);
            preparingEnlistment.ForceRollback(ex);
          }
        }
      
        public void Commit(Enlistment enlistment) {
          // Console.WriteLine("[Commit] {0}", FilePath);
          enlistment.Done();
        }
      
        public void Rollback(Enlistment enlistment) {
          // Console.WriteLine("[Rollback] {0}", FilePath);
          OnDeleteFile(FilePath);
          enlistment.Done();
        }
      
        public void InDoubt(Enlistment enlistment) {
          // in doubt operation here
          enlistment.Done();
        }
      
        // for manual clean up
        public void CleanUp() {
          // Console.WriteLine("[Manual CleanUp] {0}", FilePath);
          OnDeleteFile(FilePath);
        }
      
        protected abstract bool OnSaveFile(string filePath);
      
        protected virtual void OnDeleteFile(string filePath) {
          if (File.Exists(FilePath)) {
            File.Delete(FilePath);
          }
        }
      }
      

      关于IEnlistmentNotification 实现值得一提的是:如果资源在Prepare() 方法中调用/投票ForceRollback(),则不会触发该资源的Rollback() 方法。因此,任何应该在Rollback() 中进行的清理工作都可能需要在Prepare() 中手动调用

      【讨论】:

      • 实施IEnlistmentNotification 真的值得吗?而不是仅仅使用简单的 try/catch/finally 来完成这项工作?在处理数据库时使用TransactionScope 是有意义的,因为它知道如何为您进行回滚,否则您只是使代码过于复杂而没有实际好处?
      • @Darius 如果你知道你正在调用的代码的内部工作细节,当然,使用try/catch/finallyto 回滚一切都会工作,但如果你的代码不行'正在调用对调用者来说是一个黑匣子(因为 OP 的示例提到了不同的方法,我假设这里就是这种情况)。 IEnlistmentNotification 为其调用者提供了某种程度的保证,当发生回滚时,一切都会恢复到之前的状态。使用IEnlistmentNotification,发生回滚时应该做的是代码调用的责任,try/catch 是我们的...
      猜你喜欢
      • 2015-09-03
      • 1970-01-01
      • 2010-10-31
      • 2015-03-29
      • 2020-09-30
      • 2018-05-10
      • 2011-04-01
      • 2011-08-05
      • 1970-01-01
      相关资源
      最近更新 更多