$cs — C# Script Directive
Executes a C# code snippet compiled at runtime using Roslyn scripting. Provides full .NET access including LINQ, System.Text.Json, and registered DI services.
| Property | Value |
|---|---|
| Directive name | cs |
| Required isolation level | Trusted (minimum) |
| Project | Scripting.Cs.Services |
| Engine | Microsoft.CodeAnalysis.CSharp.Scripting (Roslyn) |
| Syntax form | Template literal: $cs`...script...` |
| Return type | Any — converted to string for expression output |
Syntax
Inline expression
{@ $cs`Math.Round(Ctx.Input.Current.amount * 1.2m, 2)` }
Multi-line script with return
{@ $cs`
var rate = decimal.Parse(Ctx.Env["VAT_RATE"] ?? "0.20");
var net = (decimal)Ctx.Input.Current.amount;
return (net * rate).ToString("F2");
` }
Trusted isolation required
$cs requires Trusted isolation. This is the highest isolation level — only grant it to workflows authored by verified developers, not end-user-created workflows. Unlike $js (sandboxed Jint), C# scripts run as real .NET code with access to the full BCL.
Script Globals Reference
Scripts receive a Ctx globals object with pre-resolved context:
| Global | Type | Equivalent directive |
|---|---|---|
Ctx.Tenant | ITenantMetadata | $ctx.tenant.* |
Ctx.User | IUserClaims | $ctx.user.* |
Ctx.Env | IReadOnlyDictionary<string,string> | $ctx.env.* |
Ctx.Now | DateTimeOffset | $ctx.now |
Ctx.Input | INodeInputAccessor | $input.* |
Ctx.Output | INodeOutputAccessor | $output.* |
Ctx.Exec | INodeExecutionFacts | $exec.* |
Ctx.Vars | IExecutionMemory | $var.* |
Ctx.Services | IServiceProvider | — |
Complex Scenarios
Scenario 1 — HMRC PAYE Calculation with Decimal Precision
UK payroll tax requires precise decimal arithmetic — C# avoids floating-point issues inherent in JavaScript:
{@ $cs`
var gross = (decimal)Ctx.Input.Current.grossPay;
var period = (string)Ctx.Input.Current.payPeriod; // "monthly" | "weekly"
var annualised = period == "weekly" ? gross * 52m : gross * 12m;
var personalAllowance = 12570m;
decimal tax = 0m;
var taxable = Math.Max(annualised - personalAllowance, 0m);
if (taxable > 125140m) tax += (taxable - 125140m) * 0.45m;
if (taxable > 37700m) tax += (Math.Min(taxable, 125140m) - 37700m) * 0.40m;
tax += Math.Min(taxable, 37700m) * 0.20m;
// Return period tax (not annualised)
var periodTax = period == "weekly" ? tax / 52m : tax / 12m;
return periodTax.ToString("F2");
` }
Scenario 2 — Regex-Based Document Classification
Classify an incoming document by scanning its reference field against known patterns:
{@ $cs`
using System.Text.RegularExpressions;
var reference = (string)(Ctx.Input.Current.reference ?? "");
if (Regex.IsMatch(reference, @"^INV-\d{4}-\d+$")) return "invoice";
if (Regex.IsMatch(reference, @"^PO-\d{6}$")) return "purchase_order";
if (Regex.IsMatch(reference, @"^CR-\d+$")) return "credit_note";
return "unknown";
` }
Scenario 3 — LINQ Aggregation Over Input Items
Use LINQ to aggregate statistics from a collection of payroll records:
{@ $cs`
using System.Linq;
using System.Text.Json;
var items = Ctx.Input.Items; // IEnumerable<dynamic>
var totals = items
.GroupBy(i => (string)i.department)
.Select(g => new {
Department = g.Key,
HeadCount = g.Count(),
TotalGross = g.Sum(i => (decimal)i.grossPay)
})
.OrderByDescending(r => r.TotalGross)
.ToList();
return JsonSerializer.Serialize(totals);
` }
Scenario 4 — Custom Service Injection via IServiceProvider
Resolve a registered DI service to perform domain-specific validation:
{@ $cs`
var svc = Ctx.Services.GetRequiredService<IPayrollRuleEngine>();
var employeeId = (int)Ctx.Input.Current.employeeId;
var period = (string)Ctx.Input.Current.payPeriod;
var result = await svc.ValidateAsync(employeeId, period, CancellationToken.None);
return result.IsValid ? "valid" : string.Join("; ", result.Errors);
` }
// Async scripts are fully supported — just use await
Scenario 5 — Cryptographic Hash for Idempotency
Generate a deterministic SHA-256 hash from key fields to detect duplicate submissions:
{@ $cs`
using System.Security.Cryptography;
using System.Text;
var key = string.Concat(
Ctx.Input.Current.supplierId, "|",
Ctx.Input.Current.invoiceNumber, "|",
Ctx.Input.Current.invoiceDate
);
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(key));
return Convert.ToHexString(bytes).ToLower();
` }
// Deterministic key — same invoice always produces same hash
Scenario 6 — XML Parsing for Legacy System Integration
Parse an XML response from a legacy system and extract a specific field:
{@ $cs`
using System.Xml.Linq;
var xmlString = (string)Ctx.Output["LegacySystem"].body;
var doc = XDocument.Parse(xmlString);
var ns = XNamespace.Get("http://schemas.acme.com/payroll/v1");
var employeeId = doc
.Descendants(ns + "Employee")
.FirstOrDefault()
?.Attribute("ID")
?.Value;
return employeeId ?? throw new InvalidOperationException("Employee ID not found in XML response");
` }
Common Errors
| Error | Cause | Fix |
|---|---|---|
IsolationViolation: $cs requires Trusted | Node isolation level is Safe or Sandboxed | Set node isolation to Trusted; restrict to developer-authored workflows |
CompilationError: CS0234 | Missing using statement for a namespace | Add the using statement inside the script block |
RuntimeError: InvalidCastException | Dynamic property cast to wrong type | Use safe casts: (decimal?)(double?)Ctx.Input.Current.amount ?? 0m |
| Script hangs | Async deadlock or service call without cancellation | Always pass CancellationToken.None (or the provided token) to async calls |
ServiceNotFound: IMyService | Service not registered in DI | Register the service in AddExpressionEvaluation() or app DI setup |