zzzprojects / Eval-Expression.NET

C# Eval Expression | Evaluate, Compile, and Execute C# code and expression at runtime.
https://eval-expression.net/
Other
459 stars 86 forks source link

How to debug issues ? #45

Closed TehWardy closed 6 years ago

TehWardy commented 6 years ago

So I hit this issue. I made the following call ...

var function = Eval.Compile("activity.Foo = activity.Previous[0].Foo", "activity");

... and got this back ...

System.NullReferenceException: Object reference not set to an instance of an object.
   at ?.?(ExpressionScope ?, SyntaxNode ?)
   at ?.?(ExpressionScope ?, SyntaxNode ?, Expression ?, Boolean ?)
   at ?.?(ExpressionScope ?, SyntaxNode ?, Expression ?, Boolean ?)
   at ?.?(ExpressionScope ?, SyntaxNode ?, Expression ?, Boolean ?)
   at Z.Expressions.CodeCompiler.CSharp.ExpressionParser.ParseSyntax(ExpressionScope scope, SyntaxNode node, Type resultType)
   at ?.?[?](EvalContext ?, String ?, IDictionary`2 ?, Type ?, EvalCompilerParameterKind ?, Boolean ?, Boolean ?)
   at Z.Expressions.EvalContext.Compile[TDelegate](String code, String[] parameterNames)
   at Z.Expressions.Eval.Compile[TDelegate](String code, String[] parameterNames)

The next line in the reported exception was the line in my code that made this call. I'm not sure how I can get a null reference exception when all i did was give it a simple string pair.

Any ideas?

JonathanMagnan commented 6 years ago

Hello @TehWardy ,

Here is a working example: https://dotnetfiddle.net/74j9j1

// @nuget: Z.Expressions.Eval

using System;
using System.Collections.Generic;
using System.Dynamic;
using Z.Expressions;

public class Program
{
    public static void Main()
    {
        var activity = new Activity() { Foo = "A", Previous = new List<Activity>() { new Activity() { Foo = "Z" }}};

        Console.WriteLine("Before: " + activity.Foo);               
        var function = Eval.Compile<Action<Activity>>("activity.Foo = activity.Previous[0].Foo", "activity");
        function(activity);
        Console.WriteLine("After: " + activity.Foo);
    }

    public class Activity
    {
        public string Foo { get; set; }

        public List<Activity> Previous { get; set; }
    }
}

Let me know if that helped you with your scenario.

Best Regards,

Jonathan

TehWardy commented 6 years ago

Hi Jonathon,

So lets assume that your code is correct (and it seems to look fine, and run ok). Oddly in a situation where activity was null I would expect the function call to fail but not the compile.

I seem to be getting null reference exception on the compile though with exactly the same params as you're passing in.

I have however managed to find a null reference in some of my code leading up to this call which having resolved that seemed to make the compile call issue go away. My question is really "how can this happen?" at this point.

I think i'm just going to have to battle on through with it and spend a lot of time stepping through with the debugger to figure out what's really the cause of the issue as i'm sure it can't be the ZZZ code.

The exceptions given are crazy cryptic though, is there anything that can be done to help with the clarity of them?

Many thanks,

Paul

On Mon, 29 Oct 2018 at 21:25, Jonathan Magnan notifications@github.com wrote:

Hello @TehWardy https://github.com/TehWardy ,

Here is a working example: https://dotnetfiddle.net/74j9j1

// @nuget: Z.Expressions.Eval using System;using System.Collections.Generic;using System.Dynamic;using Z.Expressions; public class Program { public static void Main() { var activity = new Activity() { Foo = "A", Previous = new List() { new Activity() { Foo = "Z" }}};

  Console.WriteLine("Before: " + activity.Foo);               
  var function = Eval.Compile<Action<Activity>>("activity.Foo = activity.Previous[0].Foo", "activity");
  function(activity);
  Console.WriteLine("After: " + activity.Foo);

}

public class Activity { public string Foo { get; set; }

  public List<Activity> Previous { get; set; }

} }

Let me know if that helped you with your scenario.

Best Regards,

Jonathan

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/zzzprojects/Eval-Expression.NET/issues/45#issuecomment-434086080, or mute the thread https://github.com/notifications/unsubscribe-auth/AGkmFl91qXiGtJNqUm3xxyfAmqt8lVD1ks5up3I6gaJpZM4X_XUO .

JonathanMagnan commented 6 years ago

Hello Paul,

We try every month to improve our compiler to provide better error messages. I will add this issue to the list my developer will look this month to improve.

There is a huge difference between both of our code

Your: Eval.Compile("activity.Foo = activity.Previous[0].Foo", "activity"); Mine: Eval.Compile<Action<Activity>>("activity.Foo = activity.Previous[0].Foo", "activity");

In my version, the compiler know that the first parameter named "activity" is of type Activity. In your version, the compiler probably assume it's of type object which lead to a null reference (could be for another reason).

Let me know if that make sense.

TehWardy commented 6 years ago

How do i deal with polymorphic scenarios like this ....

class Activity 
{
    public Action<Activity> Assign { get; set; }
    public Activity[] Previous { get; set; }
    ...
}

class FooActivity : Activity
{
     public Foo Foo { get; set; }
}

// assuming I have done the work and I know the current activity instance is definitely a FooActivity and not just a regular Activity I have this need to dynamically build an Action but the compiler doesn't seem to like me doing this ...

var method = Eval.Compile<Action<Activity>>("activity.Foo = activity.Previous[0].Foo", "activity");

As discussed previously (by email I believe), I'm using this to dynamically copy values computed in workflow activities from the previous to the current based on "Link Expression arrays".

in short I have something like this ...

var flow = {
     Activities = new Activity[] { ... },
     Links = new Link[] { ... }
}

Links have 3 properties "source", "destination", and "expression" and all this data is provided to the back end as a json blob which i then parse and process.

The expressions I receive are like this ...

{destination}.Foo = {source}.Foo

... which I translate to ...

activity.Foo = activity.Previous[0].Foo

... Neither of the objects needs to be specifically of type Activity but can both be of any type derived from Activity. This usually means that my expressions contain things that Activity doesn't and it causes me some problems.

so I have trying to dynamically invoke the compile method like this ...

// get the generic compile method with right signature
static MethodInfo compile = typeof(Eval).GetMethod("Compile", new Type[] { typeof(string), typeof(string[]) });

// make the generic version filling in the current activity type
var dynamicCompile = compile.MakeGenericMethod(typeof(Action<>).MakeGenericType(activity.GetType()));

// return a dynamic action cast to the generic base type for later execution
var dynamicMethod = (Action<Activity>)dynamicCompile.Invoke(null, new object[] { script, new[] { "activity" } });

... and that last line is where things fall over because activity (the base type) has an array "Previous" of type Activity which usually will contain things that derive from but are not actually Activity themselves.

I seem to be getting this null reference exception because of this issue around polymorphism in the data structure i'm using.

JonathanMagnan commented 6 years ago

Hello @TehWardy ,

If I understand well, even the C# compiler will not like it.

A previous item is of type Activity and you try to use a property of type FooActivity without casting it.

It's possible on your side to cast it? by example: https://dotnetfiddle.net/UwE89l

Otherwise, another way is maybe perhaps using ExpandoObject that could I believe support this kind of scenario without casting but then, your object will not be strongly typed.

TehWardy commented 6 years ago

Ah nice ... i wasn't aware i could do that ... This is a simple change ... bit of reflection involved in wrapping the {source} replacement i'm already doing but totally reasonable given the context in which i'm operating.

Am I correct in understanding then that Eval.Compile is essentially just calling the roslyn API under the bonnet? Also is there a way to ensure a level of system security by forcing only operations on the given parameters can be performed and not just any random expression like ...

File.OpenWrite("web.config")

TehWardy commented 6 years ago

can't seem to get this to work so I farmed this one liner out to it's own method ...

Action<T> Compile<T>(string script) where T : Activity
{
    return Eval.Compile<Action<T>>(script, "activity");
}

if I put a breakpoint on that line and check typeof(T) T is in fact a type that inherits from Activity (hense how I got here in the first place. And script is (for example)

((FooActivity)activity).Object.Companies = ((BarActivity)activity.Previous[0]).Result;

So the call is like ...

Compile<FooActivity>(script);

Then it gives me this exception ...

Value cannot be null. Parameter name: type
   at System.Linq.Expressions.Expression.Convert(Expression expression, Type type, MethodInfo method)
   at .(ExpressionScope , SyntaxNode , Expression , Boolean )
   at .(ExpressionScope , SyntaxNode , Expression , Boolean )
   at .(ExpressionScope , SyntaxNode , Expression , Boolean )
   at .(ExpressionScope , SyntaxNode , Expression , Boolean )
   at .(ExpressionScope , SyntaxNode )
   at .(ExpressionScope , SyntaxNode , Expression , Boolean )
   at .(ExpressionScope , SyntaxNode , Expression , Boolean )
   at .(ExpressionScope , SyntaxNode , Expression , Boolean )
   at Z.Expressions.CodeCompiler.CSharp.ExpressionParser.ParseSyntax(ExpressionScope scope, SyntaxNode node, Type resultType)
   at .[](EvalContext , String , IDictionary`2 , Type , EvalCompilerParameterKind , Boolean , Boolean )
   at Z.Expressions.EvalContext.Compile[TDelegate](String code, String[] parameterNames)
   at Z.Expressions.Eval.Compile[TDelegate](String code, String[] parameterNames)

....

Having dug in to this I think the problem is in the dependency graph. I've added a bunch of assemblies as references to my project that are likely (from the point of this projects point of view anyway) ... only used in this context.

I think the issue is that the types i'm casting to may or may not exist in the app domain. Does Eval have a means to provide a dependency resolver or something for scripts so it can scrape for a know type def?

JonathanMagnan commented 6 years ago

Am I correct in understanding then that Eval.Compile is essentially just calling the roslyn API under the bonnet?

You are 100% wrong. We have written our own CodeAnalisys and CodeCompiler that mostly expression tree: https://expressiontree-tutorial.net/

We might release another product in the future that use Roslyn but this library will never use it.

Also is there a way to ensure a level of system security by forcing only operations on the given parameters can be performed and not just any random expression like ...

It's possible to register only some type by default using the following code:

var evalContext = new EvalContext();
evalContext.UnregisterAll();
evalContext.RegisterDefaultAliasSafe();

The RegisterDefaultAliasSafe should only register type that should not have any impact such as string, math, and regex function (there is a lot of more!). You can also choose to register what you want your user get access.

So if the type doesn't exists, the user should not be in able to access it.

JonathanMagnan commented 6 years ago

For your second post,

You can register type or assembly using Register methods: https://eval-expression.net/register-unregister

If it still doesn't work, do you think you will be able to provide a project sample with the issue? It will be easier for us to look at it: info@zzzprojects.com

TehWardy commented 6 years ago

Oh that's interesting @JonathanMagnan ... Dam you guys went all out huh!!

Does contructing and operating on an EvalContext do something with the current appdomain under the bonnet.

So if I register a ton of assemblies in it in say static void main of a console app will all calls to Eval.Compile then use what I setup in that EvalContext as shown here ...

https://eval-expression.net/options

// using Z.Expressions; // Don't forget to include this.
var context = new EvalContext();
context.IncludeMemberFromAllParameters = true;

Eval.Execute<bool>("catPropertyName == dogPropertyName", cat, dog);

... this implies that Eval.Execute called here will honor the EvalContext information even though the two are effectively disconnected (i'm not passing the context to the Execute call).

This also implies that doing this on multiple threads might prove problematic?

JonathanMagnan commented 6 years ago

Hello @TehWardy ,

I'm sorry, I'm not sure to understand the question.

Multi-threading

In multi-threading scenario, you should only configure the default EvalContext (the one used for Eval.Execute) once. Usually on application Startup.

EvalManager.DefaultContext.IncludeMemberFromAllParameters = true;

Eval.Execute<bool>("catPropertyName == dogPropertyName", cat, dog);

If every thread have some special configuration, you need to create a new EvalContext inside the thread and use this instance instead

var context = new EvalContext();
context.IncludeMemberFromAllParameters = true;

context.Execute<bool>("catPropertyName == dogPropertyName", cat, dog);

I hope that somewhat answer your question.

TehWardy commented 6 years ago

Ah perfect :)

That works for me!