Linq.Expression TryCatch - Pass exception to Catch Block?
Asked Answered
K

2

6

So I've been tinkering with Linq.Expressions (and if anyone can suggest a more proper or more elegant way to do what I'm doing please feel free to chime in) and have hit a wall in trying to do something.

Let's imagine we have a simple math class:

public class SimpleMath {
    public int AddNumbers(int number1, int number2) {           
        return number1 + number2;
    }
}

I decide I want to convert our AddNumbers method to a simple Func<object, object, object> delegate.

To do this I do the following:

// Two collections, one for Type Object paramaters and one for converting to Type int.
List<ParameterExpression> parameters = new List<ParameterExpression>();
List<Expression> convertedParameters = new List<Expression>();

// Populate collections with Parameter and conversion
ParameterExpression parameter1 = Expression.Parameter(typeof(object));
parameters.Add(parameter1);
convertedParameters.Add(Expression.Convert(parameter1, typeof(int)));

ParameterExpression parameter2 = Expression.Parameter(typeof(object));
parameters.Add(parameter2);
convertedParameters.Add(Expression.Convert(parameter2, typeof(int)));

// Create instance of SimpleMath
SimpleMath simpleMath = new SimpleMath();

// Get the MethodInfo for the AddNumbers method
MethodInfo addNumebrsMethodInfo = simpleMath.GetType().GetMethods().Where(x => x.Name == "AddNumbers").ToArray()[0];
// Create MethodCallExpression using the SimpleMath object, the MethodInfo of the method we want and the converted parameters
MethodCallExpression returnMethodWithParameters = Expression.Call(Expression.Constant(simpleMath), addNumebrsMethodInfo, convertedParameters);

// Convert the MethodCallExpression to return an Object rather than int
UnaryExpression returnMethodWithParametersAsObject = Expression.Convert(returnMethodWithParameters, typeof(object));

// Create the Func<object, object, object> with our converted Expression and Parameters of Type Object
Func<object, object, object> func = Expression.Lambda<Func<object, object, object>>(returnMethodWithParametersAsObject, parameters).Compile();
    object result = func(20, 40); // result = 60

So if you run that code func should return simple calculations. However, it accepts parameters of Type Object, which obviously leaves it open to problems at runtime, for example:

object result1 = func(20, "f"); // Throws InvalidCastException

So I want to wrap the method in a Try...Catch (obviously this exact problem would be picked up at compile time if we were dealing with a straight call to AddNumbers and passing a string as a parameter).

So to catch this exception I can do the following:

TryExpression tryCatchMethod = TryExpression.TryCatch(returnMethodWithParametersAsObject, Expression.Catch(typeof(InvalidCastException), Expression.Constant(55, typeof(object))));
Func<object, object, object> func = Expression.Lambda<Func<object, object, object>>(tryCatchMethod, parameters).Compile();
object result = func(20, "f"); // result = 55

The TryExpression.TryCatch takes an Expression body and then a collection of CatchBlock handlers. returnMethodWithParametersAsObject is the expression we wish to wrap, Expression.Catch defines that the Exception we want to catch is of Type InvalidCastException and its Expression body is a constant, 55.

So the exception is handled, but it's not much use unless I want to always return a static value when the exception is thrown. So returning to the SimpleMath class I add a new method HandleException:

public class SimpleMath {
    public int AddNumbers(int number1, int number2) {
        return number1 + number2;
    }

    public int HandleException() {
        return 100;
    }
}

And following the same process above I convert the new method to an Expression:

MethodInfo handleExceptionMethodInfo = simpleMath.GetType().GetMethods().Where(x => x.Name == "HandleException").ToArray()[0];
MethodCallExpression returnMethodWithParameters2 = Expression.Call(Expression.Constant(simpleMath), handleExceptionMethodInfo);
UnaryExpression returnMethodWithParametersAsObject2 = Expression.Convert(returnMethodWithParameters2, typeof(object));

Then using it when creating the TryCatch block:

TryExpression tryCatchMethod2 = TryExpression.TryCatch(returnMethodWithParametersAsObject, Expression.Catch(typeof(InvalidCastException), returnMethodWithParametersAsObject2));
Func<object, object, object> func = Expression.Lambda<Func<object, object, object>>(tryCatchMethod2, parameters).Compile();
object result = func(20, "f"); // result = 100

So this time when the InvalidCastException is thrown the SimpleMath.HandleException method will be executed. So far so good, I can now execute some code when there is an exception.

My problem now is, in a normal inline Try...Catch block you actually have the exception object at your disposal. E.g.

try {
    // Do stuff that causes an exception
} catch (InvalidCastException ex) {
    // Do stuff with InvalidCastException ex
}

