Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
267 lines (184 sloc) 12.5 KB
slug title
js-utils
JavaScript Utils

The ServiceStack.Text JSON Serializers are only designed for serializing Typed POCOs, but you can still use it to deserialize dynamic JSON but you'd need to specify the Type to deserialize into on the call-site otherwise the value would be returned as a string.

A more flexible approach to read any arbitrary JavaScript or JSON data structures is to use the high-performance and memory efficient JSON utils in ServiceStack Templates implementation of JavaScript.

Install

The Templates JSON and JS Utils are available from the ServiceStack.Common NuGet package:

PM> Install-Package ServiceStack.Common

Which will enable access to the JSON API which preserves the Type which can be used to parse JavaScript or JSON literals:

JSON.parse("1")      //= int 1 
JSON.parse("1.1")    //= double 1.1
JSON.parse("'a'")    //= string "a"
JSON.parse("{a:1}")  //= new Dictionary<string, object> { {"a", 1 } }

It can be used to parse dynamic JSON and any primitive JavaScript data type. The inverse API of JSON.stringify() is also available.

Eval

Eval is useful if you want to execute custom JavaScript functions, or if you want to have a text DSL or scripting language for executing custom logic or business rules you want to be able to change without having to compile or redeploy your App. It uses Templates Sandbox which lets you evaluate the script within a custom scope that defines what functions and arguments it has access to, e.g:

public class CustomFilter : TemplateFilter
{
    public string reverse(string text) => new string(text.Reverse().ToArray());
}

var scope = JS.CreateScope(
         args: new Dictionary<string, object> { { "arg", "value"} }, 
    functions: new CustomFilter());

JS.eval("arg", scope)                                        //= "value"
JS.eval("reverse(arg)", scope)                               //= "eulav"
JS.eval("itemsOf(3, padRight(reverse(arg), 8, '_'))", scope) //= ["eulav___", "eulav___", "eulav___"]

//= { a: ["eulav___", "eulav___", "eulav___"] }
JS.eval("{a: itemsOf(3, padRight(reverse(arg), 8, '_')) }", scope)

JavaScript Expressions

The JavaScript Expressions support in ServiceStack follows the syntax tree used by Esprima, JavaScript's leading lexical language parser for JavaScript, but adapted to suit C# conventions using PascalCase properties and each AST Type prefixed with Js* to avoid naming collisions with C#'s LINQ Expression Types which often has the same name.

So Esprima's MemberExpression maps to JsMemberExpression in Templates.

In addition to adopting Esprima's AST data structures, Templates can also emit the same serialized Syntax Tree that Esprima generates from any AST Expression, e.g:

// Create AST from JS Expression
JsToken expr = JS.expression("1 - 2 + 3 * 4 / 5");

// Convert to Object Dictionary in Esprima's Syntax Tree Format
Dictionary<string, object> esprimaAst = expr.ToJsAst();

// Serialize as Indented JSON
esprimaAst.ToJson().IndentJson().Print();

Which will display the same output as seen in the new JS Expression Viewer:

From the AST output we can visualize how the different operator precedence is applied to an Expression. Expression viewer also lets us explore and evaluate different JavaScript Expressions with custom arguments:

An abusage Brendan Eich regrets that is enforced is limiting the || and && binary operators to boolean expressions, which themselves always evaluate to a boolean value.

Instead to replicate || coalescing behavior on falsy values you can use C#'s ?? null coalescing operator as seen in:

Lambda Expressions

You can use lambda expressions in all functional filters:

Using either normal lambda expression syntax:

{% raw %}{{ customers | zip(x => x.Orders)
   | let(x => { c: x[0], o: x[1] })
   | where(_ => o.Total < 500)
   | map(_ => o)
   | htmlDump }}{% endraw %}

Or shorthand syntax for single argument lambda expressions which can instead use => without brackets or named arguments where it will be implicitly assigned to the it binding:

{% raw %}{{ customers | zip => it.Orders
   | let => { c: it[0], o: it[1] }
   | where => o.Total < 500
   | map => o
   | htmlDump }}{% endraw %}

As it's results in more wrist-friendly and readable code, most LINQ Examples use the shorthand lambda expression syntax above.

Shorthand properties

Other language enhancements include support for JavaScript's shorthand property names:

{% raw %}{{ {name,age} }}{% endraw %}

But like C# also lets you use member property names:

