Script Methods

#Script scripts are sandboxed, they can't call methods on objects nor do they have any access to any static functions built into the .NET Framework, so just as Arguments define all data and objects available, script methods define all functionality available to scripts.

The only methods registered by default are the Default Scripts containing a comprehensive suite of scripts useful within #Script Pages or custom Scripting environments and HTML Scripts. There's nothing special about these script methods other than they're pre-registered by default, your scripts have access to the same APIs and functionality and can do anything that built-in scripts can do.

What are Script Methods?

Script methods are just C# public instance methods from a class that inherits from ScriptMethods, e.g:

class MyScriptMethods : ScriptMethods
{
    public string echo(string text) => $"{text} {text}";
    public double squared(double value) => value * value;
    public string greetArg(string key) => $"Hello {Context.Args[key]}";
            
    public ICacheClient Cache { get; set; } //injected dependency
    public string fromCache(string key) => Cache.Get(key);
}

Registering Methods

The examples below show the number of different ways scripts can be registered:

Add them to the ScriptContext.ScriptMethods

Methods can be registered by adding them to the context.ScriptMethods collection directly:

var context = new ScriptContext
{
    Args =
    {
        ["contextArg"] = "foo"
    },
    ScriptMethods = { new MyScriptMethods() }
}.Init();

That can now be called with:

var output = context.RenderScript("<p>{{ 'contextArg' |> greetArg }}</p>");

This also shows that Scripts are initialized and have access to the ScriptContext through the Context property.

Add them to PageResult.ScriptMethods

If you only want to use a custom script in a single Page, it can be registered on the PageResult that renders it instead:

var output = new PageResult(context.OneTimePage("<p>{{ 'hello' |> echo }}</p>"))
{
    ScriptMethods = { new MyScriptMethods() }
}.Result;
Autowired using ScriptContext IOC

Autowired instances of scripts can also be created using ScriptContext's configured IOC where they're also injected with any registered IOC dependencies. To utilize this you need to specify the Type of the ScriptMethods that should be Autowired by either adding it to the ScanTypes collection:

class MyScriptMethods : ScriptMethods
{
    public ICacheClient Cache { get; set; } //injected dependency
    public string fromCache(string key) => Cache.Get(key);
}

var context = new ScriptContext
{
    ScanTypes = { typeof(MyScriptMethods) }
};
context.Container.AddSingleton<ICacheClient>(() => new MemoryCacheClient());
context.Container.Resolve<ICacheClient>().Set("key", "foo");
context.Init();

var output = context.RenderScript("<p>{{ 'key' |> fromCache }}</p>");

When the ScriptContext is initialized it will go through each Type and create an autowired instance of each Type and register them in the ScriptMethods collection. An alternative to registering a single Type is to register an entire Assembly, e.g:

var context = new ScriptContext
{
    ScanAssemblies = { typeof(MyScriptMethods).Assembly }
};

Where it will search each Type in the Assembly for Script Methods and automatically register them.

Method Resolution

#Script will use the first matching method with the same name and argument count it can find by searching through all registered methods in the ScriptMethods collection, so you could override default methods with the same name by inserting your ScriptMethods as the first item in the collection, e.g:

Shadowing Methods
new ScriptContext {
    InsertScriptMethods = { new MyScriptMethods() }
}.Init();

Delegate Arguments

In addition to Script Methods, #Script also lets you call delegates registered as arguments:

Func<int, int, int> add = (a, b) => a + b;

var context = new ScriptContext {
    Args = {
        ["fn"] = add
    }
}.Init();

Which just like user-defined functions and other Script Methods can be called positionally or as an extension method:

fn(1,2)
1.fn(2)

Removing Default Scripts

Or if you want to start from a clean slate, the default scripts can be removed by clearing the collection:

context.ScriptMethods.Clear();

Auto coercion into Method argument Types

A unique feature of script methods is that each of their arguments are automatically coerced into the script method argument Type using the powerful conversion facilities built into ServiceStack's Auto Mapping Utils and Text Serializers which can deserialize most of .NET's primitive Types like DateTime, TimeSpan, Enums, etc in/out of strings as well being able to convert a Collection into other Collection Types and any Numeric Type into any other Numeric Type which is how, despite only accepting doubles:

double squared(double value) => value * value;

squared can also be used with any other .NET Numeric Type, e.g: byte, int, long, decimal, etc. The consequence to this is that there's no method overloading in script methods which are matched based on their name and their number of arguments and each argument is automatically converted into its script method Param Type before it's called.

Context Script Methods

Script Methods can also get access to the current scope by defining a ScriptScopeContext as it's first parameter which can be used to access arguments in the current scope or add new ones as done by the assignTo method:

public object assignTo(TemplateScopeContext scope, object value, string argName) //from filter
{
    scope.ScopedParams[argName] = value;
    return IgnoreResult.Value;
}

Block Methods

Script Methods can also write directly into the OutputStream instead of being forced to return buffered output. A Block Method is declared by its Task return Type where instead of returning a value it instead writes directly to the ScriptScopeContext OutputStream as seem with the implementation of the includeFile protected scripts:

public async Task includeFile(ScriptScopeContext scope, string virtualPath)
{
    var file = scope.Context.VirtualFiles.GetFile(virtualPath);
    if (file == null)
        throw new FileNotFoundException($"includeFile '{virtualPath}' was not found");

    using (var reader = file.OpenRead())
    {
        await reader.CopyToAsync(scope.OutputStream);
    }
}
For maximum performance all default script methods which perform any I/O use Block Methods to write directly to the OutputStream and avoid any blocking I/O or buffering.

Block Methods ends the template expression

Block methods effectively end the filter chain expression since they don't return any value that can be injected into a normal method. The only thing that can come after a Block Method are other Block Methods or Filter Transformers. If any are defined, the output of the Block Method is buffered into a MemoryStream and passed into the next Block Method or Filter Transformer in the chain, its output is then passed into the next one in the chain if any, otherwise the last output is written to the OutputStream.

An example of using a Block method with a Filter Transformer is when you want include a markdown document and then convert it to HTML using the markdown Filter Transformer before writing its HTML output to the OutputStream:

{{ 'doc.md' |> includeFile |> markdown }}

Capture Block Method Output

You can also capture the output of a Block Method and assign it to a normal argument by using the assignTo Block Method:

{{ 'doc.md' |> includeFile |> to => contents }}

Async support

An example of #Script async support is covered in the Introduction page where await is implicitly called within an template expression when a script method returns a Task<object>.

The example shows that there's no difference in syntax with calling a script method with a sync or an async Task<object> result, regardless of whether OrmLite's synchronous or asynchronous Database Scripts are used:

{{ "select customerId, companyName, city, country from customer where country=@country" |> to => sql }}
{{ sql |> dbSelect({ country: 'UK' }) |> take(3) |> textDump }}

For comparisons here's the source code for dbSelect in both the sync DbScripts and async DbScriptsAsync:

DbScripts.cs

public object dbSelect(ScriptScopeContext scope, string sql, Dictionary<string, object> args) => 
    exec(db => db.SqlList<Dictionary<string, object>>(sql, args), scope, null);

DbScriptsAsync.cs

public Task<object> dbSelect(ScriptScopeContext scope, string sql, Dictionary<string, object> args) => 
    exec(db => db.SqlListAsync<Dictionary<string, object>>(sql, args), scope, null);

Where if DbScriptsAsync was used #Script will automatically await the result of the async dbSelect method before passing the result to the take script method.

Manually handling async results

Implicit awaiting async Task<object> or Task results only occurs within a template expression, outside of a Template Expression like within a Script Block Expression or template string literal you'll need to manually access the Task result.

When using DbScriptsAsync you'll need to either explicitly access the Task's .Result property or call the .sync() script method in order to access the sync result:

{{#if sql.dbSelect({ country: 'UK' }).Result.Count > 0 }}
    {{ `UK Orders: ${sql.dbSelect({ country: 'UK' }).Result.Count}` }}
{{/if}}

Although calling Result or .sync() will synchronously block the async Task result, to await the result instead call the async method within a template expression before using the awaited async result, e.g:

{{ sql |> dbSelect({ country: 'UK' }) |> to => ukCount }}
{{#if ukCount > 0 }}
    {{ `UK Orders: ${ukCount` }}
{{/if}}

Implementing Method Exceptions

In order for your own Method Exceptions to participate in the above Script Error Handling they'll need to be wrapped in an StopFilterExecutionException including both the Script's scope and an optional options object which is used to check if the assignError binding was provided so it can automatically populate it with the Exception.

The easiest way to Implement Exception handling in methods is to call a managed function which catches all Exceptions and throws them in a StopFilterExecutionException as seen in OrmLite's DbScripts:

T exec<T>(Func<IDbConnection, T> fn, ScriptScopeContext scope, object options)
{
    try
    {
        using (var db = DbFactory.Open())
        {
            return fn(db);
        }
    }
    catch (Exception ex)
    {
        throw new StopFilterExecutionException(scope, options, ex);
    }
}

public object dbSelect(ScriptScopeContext scope, string sql, Dictionary<string, object> args) => 
    exec(db => db.SqlList<Dictionary<string, object>>(sql, args), scope, null);

public object dbSelect(ScriptScopeContext scope, string sql, Dictionary<string, object> args, object op) => 
    exec(db => db.SqlList<Dictionary<string, object>>(sql, args), scope, op);


public object dbSingle(ScriptScopeContext scope, string sql, Dictionary<string, object> args) =>
    exec(db => db.Single<Dictionary<string, object>>(sql, args), scope, null);

public object dbSingle(ScriptScopeContext scope, string sql, Dictionary<string, object> args, object op) =>
    exec(db => db.Single<Dictionary<string, object>>(sql, args), scope, op);

The overloads are so the methods can be called without specifying any method options.

For more examples of different error handling features and strategies checkout: ErrorHandlingTests.cs

made with by ServiceStack