Search code examples
c#task-parallel-librarypipelinetpl-dataflow

How to get context of Exception


I am using TaskParallelLibrary DataFlow combined with Try library designed by Stephen Cleary (https://github.com/StephenCleary/Try) to achieve what is called "railroad programming" so I could pass Exception data down the pipe. I would like to know to if it is somehow possible in ActionBlock to get some context of what or (in my case) exactly which item caused the Exception? Here is small sample code:

public async Task TestRailroadException(List<int> constructIds)
{
    var downloadBlock = new TransformBlock<int, Try<int>>(
        construct => Try.Create(() =>
    {
        //ThisMethodMyThrowException();
        return 1;
    }));

    var processBlock = new TransformBlock<Try<int>, Try<int>>(
        construct => construct.Map(value =>
    {
        //ThisMethodMyAlsoThrowException();
        return 1;
    }));

    var resultsBlock = new ActionBlock<Try<int>>(construct =>
    {
        if (construct.IsException)
        {
            var type = construct.Exception.GetType();
            //Here it would be nice to know which item(id) was faulted.
        }
    });
    downloadBlock.LinkTo(processBlock, new DataflowLinkOptions
        { PropagateCompletion = true });
    processBlock.LinkTo(resultsBlock, new DataflowLinkOptions
        { PropagateCompletion = true });
    foreach (var constructId in constructIds)
    {
        await downloadBlock.SendAsync(constructId);
    }

    downloadBlock.Complete();
    await resultsBlock.Completion;
}

Solution

  • You could use ValueTuple<TId, Try<TResult>> structs as messages for the pipeline, but it may be slightly more convenient to create a custom wrapper of the Try class that also holds the id. Since this wrapper will have two type parameters, it is allowed to name it Try as well:

    public readonly struct Try<TId, TResult>
    {
        public static Try<TId, TResult> Create(TId id, Func<TResult> func)
            => new Try<TId, TResult>(id, Try.Create(func));
    
        public static async Task<Try<TId, TResult>> Create(TId id,
            Func<Task<TResult>> func)
            => new Try<TId, TResult>(id, await Try.Create(func).ConfigureAwait(false));
    
        public readonly TId Id { get; }
        public readonly Try<TResult> Result { get; }
    
        private Try(TId id, Try<TResult> result) { Id = id; Result = result; }
    
        public Try<TId, TNewResult> Map<TNewResult>(Func<TResult, TNewResult> func)
            => new Try<TId, TNewResult>(Id, Result.Map(func));
    
        public async Task<Try<TId, TNewResult>> Map<TNewResult>(
            Func<TResult, Task<TNewResult>> func)
            => new Try<TId, TNewResult>(Id, await Result.Map(func).ConfigureAwait(false));
    }
    

    You could then use it like this:

    var downloadBlock = new TransformBlock<int, Try<int, int>>(
        construct => Try<int, int>.Create(construct, async () =>
    {
        await SometimesThrowsAsync();
        return 1;
    }));
    
    var processBlock = new TransformBlock<Try<int, int>, Try<int, int>>(
        construct => construct.Map(async value =>
    {
        await SometimesThrowsAsync();
        return 1;
    }));
    
    var resultsBlock = new ActionBlock<Try<int, int>>(construct =>
    {
        if (construct.Result.IsException)
        {
            var type = construct.Result.Exception.GetType();
            //Log that the {construct.Id} has failed.
        }
    });