I can execute code when an exception is thrown but I can't seem to figure out how to actually get my hands on the exception object like you would in a normal Try...Catch block.

Any help would be appreciated!

p.s. I know that you wouldn't actually organise anything in the way I've done above but I thought it was necessary for example purposes to show the mechanics of what I want to do.

Kiss answered 31/8, 2017 at 13:43 Comment(1)
The problem is rather too complicated for my knowledge but a suggestion that could help about passing an exception : catch{throw;}. This will pass your exception to calling method (where you MUST handle it or the program will stop !). If you want to put your hand on the exception of an already existing function, put it as an action/func in your own TryCatch method returning an exception if something wrong happened, whatever you think good if everything went properly (null?)Allot
M
3

You need to pass your catched exception to CatchBlock expression as parameter. For this you should do:

  • Change signature of HandleException. It will takes a exception as argument:

    public int HandleException(InvalidCastException exp)
    {
        // Put here some real logic. I tested it using line below
        Console.WriteLine(exp.Message);
        return 100;
    }
    
  • Use CatchBlock.Variable to pass your handled exception into catch block. You can set it use constructor. Read comment in the code below:

        // Create parameter that will be passed to catch block 
        var excepParam = Expression.Parameter(typeof(InvalidCastException));
    
        MethodInfo handleExceptionMethodInfo = simpleMath.GetType().GetMethods().Where(x => x.Name == "HandleException").ToArray()[0];
        MethodCallExpression returnMethodWithParameters2 = Expression.Call(Expression.Constant(simpleMath), handleExceptionMethodInfo, excepParam);
        UnaryExpression returnMethodWithParametersAsObject2 = Expression.Convert(returnMethodWithParameters2, typeof(object));
    
        // Put created parameter before to CatchBlock.Variable using Expression.Catch 
        // that takes the first argument as ParameterExpression
        TryExpression tryCatchMethod2 = TryExpression.TryCatch(returnMethodWithParametersAsObject, Expression.Catch(excepParam, returnMethodWithParametersAsObject2));
        var exppp = Expression.Lambda<Func<object, object, object>>(tryCatchMethod2, parameters);
        Func<object, object, object> func2 = Expression.Lambda<Func<object, object, object>>(tryCatchMethod2, parameters).Compile();
        object result2 = func2(20, "f"); // result = 100
    
Microscopy answered 31/8, 2017 at 16:30 Comment(1)
Outstanding! This is exactly what I was looking for, thank you very much.Kiss
E
1

@GeorgeAlexandria's answer is fully correct. But I found it very hard work to decode it all when I came to do the same thing.

Perhaps the following code (written as 2 helper methods), will help the next person who needs to do something like this ...

        private Expression WrapActionExpressionIn_Try_Catch_ThrowNewMessage(Expression coreExpression, string newMessage)
        {
            return
                Expression.TryCatch(
                    coreExpression,
                Expression.Catch(typeof(Exception),
                    Expression.Throw(
                        Expression.Constant(new Exception(newMessage))
                    )
                ));
        }

        private Expression WrapActionExpressionIn_Try_Catch_RethrowWithAdditionalMessage(Expression coreExpression, string additionalMessage)
        {
            var caughtExceptionParameter = Expression.Parameter(typeof(Exception));

            //We want to call `new Exception(additionalMessage, caughtException)`
            var ctorForExceptionWithMessageAndInnerException = typeof(Exception).GetConstructor(new[] {typeof(string), typeof(Exception)});
            var replacementExceptionExpresion = Expression.New(ctorForExceptionWithMessageAndInnerException, Expression.Constant(additionalMessage), caughtExceptionParameter);

            return
                Expression.TryCatch(
                    coreExpression,
                Expression.Catch(caughtExceptionParameter,
                    Expression.Throw( replacementExceptionExpresion )
                ));
        }

These two methods both start from a premise of "we already have an Expression representing an Action<TInstance> that we are about to invoke. And now we want to add a try-catch around that Action."

Without the Try-Catch, we would have done:

Action finalLamda = Expression.Lambda<Action<TInstance>>(actionExpression, instanceExpression).Compile();

now we do:

var actionWithTryCatchExpression = WrapActionExpressionIn_Try_Catch_RethrowWithAdditionalMessage(actionExpression);
Action finalLamda = Expression.Lambda<Action<TInstance>>(actionWithTryCatchExpression, instanceExpression).Compile();

Respectively the two helpers represent:

try
{
    action();
}
catch(Exception)
{
    throw new Exception(newMessage);
}

\\and

try
{
    action();
}
catch(Exception e)
{
    throw new Exception(additionalMessage, e);
}

Entertainer answered 22/11, 2019 at 18:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.