10

In my current project I'm using classes which implement the following ITransaction interface shown below. This is a generic interface for a transaction that can be undone. I also have a TransactionSet class which is used to attempt multiple Transactions or TransactionSets, and can ultimately be used to create a tree of transactions.

Some implementations of ITransaction keep temporary references to object instances or files it may use later if there is a call to Undo(). A successful transaction can later be confirmed after which Undo() is no longer allowed and there is thus also no longer a need for the temporary data. Currently I'm using Dispose() as my confirmation method to clean up any temporary resources.

However, now I'd like my transactions to also fire events to notify other classes of what has taken place. I do not want the events to fire unless the transaction is confirmed. Because I don't want to allow a transaction to fire events multiple times by being undone and then run again.

Since I'm using Dispose() to confirm a transaction is there anything wrong with also firing these events from it? Or would it be better to have a separate Confirm() method on my interface that fires the events in addition to Dispose() which cleans up the temporary data? I can't think of any case where I would want to confirm, but not dispose a transaction. Yet it's not entirely clear to me what I should and should not do within Dispose().

public enum TransactionStatus
{
    NotRun, // the Transaction has not been run, or has been undoed back to the original state
    Successful, ///the action has been run and was successful
    Error //there was an attempt to run the action but it failed
}

/// <summary>
/// Generic transaction interface
/// </summary>
public interface ITransaction
{
    TransactionStatus Status { get; }

    /// <summary>
    /// Attempts the transaction returns true if successful, false if failed.
    /// If failed it is expected that everything will be returned to the original state.
    /// Does nothing if status is already Successful
    /// </summary>
    /// <returns></returns>
    bool Go();

    /// <summary>
    /// Reverts the transaction
    /// Only does something if status is successful.
    /// Should return status to NotRun
    /// </summary>
    void Undo();

    /// <summary>
    /// A message describing the cause of the error if Status == Error
    /// Otherwise equal String.Empty
    /// </summary>
    string ErrorMessage { get; }
}
Eric Anastas
  • 21,675
  • 38
  • 142
  • 236

5 Answers5

5

Dispose is not a special method - it's not like a ctor or a finalizer or anything - it's just a helpful pattern to notify an object the consumer is done using it. There's no reason it can't raise events.

Rex M
  • 142,167
  • 33
  • 283
  • 313
  • +1. Yes, `Dispose()` is not the finalizer; do whatever you want in there. – Esteban Araya Jul 15 '10 at 04:38
  • 6
    +1. Note that the "recommended" pattern of implementing `IDisposable` has the `Dispose()` and `Dispose(bool)` methods. When `Dispose(false)` is invoked, the method *is* called from the finalizer, and events *cannot* be raised. – Stephen Cleary Jul 15 '10 at 11:22
3

IDisposable is simply a runtime-integrated design pattern that facilitates object cleanup in a more efficient manner than finalization. There is very little you "can't" do in a disposal method, however you should be wary of doing some things.

While the IDisposable.Dispose() method is not a "real" destructor or finalizer, it can adversely affect the lifetime of an object if other objects maintain (or perhaps even take) a reference to the disposing object during disposal events. If you are careful about how you implement such a system, you can mitigate the possible side effects. However, it is important to realize the potential that such an implementation offers...such as increased attack surface for a malicious coder to exploit by, say, keeping your transaction objects alive indefinitely.

jrista
  • 32,447
  • 15
  • 90
  • 130
  • A number of .net classes have a "Disposing" event, and would be very difficult to use without one. If an object will hold a reference to an IDisposable that may or may not be used elsewhere (e.g. a Control holding a BackgroundImage), a Disposing event can be used to allow the holder of the main object to clean up the nested object when necessary. – supercat Mar 15 '11 at 17:52
1

Knowing this question has been asked 4 years ago, but not content with the answers I'm adding one that combines some of the points discussed in the answers and comments with additional aspects.

Finalization: As @jrista pointed out, let's be clear that IDisposable has nothing to do with the GC or Finalization per se - it is just a convention and strongly recommended practice. Using the Dispose pattern however you may call the Dispose method from a Finalizer (like @Stephen Cleary pointed out). In this case you should absolutely not raise any events, nor should it access other managed objects for that matter.

Leaving the Dispose/Finalizer issues aside since your classes do not need a Finalizer because they do not wrap unmanaged resources, there are additional concerns though.

Memory Leaks/Liftetime Matching: This is an often cited issue with events and may apply to your transaction implementation too. When you have an event publisher whose lifetime exceeds that of an event subscriber, you may have a memory leak if that subscriber does not unsubscribe from the event because the publisher would then keep holding on to it. If your transactions are fairly long lived and you subscribe many short lived objects to them, you should think about implementing dispose in these objects and then unsubscribing from the transaction. See Should I always disconnect event handlers in the Dispose method?

Principle of Least Surprise: Is it a good idea to 'abuse' Dispose for committing a transaction? I'd say no, though there are precedents. Take Streamfor example. Usually Stream.Dispose is implemented to call Flush and thus commit the data to the underlying medium. However, note that we have an explicit Flush method still so you should add that. I find that "disposing to commit" violates the principle of least surprise, an explicit Commit method is a lot clearer (you can still call this from Dispose if this is the default behavior that you desire).

Event Cascades/Invalid Object States: I think this is the strongest argument for not raising events in Dispose. Events have a tendency to cascade (i.e. one event triggering other events and code) and if you are not careful you may end up in a situation where some of the code decides it would be a good idea to call back into the object that's being disposed and may thus be in an invalid state. No fun to debug, especially if the object may be accessed by multiple threads! Though again, there are precedents for this like Component.Disposed.

I'd advise not to raise events from a Dispose method. When you end the lifetime of the event publisher, does it really matter that all its subscribers update their state accordingly? I find in most cases that I'm getting rid of the entire object graph anyway (i.e. the publisher is longer lived than the subscribers). In some situations, you may also want to actively suppress any failure conditions that occur during disposal (e.g. when closing a TCP connection).

Community
  • 1
  • 1
Johannes Rudolph
  • 35,298
  • 14
  • 114
  • 172
0

Dispose should simply clean up. I would implement Confirm() and Rollback() methods, if dispose gets called without either of them being called first it's an error that should at least be logged.

Loren Pechtel
  • 8,945
  • 3
  • 33
  • 45
0

For sure, you can fire any events in Dispose method. However, if you want to fire events to confirm transaction being there, I think you should have a separate method to fire events. Dispose() is the way to clean up internal resources or dispose internal instances as a well-known pattern. After disposed, your transaction install should be not be there or to be used any more. Therefore, you may consider a separate method to as confirmation that the temporary will not be available, with flag or status in Transaction to indicate that.

David.Chu.ca
  • 37,408
  • 63
  • 148
  • 190