这是使用 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() 中手动调用。