zzzprojects / Eval-Expression.NET

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

Eval Dynamic Behavior #99

Closed stonstad closed 3 years ago

stonstad commented 3 years ago

Given an expression like Eval("Foo.Bar" Foo)...

If the type is dynamic and Bar is not present this results in a null reference exception (if object) or RuntimeBinderException if dynamic. Is there a configuration setting or way to have this evaluate to null instead of throw an exception?

JonathanMagnan commented 3 years ago

Hello @stonstad ,

That's something that could probably be possible.

I believe the hardest part of this request is finding a name for this option as the default behavior is to throw an error.

Any idea?

I currently find all our ideas a little bit weird.

Best Regards,

Jon

stonstad commented 3 years ago

ThrowExceptionIfNullEvaluation (defaults true) ThrowExceptionIfMemberMissing (defaults true)

I appreciate your consideration. The work-around is somewhat difficult because it appears that a parser is needed and expressions can vary greatly.

JonathanMagnan commented 3 years ago

Hello @stonstad ,

When adding to try a new option, we always make it "false" as default. That's a convention we implemented here (so we don't need anymore to ask ourselves if it should false or true by default) so your name cannot work. Also, we probably need to add "Dynamic" or "ExpandoObject" somewhere in the name as we don't want to disable it everywhere. But your suggestion gives us some idea.

I also thought about it and your sentence convert an ExpandoObject into a true dynamic type in your other issue and perhaps it could be the best solution for your 2 issues:

dynamic expando = new ExpandoObject();
expando.BAR = 2;
var dynamicObjectCaseInsensitive = new DynamicObjectCaseInsensitive(expando);

var context = new EvalContext();
var result1 = context.Execute("Foo.Bar2", new { Foo = dynamicObjectCaseInsensitive }); // return null
var result2 = context.Execute("Foo.bar", new { Foo = dynamicObjectCaseInsensitive }); // return 2;

public class DynamicObjectCaseInsensitive: DynamicObject
{
    private IDictionary<string, object> _dict;

    public DynamicObjectCaseInsensitive(ExpandoObject expando)
    {
        var expandoDict = (IDictionary<string, object>) expando;

        _dict = new Dictionary<string, object>(expandoDict, StringComparer.OrdinalIgnoreCase);
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (!_dict.TryGetValue(binder.Name, out result))
        {
            // default value when nothing is found
            result = null;
        }

        return true;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        _dict[binder.Name] = value;

        return true;
    }
}

It will return null or anything you want by default if nothing is found and already support case insensitive with the StringComparer.OrdinalIgnoreCase.

Let me know if that solution could work for both of your issues.

stonstad commented 3 years ago

I'm testing this solution and it has potential. One thing I'm hitting is that it does not work well with the System.Text.Json serializer because of https://github.com/dotnet/runtime/issues/31175. Serialized output appears as dictionary key/value pairs.

JonathanMagnan commented 3 years ago

Hello @stonstad ,

I'm less familiar with the System.Text.Json, I looked at their issue but yes, this solution will not fully work with their limitation.

Did you try Newtonsoft to check if you get the same limitation?

stonstad commented 3 years ago

It's almost working -- the challenges I am seeing are around differences in serialization output for System.Text.Json and Newtsonoft.Json when compared to a true dynamic object. It would be great to see case insensitivity work as described above -- but it does seem like the alternative solution is close to working.

stonstad commented 3 years ago

Is there a possibility for DynamicDefaultValueIfNotExists to happen? The proposed solutions change serialized output, which is undesirable. If there was a way for Eval-Expression to return a default value or not throw an exception, this would be very useful.

JonathanMagnan commented 3 years ago

We will look at it probably during the weekend.

I will give you an update at the beginning of next week

JonathanMagnan commented 3 years ago

Hello @stonstad ,

A branch is currently under validation, we are still uncomfortable with this change but so far, it will look like this:

 var context = new EvalContext();
context.DynamicGetMemberMissingValueFactory = (obj, propertyOrFieldName) => null;

So you will be able to specify a default value depending on the object type and propertyOrFieldName missing. In this case, no matter what, we return null when the member is not found instead of an error.

We preferred a Factory solutions since some other people would have get this value as "string.Empty" instead. So the factory allow to support all this kind of scenario.

If accepted, it will probably go in production at the end of the week.

stonstad commented 3 years ago

Hi @JonathanMagnan This will be of significant value to us, and help solve the problem we are encountering. Thank you!

stonstad commented 3 years ago

Hi @JonathanMagnan . Any updates around this?

JonathanMagnan commented 3 years ago

Hello @stonstad ,

The v4.0.39 has been released.

It's now possible to set the default value this way:

var context = new EvalContext();
context.DynamicGetMemberMissingValueFactory = (obj, propertyOrFieldName) => null;

We also profited of this chance to improve the case insensitive in this part of the code when you enter in the DynamicGetMember method.

Let me know if everything works as expected.

Best Regards,

Jon

stonstad commented 3 years ago

WOW! Thank you so much. This is awesome and it works perfectly. Thank you!

stonstad commented 3 years ago

This feature generally works but it seems to have a curious bug.

If a member is missing and the name of the member contains no numbers, DynamicGetMemberMissingValueFactory works. OK: " " + result.Missing + " " + result.Missing

However, if a number is present, Eval throws Object reference not set to an instance of an object. FAIL: " " + result.Missing1 + " " + result.Missing2

string.Join(",", resultMissing1, result.Missing2) works. But if there is a + sign in-between it fails.

Assigning a cast to (string) for each parameter prevents the error. It seems to be related to null + null being invalid, which makes sense except for the inconsistency of it behaving different if a trailing number exists. In a scenario which end users can build expressions for returned data, and returned data is not always present, this becomes a hard problem to solve. The inconsistent behavior is strange.

JonathanMagnan commented 3 years ago

Hello @stonstad ,

Could you provide a full example that works and one that fails. My developer and I are not exactly sure what you are saying.

We tried a few scenario but they were all working such as:

var context = new EvalContext();

context.DynamicGetMemberMissingValueFactory = (o, s) =>
{
    return 1;
};

var expando = new ExpandoObject();

var t1 = context.Execute("result.Missing1 + \" \" + result.Missing2", new {result = expando });
var t2 = context.Execute("string.Join(\",\", +result.Missing1, -result.Missing2)", new { result = expando });
var t3 = context.Execute("string.Join(\",\", result.Missing1 + \" \" + result.Missing2, -result.Missing2)", new { result = expando });

Best Regards,

Jon

stonstad commented 3 years ago

Jon, my sincerest apologies. This is, as you can imagine, completely user error.

JonathanMagnan commented 3 years ago

Thank for letting us know, have a great weekend ;)

Best Regards,

Jon