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 Sharp 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.

Shadowing Methods

You can easily override default script methods with the same name and arguments by inserting them at the start of the ScriptMethods list:

new ScriptContext {
    InsertScriptMethods = { new MyScriptMethods() }
}.Init();
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();

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.EvaluateScript("<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:

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.EvaluateScript("<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:

context.ScriptMethods.Insert(0, new MyScriptMethods());

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 | assignTo: contents }}

made with by ServiceStack