Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Wrap an IEnumerable and catch exceptions

I've got a bunch of classes that can Process() objects, and return their own objects:

public override IEnumerable<T> Process(IEnumerable<T> incoming) { ... }

I want to write a processor class that can wrap one of these processors, and log any uncaught exceptions that the wrapped Process() method might throw. My first idea was something like this:

public override IEnumerable<T> Process(IEnumerable<T> incoming) {
    try {
        foreach (var x in this.processor.Process(incoming)) {
            yield return x;
        }
    } catch (Exception e) {
        WriteToLog(e);
        throw;
    }
}

but this doesn't work, due to CS1626: Cannot yield a value in the body of a try block with a catch clause.

So I want to write something that's conceptually equivalent but compiles. :-) I've got this:

public override IEnumerable<T> Process(IEnumerable<T> incoming) {
    IEnumerator<T> walker;
    try {
        walker = this.processor.Process(incoming).GetEnumerator();
    } catch (Exception e) {
        WriteToLog(e);
        throw;
    }

    while (true) {
        T value;
        try {
            if (!walker.MoveNext()) {
                break;
            }
            value = walker.Current;
        } catch (Exception e) {
            WriteToLog(e);
            throw;
        }
        yield return value;
    }
}

but that's more complex than I'd hoped, and I'm not entirely certain of either its correctness or that there isn't a much simpler way.

Am I on the right track here? Is there an easier way?

like image 309
Ken Avatar asked Sep 30 '10 23:09

Ken


1 Answers

If what you want to do is handle an exception during the processing of the result of an enumeration, then you try logic simply needs to go directly inside your for/while loop.

But your example reads as if you are trying to catch and skip over exceptions raised by the enumeration provider.

As far as I can ascertain, there is no way in C# to iterate over an enumerator and skip and exception that occurs within the enumerator itself. If the enumerator raises an exception, then all future calls to MoveNext() will result in false output.

The easiest way to explain why this happens is with this very simple enumerable:

IEnumerable<int> TestCases()
{
    yield return 1;
    yield return 2;
    throw new ApplicationException("fail eunmeration");
    yield return 3;
    yield return 4;
}

Understandably when we look at this example it is obvious that the thrown exception will cause this whole block to exit, and the 3rd and 4th yield statement will not ever be processed. In fact the get the usual 'Unreachable code detected' compiler warning on the 3rd yield statement.

So when the enumerable is a more complex, the same rules apply:

IEnumerable<int> TestCases2()
{
    foreach (var item in Enumerable.Range(0,10))
    {
        switch(item)
        {
            case 2:
            case 5:
                throw new ApplicationException("This bit failed");
            default:
                yield return item;
                break;
        }
    }
}

When the exception is raised, the processing of this block ceases and passes back up the call stack to the nearest exception handler.

ALL of the workable examples to get around this issue that I have found on SO do not proceed to the next item in the enumeration, they all break at the first exception.

Therefore to skip en exception in the enumeration you will need the provider to facilitate it. This is only possible really if your coded the provider, or you can contact the developer who did, the following is an over-simplified example of how you could achieve this:

IEnumerable<int> TestCases3(Action<int, Exception> exceptionHandler)
{
    foreach (var item in Enumerable.Range(0, 10))
    {
        int value = default(int);
        try
        {
            switch (item)
            {
                case 2:
                case 5:
                    throw new ApplicationException("This bit failed");
                default:
                    value = item;
                    break;
            }
        }
        catch(Exception e)
        {
            if (exceptionHandler != null)
            {
                exceptionHandler(item, e);
                continue;
            }
            else
                throw;
        }
        yield return value;
    }
}

...

foreach (var item in TestCases3(
    (int item, Exception ex) 
    => 
    Console.Out.WriteLine("Error on item: {0}, Exception: {1}", item, ex.Message)))
{
    Console.Out.WriteLine(item);
}

This will produce the following output:

0
1
Error on item: 2, Exception: This bit failed
3
4
Error on item: 5, Exception: This bit failed
6
7
8
9

I hope this clears up the issue for other developers in the future as it is a pretty common idea that we all get once we start getting deep into Linq and enumerations. Powerful stuff but there are some logical limitations.

like image 93
Chris Schaller Avatar answered Oct 08 '22 20:10

Chris Schaller