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.