Giter Club home page Giter Club logo

westwind.scripting's Introduction

Westwind CSharp Scripting

Dynamically compile and execute CSharp code at runtime

NuGet

This small library provides an easy way to compile and execute C# code from source code provided at runtime. It uses Roslyn to provide compilation services for string based code via the CSharpScriptExecution class and lightweight, self contained C# script templates via the ScriptParser class that can evaluate expressions and structured C# statements using Handlebars-like ({{ expression }} and {{% code }}) script templates.

Get it from Nuget:

Install-Package Westwind.Scripting

It supports the following targets:

  • .NET 8.0 (net8.0), .NET 6.0 (net6.0), .NET 7.0 (net7.0)
  • Full .NET Framework (net462)
  • .NET Standard 2.0 (netstandard2.0)

For more detailed information and a discussion of the concepts and code that runs this library, you can also check out this introductory blog post:

Features

  • Easy C# code compilation and execution for:
    • Code blocks (generic execution)
    • Full methods (method header/result value)
    • Expressions (evaluate expressions)
    • Full classes (compile and load)
  • Async execution support
  • Caching of already compiled code
  • Ability to compile entire classes and load, execute them
  • Error Handling
    • Intercept compilation and execution errors
    • Detailed compiler error messages
    • Access to compiled output w/ line numbers
  • Roslyn Warmup
  • Template Scripting Engine using Handlebars-like with C# syntax

CSharpScriptExecution: C# Runtime Compilation and Execution

Runtime code compilation and execution is handled via the CSharpScriptExecution class.

  • ExecuteCode() - Execute an arbitrary block of code. Pass parameters, return a value
  • Evaluate() - Evaluate a single expression from a code string and returns a value
  • ExecuteMethod() - Provide a complete method signature and call from code
  • CompileClass() - Generate a class instance from C# code

There are also async versions of the Execute and Evaluate methods:

  • ExecuteMethodAsync()
  • ExecuteCodeAsync()
  • EvaluateAsync()

All method also have additional generic return type overloads.

Additionally you can also compile self-contained classes:

  • CompileClass()
  • CompileClassToType()
  • CompileAssembly()

These CompileXXX() methods provide compilation only without execution and create an instance, type or assembly respectively. You can cache these in your application for later re-use and much faster execution.

Use these methods if you need to repeatedly execute the same code and when performance is important as using re-used cached instances is an order of magnitude faster than using the ExecuteXXX() methods repeatedly.

ScriptParser: C# Template Script Expansion

Script Templating using a Handlebars like syntax that can expand C# expressions and C# structured code in text templates that produce transformed text output, can be achieved using the ScriptParser class.

Methods:

  • ExecuteScript()
  • ExecuteScriptAsync()
  • ParseScriptToCode()

Script parser expansion syntax used is:

  • {{ expression }}
  • {{% openBlock }} other text or expressions or code blocks {{% endblock }}

Important: Large Runtime Dependencies on Roslyn Libraries

Please be aware that this library has a dependency on Microsoft.CodeAnalysis which contains the Roslyn compiler components used by this component. This dependency incurs a 10+mb runtime dependency and a host of support files that are added to your project output.

Quick Start Examples

To get you going quickly here are a few simple examples that demonstrate functionality. I recommend you read the more detailed instructions below but these examples give you a quick idea on how this library works.

Execute generic C# code with Parameters and Result Value

var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();

var code = $@"
// pick up and cast parameters
int num1 = (int) @0;   // same as parameters[0];
int num2 = (int) @1;   // same as parameters[1];

var result = $""{{num1}} + {{num2}} = {{(num1 + num2)}}"";

Console.WriteLine(result);  // just for kicks in a test

return result;
";

// should return a string: (`"10 + 20 = 30"`)
string result = script.ExecuteCode<string>(code,10,20);

if (script.Error) 
{
	Console.WriteLine($"Error: {script.ErrorMessage}");
	Console.WriteLine(script.GeneratedClassCodeWithLineNumbers);
	return
}	

Execute Async Code with a strongly typed Model

