tags: - extensions - custom-functions - Callable - integration
Extension Functions¶
Guide to creating custom native functions for EverSharp.
Overview¶
EverSharp allows you to extend its functionality by creating custom native functions in C#. These functions can be called from EverSharp scripts just like built-in functions.
Callable Interface¶
All native functions must implement the Callable interface:
namespace Elements.Libs.EverSharp;
public interface Callable
{
object? Call(Interpreter interpreter, List<object?> arguments);
int? Arity();
}
Methods¶
Call(Interpreter interpreter, List<object?> arguments)
- Executes the function logic
interpreter: The EverSharp interpreter instancearguments: List of arguments passed from EverSharp- Returns: The result value (or
null)
Arity()
- Returns the number of expected arguments
- Return
nullfor variable argument functions - Return specific number (e.g.,
2) for fixed argument count
Creating a Simple Function¶
Example: Square Function¶
Create a function that squares a number:
using Elements.Libs.EverSharp;
using Elements.Libs.EverSharp.Visitors;
namespace MyExtensions;
public class Square : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var number = (decimal)arguments[0];
return number * number;
}
public int? Arity() => 1; // Expects exactly 1 argument
}
Usage in EverSharp:
Example: Add Three Numbers¶
Function with multiple parameters:
public class AddThree : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var a = (decimal)arguments[0];
var b = (decimal)arguments[1];
var c = (decimal)arguments[2];
return a + b + c;
}
public int? Arity() => 3;
}
Usage:
Variable Argument Functions¶
For functions that accept any number of arguments, return null from Arity():
Example: Sum Variable Arguments¶
public class SumAll : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
decimal sum = 0;
foreach (var arg in arguments)
{
sum += (decimal)arg;
}
return sum;
}
public int? Arity() => null; // Variable arguments
}
Usage:
result1 = $SumAll(1, 2, 3); // 6
result2 = $SumAll(10, 20, 30, 40); // 100
result3 = $SumAll(5); // 5
Registering Extension Functions¶
Register custom functions when creating the EverSharpRunner:
var extensions = new Dictionary<string, Callable>
{
["$Square"] = new Square(),
["$AddThree"] = new AddThree(),
["$SumAll"] = new SumAll()
};
var runner = new EverSharpRunner(null, extensions);
// Now use in scripts
runner.Run(@"
x = $Square(5); // 25
y = $AddThree(1, 2, 3); // 6
z = $SumAll(10, 20, 30); // 60
");
Working with Different Types¶
String Functions¶
public class Capitalize : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var text = (string)arguments[0];
if (string.IsNullOrEmpty(text))
return text;
return char.ToUpper(text[0]) + text.Substring(1).ToLower();
}
public int? Arity() => 1;
}
Usage:
Boolean Functions¶
public class IsEven : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var number = (decimal)arguments[0];
return number % 2 == 0;
}
public int? Arity() => 1;
}
Usage:
Date Functions¶
public class DaysUntil : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var targetDate = (DateTime)arguments[0];
var today = DateTime.Today;
var days = (targetDate - today).Days;
return (decimal)days;
}
public int? Arity() => 1;
}
Usage:
Working with Arrays¶
Example: Array Average¶
using Elements.Libs.EverSharp.Expressions;
public class Average : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
if (arguments[0] is not InstanceArray array)
{
throw new Exception("Argument must be an array");
}
if (array.Length == 0)
{
return 0m;
}
decimal sum = 0;
foreach (var item in array)
{
sum += (decimal)item;
}
return sum / array.Length;
}
public int? Arity() => 1;
}
Usage:
Working with Objects¶
Example: Get Property Count¶
using Elements.Libs.EverSharp.Expressions;
public class PropertyCount : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
if (arguments[0] is not Instance obj)
{
throw new Exception("Argument must be an object");
}
return (decimal)obj.Properties.Count;
}
public int? Arity() => 1;
}
Usage:
person = {
name: "John",
age: 30,
email: "john@example.com",
};
count = $PropertyCount(person); // 3
Error Handling¶
Validate Arguments¶
public class SafeDivide : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var numerator = (decimal)arguments[0];
var denominator = (decimal)arguments[1];
if (denominator == 0)
{
throw new Exception("Division by zero");
}
return numerator / denominator;
}
public int? Arity() => 2;
}
Type Checking¶
public class TypeSafeFunction : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
// Check argument type
if (arguments[0] is not decimal number)
{
throw new Exception($"Expected number, got {arguments[0]?.GetType().Name ?? "null"}");
}
return number * 2;
}
public int? Arity() => 1;
}
Advanced Examples¶
Example 1: Database Query Function¶
using System.Data.SqlClient;
public class QueryDatabase : Callable
{
private readonly string connectionString;
public QueryDatabase(string connectionString)
{
this.connectionString = connectionString;
}
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var query = (string)arguments[0];
var results = new List<object>();
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
using (var command = new SqlCommand(query, connection))
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
var row = new Instance();
for (int i = 0; i < reader.FieldCount; i++)
{
var name = reader.GetName(i);
var value = reader.GetValue(i);
row.Set(new Token(TokenType.IDENTIFIER, name), value);
}
results.Add(row);
}
}
}
return new InstanceArray(results);
}
public int? Arity() => 1;
}
Registration:
var extensions = new Dictionary<string, Callable>
{
["$Query"] = new QueryDatabase("Server=localhost;Database=MyDb;...")
};
Usage:
customers = $Query("SELECT Id, Name, Email FROM Customers WHERE Active = 1");
activeCount = customers.Length;
firstCustomer = customers[0];
Example 2: HTTP Request Function¶
using System.Net.Http;
using System.Text.Json;
public class HttpGet : Callable
{
private static readonly HttpClient client = new HttpClient();
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var url = (string)arguments[0];
var response = client.GetStringAsync(url).Result;
// Parse JSON response to EverSharp object
var jsonDoc = JsonDocument.Parse(response);
return ConvertJsonToInstance(jsonDoc.RootElement);
}
private object? ConvertJsonToInstance(JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
var obj = new Instance();
foreach (var prop in element.EnumerateObject())
{
obj.Set(new Token(TokenType.IDENTIFIER, prop.Name),
ConvertJsonToInstance(prop.Value));
}
return obj;
case JsonValueKind.Array:
var arr = new List<object?>();
foreach (var item in element.EnumerateArray())
{
arr.Add(ConvertJsonToInstance(item));
}
return new InstanceArray(arr);
case JsonValueKind.String:
return element.GetString();
case JsonValueKind.Number:
return element.GetDecimal();
case JsonValueKind.True:
return true;
case JsonValueKind.False:
return false;
case JsonValueKind.Null:
return null;
default:
return null;
}
}
public int? Arity() => 1;
}
Usage:
Example 3: Logging Function¶
using Microsoft.Extensions.Logging;
public class LogMessage : Callable
{
private readonly ILogger logger;
public LogMessage(ILogger logger)
{
this.logger = logger;
}
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var level = (string)arguments[0];
var message = (string)arguments[1];
switch (level.ToLower())
{
case "debug":
logger.LogDebug(message);
break;
case "info":
logger.LogInformation(message);
break;
case "warning":
logger.LogWarning(message);
break;
case "error":
logger.LogError(message);
break;
default:
logger.LogInformation(message);
break;
}
return null;
}
public int? Arity() => 2;
}
Usage:
$LogMessage("info", "Starting calculation");
result = calculate();
$LogMessage("debug", "Result: " + $ToString(result));
Example 4: Configuration Reader¶
using Microsoft.Extensions.Configuration;
public class GetConfig : Callable
{
private readonly IConfiguration configuration;
public GetConfig(IConfiguration configuration)
{
this.configuration = configuration;
}
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var key = (string)arguments[0];
var value = configuration[key];
if (value == null)
return null;
// Try to parse as number
if (decimal.TryParse(value, out var number))
return number;
// Try to parse as boolean
if (bool.TryParse(value, out var boolean))
return boolean;
// Return as string
return value;
}
public int? Arity() => 1;
}
Usage:
maxRetries = $GetConfig("App:MaxRetries"); // 3
timeout = $GetConfig("App:Timeout"); // 30
apiUrl = $GetConfig("App:ApiUrl"); // "https://api.example.com"
Example 5: Cache Function¶
using System.Runtime.Caching;
public class CacheValue : Callable
{
private static readonly MemoryCache cache = MemoryCache.Default;
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var key = (string)arguments[0];
var value = arguments[1];
var expirationMinutes = arguments.Count > 2 ? (int)(decimal)arguments[2] : 60;
var policy = new CacheItemPolicy
{
AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(expirationMinutes)
};
cache.Set(key, value, policy);
return value;
}
public int? Arity() => null; // 2 or 3 arguments
}
public class GetCachedValue : Callable
{
private static readonly MemoryCache cache = MemoryCache.Default;
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var key = (string)arguments[0];
return cache.Get(key);
}
public int? Arity() => 1;
}
Usage:
// Cache value for 30 minutes
$CacheValue("calculation_result", expensiveResult, 30);
// Retrieve cached value
cached = $GetCachedValue("calculation_result");
if (cached != null) {
result = cached;
} else {
result = performCalculation();
$CacheValue("calculation_result", result, 30);
}
Function Library Pattern¶
Organize related functions in a class:
public static class MathExtensions
{
public static Dictionary<string, Callable> GetFunctions()
{
return new Dictionary<string, Callable>
{
["$Pow"] = new Power(),
["$Sqrt"] = new SquareRoot(),
["$Ceil"] = new Ceiling(),
["$Floor"] = new Floor()
};
}
private class Power : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var baseNum = (decimal)arguments[0];
var exponent = (decimal)arguments[1];
return (decimal)Math.Pow((double)baseNum, (double)exponent);
}
public int? Arity() => 2;
}
private class SquareRoot : Callable
{
public object? Call(Interpreter interpreter, List<object?> arguments)
{
var number = (decimal)arguments[0];
return (decimal)Math.Sqrt((double)number);
}
public int? Arity() => 1;
}
// ... more functions
}
Usage:
var extensions = MathExtensions.GetFunctions();
var runner = new EverSharpRunner(null, extensions);
runner.Run(@"
result1 = $Pow(2, 8); // 256
result2 = $Sqrt(144); // 12
");
Best Practices¶
1. Naming Conventions¶
// Good: Prefix with $ for functions
["$Calculate"] = new Calculate()
// Avoid: No prefix
["Calculate"] = new Calculate() // Might conflict with user variables
2. Null Handling¶
public object? Call(Interpreter interpreter, List<object?> arguments)
{
if (arguments[0] == null)
{
return null; // Or throw exception, depending on requirements
}
// Process non-null argument
var value = (decimal)arguments[0];
return value * 2;
}
3. Type Safety¶
public object? Call(Interpreter interpreter, List<object?> arguments)
{
if (arguments[0] is not decimal number)
{
throw new ArgumentException($"Expected decimal, got {arguments[0]?.GetType().Name}");
}
return number * 2;
}
4. Documentation¶
/// <summary>
/// Calculates the compound interest for a given principal, rate, and time period.
/// </summary>
/// <remarks>
/// Usage: $CompoundInterest(principal, annualRate, years)
/// Example: $CompoundInterest(1000, 0.05, 10) returns 1628.89
/// </remarks>
public class CompoundInterest : Callable
{
// Implementation
}
Testing Extension Functions¶
[Fact]
public void Square_ShouldReturnCorrectValue()
{
var square = new Square();
var interpreter = new Interpreter();
var result = square.Call(interpreter, new List<object?> { 5m });
Assert.Equal(25m, result);
}
[Fact]
public void Square_WithEverSharpRunner()
{
var extensions = new Dictionary<string, Callable>
{
["$Square"] = new Square()
};
var runner = new EverSharpRunner(null, extensions);
var result = runner.RunExpression("$Square(5)");
Assert.Equal(25m, result);
}
See Also¶
- EverSharpRunner API - Runner API reference
- Environment Setup - Data passing between C# and EverSharp
- Native Functions - Built-in function reference