Introduction

#Script is a simple and elegant, highly-extensible, sandboxed, high-performance general-purpose, embeddable scripting language for .NET Core and .NET Framework. It's designed from the ground-up to be incrementally adoptable where its basic usage is simple enough for non-technical users to use whilst it progressively enables access to more power and functionality allowing it to scale up to support full server-rendering Web Server workloads and beyond. Its high-fidelity with JavaScript syntax allows it to use a common language for seamlessly integrating with client-side JavaScript Single Page App frameworks where its syntax is designed to be compatible with Vue filters.

Executing #Script in .NET

To render the pages we first create and initialize a ScriptContext

var context = new ScriptContext().Init();

The ScriptContext is the sandbox where all scripts are executed within, everything your script has access to and generates is maintained within the ScriptContext. Once initialize you can start using it to evaluate scripts which you can do with:

var output = context.RenderScript("Time is now: {{ now |> dateFormat('HH:mm:ss') }}");

Or if your script performs any async I/O, it can be evaluated asynchronously with:

var output = await context.RenderScriptAsync("Time is now: {{ now |> dateFormat('HH:mm:ss') }}");

Scripts only have access to script methods, blocks and arguments defined within its Context, which for an empty Context are the comprehensive suite of safe Default Scripts and HTML Scripts.

Rendering Script Pages

Behind the scenes this creates a dynamic page with your Script and uses the PageResult to render it to a string:

var context = new ScriptContext().Init();
var dynamicPage = context.OneTimePage("The time is now: {{ now |> dateFormat('HH:mm:ss') }}"); 
var output = new PageResult(dynamicPage).RenderScript();

//async
var output = await new PageResult(dynamicPage).RenderScriptAsync();

If you need the return value instead you can access it from:

var result = new PageResult(dynamicPage).EvaluateResult(out var returnValue)
    ? ScriptLanguage.UnwrapValue(returnValue)
    : null;

If your script source code doesn't change you can re-use dynamicPage which lets you re-evaluate your source code's cached AST.

Evaluating Scripts with return values

Sharp Scripts can be used to either render text as above, they can also have return values with the return method which can be accessed using the Evaluate() API:

var result = context.Evaluate("1 + 1 = {{ 1 + 1 |> return }}."); //= 2

The generic overload utilizes ServiceStack's powerful Auto Mapping utils to convert the return value into your preferred type, e.g:

double result = context.Evaluate<double>("1 + 1 = {{ return(1 + 1) }}."); //= 2.0
string result = context.Evaluate<string>("1 + 1 = {{ return(1 + 1) }}."); //= "2"

But can also be used for more powerful conversions like converting an Object Dictionary into your preferred POCO:

var result = context.Evaluate<Customer>("{{`select * from customer where id=@id` |> dbSingle({id}) |>return}}"
    , new ObjectDictionary {
        ["id"] = 1
    });

Usage in .NET

To evaluate #Script in .NET you'll first create the ScriptContext containing all functionality and features your Scripts have access to:

var context = new ScriptContext {
    Args = { ... },              // Global Arguments available to all Scripts, Pages, Partials, etc
    ScriptMethods = { ... },     // Additional Methods
    ScriptBlocks = { ... },      // Additional Script Blocks 
    FilterTransformers = { .. }, // Additional Stream Transformers
    PageFormats = { ... },       // Additional Text Document Formats
    Plugins = { ... },           // Encapsulated Features e.g. Markdown, Protected or ServiceStack Features

    ScanTypes = { ... },         // Auto register Methods, Blocks and Code Page Types
    ScanAssemblies = { ... },    // Auto register all Methods, Blocks and Code Page Types in Assembly
}.Init();

Where you can customize the pure sandboxed ScriptContext your Script is executed within by extending it with:

Scripting .NET Types

To find out how to enable unrestricted access to existing .NET Types see Scripting .NET Types.

Autowired using ScriptContext IOC

ScanTypes is useful for Autowiring instances of scripts created using ScriptContext's configured IOC where they're also injected with any registered IOC dependencies and can be used to autowire ScriptMethods, ScriptBlocks and Code Pages:

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, ScriptBlocks and CodePages collections.

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.

Multi page Scripts

_layout.html
page.html

Typically you'll want to use #Script to render entire pages which are sourced from its configured Virtual File System which uses an In Memory Virtual File System by default that we can programmatically populate:

context.VirtualFiles.WriteFile("_layout.html", "I am the Layout: <b>{{ page }}</b>");
context.VirtualFiles.WriteFile("page.html", "I am the Page");

Pages are rendered using a PageResult essentially a rendering context that needs to be provided the Page to render:

var pageResult = new PageResult(context.GetPage("page"));

The script page output can then be asynchronously rendered to any Stream:

await pageResult.WriteToAsync(responseStream);

