开发者

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 eithe开发者_如何学运维r its correctness or that there isn't a much simpler way.

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


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.


A linq extension can be written to skip all elements that cause an exception and allow you to pass in an action to handle the exceptions that are raised.

public static IEnumerable<T> CatchExceptions<T> (this IEnumerable<T> src, Action<Exception> action = null) 
{
    using (var enumerator = src.GetEnumerator()) 
    {
        bool next = true;

        while (next) 
        {
            try 
            {
                next = enumerator.MoveNext();
            } 
            catch (Exception ex) 
            {
                if (action != null)
                    action(ex);
                continue;
            }

            if (next) 
                yield return enumerator.Current;
        }
    }
}

Example:

ienumerable.Select(e => e.something).CatchExceptions().ToArray()

ienumerable.Select(e => e.something).CatchExceptions((ex) => Logger.Log(ex, "something failed")).ToArray()

NOTE: This solution only catches exceptions raised when processing the originally yielded or projected value, so it is a great way to handle exceptions raised when you have chained IEnumerable expressions as OP has requested. If the exception is raised in the IEnumerable provider then there is a high chance the enumeration will be aborted, this method will handle the exception gracefully but there will be no way to resume the enumeration.

Exceptions raised in providers can occur commonly with file system or database readers as they are prone to timeouts and other runtime context issues. This fiddle shows how to create such a scenario: https://dotnetfiddle.net/a43Vtt You will have to handle those issues in the source directly.


It might be nicer if the process object was something that processed just one item in the list at a time (if that's even possible), that way you could collect each individual exception and return an AggregateException just like the Task Parallel library does.

[If process operates on the list as a whole, or if a single exception needs to abort the whole process obviously this suggestion isn't appropriate.]


Instead of solving your specific problem only, you can have these helper functions in your toolkit:

    static IEnumerable<T> RunEnumerator<T>(Func<IEnumerator<T>> generator, 
        Action<Exception> onException)
    {
        using (var enumerator = generator())
        {
            if (enumerator == null) 
                yield break;
            for (; ; )
            {
                //Avoid creating a default value with
                //unknown type T, as we don't know is it possible to do so
                T[] value = null;
                try
                {
                    if (enumerator.MoveNext())
                        value = new T[] { enumerator.Current };
                }
                catch (Exception e)
                {
                    onException(e);
                }
                if (value != null)
                    yield return value[0];
                else
                    yield break;
            }
        }
    }

    public static IEnumerable<T> WithExceptionHandler<T>(this IEnumerable<T> orig, 
        Action<Exception> onException)
    {
        return RunEnumerator(() =>
        {
            try
            {
                return orig.GetEnumerator();
            }
            catch (Exception e)
            {
                onException(e);
                return null;
            }
        }, onException);
    }
}

WithExceptionHandler<T> converts an IEnumerable<T> into another, and when exception occurs, the onException callback will be called. In this way you can have your process function implemented like this:

public override IEnumerable<T> Process(IEnumerable<T> incoming) {
    return incoming.WithExceptionHandler(e => {
        WriteToLog(e);
        throw;
    }
}

In this way, you can also do something else like ignore the exception completely and let the iteration simply stops. You may also adjust it a little bit by used a Func<Exception, bool> instead of Action<Exception> and allows the exception callback to decide shall the iteration continue or not.


The loop and yield are not needed (at least in your example). Simple remove them and there is no longer a restriction on catch blocks.

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

I am assuming this.processor.Process(incoming) returns a collection implementing IEnumerable<T>, so therefore you do not need to create a new iterator. If this.processor.Process(incoming) is lazy evaluating then

public override IEnumerable<T> Process(IEnumerable<T> incoming) {
    try {
        return this.processor.Process(incoming).ToList();
    } catch (Exception e) {
        WriteToLog(e);
        throw;
    }
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