Dynamically evaluate and interpolate C# expressions at runtime with ease, leveraging a powerful script execution engine.
$ dotnet add package DollarSignEngineDynamically 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:
{expression} and dollar-sign ${expression} syntax.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 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
// When working with text that contains literal curly braces (like JSON),
// enable dollar sign syntax to specify which parts should be interpolated
var options = new DollarSignOptions { SupportDollarSignSyntax = true };
var user = new { name = "Alice", age = 30 };
// With dollar sign syntax enabled, only ${...} is interpolated
var jsonTemplate = "{ \"user\": { \"name\": \"{name}\", \"age\": ${age} } }";
var result = await DollarSign.EvalAsync(jsonTemplate, user, options);
Console.WriteLine(result);
// Outputs: { "user": { "name": "{name}", "age": 30 } }
// In standard mode (default), all {...} expressions are interpolated
var standardResult = await DollarSign.EvalAsync("{ \"user\": { \"name\": \"{name}\", \"age\": {age} } }", user);
Console.WriteLine(standardResult);
// Outputs: { "user": { "name": "Alice", "age": 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.
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.
// When enabled, {expression} is treated as literal text and ${expression} is evaluated.
// Defaults to false.
SupportDollarSignSyntax = true,
// Security level for expression validation (Strict, Moderate, Permissive)
SecurityLevel = SecurityLevel.Moderate,
// Maximum execution time for expressions in milliseconds
TimeoutMs = 5000,
// Cache configuration
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()
.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();
// Custom configuration
var customOptions = DollarSignOptions.Create(opts =>
{
opts.SecurityLevel = SecurityLevel.Strict;
opts.TimeoutMs = 1000;
opts.ThrowOnError = true;
});
The library provides detailed exceptions for different types of errors:
try
{
var result = await DollarSign.EvalAsync("{nonExistentVariable + 1}", null,
new DollarSignOptions { ThrowOnError = true });
}
catch (VariableResolutionException ex)
{
Console.WriteLine($"Variable '{ex.VariableName}' not found");
Console.WriteLine($"Available variables: {string.Join(", ", ex.AvailableVariables)}");
// Provides suggestions for similar variable names
}
catch (ExpressionValidationException ex)
{
Console.WriteLine($"Security validation failed: {ex.Message}");
Console.WriteLine($"Expression: {ex.Expression}");
if (ex.Suggestion != null)
Console.WriteLine($"Suggestion: {ex.Suggestion}");
}
catch (ExpressionTimeoutException ex)
{
Console.WriteLine($"Expression timed out after {ex.Timeout}: {ex.Expression}");
}
catch (CompilationException ex)
{
Console.WriteLine($"Compilation error: {ex.Message}");
Console.WriteLine($"Details: {ex.ErrorDetails}");
}
catch (DollarSignEngineException ex)
{
Console.WriteLine($"General error: {ex.Message}");
}
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);
var (totalEvaluations, cacheHits, hitRate) = DollarSign.GetMetrics();
Console.WriteLine($"Cache hit rate: {hitRate:P2}");
var templates = new Dictionary<string, string>
{
["greeting"] = "Hello, {name}!",
["farewell"] = "Goodbye, {name}!"
};
var results = await DollarSign.EvalManyAsync(templates, new { name = "World" });
DollarSign.ClearCache();