Simpra is a powerful expression language designed for evaluating dynamic expressions in .NET applications. It enables developers to execute logic dynamically within their applications, similar to how scripting languages work but optimized for business rules.
To install AltaSoft.Simpra, use NuGet:
dotnet add package AltaSoft.SimpraOr in your .csproj (see the NuGet badge above for the latest version):
<PackageReference Include="AltaSoft.Simpra" Version="2.1.5" />- Dynamic Expression Execution: Evaluate expressions at runtime.
- Rich Syntax: Conditionals, arithmetic, lists, pattern matching, regex, membership, null checks.
- Mutability Control: Toggle between immutable and mutable execution.
- String & List Operations: Manipulate and aggregate data easily.
- Custom Functions: Extend with user-defined or async functions.
- Safe Evaluation: No loops, recursion, or external resource access.
- Numbers:
10,2.5 - Booleans:
true,false - Strings:
'single'or"double"quoted. There are no escape sequences: a string cannot contain a backslash (\) or its own quote character. - Lists:
[1, 2, 3],['USD', 'EUR'] - Comments:
# line commentand/* block comment */ - There is no
nullliteral β usehas valueto check for missing values.
-
Arithmetic
+,-,*,/,//(integer division), postfix%(percent),min,max10 // 3 # 3 50% # 0.5 5 min 2 # 2 5 max 2 # 5 -
Comparison
is,is not,<,<=,>,>=x is 10 currency is not 'USD' a < b and < c # chained comparison -
Logical
and,or,not -
Membership
in,not intest a single value against a list;any in,all in,any not in,all not incompare a list against a list (see Extended Membership Operators).x in [1, 2, 3] x not in [1, 2, 3] -
Pattern Matching
matches(regular expression β matches anywhere in the string; use^β¦$to match the whole string)like(SQL-like patterns:%matches any sequence,_matches a single character)
email matches '^[a-z]+@[a-z]+[.][a-z]+$' name like 'Jo%'Because strings cannot contain a backslash, use character classes instead of escapes in regular expressions (
[.]instead of\.,[0-9]instead of\d). -
Null Handling
has valueNullableAmount has value
- if β¦ then β¦ else if β¦ else β¦ end (statement)
- when β¦ then β¦ when β¦ then β¦ else β¦ end (expression;
elseis required) - return
if x > 10 then
return 'big'
else if x > 5 then
return 'medium'
else
return 'small'
end
let color = 'red'
return when color is 'red' then 'Stop'
when color is 'green' then 'Go'
else 'Unknown'
end
Simpra supports directives that influence evaluation of the code that follows them:
- $mutable on/off β Enables or disables assignment to model properties (local variables are always mutable).
$mutable onis only allowed whenSimpraCompilerOptions.MutabilityOptionisDefaultImmutableorDefaultMutable; with the defaultImmutableoption it throws aSimpraException. See Compiler Options. - $case_sensitive on/off β Controls case sensitivity in string comparisons (
is,like,matches).
Example:
let str = 'abc'
$case_sensitive off
return str like 'A%' # true
-
Local variables declared with
letare always mutable:let x = 10 x += 5 # 15 x -= 2 # 13 x = x * 2 # 26 -
Model properties are read-only by default. To assign them, enable mutability in the compiler options and (unless the option is
DefaultMutable) turn it on with$mutable on:$mutable on Transfer.Amount = Transfer.Amount + 50
List indexes are 1-based:
let items = [10, 20, 30]
items[1] # 10
items[3] # 30
sum(items) # 60
length(items) # 3
An out-of-range index does not throw β for the list above, items[0] and items[4] return 0.
- Function calls:
method('hi') - Properties:
obj.prop - Indexers:
list[1],dictionary['key']
Simpra supports chained comparison expressions without repeating the left-hand operand:
let x = 5
return x > 0 and < 10 # true
This allows constructs like a < b and < c for concise range checks.
Simpra provides a set of internal functions out-of-the-box:
-
Math
abs(x)β absolute valueround(x)β round to the nearest integerround(x, decimals)β round to the given number of decimal places- For the smallest/largest of two values use the
min/maxoperators (a min b,a max b).
-
Lists
sum(list)β sum of valueslength(list)β number of elements
-
Strings
length(str)β number of characterssubstring(str, index)β the character at a 1-based positionsubstring(str, index, count)βcountcharacters starting at a 1-based position (substring('hello', 2, 3)is'ell')
-
Conversion
number(str)β converts a string (or boolean) to a numberdate(str)β converts a string to a datestring(x)β converts a number, boolean or date to a string
In addition to in and not in, Simpra supports any in, all in, any not in, and all not in. Both sides must be lists. Each not form is the negation of its positive form:
| Operator | Meaning |
|---|---|
a any in b |
at least one element of a is in b |
a all in b |
every element of a is in b |
a any not in b |
not (a any in b) β no element of a is in b |
a all not in b |
not (a all in b) β at least one element of a is not in b |
let values = [1, 2, 3]
[2, 4] any in values # true (2 is in values)
[2, 4] all in values # false (4 is not in values)
[4, 5] any not in values # true (neither 4 nor 5 is in values)
[2, 4] any not in values # false (2 is in values)
[2, 4] all not in values # true (4 is not in values)
[1, 2] all not in values # false (both are in values)
For a single value use in / not in: 2 in values.
The % operator in Simpra is postfix percent, not modulus.
50% # 0.5
200% # 2
This makes it easy to express percentages directly inside formulas.
The following words are keywords and cannot be used as variable names:
let, if, then, else, end, when, return, true, false, is, not, in, and, or, min, max, matches, like, has, value, on, off.
The examples below use this model and functions class:
public class Transfer
{
public decimal Amount { get; set; }
public string Currency { get; set; } = "";
}
public class TestModel
{
public Transfer Transfer { get; set; } = new();
}
public class TestFunctions
{
public string Upper(string str) => str.ToUpperInvariant();
}
var simpra = new Simpra();
var model = new TestModel { Transfer = new Transfer { Amount = 50, Currency = "USD" } };The compilerOptions argument is passed as null (defaults) unless an example needs specific options β see Compiler Options.
const string expression = "return Transfer.Amount * 2";
var result = simpra.Execute<int, TestModel>(model, expression, null);
Console.WriteLine(result); // Output: 100const string expression = """
let transfer = Transfer
return transfer.Amount > 100 and transfer.Currency is 'USD'
""";
var result = simpra.Execute<bool, TestModel>(model, expression, null);
Console.WriteLine(result); // Output: Falseconst string expression = """
let values = [10, 20, 30]
return sum(values) / length(values)
""";
var result = simpra.Execute<decimal, TestModel>(model, expression, null);
Console.WriteLine(result); // Output: 20const string expression = """
let code = 'abc123'
return code matches '^[a-zA-Z_][a-zA-Z_0-9]*$'
""";
var result = simpra.Execute<bool, TestModel>(model, expression, null);
Console.WriteLine(result); // Output: Trueconst string expression = """
let amount = Transfer.Amount
let divisor = 0
let result = when divisor is not 0 then amount / divisor else 0 end
return result
""";
var result = simpra.Execute<int, TestModel>(model, expression, null);
Console.WriteLine(result); // Output: 0const string expression = """
let str = 'hello'
return Upper(str) + ' WORLD'
""";
var result = simpra.Execute<string, TestModel, TestFunctions>(model, new TestFunctions(), expression);
Console.WriteLine(result); // Output: HELLO WORLDconst string expression = """
let a = 10
let b = 3
let half = 50%
return [ a // b, half, a min b, a max b ]
""";
var result = simpra.Execute<List<decimal>, TestModel>(model, expression, null);
Console.WriteLine(string.Join(", ", result)); // Output: 3, 0.50, 3, 10SimpraCompilerOptions controls how code is compiled. It is the compilerOptions argument of Execute, ExecuteAsync, CompileCode and CompileAsyncCode:
- The overloads without external functions (
Execute<TResult, TDataModel>,ExecuteAsync<TResult, TDataModel>,CompileCode<TResult, TDataModel>,CompileAsyncCode<TResult, TDataModel>) require it β passnullto use the defaults, or an options instance.ExecuteAsync<TResult, TDataModel>also requires aCancellationToken. Execute/ExecuteAsyncwith external functions take it as an optional argument.
// defaults (case-sensitive, immutable)
var result = simpra.Execute<bool, TestModel>(model, expression, null);
// custom options
var options = new SimpraCompilerOptions { StringComparisonOption = StringComparisonOption.IgnoreCase };
var result = simpra.Execute<bool, TestModel>(model, expression, options);| Option | Values | Default |
|---|---|---|
MutabilityOption |
Immutable β model properties can never be assigned; $mutable on throwsDefaultImmutable β read-only until $mutable onDefaultMutable β assignable unless $mutable off |
Immutable |
StringComparisonOption |
CaseSensitive, IgnoreCase (can be changed in code with $case_sensitive) |
CaseSensitive |
const string expression = """
$mutable on
Transfer.Amount = Transfer.Amount + 50
return true
""";
var options = new SimpraCompilerOptions { MutabilityOption = MutabilityOption.DefaultImmutable };
var model = new TestModel { Transfer = new Transfer { Amount = 100 } };
var result = simpra.Execute<bool, TestModel>(model, expression, options);
Console.WriteLine(result); // Output: True
Console.WriteLine(model.Transfer.Amount); // Output: 150Without MutabilityOption.DefaultImmutable (or DefaultMutable), the $mutable on line throws a SimpraException.
You can define custom functions (instance or static) and use them in expressions:
public class CustomFunctions
{
public static string Reverse(string input) => new string(input.Reverse().ToArray());
}
const string expression = "return Reverse('hello')";
var result = simpra.Execute<string, TestModel, CustomFunctions>(model, new CustomFunctions(), expression);
Console.WriteLine(result); // Output: ollehSimpra supports calling asynchronous methods (Task<T> / ValueTask<T>) within expressions. The Async suffix may be omitted, and a trailing CancellationToken parameter is supplied automatically:
public class AsyncFunctions
{
public Task<int> GetExchangeRateAsync() => Task.FromResult(3);
}
const string expression = "return GetExchangeRate() * 10";
var result = await simpra.ExecuteAsync<int, TestModel, AsyncFunctions>(model, new AsyncFunctions(), expression);
Console.WriteLine(result); // Output: 30Execute and ExecuteAsync parse and compile the code on every call. To evaluate the same code repeatedly, compile it once and reuse the delegate:
var isLargeTransfer = simpra.CompileCode<bool, TestModel>("return Transfer.Amount > 1000", null);
var result = isLargeTransfer(model);
Console.WriteLine(result); // Output: FalseAltaSoft.Simpra is licensed under the MIT License.