1

I'm trying to refresh my memory on C# and using pattern. Is there a possibility to have some control flow be executed automatically on exception?

For instance:

class Test : IDisposable
{
     public void Dispose()
     {
         Console.WriteLine("ok");
     }

     public void XX()
     {
         Console.WriteLine("KO");
     }
}

using (new Test()) 
{
}
// prints "ok"


using (new Test())
{
    throw new Exception();
}
// this would print "KO"

Is there a way to achieve this effect in C# ? Example of use case would be : a database system where I want to commit result on correct execution, but rollback on exception

For example, currently to handle transaction commits/rollback, it's possible to do this :

using(var tran = conn.BeginTransaction()) 
{
    try 
    {
        // DO WORK
        tran.Commit();
    } catch {
        tran.Rollback();
        throw;
    }
}

But this means that clients of "tran" object have to correctly write the try/catch code. I'm looking for a way to provide the same functionality out of the box, so that users just have to write the "DO WORK" code (and the "using") and not have to write the "using" and the try/catch code

Ryan Wilson
  • 10,223
  • 2
  • 21
  • 40
lezebulon
  • 7,607
  • 11
  • 42
  • 73
  • Do you understand why your current code would print "ok" twice? – gunr2171 Jul 26 '22 at 20:02
  • @gunr2171 yes I understand why. What I'm trying to find is if there is an elegant way to do the behavior I wrote without requiring boilerplate – lezebulon Jul 26 '22 at 20:08
  • Dispose runs when you leave the scope. Doesn't matter _how_ that happens. Is there something more advanced that a try/catch can't solve? – gunr2171 Jul 26 '22 at 20:11
  • @gunr2171 the "pattern" I'm looking for would be that the Test class handles the try/catch logic, and not the user of the Test class – lezebulon Jul 26 '22 at 20:12
  • @lezebulon Why can't you create one method that accepts an `Action` delegate and wrap that inside the try/catch you have in your example? The user would then provide the `DoWork` via `Action` delegate and you would handle the try/catch for them. – Ryan Wilson Jul 26 '22 at 20:22
  • I can, but it's just less clear to read in terms of syntax / control flow. – lezebulon Jul 26 '22 at 20:23
  • @lezebulon How is that less clear? Please explain. You could also handle the `using` statement for the user as well. – Ryan Wilson Jul 26 '22 at 20:24
  • C# does not provide a custom syntax for that. You have to do manual try/catch. – freakish Jul 26 '22 at 20:24
  • _"or example, currently to handle transaction commits/rollback, it's possible to do this :"_ - AFAIK there is no need to write inner try-catch block, transaction will be automatically rolled back unless you commit it inside the using block. – Guru Stron Jul 26 '22 at 20:26
  • @GuruStron that depends on the (transaction) implemention. – freakish Jul 26 '22 at 20:27
  • "But this means that clients of "tran" object have to correctly write the try/catch code." That's actually not true. SqlTransaction will rollback for you if you don't explicitly commit. It just doesn't have access to the exception information. – gunr2171 Jul 26 '22 at 20:27
  • @freakish it is a standard practice, at least for `SqlTransaction` (see for example this [question](https://stackoverflow.com/questions/1127830/why-use-a-using-statement-with-a-sqltransaction)), but in general - yes. – Guru Stron Jul 26 '22 at 20:28
  • ok, maybe the "rollback" catch part is no needed. But it still means that users in that case have to manually (remember to) call commit() as the last statement. My question is how to avoid this : have commit() be called at the end of regular control flow, and rollback() (or nothing) in case of exception unwind – lezebulon Jul 26 '22 at 20:29
  • @lezebulon as I said earlier: C# does not provide a syntax for that. You have to manually try/catch. Maybe inconvenient, but it is what it is. – freakish Jul 26 '22 at 20:30
  • @lezebulon it is not possible. The transaction approach is the [way to go](https://stackoverflow.com/a/3301548/2501279) – Guru Stron Jul 26 '22 at 20:30

1 Answers1

1

What you want isn't 100% achievable. IDisposable is not designed to handle exceptions. It's designed to free up a resource when that resource goes out of scope.

SqlTransaction does something similar. You create a using scoped variable, and if you don't explicitly commit the transaction, it will rollback the transaction.

using (var transaction = connection.BeginTransaction())
{
   // ...
   transaction.Commit();
}

You could emulate something like this, and require the user to say if the body of the using block was successful or not.

public class Test : IDisposable
{
    private bool Successful { get; private set; } = false;

    public void Dispose()
    {
        if (Successful)
            // ...
        else
            // ...
    }

    public void Success() => Successful = true;
}
using (var test = new Test())
{
    // ...
    test.Success();
}

Just note, you cannot get the exception information from Dispose. You also can't be sure if an exception was thrown or the user forgot to call .Success() when Successful is false.


Instead, you really need to do error handling inside the using scope.

gunr2171
  • 16,104
  • 25
  • 61
  • 88
  • Ok I see Indeed your alternate version is something I had in mind, but in any case it doesnt change much whether the user has to call either Success() or Commit() at the end – lezebulon Jul 26 '22 at 20:35