Or to access the output as a string you can use the convenience extension method:

string output = await pageResult.RenderToStringAsync();

All I/O within #Script is non-blocking, but if you're evaluating an adhoc script or using the default In Memory Virtual FileSystem there's no I/O so you can safely block to get the generated output with:

string output = pageResult.Result;

Both APIs returns the result you see in the Live Example above.

Cascading Resolution

There's no forced special centralized folders like /Views or /Views/Shared required to store layouts or share partials or artificial "Areas" concept to isolate website sections. Different websites or sections are intuitively grouped into different folders and #Script automatically resolves the closest layout it finds for each page. Cascading resolution also applies to including files or partial pages where you can use just its name to resolve the closest one, or an absolute path from the WebRootPath to include a specific partial or file from a different folder.

Instant Startup

There's no pre-compilation, pre-loading or Startup penalty, all Pages are lazily loaded on first use and cached for fast subsequent evaluation. Its instant Startup, fast runtime performance and sandboxed isolation opens it up to a myriad of new use-cases which can enhance .NET Apps with a rich Live programming experience.

Fast Runtime Performance

#Script is fast, parsing is done using StringSegment for minimal GC pressure, all I/O is non-blocking inc. async writes to OutputStream's. There's no buffering: Layouts, Pages and Partials are asynchronously written to a forward only stream. There's no runtime reflection, each filter or binding within #Script expressions executes compiled and cached C# Expressions.

Pure, Functional and Reactive

#Script is pure at both the library-level where they're a clean library with no external dependencies outside ServiceStack.Common, no coupling to web frameworks, external configuration files, designer tooling, build tools, pre-compilation steps or require any special deployment requirements. It binds to simple, clean, small interfaces for its Virtual File System, IOC and AppSettings providers which are easily overridden to integrate cleanly into external web frameworks.

It's also pure at the code-level where it doesn't have any coupling to concrete dependencies, components or static classes. Default script methods don't mutate any external state and return an expression. This forces a more readable and declarative programming-style, one where it is easier to quickly determine the subject of expressions and the states that need to be met for methods to be executed. Conceptually scripts are "evaluated" in that they take in arguments, methods and script pages as inputs and evaluates them to an output stream. They're highly testable by design where the same environment used to create the context can easily be re-created in Unit tests, including simulating pages in a File System using its In Memory Virtual File System.

Optimal Development Experience

The above attributes enables an instant iterative development workflow with a Live Development experience that supports configuration-free Hot Reloading out of the box that lets you build entire Web Apps and Web APIs without ever having to compile or manually Refresh pages.

Simplicity end-to-end

There are 3 main concepts in #Script: Arguments - variables which can be made available through a number of cascading sources, Script Blocks which define the statements available and Script Methods - public C# methods that exist in the list of ScriptMethods registered in the PageResult or ScriptContext that scripts are executed within.

Layouts, Pages and Partials are all just "pages", evaluated in the same way with access to arguments and filters. Parameters passed to partials are just scoped arguments, accessed like any other arguments. Typically pages are sourced from the configured File System but when access to more advanced functionality is required they can also be Code Pages implemented in pure C# that are otherwise interchangeable and can be used anywhere a normal page is requested.

There's no language constructs or reserved words in #Script itself, all functionality is implemented inside script methods. There's also nothing special about the Default Scripts included with #Script other than they're pre-registered by default. External script methods can do anything built-in methods can do which can just as easily be shadowed or removed giving you a clean slate where you're free to define your own language and preferred language naming scheme.

They're non-invasive, by default #Script Pages is configured to handle .html files but can be configured to handle any html extension or text file format. When a page doesn't contain any #Script Expressions it's contents are returned as-is, making it easy to adopt in existing static websites.

#Script scripts are sandboxed, they can't call static or instance methods, invoke setters or access anything outside of the arguments, methods and blocks made available to them. Without methods or blocks, expressions wouldn't have any methods they can call, leaving them with the only thing they can do which is access arguments and replace their variable placeholders, including the {{ page }} placeholder to tell the Layout where to render the page.

High-level, Declarative and Intent-based

High-level APIs are usually synonymous with being slow by virtue of paying a penalty for their high-level abstraction, but in the domain of I/O and Streams such-as rendering text to Streams they make it trivial to compose high-level functionality that's implemented more efficiently than would be typically written in C# / ASP.NET MVC Apps.

As an example let's analyze the script below:

{{ 'select url from quote where id= @id' |> dbScalar({ id }) |> urlContents |> markdown |> to => quote }}

{{ quote |> replace('Razor', 'Templates') |> replace('2010', now.Year) |> raw }}

The intent of #Script code should be clear even if it's the first time reading it. From left-to-right we can deduce that it retrieves a url from the quote table, downloads its contents of and converts it to markdown before replacing the text "Razor" and "2010" and displaying the raw non-HTML encoded html output.

Implementation using ASP.NET MVC

In MVC the typical and easiest approach would be to create a an MVC Controller Action, use EF to make a sync call to access the database, a sync call with a new HTTP Client to download the content which is buffered inside the Controller action before returned inside a View Model that is handed off to MVC to execute the View inside the default Layout.

Implementation using #Script

What does #Script do? lets step through the first filter:

What filter implementation gets called depends on which DB Scripts is registered, if your RDBMS supports ADO.NET's async API you can register DbScriptsAsync to execute all queries asynchronously, otherwise if using an RDBMS whose ADO.NET Provider doesn't support async you can register the DbScripts to execute each DB request synchronously without paying for any pseudo-async overhead, in each case the exact same code executes the most optimal ADO.NET APIs. #Script also benefits from using the much faster OrmLite and also saves on the abstraction cost from generating a parameterized SQL Statement from a Typed Expression-based API.

Arguments chain

The next question becomes what is id bound to? Similar to JavaScript's prototype chain it resolves the closest argument in its Arguments hierarchy, e.g. when evaluated as a view page it could be set by arguments in [paged based routes](/docs/sharp-pages#page-based-routing) or when the same page is evaluated as a partial it could be set from the scoped arguments it was called with.

Async I/O

The url returned from the db is then passed to the urlContents filter which if it was the last filter in the expression avoids any buffering by asynchronously streaming the url content stream directly to the forward-only HTTP Response Stream:

{{ url |> urlContents }}

urlContents is a Block Method which instead of returning a value writes its response to the OutputStream it's given. But then how could we convert it to Markdown if it's already written to the Response Stream? #Script analyzes the Expression's AST to determine if there's any filters remaining, if there is it gives the urlContents Block filter a MemoryStream to write to, then forwards its buffered output to the next filter. Since they don't return values, the only thing that can come after a Block Filter are other Block filters or Stream Transformers. markdown is one such Filter Transformer which takes in a stream of Markdown and returns a Stream of HTML.

{{ url |> urlContents |> markdown }}
Same intent, different implementations

The assignTo filter is used to set a value in the current scope. After Block Filters, a different assignTo Block Filter is used with the same name and purpose but a different implementation that reads all the contents of the stream into a UTF-8 string and sets a value in the current scope before returning an empty Stream so nothing is written to the Response.

{{ url |> urlContents |> markdown |> to => quote }}

Once the streamed output is captured and assigned it goes back into becoming a normal argument that opens it up to be able to use all filters again, which is how we're able to use the string replace filters before rendering the final result :)

/examples/qotd?id=1

Using the most efficient implementation allowable

So whilst it conceptually looks like each filter is transforming large buffered strings inside every filter, the expression is inspected to utilize the most efficient implementation allowable. At the same time filters are not statically bound to any implementation so you could for instance insert a Custom Filter before the Default Filters containing the same name and arguments count to have #Script execute your custom script methods instead, all whilst the script source code and intent remains untouched.

Intent-based code is easier to augment

If it was later discovered that some URLs were slow or rate-limited and you needed to introduce caching, your original C# code would require a fair amount of rework, in #Script you can simply add WithCache to call the urlContentsWithCache filter to return locally cached contents on subsequent requests.

{{ url |> urlContentsWithCache |> markdown }}

Simplified Language

As there's very little ceremony in #Script, a chain of filters looks like it's using its own DSL to accomplish each task and given implementing and registering custom filters is trivial you're encouraged to write the intent of your code first then implement any filters that are missing to realize its intent. Once you've captured the intent of what you want to do, it's less likely to ever need to change, focus is instead on fixing any bugs and making the filter implementations as efficient as possible, which benefits all existing code using the same filter.

To improve readability and make it more approachable, #Script aims to normalize the mechanics of the underlying implementation from the code's intent so you can use the same syntax to access an argument, e.g. {{ arg }} as you would a filter without arguments {{ now }} and just like JavaScript you can use obj.Property syntax to access both a public property on a Type or an entry in a Dictionary.

Late-bound flexibility

There's no static coupling to concrete classes, static methods or other filters, ambiguous method exceptions or namespace collisions. Each filter is self-contained and can easily be shared and dropped into any Web App by registering them in a list. Inspired by the power and flexibility in Smalltalk and LISP, filters are late-bound at run-time to the first matching filter in the user-defined list of ScriptMethods. This ability to shadow filters enables high-level intent-based APIs decoupled from implementations which sendToAutoQuery leverages to automatically route filter invocations to the appropriate implementation depending of if it's an AutoQuery RDBMS or an AutoQuery Data request, masking their implementations as a transparent detail. This flexibility also makes it easy create proxies, intercept and inject behavior like logging or profiling without modifying existing script method implementations or script source code.

made with by ServiceStack