Scripting .NET

The recommended way to add functionality to your scripts is by extending your ScriptContext with arguments, scripts, blocks and transformers - encapsulating it in a well-defined scope akin to Vue Components where all functionality is clearly defined in a componentized design that promotes decoupling and reuse of testable components where the same environment can be easily simulated and re-created to allow reuse of scripts in multiple contexts.

When scripting .NET Types or C# delegates directly it adds coupling to those Types making it harder to substitute and potentially limits reuse where if the Types were defined in a Host project, it can prevent reuse in different Apps or Test projects, and Types defined in .NET Framework-only or .NET Core-only dll's can prohibit platform portability

Although being able to Script .NET Types directly gives your Scripts greater capabilities and opens it up to a lot more use-cases that's especially useful in predominantly #Script-heavy contexts like Sharp Apps and Shell Scripts giving them maximum power that would otherwise require the usage of Plugins.

We can visualize the different scriptability options from the diagram below where by default scripts are limited to functionality defined within their ScriptContext, whether to limit access to specific Types and Assemblies or whether to lift the escape hatch and allow scripting of any .NET Types.

Scripting of .NET Types is only available to Script's executed within trusted contexts like #Script Pages, Sharp Apps and Shell Scripts that are registered with Protected Scripts. The different ways to allow scripting of .NET Types include:

Script Assemblies and Types

Using ScriptTypes to limit scriptability to only specific Types:

var context = new ScriptContext {
    ScriptMethods = {
        new ProtectedScripts()
    },
    ScriptTypes = {
        typeof(MyType),
        typeof(MyType2),
    }
}.Init();

Or you can use ScriptAssemblies to allow scripting of all Types within an Assembly:

var context = new ScriptContext {
    ScriptMethods = {
        new ProtectedScripts()
    },
    ScriptAssemblies = {
        typeof(MyType).Assembly,
    }
}.Init();

AllowScriptingOfAllTypes

To give your Scripts maximum accessibility where they're able to pierce the well-defined ScriptContext sandbox, you can set AllowScriptingOfAllTypes to allow scripting of all .NET Types available in loaded assemblies:

var context = new ScriptContext {
    ScriptMethods = {
        new ProtectedScripts()
    },
    AllowScriptingOfAllTypes = true,
    ScriptNamespaces = {
        typeof(MyType).Namespace,
    }
}.Init();

ScriptNamespaces is used to include additional Lookup namespaces for resolving Types akin to C# using statements.

Using AllowScriptingOfAllTypes also allows access to both public and non-public Types.

Scripting .NET APIs

The following Protected Scripts are all that's needed to create new instances, call methods and populate instances of .NET Types, including generic types and generic methods.

// Resolve Types
Type typeof(string typeName);

// Call Methods
object call(object instance, string name);
object call(object instance, string name, List<object> args);
Delegate Function(string qualifiedMethodName);                    // alias F(string)

// Create Instances
object new(string typeName);
object new(string typeName, List<object> constructorArgs);
object createInstance(Type type);
object createInstance(Type type, List<object> constructorArgs);
ObjectActivator Constructor(string qualifiedConstructorName);     // alias C(string)

// Populate Instance
object set(object instance, Dictionary<string, object> args);

Note: only a Type's public members can be accessed in #Script

Type Resolution

If you've registered Types using either ScriptTypes or ScriptAssemblies than you'll be able to reference the Type using just the Type Name, unless multiple Types of the same name are registered in which case the typeof() will return the first Type registered, all other subsequent Types with the same Name will need to be referenced with their Full Name.

typeof('MyType')
typeof('My.Namespace.MyType')

When AllowScriptingOfAllTypes=true is enabled, you can use ScriptNamespaces to add Lookup Namespaces for resolving Types, which for #Script Pages, Sharp Apps and Sharp Scripts are pre-configured with:

var context = new ScriptContext {
    //...
    ScriptNamespaces = {
        "System",
        "System.Collections.Generic",
        "ServiceStack",
    }
}.Init();

All other Types (other than .NET built-in types) not registered in ScriptTypes, ScriptAssemblies or have their namespace defined in ScriptNamespaces will need to be referenced using their Full Type Name. This same Type resolution applies for all references of Types in #Script.

Examples Configuration

The examples below assumes a ScriptContext configured with:

var context = new ScriptContext {
    ScriptMethods = { new ProtectedScripts() },
    AllowScriptingOfAllTypes = true,
    ScriptNamespaces = {
        "System",
        "System.Collections.Generic",
    },
    ScriptTypes = {
        typeof(Ints),
        typeof(Adder),
        typeof(StaticLog),
        typeof(InstanceLog),
        typeof(GenericStaticLog<>),
    },
}.Init();

