SkillAgentSearch skills...

Westwind.Scripting

Dynamically compile and execute C# Code and Expressions at runtime. Also includes a light weight script templating engine using Handlebar style C# syntax.

Install / Use

/learn @RickStrahl/Westwind.Scripting
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Westwind C# Scripting and Templating

Dynamically compile and execute C# code at runtime, as well as a light weight, Handlebars style C# template script engine

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.

There's also a lightweight, self contained C# script template engine via the ScriptParser class that can evaluate expressions and structured C# statements using Handlebars-like ({{ expression }} and {{% code-block }}) script templates. Unlike other script engines, this engine uses plain C# syntax for expressions and code block execution.

Get it from Nuget:

dotnet add package Westwind.Scripting

It supports the following targets:

v2.0 and later

  • .NET 10 (net10.0), .NET 8.0 (net8.0)

for .NET Framework and .NET Standard support you can use v1:

v1.x

  • .NET 10 (net10.0), .NET 8.0 (net8.0)
  • .NET 4.72
  • .NET Standard 2.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 C# syntax
    • Script Execution from strings
    • Script Execution from files
    • Support for Partials, Layout and Sections
  • Script Evaluator
    • Runtime Expression Evaluation
    • Non-compiled, Reflection based evaluation of {{ expressions }} in strings

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

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

Related Skills

View on GitHub
GitHub Stars248
CategoryDevelopment
Updated17d ago
Forks49

Languages

C#

Security Score

85/100

Audited on Mar 9, 2026

No findings