Skip to content

Conversation

@JobaDiniz
Copy link

@JobaDiniz JobaDiniz commented Oct 1, 2025

Summary

Adds a new public interface IBindingLookup that allows custom built-in functions to access JSONata bindings (variables and functions) during evaluation.

Use Case

This enables advanced scenarios where custom functions need to interact with the evaluation context. For example, when working with complex data structures like Draft.js that define their own variable systems, custom functions can now map those internal variables to JSONata bindings dynamically.

Usage Example

public static JToken ProcessTemplate(string template, IBindingLookup lookup)
{
    JToken header = lookup.Lookup("headerText");
    return new JValue($"{header}: {template}");
}

env.BindFunction("process", ProcessTemplate);
env.BindValue("headerText", new JValue("Title"));

Implementation

  • New IBindingLookup interface with Lookup(string name) method
  • Parameters of type IBindingLookup are automatically detected and injected
  • No attributes required - simply declare the parameter with the interface type
  • Injected parameters don't count toward function argument requirements

Breaking Changes

None - this is a purely additive feature.

Excludes thoughts/ directory and CLAUDE.md file.
@JobaDiniz JobaDiniz force-pushed the feature/binding-lookup-interface branch from 274ea5d to d211dc0 Compare October 1, 2025 20:13
Implements IBindingLookup interface allowing custom built-in functions
to access JSONata bindings (variables and functions) via the
[BindingLookupArgument] attribute. Parameters marked with this attribute
receive an IBindingLookup instance automatically and don't count toward
the function's required argument count.

- Created IBindingLookup interface with Lookup(string) method
- Implemented explicit interface on EvaluationEnvironment
- Added public BindingLookupArgumentAttribute with XML documentation
- Extended FunctionTokenCsharp to detect, validate, and inject lookup
- Added comprehensive tests covering behavior and error cases

Signed-off-by: JobaDiniz <[email protected]>
@JobaDiniz JobaDiniz force-pushed the feature/binding-lookup-interface branch from d211dc0 to c97e2b0 Compare October 1, 2025 20:20
@mikhail-barg
Copy link
Owner

mikhail-barg commented Oct 5, 2025

Hi @JobaDiniz thank you for the proposal! I seem to get the general idea of your proposal, but I'm not sure if it should be implemented this way.

What worries me is that your solution is specific to our dotnet implementation of the JSONata language, which may be not ideal.

Therefore I suggest that you propose such functionality to be added to the JSONata language itself (for example - via a new system function that returns a value given a name, i.e. $binding_lookup($name) -> value).

What do you think?

@mikhail-barg
Copy link
Owner

Also, have you seen the $lookup function? Maybe it would be OK in your case to just put all bindings that are to be looked up into a single JObject container in the bindings and then use lookup?

Something like this:

JObject bindings = new JObject();
bindings.Add("headerText", new JValue("Title"));
env.BindValue("bindings", bindings);

and then have a jsonata function

$processTemplate := function($template) { $template & ": " &  $lookup($bindings, "headerText") };

@JobaDiniz
Copy link
Author

TLDR: Our C# custom function processes Draft.js rich text structures that contain nested variable references. During C# runtime processing (not JSONata evaluation), we need to look up those nested variables from the JSONata bindings to resolve their values. $lookup() doesn't help because it's a JSONata function, not accessible from C# code.


The Problem in Detail

We work with Draft.js rich text structures that can reference other variables. Our C# custom function needs to resolve these nested references by looking them up in the JSONata environment.

Complete Example with Actual Values

All variables bound in JSONata environment:

env.BindValue("___f", new JValue(42));        // number
env.BindValue("___d", new JValue(8));         // number
env.BindValue("report", JToken.Parse(...));   // Draft.js structure (see below)

The report variable value (Draft.js structure):

{
  "blocks": [{
    "text": "The sum is ___f and difference is ___d",
    "entityRanges": [
      { "key": 0, "offset": 11, "length": 4 },
      { "key": 1, "offset": 35, "length": 4 }
    ]
  }],
  "entityMap": {
    "0": {
      "type": "VARIABLE",
      "data": { "variableName": "___f" }
    },
    "1": {
      "type": "VARIABLE",
      "data": { "variableName": "___d" }
    }
  }
}

JSONata expression:

$draftjsFormat($report)

The Transformation Flow

Step 1: JSONata evaluates $report → returns the Draft.js structure (JToken)

Step 2: JSONata calls our custom C# function DraftjsFormat(JToken value, IBindingLookup lookup)

Step 3: Inside our C# code, we parse the structure:

// We're now in C# runtime code, not JSONata evaluation
foreach (var block in definition.Blocks)
{
    var text = block.Text; // "The sum is ___f and difference is ___d"
    
    foreach (var entityRange in block.EntityRanges)
    {
        var entityType = GetEntityType(entityRange, entityMap); // "VARIABLE"
        var variableName = GetVariableName(entityRange, entityMap); // "___f"
        
        // HERE'S THE PROBLEM:
        // We're in C# code and need to get the value of "___f" from bindings
        // We can't write a JSONata expression here - this is C# runtime
        var value = lookup.Lookup(variableName); // Gets JValue(42)
        
        // Convert to string and substitute in text
        text = ReplaceRange(text, entityRange.Offset, entityRange.Length, "42");
    }
}

Step 4: After processing all nested variables:

"The sum is 42 and difference is 8"

Step 5: Convert to markdown with formatting, return to JSONata

Why We Need IBindingLookup

  1. Variables are already bound - ___f, ___d, and report are all in the JSONata environment
  2. The problem occurs during C# data transformation, not during JSONata evaluation
  3. We're processing a data structure that contains variable names as strings ("___f")
  4. We need programmatic access to resolve these string names to their bound values

Why $lookup() Doesn't Work

$lookup() is a JSONata function that works on JObject data:

$lookup($myObject, "propertyName")

But we need to:

  1. Look up JSONata bindings (not object properties)
  2. Do it from C# runtime code (not a JSONata expression)
  3. Access by variable name string that we extract from the Draft.js structure

The situation is analogous to: imagine if a custom function needed to generate a random number based on data in the structure it's processing - it would use EvalSupplement.Random. Similarly, we need to resolve variable references found in the data we're processing.

Implementation Scope

This is a .NET-specific extension for custom function authors who need to:

  • Process complex data structures
  • Resolve variable references embedded in that data
  • Access the evaluation context programmatically

It doesn't change JSONata language semantics - it extends what custom C# functions can do, similar to how EvalSupplement provides access to system services.

Would you be open to this as a capability for custom function implementations? Happy to discuss alternative approaches if you have suggestions!

@mikhail-barg
Copy link
Owner

@JobaDiniz thanks for a detailed explanation! I do agree now that your proposal is out of Jsonata language spec scope.

Though I think there's a way it could be done in a bit another way like this:

All variables bound in JSONata environment via single JObject named e.g. vars:

JObject vars = new JObject();
vars.Add("___f", new JValue(42))
vars.Add("___d", new JValue(8))
env.BindValue("vars", vars);
env.BindValue("report", JToken.Parse(...)); 

JSONata expression - pass $vars as a second argument of draftjsFormat c# function from jsonata call:

$draftjsFormat($report, $vars)  

inside C# code:

