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)