With the types for the above classes defined in ScriptTypes.cs. This is the definition of the Adder class that's referenced frequently in the examples below:

public class Adder
{
    public string String { get; set; }
    public double Double { get; set; }

    public Adder(string str) => String = str;
    public Adder(double num) => Double = num;

    public string Add(string str) => String += str;
    public double Add(double num) => Double += num;

    public override string ToString() => String != null ? $"string: {String}" : $"double: {Double}";
}

Creating Instances

There are 3 different APIs for creating instances of Types:

  • new - create instances from Type name with specified List of arguments
  • createInstance - create instance of Type with specified List of arguments
  • Constructor - create a Constructor delegate that can create instances via method invocation

Built-in .NET Types and Types in ScriptTypes, ScriptAssemblies or ScriptNamespaces can be created using their Type Name, including generic Types:

'int'.new()
'DateTime'.new()
'Dictionary<string,DateTime>'.new()

Otherwise new instances of Types can be created using their full Type Name:

'System.Int32'.new()
'System.Text.StringBuilder'.new()

A list of arguments can be passed to the new method to call the constructor with the specified arguments:

'Ints'.new([1,2])
'Adder'.new([1.0])
'KeyValuePair<string,int>'.new(['A',1])

Constructor Resolution

#Script will use the constructor that matches the same number of specified arguments, when needed it uses ServiceStack's Auto Mapping to convert instances when their Types don't match, e.g:

'Ints'.new([1.0,2.0])
'KeyValuePair<char,double>'.new(['A',1])

However if there are multiple constructors with the same number of arguments, it will only use the constructor where all its argument Types match with the supplied arguments. Attempting to create an instance of the Adder class which only has constructors for string or double will fail with an Ambiguous Match Exception when trying to create it with an int:

'Adder'.new([1])  // FAIL: Ambiguous Constructor

In this case you'll need to convert the arguments so its Types matches one of the available constructors:

'Adder'.new([1.0])
'Adder'.new([intArg.toDouble()])
'Adder'.new(['A'])
'Adder'.new([`${instance}`]) // or 'Adder'.new([instance.toString()]) 

Constructor function

Alternatively you can use the Constructor method to specify the constructor you want by specifying the argument types of the constructor you want to use, which will return a delegate that lets you call a method to create instances using that Type's constructor:

Constructor('Adder(double)') |> to => doubleAdder
Constructor('Adder(string)') |> to => stringAdder

In this case you will be able to create instances of Adder using an int argument as the built-in automapping will convert it to the Argument Type of the Constructor you've chosen:

doubleAdder(1)
stringAdder(1)

// equivalent to:
Constructor('Adder(double)')(1)
Constructor('Adder(string)')(1)

As the Constructor Function returns a delegate you will be able to invoke it like a normal method where it can also be invoked as an extension method or inside a filter expression:

Constructor('Uri(string)') |> to => url

url('http://example.org')
'http://example.org'.url()
'http://example.org' |> url

// equivalent to:
'Uri'.new(['http://example.org'])
Constructor('Uri(string)')('http://example.org')

C() alias

To reduce syntax noise when needing to create a lot of constructors you can use the much shorter alias C instead of Constructor:

C('Uri(string)') |> to => url
C('Adder(double)')(1)

createInstance

The createInstance is like new except it's used to create instances from a Type instead of its string Type Name:

typeof('Ints').createInstance([1,2])
typeof('Adder').createInstance([1.0])
typeof('KeyValuePair<string,int>').createInstance(['A',1])

set

Once you've created instance you can further populate it using the set method which will let you populate public properties with a JS Object literal, performing any auto-mapping conversions as needed:

'Ints'.new([1,2]).set({ C:3, D:4.0 })
Constructor('Ints(int,int)')(1,2).set({ C:3, D:4.0 })

As set returns the instance, it can be used within a chained expression:

instance.set({ C:3 }).set({ D:4.0 }).call('GetTotal')

Calling Methods

Use the call and Function APIs to call methods on .NET Types:

  • call - invoke a method on an instance
  • Function - create a Function delegate that can invoke methods via normal method invocation

call

In its most simplest form you can invoke an instance method that doesn't have any arguments using just its name:

'Ints'.new([1,2]) |> to => ints
ints.call('GetMethod')

Any arguments can be specified in an arguments list:

'Adder'.new([1.0,2.0]) |> to => adder3
adder3.call('Add',[3.0]) //= 6.0

Method Resolution

The same Resolution rules in Constructor Resolution also applies when calling methods where any ambiguous methods needs to be called with arguments containing the exact types (as above), or you can specify the argument types of the method you want to call, in which case it will let you use the built-in Auto Mapping to call a method expecting a double with an int argument:

adder3.call('Add(double)',[3])

Generic Methods

You can call generic methods by specifying the Generic Type in the method name:

'Ints'.new([1,2]).call('GenericMethod<string>',['A'])

call only invokes instance methods, to call static methods you'll need to use Function.

Function

Function is a universal accessor for .NET Types where it can create a cached delegate to access Instance, Static and Generic Static Types - Including Nested Types (aka Inner Classes), Instance, Static and Generic Methods of those Types as well as their Instance and Static Properties, Fields and Constants.

As a simple example we'll use Function to create a delegate to call .NET's System.Console.WriteLine(string) static method:

Function('Console.WriteLine(string)') |> to => writeln

Which lets you call it like a regular Script method:

writeln('A')
'A'.writeln()
Function('Console.WriteLine(string)')('A')

All Examples below uses classes defined in ScriptTypes.cs.

Instance Methods

Function create delegates that lets you genericize the different types of method invocations in .NET, including instance methods, generic methods and void Action methods on an instance:

'InstanceLog'.new(['A']) |> to => o
Function('InstanceLog.Log') |> to => log              // instance void method
Function('InstanceLog.AllLogs') |> to => allLogs      // instance method
Function('InstanceLog.Log<int>') |> to => genericLog  // instance generic method

o.log('B')
log(o,'C')
o.genericLog(1)
o |> genericLog(2)    
o.allLogs() |> to => snapshotLogs

Static Type Methods

As well as calling static methods and static void Action methods on a static Type:

Function('StaticLog.Clear')()
Function('StaticLog.Log') |> to => log                // static void method
Function('StaticLog.AllLogs') |> to => allLogs        // static method
Function('StaticLog.Log<int>') |> to => genericLog    // static generic method

log('A')
'B'.log()
genericLog('C')
allLogs() |> to => snapshotLogs

Generic Static Type Methods

Including calling generic static methods on a generic static Type:

Function('GenericStaticLog<string>.Clear()')()
Function('GenericStaticLog<string>.Log(string)') |> to => log      // generic type static void method
Function('GenericStaticLog<string>.AllLogs') |> to => allLogs      // generic type static method
Function('GenericStaticLog<string>.Log<int>') |> to => genericLog  // generic type generic static method

log('A')
'B'.log()
genericLog('C')
allLogs() |> to => snapshotLogs

F() alias

You can use the shorter F() alias to reduce syntax noise when writing #Script that heavily interops directly with .NET Classes.

Instance and Static Properties, Fields and Constants

In addition to being able to create Delegates that genericize access to .NET Methods, it can also be used to create a delegate for accessing Instance and Static Properties, Fields and Constants including members of Inner Classes, e.g:

Each of the members of the following Type definition:

public class StaticLog
{
    public static string Prop { get; } = "StaticLog.Prop";
    public static string Field = "StaticLog.Field";
    public const string Const = "StaticLog.Const";

    public string InstanceProp { get; } = "StaticLog.InstanceProp";
    public string InstanceField = "StaticLog.InstanceField";

    public class Inner1
    {
        public static string Prop1 { get; } = "StaticLog.Inner1.Prop1";
        public static string Field1 = "StaticLog.Inner1.Field1";
        public const string Const1 = "StaticLog.Inner1.Const1";

        public string InstanceProp1 { get; } = "StaticLog.Inner1.InstanceProp1";
        public string InstanceField1 = "StaticLog.Inner1.InstanceField1";

        public static class Inner2
        {
            public static string Prop2 { get; } = "StaticLog.Inner1.Inner2.Prop2";
            public static string Field2 = "StaticLog.Inner1.Inner2.Field2";
            public const string Const2 = "StaticLog.Inner1.Inner2.Const2";
        }
    }
}

Can be accessed the same way, where you can use Function to create a zero-argument delegate for static members that can be immediately invoked, or a 1 argument Delegate for instance members.

Examples below uses Function's shorter F() alias:

F('StaticLog.Prop')()
F('StaticLog.Field')()
F('StaticLog.Const')()

F('StaticLog.Inner1.Prop1')()
F('StaticLog.Inner1.Field1')()
F('StaticLog.Inner1.Const1')()

F('StaticLog.Inner1.Inner2.Prop2')()
F('StaticLog.Inner1.Inner2.Field2')()
F('StaticLog.Inner1.Inner2.Const2')()

'StaticLog'.new() |> to => o
F('StaticLog.InstanceProp')(o)
F('StaticLog.InstanceField')(o)

'StaticLog.Inner1'.new() |> to => o
F('StaticLog.Inner1.InstanceProp1')(o)
F('StaticLog.Inner1.InstanceField1')(o)

made with by ServiceStack