Saturday, August 8, 2009

Making IDisposable more usable, part 1

IDisposable is used quite frequently, so here I’m going to provide few hacks allowing to improve its usability. First of all, let's create IDisposableExtensions with a single method:

/// <summary>
/// Safely disposes an <see cref="IDisposable"/> object.
/// </summary>
/// <param name="disposable">Object to dispose (can be <see langword="null"/>).</param>
public static void DisposeSafely(this IDisposable disposable)
{
if (disposable!=null)
disposable.Dispose();
}

This gives us an ability to dispose the objects without repeating null-check:

try {
writer.DisposeSafely();
}
finally {
writer = null;
}

Let's go further now. Frequently you must safely dispose two or more IDisposable objects. Note that "safely" is an essential word here. You can't write something like:

disposable1.DisposeSafely();
disposable2.DisposeSafely();

This code may fail: an exception can be thrown on execution of its first line, and if this happens, its second line won't be executed at all. So disposable2 has a chance of not being disposed. That's why it's a good idea to implement few helpers allowing us to deal with such issues safely.

Let's add JoiningDisposable type:

/// <summary>
/// Disposes two <see cref="IDisposable"/> objects.
/// </summary>
[Serializable]
public sealed class JoiningDisposable : IDisposable
{
private IDisposable first;
private IDisposable second;

/// <summary>
/// Gets the first object to dispose.
/// </summary>
public IDisposable First {
get { return first; }
}

/// <summary>
/// Gets the second object to dispose.
/// </summary>
public IDisposable Second {
get { return second; }
}

/// <summary>
/// Joins the <see cref="JoiningDisposable"/> and <see cref="IDisposable"/>.
/// </summary>
/// <param name="first">The first disposable to join.</param>
/// <param name="second">The second disposable to join.</param>
/// <returns>New <see cref="JoiningDisposable"/> that will
/// dispose both of them on its disposal</returns>
public static JoiningDisposable operator &(JoiningDisposable first, IDisposable second)
{
if (second==null)
return first;
return new JoiningDisposable(first, second);
}


// Constructors

/// <summary>
///   <see cref="ClassDocTemplate.Ctor" copy="true"/>
/// </summary>
/// <param name="disposable1">The first disposable.</param>
/// <param name="disposable2">The second disposable.</param>
public JoiningDisposable(IDisposable disposable1, IDisposable disposable2)
{
this.first = disposable1;
this.second = disposable2;
}

/// <inheritdoc/>
public void Dispose()
{
var d1 = first;
first = null;
try {
d1.DisposeSafely();
}
catch (Exception ex) {
using (var ea = new ExceptionAggregator()) {
ea.Execute(_this => {
var d2 = _this.second;
_this.second = null;
d2.DisposeSafely();
}, this);
ea.Execute(e => { throw e; }, ex);
}
}
d1 = second;
second = null;
d1.DisposeSafely();
}
}

As you see, the code in its Dispose method relies on ExceptionAggregator. Here it is:

/// <summary>
/// Provides exception aggregation support.
/// </summary>
[Serializable]
public class ExceptionAggregator : 
IDisposable, 
ICountable<Exception>
{
private Action<Exception> exceptionHandler;
private List<Exception> exceptions;
private string exceptionMessage;

private bool isDisposed = false;

/// <summary>
/// Gets or sets the exception handler.
/// </summary>
public Action<Exception> ExceptionHandler
{
[DebuggerStepThrough]
get { return exceptionHandler; }
[DebuggerStepThrough]
set { exceptionHandler = value; }
}

/// <summary>
/// Gets the number of caught exceptions.
/// </summary>
public long Count
{
[DebuggerStepThrough]
get { return exceptions!=null ? exceptions.Count : 0; }
}

#region Execute(...) methods

/// <summary>
/// Executes the specified action catching all the exceptions from it,
/// adding it to internal list of caught exceptions and
/// and passing it to <see cref="ExceptionHandler"/> handler.
/// </summary>
/// <param name="action">The action to execute.</param>
/// <exception cref="ObjectDisposedException">Aggregator is already disposed.</exception>
public void Execute(Action action)
{
if (isDisposed)
throw Exceptions.AlreadyDisposed(null);
try {
action();
}
catch (Exception e) {
HandleException(e);
}
}

/// <summary>
/// Executes the specified action catching all the exceptions from it,
/// adding it to internal list of caught exceptions and
/// and passing it to <see cref="ExceptionHandler"/> handler.
/// </summary>
/// <typeparam name="T">The type of action argument.</typeparam>
/// <param name="action">The action to execute.</param>
/// <param name="argument">The action argument value.</param>
/// <exception cref="ObjectDisposedException">Aggregator is already disposed.</exception>
public void Execute<T>(Action<T> action, T argument)
{
if (isDisposed)
throw Exceptions.AlreadyDisposed(null);
try {
action(argument);
}
catch (Exception e) {
HandleException(e);
}
}

/// <summary>
/// Executes the specified action catching all the exceptions from it,
/// adding it to internal list of caught exceptions and
/// and passing it to <see cref="ExceptionHandler"/> handler.
/// </summary>
/// <typeparam name="T1">The type of the 1st action argument.</typeparam>
/// <typeparam name="T2">The type of the 2nd action argument.</typeparam>
/// <param name="action">The action to execute.</param>
/// <param name="argument1">The 1st action argument value.</param>
/// <param name="argument2">The 2nd action argument value.</param>
/// <exception cref="ObjectDisposedException">Aggregator is already disposed.</exception>
public void Execute<T1, T2>(Action<T1, T2> action, T1 argument1, T2 argument2)
{
if (isDisposed)
throw Exceptions.AlreadyDisposed(null);
try {
action(argument1, argument2);
}
catch (Exception e) {
HandleException(e);
}
}

/// <summary>
/// Executes the specified action catching all the exceptions from it,
/// adding it to internal list of caught exceptions and
/// and passing it to <see cref="ExceptionHandler"/> handler.
/// </summary>
/// <typeparam name="T1">The type of the 1st action argument.</typeparam>
/// <typeparam name="T2">The type of the 2nd action argument.</typeparam>
/// <typeparam name="T3">The type of the 3rd action argument.</typeparam>
/// <param name="action">The action to execute.</param>
/// <param name="argument1">The 1st action argument value.</param>
/// <param name="argument2">The 2nd action argument value.</param>
/// <param name="argument3">The 3rd action argument value.</param>
/// <exception cref="ObjectDisposedException">Aggregator is already disposed.</exception>
public void Execute<T1, T2, T3>(Action<T1, T2, T3> action, T1 argument1, T2 argument2, T3 argument3)
{
if (isDisposed)
throw Exceptions.AlreadyDisposed(null);
try {
action(argument1, argument2, argument3);
}
catch (Exception e) {
HandleException(e);
}
}

/// <summary>
/// Executes the specified function catching all the exceptions from it,
/// adding it to internal list of caught exceptions and
/// and passing it to <see cref="ExceptionHandler"/> handler.
/// </summary>
/// <typeparam name="TResult">The type of function result.</typeparam>
/// <param name="function">The function to execute.</param>
/// <returns>Function execution result, if no exception was caught;
/// otherwise, <see langword="default(TResult)"/>.</returns>
/// <exception cref="ObjectDisposedException">Aggregator is already disposed.</exception>
public TResult Execute<TResult>(Func<TResult> function)
{
if (isDisposed)
throw Exceptions.AlreadyDisposed(null);
try {
return function();
}
catch (Exception e) {
HandleException(e);
}
return default(TResult);
}

/// <summary>
/// Executes the specified function catching all the exceptions from it,
/// adding it to internal list of caught exceptions and
/// and passing it to <see cref="ExceptionHandler"/> handler.
/// </summary>
/// <typeparam name="T">The type of the function argument.</typeparam>
/// <typeparam name="TResult">The type of function result.</typeparam>
/// <param name="function">The function to execute.</param>
/// <param name="argument">The function argument value.</param>
/// <returns>Function execution result, if no exception was caught;
/// otherwise, <see langword="default(TResult)"/>.</returns>
/// <exception cref="ObjectDisposedException">Aggregator is already disposed.</exception>
public TResult Execute<T, TResult>(Func<T, TResult> function, T argument)
{
if (isDisposed)
throw Exceptions.AlreadyDisposed(null);
try {
return function(argument);
}
catch (Exception e) {
HandleException(e);
}
return default(TResult);
}

/// <summary>
/// Executes the specified function catching all the exceptions from it,
/// adding it to internal list of caught exceptions and
/// and passing it to <see cref="ExceptionHandler"/> handler.
/// </summary>
/// <typeparam name="T1">The type of the 1st function argument.</typeparam>
/// <typeparam name="T2">The type of the 2nd function argument.</typeparam>
/// <typeparam name="TResult">The type of function result.</typeparam>
/// <param name="function">The function to execute.</param>
/// <param name="argument1">The 1st function argument value.</param>
/// <param name="argument2">The 2nd function argument value.</param>
/// <returns>Function execution result, if no exception was caught;
/// otherwise, <see langword="default(TResult)"/>.</returns>
/// <exception cref="ObjectDisposedException">Aggregator is already disposed.</exception>
public TResult Execute<T1, T2, TResult>(Func<T1, T2, TResult> function, T1 argument1, T2 argument2)
{
if (isDisposed)
throw Exceptions.AlreadyDisposed(null);
try {
return function(argument1, argument2);
}
catch (Exception e) {
HandleException(e);
}
return default(TResult);
}

/// <summary>
/// Executes the specified function catching all the exceptions from it,
/// adding it to internal list of caught exceptions and
/// and passing it to <see cref="ExceptionHandler"/> handler.
/// </summary>
/// <typeparam name="T1">The type of the 1st function argument.</typeparam>
/// <typeparam name="T2">The type of the 2nd function argument.</typeparam>
/// <typeparam name="T3">The type of the 3rd function argument.</typeparam>
/// <typeparam name="TResult">The type of function result.</typeparam>
/// <param name="function">The function to execute.</param>
/// <param name="argument1">The 1st function argument value.</param>
/// <param name="argument2">The 2nd function argument value.</param>
/// <param name="argument3">The 3rd function argument value.</param>
/// <returns>Function execution result, if no exception was caught;
/// otherwise, <see langword="default(TResult)"/>.</returns>
/// <exception cref="ObjectDisposedException">Aggregator is already disposed.</exception>
public TResult Execute<T1, T2, T3, TResult>(Func<T1, T2, T3, TResult> function, T1 argument1, T2 argument2, T3 argument3)
{
if (isDisposed)
throw Exceptions.AlreadyDisposed(null);
try {
return function(argument1, argument2, argument3);
}
catch (Exception e) {
HandleException(e);
}
return default(TResult);
}

#endregion

#region IEnumerable<...> methods

/// <inheritdoc/>
[DebuggerStepThrough]
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}

/// <inheritdoc/>
public IEnumerator<Exception> GetEnumerator()
{
if (exceptions==null)
return EnumerableUtils<Exception>.EmptyEnumerator;
else 
return exceptions.GetEnumerator();
}

#endregion

/// <summary>
/// Invoked on any exception caught by <see cref="Execute"/> methods.
/// </summary>
/// <param name="exception">The caught exception.</param>
/// <remarks>
/// If this method throws an exception, it won't be caught.
/// I.e. it will throw "through" any of <see cref="Execute"/> methods.
/// </remarks>
protected virtual void HandleException(Exception exception)
{
if (exceptionHandler!=null)
exceptionHandler(exception);
if (exceptions==null)
exceptions = new List<Exception>();
exceptions.Add(exception);
}


// Constructors

/// <summary>
/// <see cref="ClassDocTemplate.Ctor" copy="true"/>
/// </summary>
public ExceptionAggregator()
: this(null, null)
{
}

/// <summary>
/// <see cref="ClassDocTemplate.Ctor" copy="true"/>
/// </summary>
/// <param name="exceptionMessage">The message of <see cref="AggregateException"/>.</param>
public ExceptionAggregator(string exceptionMessage)
: this (null, exceptionMessage)
{
}

/// <summary>
/// <see cref="ClassDocTemplate.Ctor" copy="true"/>
/// </summary>
/// <param name="exceptionHandler">The exception handler.</param>
/// <param name="exceptionMessage">The message of <see cref="AggregateException"/>.</param>
public ExceptionAggregator(Action<Exception> exceptionHandler, string exceptionMessage)
{
this.exceptionHandler = exceptionHandler;
this.exceptionMessage = exceptionMessage;
}

// Descructor

/// <see cref="ClassDocTemplate.Dispose" copy="true"/>
/// <exception cref="AggregateException">Thrown if at least one exception was caught 
/// by <see cref="Execute"/> methods.</exception>
public void Dispose()
{
if (exceptions!=null && exceptions.Count>0) {
Exception exception = string.IsNullOrEmpty(exceptionMessage) ? 
new AggregateException(exceptions) : 
new AggregateException(exceptionMessage, exceptions);        
exceptions = null;
isDisposed = true;
throw exception;
}
}
}

Finally, both these types use AggregateException:

/// <summary>
/// Aggregates a set of caught exceptions.
/// </summary>
[Serializable]
public class AggregateException : Exception,
IHasExceptions<Exception>
{
private ReadOnlyList<Exception> exceptions;

/// <summary>
/// Gets the list of caught exceptions.
/// </summary>
public ReadOnlyList<Exception> Exceptions
{
[DebuggerStepThrough]
get { return exceptions; }
}

/// <inheritdoc/>
IEnumerable<Exception> IHasExceptions.Exceptions
{
[DebuggerStepThrough]
get { return Exceptions; }
}

/// <inheritdoc/>
IEnumerable<Exception> IHasExceptions<Exception>.Exceptions
{
[DebuggerStepThrough]
get { return Exceptions; }
}

/// <summary>
/// Gets the "flat" list with all aggregated exceptions. 
/// If other <see cref=" AggregateException"/>s were aggregated, 
/// their inner exceptions are included instead of them.
/// </summary>
/// <returns>Flat list of aggregated exceptions.</returns>
public List<Exception> GetFlatExceptions()
{
var result = new List<Exception>();

foreach (var exception in exceptions) {
var ae = exception as AggregateException;
if (ae!=null)
result.AddRange(ae.GetFlatExceptions());
else
result.Add(exception);
}

return result;
}

/// <inheritdoc/>
public override string ToString()
{
StringBuilder sb = new StringBuilder(64);
sb.Append(base.ToString());
sb.AppendFormat("\r\n{0}:", Strings.OriginalExceptions);
int i = 1;
foreach (Exception exception in exceptions)
sb.AppendFormat("\r\n{0}: {1}", i++, exception);
return sb.ToString();
}

#region Private \ internal methods

private void SetExceptions(IEnumerable<Exception> exceptions)
{
var list = exceptions as IList<Exception> ?? exceptions.ToList();
this.exceptions = new ReadOnlyList<Exception>(list);
}

private void SetExceptions(Exception exception)
{
var list = new List<Exception>();
list.Add(exception);
exceptions = new ReadOnlyList<Exception>(list);
}

#endregion


// Constructors

/// <summary>
/// <see cref="ClassDocTemplate.Ctor" copy="true" />
/// </summary>
public AggregateException()
: base(Strings.ExASetOfExceptionsIsCaught)
{
}

/// <summary>
/// <see cref="ClassDocTemplate.Ctor" copy="true" />
/// </summary>
/// <param name="text">Text of message.</param>
public AggregateException(string text)
: base(text)
{
}

/// <summary>
/// <see cref="ClassDocTemplate.Ctor" copy="true" />
/// </summary>
/// <param name="message">Text of message.</param>
/// <param name="innerException">Inner exception.</param>
public AggregateException(string message, Exception innerException) 
: base(message, innerException)
{
SetExceptions(innerException);
}

/// <summary>
/// <see cref="ClassDocTemplate.Ctor" copy="true" />
/// </summary>
/// <param name="exceptions">Inner exceptions.</param>
public AggregateException(IEnumerable<Exception> exceptions) 
: base(Strings.ExASetOfExceptionsIsCaught, exceptions.First())
{
SetExceptions(exceptions);
}

/// <summary>
/// <see cref="ClassDocTemplate.Ctor" copy="true" />
/// </summary>
/// <param name="message">Text of message.</param>
/// <param name="exceptions">Inner exceptions.</param>
public AggregateException(string message, IEnumerable<Exception> exceptions) 
: base(message, exceptions.First())
{
SetExceptions(exceptions);
}


// Serialization

/// <see cref="SerializableDocTemplate.Ctor" copy="true" />
protected AggregateException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
exceptions = (ReadOnlyList<Exception>)info.GetValue("Exceptions", typeof (ReadOnlyList<Exception>));
}

/// <see cref="SerializableDocTemplate.GetObjectData" copy="true" />
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("Exceptions", exceptions);
}
}

And now we're going to the final step. Let's add one more extension method to our DisposableExtensions:

/// <summary>
/// Joins the specified disposable objects by returning
/// a single <see cref="JoiningDisposable"/> that will
/// dispose both of them on its disposal.
/// </summary>
/// <param name="disposable">The first disposable.</param>
/// <param name="joinWith">The second disposable.</param>
/// <returns>New <see cref="JoiningDisposable"/> that will
/// dispose both of them on its disposal</returns>
public static JoiningDisposable Join(this IDisposable disposable, IDisposable joinWith)
{
return new JoiningDisposable(disposable, joinWith);
}

When this is done, you can use the following code with Join to safely dispose two or more IDisposables:

/// <inheritdoc/>
[DebuggerStepThrough]
public override object OnEntry(object instance)
{
// ...
var sessionScope = sessionBound.ActivateContext();
var transactionScope = Transaction.Open(sessionBound.Session, true)
return transactionScope.Join(sessionScope); // Joins 2 disposables into one
}

// ...

/// <inheritdoc/>
[DebuggerStepThrough]
public override void OnExit(object instance, object onEntryResult)
{
var d = (IDisposable) onEntryResult;
d.DisposeSafely(); // Safely disposes 2 disposables
}  

P.S. All the code provided here was taken from our Xtensive.Core assembly. There are JoiningDisposable, DisposableExtensions and everything else I just described.

Next time I'll tell you how to simplify safe dealing with IDisposable further.

14 comments:

  1. maybe it's a stupid suggestion but why don't you use the 'using ***' pattern?

    if you use a resource with
    using (var x = XXX)
    {
    }

    you don't have problem about null checks, also about using more objects i think that the 'using' is more clear that your solution

    using (var x1 = new x1())
    using (var x2 = new x2())
    {

    }
    i don't remember but maybe works also

    using (var x1 = new x1(),
    var x2 = new x2())
    {
    }

    ReplyDelete
  2. "using" statement is limited to the same block of code. But in real life you frequently create IDisposable objects in constructors and dispose them in your own IDisposable.Dispose - so these patterns are designed mainly for this case.

    ReplyDelete
  3. Interesting stuff, thx.

    About aggregated exception. Exception handling just cannot be done right. Otherwise it's not exception handling, it's control flow management. I'ts better to avoid exception features under this angle of view. I suppose something is wrong with exception aggregation...

    ReplyDelete
  4. Nearly the same approach is used in Parallel Extensions \ PLINQ - it also may throw AggregatedException in case of multiple concurrent errors. So I suspect that's the best option we had.

    Btw, exception handling in general is really a bit ugly way. Check out how this is implemented in pure functional languages like Haskell.

    ReplyDelete
  5. >>Nearly the same approach is used in Parallel Extensions \ PLINQ - it also may throw AggregatedException in case of multiple concurrent errors. So I suspect that's the best option we had.

    I suppose it's the only considerable option PLINQ have.

    Aggregated exceptions causes separating catches.
    I think exception handling is something like tolerance level definition. Also exception handling by design can be used to fight unsuitable API of 3rd party products.

    Btw, Thx God I know imperative programming, hence I don't like Haskell.

    ReplyDelete
  6. Errrmm... "hence" is not exact word.
    it's evoked by agressive functional programming funs, epic vsl followers.

    ReplyDelete
  7. Concerning Haskell: well, there are lots of practically good ideas:
    - LINQ came from it (monads)
    - Parallelism is absolutely free in pure functional languages.
    - It syntax is also very nice: short & clean.

    So in fact, C# and F# took a lot from functional languages. But agree, currently usage of imperative style seems inevitable (= practically efficient).

    ReplyDelete