public static JToken draftjsFormat(JToken repors, JToken vars)
{
  Dictionary<string, JToken> varsLookup = ((JObject)vars).ChildrenTokens;
  ...
  // We're now in C# runtime code, not JSONata evaluation
  foreach (var block in definition.Blocks)
  {
      var text = block.Text; // "The sum is ___f and difference is ___d"
      
      foreach (var entityRange in block.EntityRanges)
      {
          var entityType = GetEntityType(entityRange, entityMap); // "VARIABLE"
          var variableName = GetVariableName(entityRange, entityMap); // "___f"
          
          // HERE:
          var value = varsLookup[variableName]; // Gets JValue(42)
          
          // Convert to string and substitute in text
          text = ReplaceRange(text, entityRange.Offset, entityRange.Length, "42");
      }
  }

What do you think, will this do?

@JobaDiniz
Copy link
Author

I think there might be a misunderstanding about what we're trying to achieve. The variables are already bound in the evaluation environment - we're not asking to pass new data.

Current State (What We Already Do)

All variables are bound in the JSONata environment:

env.BindValue("___f", new JValue(42));
env.BindValue("___d", new JValue(8));
env.BindValue("report", JToken.Parse(...));

JSONata expressions can access these bindings:

$___f           // Returns 42
$___d           // Returns 8
$report         // Returns the Draft.js structure

The Problem

When our C# custom function runs, it needs to resolve nested variable references found inside the data structure it's processing. But there's no API for C# code to access these bindings.

Your Proposed Workaround

Your workaround would require passing the same data twice:

// First: bind all variables (already doing this)
env.BindValue("___f", new JValue(42));
env.BindValue("___d", new JValue(8));
env.BindValue("report", JToken.Parse(...));

// Second: create a JObject with the SAME data
JObject vars = new JObject();
vars.Add("___f", new JValue(42));  // Duplicating ___f
vars.Add("___d", new JValue(8));   // Duplicating ___d
env.BindValue("vars", vars);       // Passing data a second time

// Now we have the same data in two places:
// 1. As individual bindings: $___f, $___d
// 2. As properties in $vars: $vars.___f, $vars.___d

Then every function call needs to explicitly pass this duplicated data:

$draftjsFormat($report, $vars)  // Passing $vars explicitly

This seems redundant - we're passing data that's already in the binding context.

What We're Actually Asking For

We're asking for C# custom functions to access what's already there. The binding context exists, it's already populated, we just need an API to access it from C# code.

This is similar to how EvalSupplement works - it provides custom functions access to the evaluation context (Random, DateTimeOffset). We're following the exact same pattern, just for a different aspect of the context (bindings).

The Core Question

Why shouldn't C# custom functions have access to the binding context they're running in?

  • The binding context already exists
  • It's already populated with all variables
  • JSONata expressions can access it via $varName
  • EvalSupplement already provides context access for other needs
  • Custom functions just need the same level of access

The alternative (passing data twice - once in bindings, once as parameters) seems like a workaround for a missing API, not a proper solution. What's the concern with providing direct access to what's already in the context?

@mikhail-barg
Copy link
Owner

mikhail-barg commented Oct 30, 2025

Hi @JobaDiniz and sorry for a late answer.

Why shouldn't C# custom functions have access to the binding context they're running in?

I am not totally against allowing access for c# function to the binding environment. I think it's a valid feature.

The thing is that I'm currently working on a global rewrite of Jsonata.Net.Native (see new_attempt branch), which is inspired by the recent java port. The goal is to make our implementation much more complete in regard to the reference implementation — stuff like # and @ operators, explicit arrays issues, and other things that were haunting me since the beginning of this repo. The good thing ise — these features are already working!

The change is rather significant and will surely be a breaking one (at least in regard to introspection api). I will try to make the transition as easy as possible, but right now I'm focused on the core implementation features.

So while I'm at it, I'm not very inclined to add features to the current implementation. For example, I'm not sure I'd like to introduce the new IBindingLookup interface that you suggested. Maybe it would be better to just expose EvaluationEnvironment as it is. Or maybe there would be a change to the argument binding method for c# functions in general. So I'd like to postpone the decision on this matter untill the rewrite is done. Hope you understand.

This is why I'm suggesting you to try a workaround that would work right now, without any changes to the library (and would work later as well).

You say:

Your workaround would require passing the same data twice:

But why? Let's see once again. Your code from above:

// First: bind all variables (already doing this) 
env.BindValue("___f", new JValue(42));
env.BindValue("___d", new JValue(8));
env.BindValue("report", JToken.Parse(...));

don't do this ^^^

// Second: create a JObject with the SAME data 
JObject vars = new JObject();
vars.Add("___f", new JValue(42));  // Duplicating ___f
vars.Add("___d", new JValue(8));   // Duplicating ___d
env.BindValue("vars", vars);       // Passing data a second time

just do this ^^^

The only potential problem I see there, is that if you are using the vars in other parts of query as is, you'll cave to change them from $___f to $vars.___f. But thi probably is not too much of an issue, is it?

@mikhail-barg
Copy link
Owner

Once again, I agree that my proposal is more a hack/workaround than proper API, and I'd like to add the proper API, but I'd prefer to do this after or during v3 release.

@JobaDiniz
Copy link
Author

Why are you rewriting?

@mikhail-barg
Copy link
Owner

Why are you rewriting?

as I wrote above — because current version has a number of not implemented features, that you may find here (eg, # and @ operators, or issues with array handling: #29, #34, #36 ). Even the % operator that I managed to partially implement added few unfortunate hacks.

So I decided that sticking more to the reference implementation would solve a number of existing problems, and also will simplify things in future, when new features added to the reference.

So while it's a significant amount of work, it seems that the main part is completed already. At the moment of writing, the new implementation passes 1527 tests of the 1655 in the reference test-suite, while the old one had 1267. So seems that I'm getting near.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants