Closed stonstad closed 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
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.
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.
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.
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?
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.
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.
We will look at it probably during the weekend.
I will give you an update at the beginning of next week
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.
Hi @JonathanMagnan This will be of significant value to us, and help solve the problem we are encountering. Thank you!
Hi @JonathanMagnan . Any updates around this?
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
WOW! Thank you so much. This is awesome and it works perfectly. Thank you!
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.
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
Jon, my sincerest apologies. This is, as you can imagine, completely user error.
Thank for letting us know, have a great weekend ;)
Best Regards,
Jon
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?