Dynamically evaluate and interpolate C# expressions at runtime with ease, leveraging the power of the Roslyn compiler.
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.
-
Core Purpose: Enable runtime evaluation of C# string interpolation (
$"{}") exactly as it works in compile-time C#. -
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
- 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.
The library is available on NuGet. You can install it using the following command:
dotnet add package DollarSignEngine
Below are some examples of how to use the DollarSignEngine to evaluate expressions dynamically.
// 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 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()}
// 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.
// 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%
// 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
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 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
When using method calls and LINQ expressions within interpolated strings, there are a few important considerations:
-
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) };
-
Required References: The engine automatically adds references to common assemblies like System.Linq, but custom assemblies might need to be explicitly included via reflection.
-
Type Compatibility: Parameters in method calls must be compatible with expected parameter types. The engine attempts to convert types when possible.
-
Anonymous Method Limitations: Methods defined inline as lambda expressions in anonymous objects may have restricted functionality compared to methods in concrete classes.
-
Extension Method Support: Extension methods (like many LINQ methods) require the appropriate namespace to be in scope. The engine includes common namespaces by default.
-
Performance Considerations: Complex method calls and LINQ expressions require more compilation resources than simple property access.
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; });
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}"); }
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.
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);
- 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.
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
TimeoutMsreturn control to the caller and throwExpressionTimeoutException(the abandoned computation continues in the background until it finishes).
Deprecated:
SecurityLevel,DollarSignOptions.Strict/Moderate,DollarSign.ValidateExpression,WithSecurity,WithStrictSecurity,OptimizedForSecurity, andExpressionValidationExceptionare retained for source compatibility but no longer restrict evaluation.
- 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();
- .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