Skip to content

About

.NET runtime C# expression evaluator powered by Roslyn — dynamic string interpolation, LINQ support, configurable security levels, and expression caching for templates.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Latest commit

 

History

59 Commits

Folders and files

Repository files navigation

DollarSignEngine

NuGet Version NuGet Downloads Build Status License .NET Compatibility

Dynamically evaluate and interpolate C# expressions at runtime with ease, leveraging the power of the Roslyn compiler.

Introduction

The DollarSignEngine is a robust C# library designed to simplify the process of dynamically evaluating and interpolating expressions at runtime. Ideal for applications requiring on-the-fly evaluation of string templates, it offers developers the flexibility to inject variables and execute complex C# expressions with the same syntax as C# string interpolation.

Guiding Principles

  1. Core Purpose: Enable runtime evaluation of C# string interpolation ($"{}") exactly as it works in compile-time C#.

  2. Extension Rules:

    • All features natively supported in C# string interpolation must be maintained
    • Avoid extending functionality beyond standard C# interpolation syntax
    • Only add features essential for runtime evaluation

Features

  • Dynamic Expression Evaluation: Evaluate C# expressions dynamically at runtime, with support for interpolation and complex logic.
  • Flexible Parameter Injection: Easily pass parameters into expressions using dictionaries, anonymous objects, or regular C# objects.
  • Method & LINQ Support: Call methods on objects and use LINQ expressions within interpolated strings.
  • Format Specifiers & Alignment: Full support for C# format specifiers and alignment in interpolated expressions.
  • Custom Variable Resolution: Provide custom variable resolvers for advanced use cases.
  • Multiple Syntax Options: Support for both standard C# interpolation {expression} and dollar-sign ${expression} syntax for mixed content templates.
  • Comprehensive Error Handling: Detailed exceptions with helpful error messages and suggestions for common issues.
  • Expression Caching: Compiled expressions are cached for improved performance with repeated evaluations.
  • Resource Guards: Expression length and nesting-depth limits to reject pathological input. (Note: not a security sandbox — see the Security section.)
  • Multi-Framework Support: Compatible with .NET Standard 2.1, .NET 5, 6, 7, 8, and 9.
  • Performance Monitoring: Built-in metrics for cache hit rates and evaluation performance.

Installation

The library is available on NuGet. You can install it using the following command:

dotnet add package DollarSignEngine

Usage

Below are some examples of how to use the DollarSignEngine to evaluate expressions dynamically.

Basic Interpolation

// Simple interpolation with current date
var result = await DollarSign.EvalAsync("Today is {DateTime.Now:yyyy-MM-dd}");
Console.WriteLine(result); // Outputs: Today is 2023-05-01 (based on current date)

// With parameters using anonymous object
var name = "John";
var result = await DollarSign.EvalAsync("Hello, {name}!", new { name });
Console.WriteLine(result); // Outputs: Hello, John!

// With parameters using dictionary
var parameters = new Dictionary<string, object?> { { "name", "John" } };
var result = await DollarSign.EvalAsync("Hello, {name}!", parameters);
Console.WriteLine(result); // Outputs: Hello, John!

// Using dollar sign syntax for mixed content (JSON templates, etc.)
var options = new DollarSignOptions { SupportDollarSignSyntax = true };
var user = new { name = "Alice", age = 30 };
var jsonTemplate = "{ \"user\": { \"name\": \"{name}\", \"age\": ${age} } }";
var result = await DollarSign.EvalAsync(jsonTemplate, user, options);
Console.WriteLine(result); // Outputs: { "user": { "name": "{name}", "age": 30 } }

Using Custom Objects

// Using a class directly
public class User
{
    public string Username { get; set; } = string.Empty;
    public int Age { get; set; }
}

var user = new User { Username = "Alice", Age = 30 };
var result = await DollarSign.EvalAsync("User: {Username}, Age: {Age}", user);
Console.WriteLine(result); // Outputs: User: Alice, Age: 30

// Using anonymous types with nested properties
var person = new { 
    Name = "Jane", 
    Address = new { City = "New York", Country = "USA" } 
};
var result = await DollarSign.EvalAsync("Person: {Name} from {Address.City}", person);
Console.WriteLine(result); // Outputs: Person: Jane from New York

// Calling a method on a custom object
public class Greeter
{
    public string Name { get; set; } = string.Empty;

    public string Hello()
    {
        return $"hello, {Name}";
    }
}