var script = new CSharpScriptExecution() {  SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();

// have to add references so compiler can resolve
script.AddAssembly(typeof(ScriptTest));
script.AddNamespace("Westwind.Scripting.Test");

var model = new ScriptTest() { Message = "Hello World " };

var code = @"
// To Demonstrate Async support
await Task.Delay(10); // test async

string result =  Model.Message +  "" "" + DateTime.Now.ToString();
return result;
";

// Use generic version to specify result and model types
string execResult = await script.ExecuteCodeAsync<string, ScriptTest>(code, model);

Note that you can forego the strongly typed model by using the non-generic ExecuteCodeAsync() or ExecuteCode() methods which use dynamic instead of the strong type. This allows the compiler to resolve Model without explicitly having to add the reference.

Evaluate a single expression

var script = new CSharpScriptExecution();
script.AddDefaultReferencesAndNamespaces();

// Numbered parameter syntax is easier
var result = script.Evaluate<decimal>("(decimal) @0 + (decimal) @1", 10M, 20M);

Assert.IsFalse(script.Error, script.ErrorMessage );

Template Script Parsing

var model = new TestModel {Name = "rick", DateTime = DateTime.Now.AddDays(-10)};

string script = @"
Hello World. Date is: {{ Model.DateTime.ToString(""d"") }}!
{{% for(int x=1; x<3; x++) {
}}
{{ x }}. Hello World {{Model.Name}}
{{% } }}

And we're done with this!
";

Console.WriteLine(script);


// Optional - build customized script engine
// so we can add custom

var scriptParser = new ScriptParser();

// add dependencies
scriptParser.AddAssembly(typeof(ScriptParserTests));
scriptParser.AddNamespace("Westwind.Scripting.Test");

// Execute
string result = scriptParser.ExecuteScript(script, model);

Console.WriteLine(result);

Console.WriteLine(scriptParser.ScriptEngine.GeneratedClassCodeWithLineNumbers);
Assert.IsNotNull(result, scriptParser.ScriptEngine.ErrorMessage);

which produces:

Hello World. Date is: 5/22/2022!

1. Hello World rick

2. Hello World rick

And we're done with this!

Usage

Using the CSharpScriptExecution class is very easy to use. It works with code passed as strings for either a block of code, an expression, one or more methods or even as a full C# class that can be turned into an instance.

There are methods for:

  • Generic code execution
  • Complete method execution
  • Expression Evaluation
  • In-memory class compilation and loading

Important: Large Roslyn Dependencies

If you choose to use this library, please realize that you will pull in a very large dependency on the Microsoft.CodeAnalysis Roslyn libraries which accounts for 10+mb of runtime files that have to be distributed with your application.

Setting up for Compilation: CSharpScriptExecution

Compiling code is easy - setting up for compilation and ensuring that all your dependencies are available is a little more complicated and also depends on whether you're using full .NET Framework or .NET Core or .NET Standard.

In order to compile code the compiler requires that all dependencies are referenced both for assemblies that need to be compiled against as well as any namespaces that you plan to access in your code and don't want to explicitly mention.

Adding Assemblies and Namespaces

There are a number of methods that help with this:

  • AddDefaultReferencesAndNamespaces()
  • AddLoadedReferences()
  • AddAssembly()
  • AddAssemblies()
  • AddNamespace()
  • AddNamespaces()

Initial setup for code execution then looks like this:

var script = new CSharpScriptExecution()
{
    SaveGeneratedCode = true  // useful for errors and trouble shooting
};

// load a default set of assemblies that provides common base class functionality
script.AddDefaultReferencesAndNamespaces();

// Add any additional dependencies
script.AddAssembly(typeof(MyApplication));       // by type
script.AddAssembly("Westwind.Utilitiies.dll");   // by assembly file

// Add additional namespaces you might use in your code snippets
script.AddNamespace("MyApplication");
script.AddNamespace("MyApplication.Utilities");
script.AddNamespace("Westwind.Utilities");
script.AddNamespace("Westwind.Utilities.Data");

Allowing Assemblies and Namespaces in Code

You can also add namespaces and - optionally - assembly references in code.

Namespaces

You can add valid namespace references in code by using the following syntax:

using Westwind.Utilities

var errors = StringUtils.GetLines(Model.Errors);

Namespaces are always parsed if present.

Assembly References

Assembly references are disabled by default as they are a potential security issue. But you can enable them via the AllowReferencesInCode property set to true.

Once enabled you can embed references and references in script code like this:

#r MarkdownMonster.exe
using MarkdownMonser

var title = mmApp.Configuration.ApplicationName;

The assembly is reference and any namespaces are moved to the top of the class and removed from the execution code.

Assemblies are searched for in the application folder and in the runtime folder.

#r only works with String Scripts/Classes

Reference and Usings parsing only works with string code inputs. If you need reference syntax make sure you convert your stream to string first.

#r only for classes, #r and using for Snippets and Methods

Class compilation parses only #r references. Method and Code execution - (ie. non-self contained code files) parse both #r and using.

Configuration Properties

The CSharpScriptExecution has only a few configuration options available:

  • SaveGeneratedCode
    If true captures the generated class code for the compilation that is used to execute your code. This will include the class and method wrappers around the code. You can use the GeneratedCode or GeneratedCodeWithLineNumbers properties to retrieve the code. The line numbers will match up with compilation errors return in the ErrorMessage so you can display an error message with compiler errors along with the code to optionally review the code in failure.

  • AllowReferencesInCode
    If true allows references to be added in script code via #r assembly.dll.

  • OutputAssembly
    You can optionally specify a filename to which the assembly is compiled. If this value is not set the assembly is generated in-memory which is the default.

  • GeneratedClassName
    By default the class generated from any of the code methods generates a random class name. You can override the class name so you can load any generated types explicitly. Generally it doesn't matter what the class name is as the dynamic methods find the single class generated in the assembly.

  • ThrowExceptions
    If true runtime errors will throw runtime execution exceptions rather than silently failing and setting error properties.
    The default is false and the recommended approach is to explicitly check for errors after compilation and execution, by checking Error, ErrorMessage and LastException properties which we highly recommend.

Note: Compiler errors don't throw - only runtime errors do. Compiler errors set properties of the object as do execution errors when ThrowExecptions = false.

Error Properties

When compilation errors occur the following error properties are set:

  • Error
    A simple boolean flag that lets you quickly check for an error.

  • ErrorMessage
    An error message string that shows any compilation errors along with line numbers into the generated code.

  • ErrorType
    Determines whether the error is Compilation or Runtime error.

  • GeneratedCode and GeneratedCodeWithLineNumbers
    If you receive Error Messages with line numbers it might be useful to have the source code that was generated to co-relate the error to. If true compiled source code is saved - otherwise this property is null.

Error Properties

CSharpScriptExecution has two error modes:

  • Compilation Errors
  • Runtime Errors

By default runtime errors are captured and forwarded into the error properties of this class. You can always check the Error property to determine if a script error occurred.

If you perfer you can set the ThrowExceptions property to true to throw on execution errors.

Executing Code

Let's start with the most generic execution functionality which is ExecuteCode() and ExecuteCodeAsync() which let you execute a block of code, optionally pass in parameters and return a result value.

The code you pass can use a object[] parameters array, to access any parameters you pass and can return a result value that you can pick up when executing the code. Note that you can also replace parameters[0] with @0 and parameters[1] with @1 and so on.

ExecuteCode()

The following is a simple example of a code snippet that performs a calculation by adding to values and returning a string:

var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();

var code = $@"
// pick up and cast parameters
int num1 = (int) @0;   // same as parameters[0];
int num2 = (int) @1;   // same as parameters[1];

var result = $""{{num1}} + {{num2}} = {{(num1 + num2)}}"";

Console.WriteLine(result);  // just for kicks in a test

return result;
";

// should return (`"10 + 20 = 30"`)
string result = script.ExecuteCode(code,10,20) as string;

Console.WriteLine($"Result: {result}");
Console.WriteLine($"Error: {script.Error}");
Console.WriteLine(script.ErrorMessage);
Console.WriteLine(script.GeneratedClassCodeWithLineNumbers);

Assert.IsFalse(script.Error, script.ErrorMessage);
Assert.IsTrue(result.Contains(" = 30"));

Note that the return in the code snippet is optional so you can omit it if you don't need to pass anything back.

This non-generic version returns a result of type object. You can use generic overloads to specify the result type as well as an optional single input model type.

Basic Error Handling

If an error occurs during compilation the error is handled and the Error and ErrorMessage properties are set. If a runtime error occurs the code fires an exception in your code. You can also access the generated source code that is actually executed using GeneratedClassCode or GeneratedClassCodeWithLineNumbers - if the SaveGeneratedCode property is true.

var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();

string result = null;
result = script.ExecuteCode(code,10,20) as string;

// compilation or runtime error
if (script.Error)   
{
	Console.WriteLine(script.ErrorMessage + " (" + script.ErrorType + ")");
    Console.WriteLine(script.GeneratedClassCodeWithLineNumbers);
}
else 
{
	Console.WriteLine($"Result: {result}");
}

ExecuteCodeAsync()

If your code snippet requires await calls or uses Task operations, you probably want to execute your code using async await functionality.

var script = new CSharpScriptExecution() {SaveGeneratedCode = true,};
script.AddDefaultReferencesAndNamespaces();

string code = @"
await Task.Run(async () => {
    {
        Console.WriteLine($""Time before: {DateTime.Now.ToString(""HH:mm:ss:fff"")}"");        
        await Task.Delay(20);
        Console.WriteLine($""Time after: {DateTime.Now.ToString(""HH:mm:ss:fff"")}"");        
    }
});

return $""Done at {DateTime.Now.ToString(""HH:mm:ss:fff"")}"";
";

string result = await script.ExecuteCodeAsync<string>(code, null);

if (script.Error)   // compile error
{
	Console.WriteLine(script.ErrorMessage);
    Console.WriteLine(script.GeneratedClassCodeWithLineNumbers);
    return;
}

// all good!
Console.WriteLine($"Result: {result}");

Note also in this code I'm using the generic ExecuteCodeAsync<TResult>() method which allows me to explicitly specify what type to return, to avoid the object conversion from the first sample.

From here on out I'm not going to show error handling in the samples except where relevant to keep samples brief

More Control with ExecuteMethod()

If you need more control over your code execution, rather than having a method created for execution you can provide a complete method as a string instead. The method can include a method header and return value. This allows you to exactly specify what types to pass as parameters, what types to return etc.

If your method has an async or Task or Task<T> signature you should likely use ExecuteMethodAsync() to call the method and await the call.

var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();

string code = $@"
public string HelloWorld(string name)
{{
	string result = $""Hello {{name}}. Time is: {{DateTime.Now}}."";
	return result;
}}";

string result = script.ExecuteMethod(code, "HelloWorld", "Rick") as string;

As you can see I'm providing a full method signature with signature header, body and a return value. Because I'm writing the method explicitly I can strongly type the method inputs and result values explicitly.

ExecuteMethodAsync()

The async version looks like this:

var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();

string code = $@"
public async Task<string> HelloWorldAsync(string name)
{{
	await Task.Delay(10);  // some async task
	string result = $""Hello {{name}}. Time is: {{DateTime.Now}}."";
	return result;
}}";

string result = await script.ExecuteMethodAsync<string>(code, "HelloWorldAsync", "Rick");

Evaluating an expression: EvaluateMethod()

If you want to evaluate a single expression, there's a shortcut Evalute() method that works pretty much the same:

var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();

// Numbered parameter syntax is easier
var result = script.Evaluate<decimal>("(decimal) @0 + (decimal) @1", 10M, 20M);

Console.WriteLine($"Result: {result}");  // 30
Console.WriteLine(script.ErrorMessage);

I'm using the generic version here, but there are overloads that return object more generically.

The async version works similar and allows you to evaluate expressions of methods or code that is async:

var script = new CSharpScriptExecution() {SaveGeneratedCode = true,};
script.AddDefaultReferencesAndNamespaces();

string code = $@"
await Task.Run( async ()=> {{
	await Task.Delay(1);
	return (decimal) @0 + (decimal) @1;
}})";

// Numbered parameter syntax is easier
var result = await script.EvaluateAsync<decimal>(code, 10M, 20M);

Console.WriteLine($"Result: {result}");  // 30
Console.WriteLine($"Error: {script.Error}");

Compiling and Executing Entire Classes

You can also generate an entire class, load it and then execute methods on it using the CompileClass() method. This method passes in a complete C# class as a string and returns back an instance of the class as a dynamic object.

var script = new CSharpScriptExecution() { SaveGeneratedCode = true };
script.AddDefaultReferencesAndNamespaces();

var code = $@"
using System;

namespace MyApp
{{
	public class Math
	{{
		public string Add(int num1, int num2)
		{{
			// string templates
			var result = num1 + "" + "" + num2 + "" = "" + (num1 + num2);
			Console.WriteLine(result);
		
			return result;
		}}
		
		public string Multiply(int num1, int num2)
		{{
			// string templates
			var result = $""{{num1}}  *  {{num2}} = {{ num1 * num2 }}"";
			Console.WriteLine(result);
			
			result = $""Take two: {{ result ?? ""No Result"" }}"";
			Console.WriteLine(result);
			
			return result;
		}}
	}}
}}";

// need dynamic since current app doesn't know about type
dynamic math = script.CompileClass(code);

Console.WriteLine(script.GeneratedClassCodeWithLineNumbers);
Assert.IsFalse(script.Error,script.ErrorMessage);
Assert.IsNotNull(math);

string addResult = math.Add(10, 20);
string multiResult = math.Multiply(3 , 7);

Assert.IsTrue(addResult.Contains(" = 30"));
Assert.IsTrue(multiResult.Contains(" = 21"));

// if you need access to the assembly or save it you can
var assembly = script.Assembly; 

Reusing Compiled Classes, Types and Assemblies for Better Performance

If you plan on repeatedly calling the same C# code, you want to avoid re-compiling or even reloading the code from string or even a cached assembly using the ExecuteXXX() methods. While these methods cache code after initial compilation, they still have to re-load the type to execute each time, and then execute using Reflection. Initial compilation is always very slow, but even cached code assembly and type loading has significant overhead, that is much slower than directly invoking code.

For multiple run code we recommend you use a lower level approach using the CompileXXX() methods to create an instance or type, and hang on to it in your application. Whenever you need to re-run the code you can then use the cached instance or type to execute your code. This removes assembly and type loading which add significant overhead.

Performance using these cached instances will be an order of magnitude faster than using ExecuteMethod() or ExecuteCode() (even with cached assemblies). Cached instances can simply make a dynamic or Reflection call to the relevant code without reloading or matching code to an assembly and type creation.

If speed is important this is the most efficient approach.

Template Script Execution: ScriptParser

Template script execution allows you to transform a block of text with embedded C# expressions to make the text dynamic by using the ScriptParserclass. It uses HandleBars like syntax with {{ }} expressions and {{% }} code statements that allow for structured operations like if blocks or for/while loops.

You can embed C# expressions and code blocks to expand dynamic content that is generated at runtime. This class works by taking a template and turning it into executing code that produces a string output result.

This class has two operational methods:

  • ExecuteScript()
    This is the highlevel execution method that you pass a template and a model to, and it processes the template, expanding the data and returns a string of the merged output.

  • ParseScriptToCode()
    This method takes a template and parses it into a block of C# code that can be executed to produce a string result of merged content. This is a lower level method that can be used to customize how the code is eventually executed. For example, you might want to combine multiple snippets into a single class via multiple methods rather than executing individually.

Templates expost two special variables:

  • Model
    Exposes the model passed when calling the ExecuteScript() method or variation. Access with {{ Model.Property }} or {{ Model.MethodToCall("parameter") }}.

  • Script
    Allows the ability to render partial templates from disk and embed them into the current page as well as potentially executing nested script code from rendered content. Call these methods with:

    • Script.RenderPartial()
    • Script.RenderPartialAsync()
    • Script.RenderScript() (allows content to run script code)
    • Script.RenderScriptAsync()

Automatic Script Processing with ScriptParser

The ExecuteScript() method is the all in one method that parses and executes the script and model passed to it.

Here's how this works:

var model = new TestModel {Name = "rick", DateTime = DateTime.Now.AddDays(-10)};

string script = @"
Hello World. Date is: {{ Model.DateTime.ToString(""d"") }}!
{{% for(int x=1; x<3; x++) {
}}
{{ x }}. Hello World {{Model.Name}}
{{% } }}

And we're done with this!
";

var scriptParser = new ScriptParser();

// add dependencies - sets on .ScriptEngine instance
scriptParser.AddAssembly(typeof(ScriptParserTests));
scriptParser.AddNamespace("Westwind.Scripting.Test");

// Execute the script
string result = scriptParser.ExecuteScript(script, model);

Console.WriteLine(result);

Console.WriteLine(scriptParser.ScriptEngine.GeneratedClassCodeWithLineNumbers);
Console.WriteLine(scriptParser.ErrorType);  // if there's an error
Assert.IsNotNull(result, scriptParser.ScriptEngine.ErrorMessage);

Notice that ScriptParser() mirrors most of the CSharpScriptExecution properties and methods. Behind the scenes there is a ScriptEngine property that holds the actual CSharpScriptExecution instance that will be used when the template is executed. You can optionally override the ScriptEngine instance although that should be rare.

Manual Parsing

If you want direct access to the parsed code you can use ParseScriptToCode() to parse a template into C# code and return it as a string. We can then manually execute the code or create a custom execution strategy such as combining multiple templates into a single class.

Here's the basic functionality to parse a template and then manually execute as a method:

var model = new TestModel {Name = "rick", DateTime = DateTime.Now.AddDays(-10)};

string script = @"
Hello World. Date is: {{ Model.DateTime.ToString(""d"") }}!
{{% for(int x=1; x<3; x++) {
}}
{{ x }}. Hello World {{Model.Name}}
{{% } }}

And we're done with this!
";


var scriptParser = new ScriptParser();

// Parse template into source code
var code = scriptParser.ParseScriptToCode(script);

Assert.IsNotNull(code, "Code should not be null or empty");

Console.WriteLine(code);

// ScriptEngine is a pre-configured CSharpScriptExecution instance
scriptParser.AddAssembly(typeof(ScriptParserTests));
scriptParser.AddNamespace("Westwind.Scripting.Test");

var method = @"public string HelloWorldScript(TestModel Model) { " +
             code + "}";

// Execute using the internal CSharpScriptExecution instance
var result = scriptParser.ScriptEngine.ExecuteMethod(method, "HelloWorldScript", model);

Console.WriteLine(scriptParser.GeneratedClassCodeWithLineNumbers);
Assert.IsNotNull(result, scriptParser.ErrorMessage);

Console.WriteLine(result);

This is a bit contrived since this in effect does the same thing that ExecuteScript() does implicitly. However, it can be useful to retrieve the code and use in other situations, such as building a class with several generated template methods rather than compiling and running each template in it's own dedicated assembly.

Not a very common use case but it's there if you need it.

Nested Script Code

In addition to direct template rendering you can also embed nested template content into the page using the Script object which exposed as an in-scope variable. You can use the script object to execute a template from disk or provide a string expression as a template to dynamically execute script in application provided code.

RenderPartial

RenderPartial lets you render an external template from disk by specifying a path to the file.

var model = new TestModel { Name = "rick", DateTime = DateTime.Now.AddDays(-10) };
string script = """
<div>
Hello World. Date is: {{ DateTime.Now.ToString() }}

{{ await Script.RenderPartialAsync("./Templates/Time_Partial.csscript") }}
Done.
</div>
""";
Console.WriteLine(script + "\n---" );


var scriptParser = new ScriptParser();
scriptParser.AddAssembly(typeof(ScriptParserTests));


string result = await scriptParser.ExecuteScriptAsync(script, model);
Console.WriteLine(result);
Console.WriteLine(scriptParser.GeneratedClassCodeWithLineNumbers);

Assert.IsNotNull(result, scriptParser.ErrorMessage);

The template is just a file with text and script expressions embedded:

Current Time: <b>{{ DateTime.Now.ToString("HH:mm:ss") }}</b>

and if loaded will be rendered in place of the RenderPartial call.

When using ExecuteScript() and its varients, you can also pass in a basePath parameter which allows you specify a root path to resolve via these leading characters:

  • ~
  • /
  • \

When these start off the passed in path and a basePath is provided the root path is replaced in place of these values (ie. ~/sub1/page.csscript becomes \temp\templates\sub1\page.csscript).

You need to be consistent with your use of directory slashes using forward or backwards slashes but not both in the base path and template paths or you may run into invalid path issues.

RenderScript

var model = new TestModel { 
    Name = "rick", 
    Expression="Time: {{ DateTime.Now.ToString(\"HH:mm:ss\") }}" 
};

string script = """
<div>
Hello World. Date is: {{ DateTime.Now.ToString() }}
<b>{{ Model.Name }}</b>

{{ await Script.RenderScriptAsync(Model.Expression,null) }}

Done.
</div>
""";
Console.WriteLine(script + "\n---");


var scriptParser = new ScriptParser();
scriptParser.AddAssembly(typeof(ScriptParserTests));


string result = await scriptParser.ExecuteScriptAsync(script, model);

Console.WriteLine(result);
Console.WriteLine(scriptParser.Error + " " + scriptParser.ErrorType + " " + scriptParser.ErrorMessage + " " );
Console.WriteLine(scriptParser.GeneratedClassCodeWithLineNumbers);

Assert.IsNotNull(result, scriptParser.ErrorMessage);

ScriptParser Methods and Properties

Main Execution

  • ExecuteScript()
  • ExecuteScriptAsync()

Script Parsing

  • ParseScriptToCode()

C# Script Engine Configuration and Access

  • ScriptEngine
  • AddAssembly()
  • AddAssemblies()
  • AddNamespace()
  • AddNamespaces()

The ScriptEngine property is initialized using default settings which use:

  • AddDefaultReferencesAndNamespaces()
  • SaveGeneratedCode = true

You can optionally replace ScriptParser instance with a custom instance that is configured exactly as you like:

var scriptParser = new ScriptParser();

var exec = new CSharpScriptExection();
exec.AddLoadedReferences();

scriptParser.ScriptEngine = exec;

string result = scriptParser.ExecuteScript(template, model);

Error and Debug Properties

  • ErrorMessage
  • ErrorType
  • GeneratedClassCode
  • GeneratedClassCodeWithLineNumbers

The various Addxxxx() methods and error properties are directly forwarded from the ScriptEngine instance as readonly properties.

Some Template Usage Examples

An example usage is for the Markdown Monster Editor which uses this library to provide text snippet expansion into Markdown (or other) documents.

A simple usage scenario might be to expand a DateTime stamp into a document as a snippet via a hotkey or explicitly

---
- created on {{ DateTime.Now.ToString("MMM dd, yyyy") }}

You can also use this to expand logic. For example, this is for a custom HTML expansion in a Markdown document by wrapping an existing selection into the template markup:

### Breaking Changes

<div class="alert alert-warning">
{{ await Model.ActiveEditor.GetSelection() }}
</div>

Here's an example that uses script to retrieves some information from the Web parses out a version number and embeds a string with the version number into the document:

{{%
var url = "https://west-wind.com/files/MarkdownMonster_version.xml";

var wc = new WebClient();
string xml = wc.DownloadString(url);
string version =  StringUtils.ExtractString(xml,"<Version>","</Version>");
}}
# Markdown Monster v{{version}}

This is a bit contrived but you can iterate over a list of open documents and display them in the template output:

**Open Editor Documents**

{{% foreach(var doc in Model.OpenDocuments) { }}
* {{ doc.Filename }}
{{% } }}

Usage Notes

Code snippets, methods and evaluations as well as templates are compiled into assemblies which are then loaded into the host process. Each script or snippet by default creates a new assembly.

Cached Assemblies

Assemblies are cached based on the code that is used to run them so repeatedly running the exact same template uses the cached version automatically.

You can disable this functionality with the DisableAssemblyCaching which can be a little more efficient and resource conscious if you know that scripts are either always recreated and never reused.

No Unloading

Assemblies, once loaded, cannot be unloaded until the process shuts down or the AssemblyLoadContext is unloaded. In .NET Framework there's no way to unload, but in .NET Core you can use an alternate AssemblyLoadContext.

Alternate AssemblyLoadContext for Unloading (.NET Core)

In .NET Core it's possible to unload assemblies using the CSharpScriptExecution.AlternateAssemblyLoadContext which if provided can be used to unload assemblies loaded in the context conditionally.

Westwind.Scripting FAQ

In Memory Types should only be used for top level Compilation

If you are creating multiple compilations that are dynamically compiled, and you need to reference one dynamic compilation in a second compilation, you have to ensure that referenced type was compiled to disk, not into memory.

The reason for this revolves around the fact that Activator.CreateInstance() or other similar load operations can't resolve the dynamically compiled type at runtime even if it was previously loaded. (see here and here).

Bottom line: If you need a dynamically compiled type from another compilation use to-disk compilation for the referenced type's code.

Said another way, you can only use in-memory compilation for top level execution, not for inclusion as a reference unless you build a custom assembly resolver (which I have not been able to figure out since there's no physical assembly to resolve from).

Westwind.Scripting Performance

A number of people have raised issues commenting that startup performance is slow. Yes that's the case, because the first time this library is called it has to load Roslyn which is a huge library and it takes time to load; it's slow. Depending on the type of machine you're running on this can take a couple of seconds for the first hit. So yes that overhead will happen and there's no way to avoid it.

There are couple of things to mitigate this issue:

  • Pre-compile and Save your compiled assembly
  • Try to pre-load Roslyn at App startup

Precompile your Code and Save Assembly

At the end of the day this library compiles code that ends up in an assembly, so rather than compiling your code every time you execute it, try to compile ahead of time and save your compiled assembly when you capture the code to be executed. You can store the assembly for later execution either on disk or some other stream based data store.

This may allow you to avoid loading Roslyn at all in most runtime situations, and only load it when you add new code that needs to be compiled. For example, if you're adding code snippets that a user enters, you can compile and capture the code snippet when the user enters the code. Then when the application starts you can load the already compiled assembly to execute the code.

Another related tip especially for snippet libraries that are user provided is to combine many snippets into a single class and map each snippet to a method. So rather than loading many types you can load up one type of code snippets that get executed as needed from an already loaded instance.

Pre-Load Roslyn on Startup

You can warm up Roslyn in the background during application startup, using RoslynLifetimeManager.WarmupRoslyn(). This method does a Task.Run() to create a very simple expression that is compiled into memory and executed to force Roslyn to load outside of the main application thread.

To do this call:

// at app startup - runs a background task, but don't await
_ = RoslynLifetimeManager.WarmupRoslyn();

Performance Tips

Running Code in a Loop

If you are running code repetitively, you should avoid using the various ExecuteXXX() methods and instead use CompileClass() to create a type instance, then re-use that type instance for execution. Although this library caches assemblies for the exact same code and doesn't recompile it, ExecuteXXX() methods still have to load an instance of the type each time which adds a bit of overhead.

It's much more efficient using CompileClass() to create a type instance, and then calling a method on it. Better yet, cache the MethodInfo to execute or create a delegate that can be reused for the specific method.

Change Log

1.2.7

  • CSharpScriptExecution.ExecuteMethodAsyncVoid for Async Void Methods
    Due to the way tasks are handled in .NET it's not possible to cast a void Task to a Task<T> result. For this reason separate methods are needed for the two versions of ExecuteMethodAsync() and ExecuteMethodAsync<TResult>() as ExecuteMethodAsyncVoid() and ExecuteMethodAsyncVoid<TResult>() respectively. Use these methods if you explicitly don't want to return a value.

1.2.5

  • Add CSharpScriptExecution.AlternateAssemblyLoadContext which allows for Assembly Unloading
    In .NET Core you can now assign an alternate AssemblyLoadContext to load assemblies into via the AlternateAssemblyLoadContext, which allows for unloading of assemblies. PR #19

  • CSharpScriptExecution.DisableAssemblyCaching
    By default this library caches generated assemblies based on the code that is passed in to execute. The CSharpScriptExecution.DisableAssemblyCaching property disables this caching in scenarios where you know code is never re-executed. PR #19

1.4

  • Add .NET 8.0 Target Added explicit target for .NET 8.0.

1.3

  • Add .NET 7.0 Target
    Added explicit target for .NET 7.0.

  • Updated Roslyn Libraries with support for C# 11
    Updated to latest Roslyn compiler libraries that support C# 11 syntax.

1.1

  • Breaking Change: ScriptParser Refactor to non-static Class
    The original implementation of ScriptParser relied on several static methods to make it very easy to access and use. In this release ScriptParser is now a class that has be instantiated.

  • ScriptParser includes ScriptEngine Property
    The Script execution engine is now an auto-initialized property on the ScriptParser class. You can customize this instance or replace it with a custom instance.

  • ScriptParser Simplification for ScriptEngine Access
    Rather than requiring configuration directly on the ScriptEngine instance for dependencies and errors, the relevant properties have been forwarded to the ScriptParser class. You can now use AddAssembly()/AddNameSpace() and the various Error, ErrorMessage etc. properties directly on the ScriptParser instance which makes the code cleaner and exposes the relevant features only.

1.0.10

  • Add Stream Inputs for Class Compilation
    The various CompileClass() methods now take stream inputs to allow directly reading from files or memory streams.

  • Clean up Default References and Namespaces for .NET Core
    Modified the default reference imports to create a minimal but somewhat functional baseline that allows running a variety of code running against common BCL/FCL functionality.

1.0

  • Switch to Roslyn CodeAnalysis APIs
    Switched from CodeDom compilation to Roslyn CodeAnalysis compilation which improves compiler startup, compilation performance and provides more detailed compilation information on errors.

  • Add Support for Async Execution
    You can now use various xxxAsync() overloads to execute methods as Task based operations that can be awaited and can use await inside of scripts.

  • Add ScriptParser for C# Template Scripting
    Added a very lightweight scripting engine that uses Handlebars style syntax for processing C# expressions and code blocks. Look at the ScriptParser class.

BREAKING CHANGES FOR v1.0

This version is a breaking change due to the changeover to the Roslyn APIs. While the APIs have stayed mostly the same, some of the dependent types have changed. Runtime requirements are also different with different libraries that are installed differently than the CodeDom dependencies. You may have to explicitly cleanup old application folders.

Version 0.4.5

  • Last CodeDom Version This version is the last version that works with CodeDom and that is fixed to .NET Framework. All later versions support .NET Framework and .NET Core.

Version 0.3

  • Updated to latest Microsoft CodeDom libraries
    Updated to the latest Microsoft CodeDom compilation libraries which streamline the compiler process for Roslyn compilation with latest language features.

  • Remove support to pre .NET 4.72
    In order to keep the runtime dependencies down switched the library target to net472 which is .NET Standard compliant and so pulls in a much smaller set of dependencies. This is potentially a breaking change for older .NET applications, which will have to stick with the 0.2.x versions.

  • Switch Projects to SDK Projects
    Switched from classic .NET projects to the new simpler .NET SDK project format for the library and test projects.

License

This library is published under MIT license terms.

Copyright © 2014-2022 Rick Strahl, West Wind Technologies

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sub license, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Give back

This library is free to use and integrate with for both personal and commercial use.

If you find this library useful, consider sponsoring the author, or making a small donation:

westwind.scripting's People

Contributors

pyrocumulus avatar rickstrahl avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

westwind.scripting's Issues

Unity (Game Engine) support

does this support Unity ?

# current packages for working C# api in unity

Microsoft.CodeAnalysis.Common.4.9.0-2.final
Microsoft.CodeAnalysis.CSharp.4.9.0-2.final
Microsoft.CodeAnalysis.CSharp.Scripting.4.9.0-2.final
Microsoft.CodeAnalysis.Scripting.Common.4.9.0-2.final
System.Buffers.4.5.1
System.Collections.Immutable.7.0.0
System.Memory.4.5.5
System.Numerics.Vectors.4.5.0
System.Reflection.Metadata.7.0.0
System.Runtime.CompilerServices.Unsafe.6.0.0
System.Runtime.Loader.4.0.0

Mono runtime

netstandard2.0

Compiling a class with another reference class

Hi,

I am trying to dynamically compile classes. For most part, it is working fine, except when it comes to classes which have reference to another class which was previously compiled. Plz see the example below. Any idea how this can be done.

FdynzMMXoAEkgNr

Any way to compile method? can only find compile class or assembly methods?

Hi Rick,

I was looking for a quick way to allow users to provide snippets of code where I can pass in a context style variable which contains high level objects they should have access to, i.e some variables, an event system etc.

So based off these requirements it looked like ExecuteMethodAsync was the best bet as I could then do something like:

var scriptExecutor = new CSharpScriptExecution { AllowReferencesInCode = true, ThrowExceptions = true };
scriptExecutor.AddDefaultReferencesAndNamespaces();
scriptExecutor.AddAssembly(typeof(ExecutionContext));

var context = new ExecutionContext(Logger, AppState.UserVariables, AppState.TransientVariables, flowVars, EventBus);
await scriptExecutor.ExecuteMethodAsync(data.CSharpCode, "Execute", context);
return ExecutionResult.Success();

However it seemed reasonable that before the user could run the execution phase (above) they would probably want to validate their code by compiling it, which is where I stumbled into this issue, here is the code I am currently using:

private void CompileCode()
{
    var scriptExecutor = new CSharpScriptExecution { AllowReferencesInCode = true };
    scriptExecutor.AddDefaultReferencesAndNamespaces();
    scriptExecutor.AddAssembly(typeof(ExecutionContext));
    scriptExecutor.CompileClass(Data.CSharpCode);

    IsErrored = scriptExecutor.Error;
    CompilationResult = scriptExecutor.ErrorMessage;
    // The UI handles this elsewhere
}

However it blows up here

(1,1): error CS0106: The modifier 'public' is not valid for this item
(1,1): error CS8805: Program using top-level statements must be an executable.
(1,19): error CS0161: 'Execute(ExecutionContext)': not all code paths return a value
(1,14): error CS0246: The type or namespace name 'Task' could not be found (are you missing a using directive or an assembly reference?)
(1,27): error CS0246: The type or namespace name 'ExecutionContext' could not be found (are you missing a using directive or an assembly reference?)
(1,19): warning CS1998: This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
(1,19): warning CS8321: The local function 'Execute' is declared but never used

So its blowing up for good reason I assume because its expecting a whole class not just a method, but I was unsure how to verify the syntax in this scenario, here is the example code that the user starts with which I am verifying for now:

public async Task Execute(ExecutionContext context)
{
    // Your code goes here    
};

Worst case I can probably work around by using a class based approach, but given the execution allows methods I just wanted to see if this use case was supported or any guidance on how I should solve the problem.

From my perspective my only requirement is that I NEED to provide the user access to runtime variables that already exist within their code, the less code I have to provide in the "harness" for them the better (i.e method seemed least boilerplate for them with ability to pass in var).

Question: Isolating Code

Context

I'm currently evaluating ways of using scripts to transform data. These scripts would be hot-deployable at runtime.

One thing I found is Jint, a .NET JavaScript executor. But I'm also looking if it is possible to use C# for this and found your library.

Security

As loading code at runtime is always risky, I'm looking for ways of locking down the scripts' abilities.
Eg. in Jint, there is a possibility to lock down .NET usage (eg. sebastienros/jint#275).

I was wondering if your library supports something like this too, so my question is:
Is isolating/locking down the C# scripts possible?

Install-Package Westwind.Scripting -IncludePreRelease; Error; Install-Package : Project 'Default' is not found.

Hi Rick,

when I try to install your NuGet package into one of my projects, I get the following error:

Install-Package : Project 'Default' is not found.
At line:1 char:1
+ Install-Package Westwind.Scripting  -IncludePreRelease
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Default:String) [Install-Package], ItemNotFoundException
    + FullyQualifiedErrorId : NuGetProjectNotFound,NuGet.PackageManagement.PowerShellCmdlets.InstallPackageCommand
 

And yes - my project is not called 'default'. So the error message makes sense - but should your nuget package not install into any existing project?

With kind regards,

John

Recompile .dll for rule engine and effect on memory leak (old .dlls)

Hi Rick - I have looked at your library; it's quite impressive. I am evaluating it to use in my current project. Basically I want to use to implement rule engine. I am thinking about generating one dynamic .dll per rule. And these .dll would be cahced (along with instance) and it will be executed with data whenever it's required.

Along with this; Basically I want to give users to change rules at run time; that means I have to recompile the rule; unload old dll and load new .dll. While creating new .dll I do use one .dll which is specific to my project. Is there any clean way to load / unload .dll in Westwind.Scripting so I don't have memory leak.

I do use AssemblyLoadContext.Unload but it does not actually unload old assembly. So on recompile code during runtime assemblies are keep increasing (i.e. memory leak).

If user changes rules multiple times I want to keep unload old .dlls. How efficient I can do that ?

Conflict with Microsoft.CodeAnalysis.Common and Microsoft.EntityFrameworkCore.Tools 8.0.0

@RickStrahl Here is a similar issue to #18

Severity	Code	Description	Project	File	Line	Suppression State
Error	NU1107	Version conflict detected for Microsoft.CodeAnalysis.Common. Install/reference Microsoft.CodeAnalysis.Common 4.6.0 directly to project OPG.VIIC.Web.Core to resolve this issue. 
 OPG.VIIC.Web.Core -> Westwind.Scripting 1.3.3 -> Microsoft.CodeAnalysis.Scripting.Common 4.6.0 -> Microsoft.CodeAnalysis.Common (= 4.6.0) 
 OPG.VIIC.Web.Core -> Microsoft.EntityFrameworkCore.Tools 8.0.0 -> Microsoft.EntityFrameworkCore.Design 8.0.0 -> Microsoft.CodeAnalysis.CSharp.Workspaces 4.5.0 -> Microsoft.CodeAnalysis.Common (= 4.5.0).	OPG.VIIC.Web.Core	C:\TFS\Intranet\OPG.VIIC.Web.Core\OPG.VIIC.Web.Core.csproj	1	

Trying to upgrade to .NET Core 8.0 and the corresponding EF packages. There is a conflict now with Microsoft.EntityFrameworkCore.Tools 8.0.0 which uses Microsoft.CodeAnalysis.Common 4.5.0, but your package uses Microsoft.CodeAnalysis.Scripting.Common 4.6.0.

I kinda wonder if separate build profiles for different versions of .NET would fix this? For instance if you are targeting .NET Core 8.0, then the assumption being is you would use .NET 8.0 packages for things like Microsoft.EntityFrameworkCore.Tools which seem to use Microsoft.CodeAnalysis.Common 4.5.0 (instead of the later 4.6.0)?

In legacy .NET framework, we had this mapping functionality you could do in the web.config:

      <dependentAssembly>
        <assemblyIdentity name="Antlr3.Runtime" publicKeyToken="eb42632606e9261f" culture="neutral"/>
        <bindingRedirect oldVersion="0.0.0.0-3.5.0.2" newVersion="3.5.0.2"/>
      </dependentAssembly>

In the above example it would cast any references from Antlr3.Runtime from versions 0.0.0.0 to 3.5.0.2 to 3.5.0.2 assembly version.

I tried adding this to my .NET 8.0 application .csproj, but it didn't help:

<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>

Conflict with Microsoft.VisualStudio.Web.CodeGeneration and Westwind.Scripting 1.2.1

When you install Westwind.Scripting 1.2.1 on an existing .NET Core 6.0 MVC project that has never used the code analysis features that are required to scaffold new views using (Microsoft.VisualStudio.Web.CodeGeneration) you are unable to properly get scaffolding to work afterwards.

Steps to Recreate:

  1. Create a new .NET Core 6.0 MVC Project
  2. Install Westwind.Script 1.2.1
  3. Right click the Views folder, and click "Add", then click "View", then pick Razor View.

Pick any view with a model class, here is an example:
image

Click Add

image

You get the above error message.

In further analysis it looks like this is erroring behind the scenes (very hard to see why). But I think I know reason.

Your nuget using packages that cause conflict with adding Microsoft.VisualStudio.Web.CodeGeneration to project that is required to support visual studio scaffolding. Here is the nuget restore chain if you try to do what visual studio does behind the scenes:

image

Severity Code Description Project File Line Suppression State
Error NU1107 Version conflict detected for Microsoft.CodeAnalysis.Common. Install/reference Microsoft.CodeAnalysis.Common 4.4.0 directly to project WebApplication2 to resolve this issue.
WebApplication2 -> Westwind.Scripting 1.2.1 -> Microsoft.CodeAnalysis.Scripting.Common 4.4.0 -> Microsoft.CodeAnalysis.Common (= 4.4.0)
WebApplication2 -> Microsoft.VisualStudio.Web.CodeGeneration 6.0.13 -> Microsoft.VisualStudio.Web.CodeGeneration.EntityFrameworkCore 6.0.13 -> Microsoft.DotNet.Scaffolding.Shared 6.0.13 -> Microsoft.CodeAnalysis.CSharp.Features 4.0.0 -> Microsoft.CodeAnalysis.Common (= 4.0.0). WebApplication2 C:\TFS\WebApplication2\WebApplication2\WebApplication2.csproj 1

runtime line number Exception

Hello Rick

I'am using the package and it works very well for my purposes

I would like to know if it's possible to trace back the line number of the script that generated an exception when it is executed

if it's not possible , can you suggest me the best alternative strategy to trace the origin of a script execution error ?

thanks for your good work

Issue about embedded interop types

Coding with Net Framework 4.8

I want to Refer "Microsoft.Office.Interop.Excel" which is embedded interop types while compile a class.

If I use dynamic type,it can compile succefully and run.

public class Module{
		public void Main(){
			dynamic xlApp = System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
			dynamic wb = xlApp.Workbooks.Add();
			MessageBox.Show(wb.Name);
		}
	}

When I change the Type, compile Error occur.

public class Module{
		public void Main(){
			var xlApp = (Microsoft.Office.Interop.Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
			var wb = xlApp.Workbooks.Add();
			MessageBox.Show(wb.Name);
		}
	}

Replace word "using " problem

Hello, thank you very much for providing this high quality library.

I'm having a problem with it.

I generated a code string and submitted it to script.ExecuteCodeAsync for execution.

If the string contains the word "using ", execution will get an error.

The "using" word is not used to refer to another namespace, but appears in the string definition, for example:
string str = "I am using a folk";

In CSharpScriptExecution.cs at line 1574. The word "using" is trimmed and replaced directly.

When "using" appears inside a string or in a comment, the code may be broken.

Can you provide a switch to turn off this behavior?

Redirecting Standard Output

I'm curious how Console.WriteLine can be used when using this:

CSharpScriptExecution script = new() { SaveGeneratedCode = true };
script.AddAssembly(Utils.assembly); // Reference this assembly in the script
script.AddDefaultReferencesAndNamespaces();
            
// need dynamic since current app doesn't know about type
dynamic compiledClass = script.CompileClass(code);

My compiled class is running, I know that much from fetching simple variables within it. But the methods that fetch variables are supposed to send a Console.WriteLine message letting me know they're all clear. Is there a way to capture / redirect these messages from the compiled code so that they will perform the same as output messages in the regular code file?

using 0HarmonyLib.dll

I'm using a component from the following link (0Harmony.dll).
https://github.com/pardeike/Harmony

Its DLL file has a somewhat odd name starting with a number, causing the generated code to become 'using HarmonyLib,' but it should actually be 'using 0HarmonyLib.'

How add dynamic generated Assembly to script

Hello! I'm trying to understand how work with reflection and CSharp scripting. I want compile separately each part of code in order to get compile errors if there are exist. Next add compiled Challenge Class to Hello Class. In this code, I have error that Challenge file is not found. If you know how resolve it, I will be very grateful. Thanks

I have following code:

Class which have access to Assembly.

public class Executor : CSharpScriptExecution
{
    public void CompileAndCache(string code)
    {
        var hash = code.GetHashCode();
        CompileAssembly(code);
        CachedAssemblies[hash] = Assembly;
    }

    public Assembly GetAssembly()
    {
        return Assembly;
    }
}

Executor factories

private Executor CreateChallengeExecutor(string code)
{
    var script = new Executor()
    {
        SaveGeneratedCode = true,
        ThrowExceptions = true,
    };

    script.AddNetCoreDefaultReferences();
    script.CompileAssembly(code);

    return script;
}

private Executor CreateSolutionExecutor(string code, Type challengeType)
{
    var script = new Executor()
    {
        SaveGeneratedCode = true,
        ThrowExceptions = true,
    };

    script.AddNetCoreDefaultReferences();
    script.AddAssembly(typeof(FactAttribute));
    script.AddAssembly(challengeType); // <---------

    script.CompileAndCache(code);

    return script;
}

Using

var challengeExecutor = CreateChallengeExecutor(clientCode);
var clientAssembly = challengeExecutor.GetAssembly();
var clientClass = clientAssembly.GetType("Challenger.Challenge");

var solutionExecutor = CreateSolutionExecutor(testCode, clientClass);

solutionExecutor.ExecuteMethod(testCode, "World");

Input data:

var clientCode = @"
  using System;
  
  namespace Challenger 
  {
      public static class Challenge
      {
          public static string Test() 
          {
              return ""TEST"";
          }
      }
  }   
";
var testCode = @"
  using System;
  using Xunit;
  using Challenger; // <-------------

  namespace Solution
  {
      public class Hello {
          [Fact]
          public string World()
          {
              var test = Challenge.Test(); <-------------
              return ""Wow"";
          }
      }
  }
";

ThrowExceptions throws both compilation and runtime errors

Hey Rick,

I wanted to bring something to your attention that seemed a bit off based on the documentation. When I set the ThrowsException flag to true on the CSharpScriptExecution instance, I'm seeing exceptions thrown for both compile time and runtime errors. The documentation seems to contradict this saying that the ThrowsException flag should only effect runtime errors. Here's a test that I wouldn't expect to pass, but does:

[TestMethod]
public void CompileInvalidClassDefinition()
{
    var code = @"
        using System;

        namespace Testing.Test
        {
            public class Test
            {
                public void Foo();
            }
        }";

    var script = new CSharpScriptExecution()
    {
        ThrowExceptions = true
    };

    script.AddDefaultReferencesAndNamespaces();

    Assert.ThrowsException<ApplicationException>(() => script.CompileClass(code));
}

My apologies if I missed something, and this ends up just being noise.

-Brett

External references are not parsed when using CompileClass()

Hi Rick,

When using the CompileClass() methods on the CSharpScriptExecution, it doesn't look like external references are parsed. The function call to ParseReferencesInCode() is missing. Was this intentional or maybe just an oversight?

Thanks,
Brett

Wrong code gets executed on second invoke of ExecuteCode

When invoking the method ExecuteCode for a second time on the same instance of CSharpScriptExecution the code parameter is not used. The code from the first invoke is used. In my opinion this happens because after compiling the new assembly in ExecuteMethod the method CreateInstance should be invoked with parameter force = true

Bugfix or docfix: README.md documentation incorrect or script.Assembly property needs to be changed from protected level to public

In README.md, the example provided in the section "Compiling and Executing Entire Classes" notes to use the following if you need access to the assembly:

var assembly = script.Assembly

This does not work as an CS0122 error is thrown stating "'CSharpScriptExecutionAssembly' is inaccessible due to its protection level."

I would submit a PR to change from protected to public, but was not sure if there was a specific reason for it to be designated as protected.

Detecting/Aborting long running scripts

it seems that scripts run via script.ExecuteCodeAsync() do not yield unless the script itself includes await. So if there is an infinite loop in a poorly written script the entire application blocks.

Is there a workaround for this? Ideally running a script via ExecuteCodeAsync() did not block the caller, and a CancellationToken could be specified.

Poor performance when compared to precompiled code

Hi

I am currently looking for a solution that would allow users to write the function that would filter out some of the "results". The function has a bool return type and it should take a few input parameters that could be used in filtering logic, i.e. native .Net filter function would look like this:

        public bool Filter(MyItem item, MyStats stats)
        {
            var res1 = item.Sth;
            var res2 = stats.SomeResults;

            if (res2[0] >= 0)
            {
                return true;
            }

            return false;
        }

Now I am trying to execute the same function using the Westwind.Scripting, to imitate the user-created filter function:

            var stats = new MyStats() { SomeResults = new decimal[] { 0, 15, 30, 45, 100 }};

            var items = new List<MyItem>();
            for (int i = 0; i < 10_000_000; i++)
                items.Add(new MyItem() { Sth = i });

            var exec = new CSharpScriptExecution() { SaveGeneratedCode = true };
            exec.AddDefaultReferencesAndNamespaces();

            exec.AddAssembly(typeof(MyItem));
            exec.AddAssembly(typeof(MyStats));
            exec.AddNamespace("ClassLibrary1");

            var code = @"
public bool Filter(MyItem item, MyStats stats)
{
var res1 = item.Sth;
var res2 = stats.SomeResults;

if (res2[0] >= 0)
{
    return true;
}

return false;
}
";
            int numberOfPositives = 0;
            var sw = Stopwatch.StartNew();
            for (int i = 0; i < items.Count(); i++)
            {
                var item = items[i];

                bool result = exec.ExecuteMethod<bool>(code, "Filter", item, stats); // 10sec+
                //var result = Filter(item, stats); // sub 1 sec

                if (res)
                {
                    numberOfPositives++;
                }
            }

            sw.Stop();
            MessageBox.Show($"Total elapsed milliseconds: {sw.ElapsedMilliseconds}");

and the classes are:

namespace ClassLibrary1
{
    public class MyStats
    {
        public decimal[] SomeResults { get; set; }
    }

    public class MyItem
    {
        public int Sth { get; set; }
    }
}

The problem is

  • when I execute the code natively - it runs blazingly fast 249ms
  • when I execute the script with runtime compilation (Westwind.Scripting) the execution time is 11_141ms

I was under the impression that the performance would be nearly the same as for the precompiled code. Since the assembly is getting compiled into in-memory dll once, and then get's re-executed just as a standard assembly would?

is there a way to speed up the solution?

note: I apologize for classes and names that do not make sense :) I am just prototyping the solution and trying to make a minimum viable working version of it without paying to much attention to names and "logic" etc. Per use-case the filtering function will be called tens thousands time in a loop, so the code is still perfectly valid from that perspective.

Sandboxing code

Hi,
as you know in .net core Microsoft has deprecated the CAS.
Have you think to an alternative to execute the code compiled at runtime in a security context?
For example the code that run do not save files on disk o do not naivgate on the internet
Thanks

Please help with the extension methods

Hello @rickjet,
first of all great work. thanks for that

I have a problem with the extension methods.

I have the following code that I want to run.

here is the script:

string code = $@"
      public async Task<bool> Run()
      {{
                  var app = FlaUI.Core.Application.Launch(@""C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE"");
                  using (var automation = new UIA3Automation())
                  {{
                      Task.Delay(2000).Wait();
                      var window = app.GetMainWindow(automation);
                      var button1 = window.FindFirstDescendant(cf => cf.ByAutomationId(""msotcidPlaceOfficeStart"")).AsListBoxItem();
                      button1?.Select();
                  }}
      
                  return true;
      }}";
        public async Task<bool> RunCode(string code)
        {
            if (!string.IsNullOrEmpty(code))
            {

                var script = CSharpScriptExecution.CreateDefault();

                script.AddNetCoreDefaultReferences();

                script.AddAssembly("FlaUI.Core.dll");
                script.AddAssembly("FlaUI.UIA3.dll");
                script.AddAssembly("System.Diagnostics.Process.dll");
                script.AddAssembly("System.Threading.Tasks.dll");
                
                
                script.AddNamespace("FlaUI.UIA3");
                script.AddNamespace("FlaUI.Core");
                script.AddNamespace("System.Threading.Tasks");

                var result = await script.ExecuteMethodAsync<bool>(code, "Run");

            }

            return true;
        }

But I keep getting the following exception

(30,110): error CS1061: \"AutomationElement\" enthält keine Definition für \"AsListBoxItem\", und es konnte keine zugängliche AsListBoxItem-Erweiterungsmethode gefunden werden, die ein erstes Argument vom Typ \"AutomationElement\" akzeptiert (möglicherweise fehlt eine using-Direktive oder ein Assemblyverweis).\r\n(23,25):

It does not find the extension method, namely this one

var button1 = window.FindFirstDescendant(cf => cf.ByAutomationId("msotcidPlaceOfficeStart")).AsListBoxItem();
AsListBoxItem()

Can External References (#r) target netstandard?

Hi Rick,

Can external references target netstandard or should they always target the same framework of the app they will be compiled and ran in? You can verify the behavior I'm seeing by when you change the ReferenceTest.csproj to target netstandard2.0 instead of net70. A few of the tests will break.

Thanks,
Brett

Multiple CompileClass calls with a single CSharpScriptExecution instance?

Let's say I have two different classes that I will compile with CompileClass(). Can I compile them using one instance of CSharpScriptExecution, or do I need a new instance of CSharpScriptExecution for each class?

When I tried to use a single instance of CSharpScriptExecution, the second call to CompileClass() returned an instance of the first class, not the second.

Before each call to CompileClass(), I set the GeneratedClassName to a new, unique name.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.