{% raw %}{{ people | let => { it.Name, it.Age } | select: {Name},{Age} }}{% endraw %}

Template Literals

Many of ES6/7 features are also implemented like Template Literals:

Backtick quoted strings also adopt the same escaping behavior of JavaScript strings whilst all other quoted strings preserve unescaped string values.

Spread Operators

Other advanced ES6/7 features supported include the object spread, array spread and argument spread operators:

Bitwise Operators

All JavaScript Bitwise operators are also supported:

Essentially Templates supports most JavaScript Expressions, not statements which are covered with Templates Blocks support or mutations using Assignment Expressions and Operators. All assignments still need to be explicitly performed through an Assignment Filter.

Evaluating JavaScript Expressions

The built-in JavaScript expressions support is also useful outside of Templates where they can be evaluated with JS.eval():

JS.eval("pow(2,2) + pow(4,2)") //= 20

The difference over JavaScript's eval being that methods are calling C# method filters in a sandboxed context.

By default expressions are executed in an empty scope, but can also be executed within a custom scope which can be used to define the arguments expressions are evaluated with:

var scope = JS.CreateScope(args: new Dictionary<string, object> {
    ["a"] = 2,
    ["b"] = 4,
}); 
JS.eval("pow(a,2) + pow(b,2)", scope) //= 20

Custom methods can also be introduced into the scope which can override existing filters by using the same name and args count, e.g:

class MyFilters : TemplateFilter {
    public double pow(double arg1, double arg2) => arg1 / arg2;
}
var scope = JS.CreateScope(functions: new MyFilters());
JS.eval("pow(2,2) + pow(4,2)", scope); //= 3

An alternative to injecting arguments by scope is to wrap the expression in a lambda expression, e.g:

var expr = (JsArrowFunctionExpression)JS.expression("(a,b) => pow(a,2) + pow(b,2)");

Which can then be invoked with positional arguments by calling Invoke(), e.g:

expr.Invoke(2,4)        //= 20
expr.Invoke(2,4, scope) //= 3

Parsing JS Expressions

Evaluating JS expressions with JS.eval() is a wrapper around parsing the JS expression into an AST tree then evaluating it, e.g:

var expr = JS.expression("pow(2,2) + pow(4,2)");
expr.Evaluate(); //= 20

When needing to evaluate the same expression multiple times you can cache and execute the AST to save the cost of parsing the expression again.

DSL example

If implementing a DSL containing multiple expressions as done in many of the Block argument expressions you can instead use the ParseJsExpression() extension method to return a literal Span advanced to past the end of the expression with the parsed AST token returned in an out parameter.

This is what the Each block implementation uses to parse its argument expression which can contain a number of LINQ-like expressions:

var literal = "where c.Age == 27 take 1 + 2".AsSpan();
if (literal.StartsWith("where "))
{
    literal = literal.Advance("where ".Length);     // 'c.Age == 27 take 1 + 2'
    literal = literal.ParseJsExpression(out where); // ' take 1 + 2'
}
literal = literal.AdvancePastWhitespace();          // 'take 1 + 2'
if (literal.StartsWith("take "))
{
    literal = literal.Advance("take ".Length);      // '1 + 2'
    literal = literal.ParseJsExpression(out take);  // ''
}

Resulting in where populated with the c.Age == 27 BinaryExpression and take with the 1 + 2 BinaryExpression.

Immutable and Comparable

Unlike C#'s LINQ Expressions which can't be compared for equality, Template Expressions are both Immutable and Comparable which can be used in caches and compared to determine if 2 Expressions are equivalent, e.g:

var expr = new JsLogicalExpression(
    new JsBinaryExpression(new JsIdentifier("a"), JsGreaterThan.Operator, new JsLiteral(1)),
    JsAnd.Operator,
    new JsBinaryExpression(new JsIdentifier("b"), JsLessThan.Operator, new JsLiteral(2))
);

expr.Equals(JS.expression("a > 1 && b < 2"));  //= true

expr.Equals(new JsLogicalExpression(
    JS.expression("a > 1"), JsAnd.Operator, JS.expression("b < 2")
));                                            //= true

Showing Expressions whether created programmatically, entirely from strings or any combination of both can be compared for equality and evaluated in the same way:

var scope = JS.CreateScope(args:new Dictionary<string, object> {
    ["a"] = 2,
    ["b"] = 1
});

expr.Evaluate(scope) //= true