var greeter = new Greeter { Name = "Bob" };
var options = new DollarSignOptions { SupportDollarSignSyntax = true };
var result = await DollarSign.EvalAsync("Greeting: ${Hello()}", greeter, options);
Console.WriteLine(result); // Outputs: Greeting: hello, Bob

// Without dollar sign syntax, the expression is treated as literal
var standardResult = await DollarSign.EvalAsync("Greeting: ${Hello()}", greeter);
Console.WriteLine(standardResult); // Outputs: Greeting: ${Hello()}

Method Calls and LINQ Expressions

// Calling methods on strings
var text = "hello world";
var result = await DollarSign.EvalAsync("Uppercase: {text.ToUpper()}", new { text });
Console.WriteLine(result); // Outputs: Uppercase: HELLO WORLD

// Using LINQ with collections
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = await DollarSign.EvalAsync("Even numbers: {numbers.Where(n => n % 2 == 0).Count()}", new { numbers });
Console.WriteLine(result); // Outputs: Even numbers: 2

// Chaining method calls
var data = "  sample text  ";
var result = await DollarSign.EvalAsync("Processed: {data.Trim().ToUpper().Replace('A', 'X')}", new { data });
Console.WriteLine(result); // Outputs: Processed: SXMPLE TEXT

// Using strongly-typed custom objects for method calls
public class Calculator
{
    public int Add(int a, int b) => a + b;
    public int Multiply(int a, int b) => a * b;
}

var calc = new Calculator();
var result = await DollarSign.EvalAsync("Sum: {calc.Add(5, 3)}, Product: {calc.Multiply(5, 3)}", new { calc });
Console.WriteLine(result); // Outputs: Sum: 8, Product: 15

Important Note: For method calls and LINQ expressions to work properly, you need to pass strongly-typed objects rather than anonymous types. This is because the engine needs access to the type information at runtime to properly compile and execute the methods.

Format Specifiers and Alignment

// Using format specifiers
var price = 123.456;
var result = await DollarSign.EvalAsync("Price: {price:C2}", new { price });
Console.WriteLine(result); // Outputs: Price: $123.46

// Using alignment
var number = 42;
var result = await DollarSign.EvalAsync("Left aligned: {number,-10} | Right aligned: {number,10}", new { number });
Console.WriteLine(result); // Outputs: Left aligned: 42         | Right aligned:         42

// Combined alignment and format
var percentage = 0.8654;
var result = await DollarSign.EvalAsync("Progress: {percentage,8:P1}", new { percentage });
Console.WriteLine(result); // Outputs: Progress:    86.5%

Conditional Logic

// Simple ternary operation
var age = 20;
var result = await DollarSign.EvalAsync("You are {(age >= 18 ? \"adult\" : \"minor\")}.", new { age });
Console.WriteLine(result); // Outputs: You are adult.

// Nested ternary operations
var score = 85;
var result = await DollarSign.EvalAsync("Grade: {(score >= 90 ? \"A\" : score >= 80 ? \"B\" : \"C\")}.", new { score });
Console.WriteLine(result); // Outputs: Grade: B.

// Complex condition with formatting
var price = 123.456;
var discount = true;
var result = await DollarSign.EvalAsync("Final price: {(discount ? price * 0.9 : price):C2}", 
    new { price, discount });
Console.WriteLine(result); // Outputs: Final price: $111.11

Working with Collections

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var result = await DollarSign.EvalAsync("Total sum: {numbers.Sum()}. Items count: {numbers.Count}", new { numbers });
Console.WriteLine(result); // Outputs: Total sum: 15. Items count: 5

var settings = new Dictionary<string, string> { { "Theme", "Dark" }, { "FontSize", "12" } };
var result = await DollarSign.EvalAsync("Theme: {settings[\"Theme\"]}, Font Size: {settings[\"FontSize\"]}", new { settings });
Console.WriteLine(result); // Outputs: Theme: Dark, Font Size: 12

Dollar Sign Syntax (Commonly Used)

Dollar sign syntax is frequently used when working with templates that contain literal curly braces, such as JSON, XML, or other structured formats.

// Enable dollar sign syntax for selective interpolation
var options = new DollarSignOptions { SupportDollarSignSyntax = true };
var user = new { name = "Alice", age = 30, isActive = true };

// Common use case: JSON templates with mixed literal and interpolated values
var jsonTemplate = """
{
  "user": {
    "name": "{name}",           // Literal curly braces (not interpolated)
    "age": ${age},              // Dollar syntax (interpolated)
    "status": ${isActive ? "\"active\"" : "\"inactive\""},
    "metadata": {
      "created": "{created_date}",  // Literal (placeholder for later)
      "updated": "${DateTime.Now:yyyy-MM-dd}"  // Interpolated
    }
  }
}
""";

var result = await DollarSign.EvalAsync(jsonTemplate, user, options);
Console.WriteLine(result);
// Outputs: { "user": { "name": "{name}", "age": 30, "status": "active", ... } }

// Configuration files, SQL templates, and document generation
var configTemplate = "server={server_name}&port=${port}&timeout=${timeout}";
var config = new { port = 5432, timeout = 30 };
var connectionString = await DollarSign.EvalAsync(configTemplate, config, options);
// Outputs: server={server_name}&port=5432&timeout=30

Method Calls and LINQ: Requirements and Limitations

When using method calls and LINQ expressions within interpolated strings, there are a few important considerations:

Requirements

  1. Strongly-Typed Objects: For method calls to work properly, pass strongly-typed objects rather than anonymous types whenever possible. The engine needs access to type information at runtime.

    // This works well - using a concrete class
    var calculator = new Calculator();
    var result = await DollarSign.EvalAsync("Result: {calculator.Add(5, 3)}", new { calculator });
    
    // May have limitations with anonymous types
    var data = new { calc = (Func<int, int, int>)((a, b) => a + b) };
  2. Required References: The engine automatically adds references to common assemblies like System.Linq, but custom assemblies might need to be explicitly included via reflection.

  3. Type Compatibility: Parameters in method calls must be compatible with expected parameter types. The engine attempts to convert types when possible.

Limitations

  1. Anonymous Method Limitations: Methods defined inline as lambda expressions in anonymous objects may have restricted functionality compared to methods in concrete classes.

  2. Extension Method Support: Extension methods (like many LINQ methods) require the appropriate namespace to be in scope. The engine includes common namespaces by default.

  3. Performance Considerations: Complex method calls and LINQ expressions require more compilation resources than simple property access.

Configuration Options

The DollarSignOptions class provides several options to customize the behavior of the expression evaluation:

var options = new DollarSignOptions
{
    // Whether to cache compiled expressions. Defaults to true.
    UseCache = true,

    // Whether to throw exceptions on errors during evaluation. Defaults to false.
    ThrowOnError = false,

    // Custom variable resolver function.
    VariableResolver = variableName => /* Return value for variable */,

    // Custom error handler function. When set, its return value becomes the result and
    // ThrowOnError is not consulted — the two settings are not independent. To log an
    // error here and still fail the evaluation, throw from inside the handler.
    ErrorHandler = (expression, exception) => /* Return replacement text for errors */,

    // The culture to use for formatting. If null, the current culture is used.
    CultureInfo = new CultureInfo("en-US"),

    // Whether to support dollar sign syntax in templates (commonly used).
    // When enabled, {expression} is treated as literal text and ${expression} is evaluated.
    // Essential for JSON templates, config files, and mixed content. Defaults to false.
    SupportDollarSignSyntax = true,

    // [Deprecated] No longer restricts evaluation. See the Security section.
    SecurityLevel = SecurityLevel.Moderate,

    // Maximum execution time for expressions in milliseconds
    TimeoutMs = 5000,

    // [Deprecated] Ignored — the evaluator cache is a fixed-size process-wide singleton.
    CacheSize = 1000,
    CacheTtl = TimeSpan.FromHours(1)
};

You can also use the fluent configuration methods:

// Default options
var options = DollarSignOptions.Default;

// Chained configuration
var secureOptions = DollarSignOptions.Default
    .WithStrictSecurity()   // [Deprecated] no-op for security; still sets timeout/throw
    .WithTimeout(TimeSpan.FromSeconds(2))
    .WithCache(500, TimeSpan.FromMinutes(30));

// Pre-configured options for common scenarios
var productionOptions = DollarSignOptions.Default.OptimizedForProduction();
var performanceOptions = DollarSignOptions.Default.OptimizedForPerformance();
var securityOptions = DollarSignOptions.Default.OptimizedForSecurity(); // [Deprecated] no-op for security

// Custom configuration
var customOptions = DollarSignOptions.Create(opts =>
{
    opts.SecurityLevel = SecurityLevel.Strict; // [Deprecated] no longer restricts evaluation
    opts.TimeoutMs = 1000;
    opts.ThrowOnError = true;
});

Error Handling

By default (ThrowOnError = false), a failing expression — including an unresolved variable — is replaced with an empty string and does not throw. Set ThrowOnError = true to surface failures as exceptions instead:

try
{
    var result = await DollarSign.EvalAsync("{nonExistentVariable + 1}", null,
        new DollarSignOptions { ThrowOnError = true });
}
catch (CompilationException ex)
{
    // An unresolved variable is a compile-time error, exactly as it would be in
    // compile-time C# string interpolation.
    Console.WriteLine($"Compilation error: {ex.Message}");
    Console.WriteLine($"Details: {ex.ErrorDetails}");
}
catch (ExpressionTimeoutException ex)
{
    Console.WriteLine($"Expression timed out after {ex.Timeout}: {ex.Expression}");
}
catch (DollarSignEngineException ex)
{
    Console.WriteLine($"General error: {ex.Message}");
}

Diagnostic Logging

Separately from ErrorHandler/ThrowOnError (which control what an evaluation returns), DollarSignEngine emits its own internal diagnostics — e.g. a compilation error — as it evaluates. By default these go to Console. Use DollarSignLogging to redirect or suppress them, for example when embedding DollarSignEngine inside another library that owns its own logging pipeline:

// Redirect diagnostics into your own logging instead of Console.
DollarSignLogging.SetHandler((level, message) => myLogger.Log(level.ToString(), message));

// Or suppress them entirely.
DollarSignLogging.SetMinimumLevel(LogLevel.None);

This configuration is process-wide (DollarSignEngine has no per-call logging scope) — set it once at startup.

Synchronous API

The library also provides synchronous versions of the evaluation methods:

// Using object variables
var result = DollarSign.Eval("Hello, {name}!", new { name = "World" });

// Using dictionary variables
var parameters = new Dictionary<string, object?> { { "value", 42 } };
var result = DollarSign.Eval("The answer is {value}.", parameters);

Performance and Security

Performance Features

  • Expression Caching: Compiled expressions are cached automatically for improved performance
  • Performance Metrics: Monitor cache hit rates and evaluation performance:
    var (totalEvaluations, cacheHits, hitRate) = DollarSign.GetMetrics();
    Console.WriteLine($"Cache hit rate: {hitRate:P2}");
  • Parallel Evaluation: Evaluate multiple templates concurrently:
    var templates = new Dictionary<string, string>
    {
        ["greeting"] = "Hello, {name}!",
        ["farewell"] = "Goodbye, {name}!"
    };
    var results = await DollarSign.EvalManyAsync(templates, new { name = "World" });

Note on per-call overhead: Compiled scripts are cached, but the evaluation context (the variable dictionary and script host) is rebuilt on every call, even on a cache hit. For tight loops over the same template, this is the dominant cost; caching the prepared context is a possible future optimization.

Security

DollarSignEngine compiles and executes arbitrary C#. It is not a sandbox and must not be used to evaluate templates from untrusted sources. A keyword blocklist cannot make arbitrary code execution safe — reflection, Type.GetType, and aliases all bypass it — so no such blocklist is applied.

If you must evaluate untrusted input, isolate evaluation at the process or container level (separate process with reduced privileges, container, or a dedicated sandbox).

The engine does enforce two resource guards to avoid pathological input:

  • Maximum expression length (10,000 characters)

  • Maximum nesting depth (20)

  • Timeout Protection: Expressions that exceed TimeoutMs return control to the caller and throw ExpressionTimeoutException (the abandoned computation continues in the background until it finishes).

Deprecated: SecurityLevel, DollarSignOptions.Strict/Moderate, DollarSign.ValidateExpression, WithSecurity, WithStrictSecurity, OptimizedForSecurity, and ExpressionValidationException are retained for source compatibility but no longer restrict evaluation.

Best Practices

  • Use expression caching for repeated evaluations of the same template
  • Consider security implications when allowing user-provided expressions
  • Use strongly-typed objects for better performance with method calls
  • Monitor performance metrics in production environments
  • Clear the cache periodically if memory usage becomes a concern:
    DollarSign.ClearCache();

Framework Compatibility

  • .NET Standard 2.1 - Compatible with .NET Framework 4.6.1+, .NET Core 2.1+
  • .NET 5, 6, 7, 8, 9, 10 - Full support for modern .NET versions
  • Cross-platform - Works on Windows, Linux, and macOS

About

.NET runtime C# expression evaluator powered by Roslyn — dynamic string interpolation, LINQ support, configurable security levels, and expression caching for templates.

Topics

Resources

Contributing

Security policy

Stars

8 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages