I’m implementing a feature where a user can write an arbitrary expression that needs to be evaluated against some context. Users should be able to write expressions referring to a dynamic JSON payload.
I’m using Roslyn CSharpScript to execute the expression.
I managed to make it work using Newtonsoft, however I would like to do the same using System.Text.Json.
Working approach with Newtonsoft:
[Fact]
public async Task Should_Evaluate_Dynamic_Payload_Using_Newtonsoft()
{
// Arrange
const string jsonPayload = """
{
"dog": {
"name": "Slayer"
}
}
""";
var evaluationContext = new EvaluationContext
{
Payload = JObject.Parse(jsonPayload),
};
const string expression = @"Payload.dog.name == ""Slayer""";
var options = ScriptOptions.Default;
options = options.AddReferences(typeof(CSharpArgumentInfo).Assembly);
var script = CSharpScript.Create<bool>(expression, options, globalsType: typeof(EvaluationContext));
var scriptRunner = script.CreateDelegate();
// Act
var result = await scriptRunner(evaluationContext);
// Assert
result.Should().BeTrue();
}
Not working approach with System.Text.Json:
[Fact]
public async Task Should_Evaluate_Dynamic_Payload_Using_SystemTextJson()
{
// Arrange
const string jsonPayload = """
{
"dog": {
"name": "Slayer"
}
}
""";
var evaluationContext = new EvaluationContext
{
Payload = JsonSerializer.Deserialize<JsonElement>(jsonPayload),
};
const string expression = @"Payload.dog.name == ""Slayer""";
var options = ScriptOptions.Default;
options = options.AddReferences(typeof(CSharpArgumentInfo).Assembly);
var script = CSharpScript.Create<bool>(expression, options, globalsType: typeof(EvaluationContext));
var scriptRunner = script.CreateDelegate();
// Act
var result = await scriptRunner(evaluationContext);
// Assert
result.Should().BeTrue();
}
This fails with 'System.Text.Json.JsonElement' does not contain a definition for 'dog'
. It seems like the parsed JsonElement is not being correctly identified as a dynamic, hence accessing the property with dot notation fails.
I know I could use Payload.GetProperty("dog").GetProperty("name").ToString()
, but I’d much prefer keeping the dot notation (Payload.dog.name
).
I tried parsing the JSON with JsonDocument
, JsonElement
, JsonSerializer.Deserialize
, but none seem to work.
2
Answers
Text.Json using the same syntax as Newtonsoft.Json
but System.Text.Json supports only get path. IMHO Text.Json still is too raw for using. I highly recommend you to use Newtonsoft.Json. It is not ony one problem, the longer you try to use Text.Json the more problem you have. Sooner or later you will give up.
JToken
implementsIDynamicMetaObjectProvider
which is what allows your Json.NET code to work: it dynamically forwards .NET property references to JSON property dictionary lookups, so that:Is evaluated in runtime as:
JsonElement
andJsonNode
, however, do not implementIDynamicMetaObjectProvider
, so the above dynamic expression will not work with them. See Json Deserialization of "dynamic" does not work #66638 for confirmation.What you could do instead is to deserialize your JSON to an
ExpandoObject
, then pass that into yourCSharpScript
. To do that, copyObjectAsPrimitiveConverter
from this answer to C# – Deserializing nested json to nested Dictionary<string, object>. Assuming yourEvaluationContext
type looks like:You can modify your code as follows:
And the remainder should work as-is.
Demo fiddle here.
As an alternative, since your expressions look like JSONPath queries, you might consider using a 3rd party JSONPath evaluator for System.Text.Json (which does not support JSONPath itself, unlike Newtonsoft). There are several mentioned in Add JsonPath support to JsonDocument/JsonElement #31068 including: