> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gorules.io/llms.txt
> Use this file to discover all available pages before exploring further.

# C# Rules Engine

> Integrate GoRules into your .NET application.

Install the ZEN Engine and evaluate your first decision in C#.

## Installation

```bash theme={null}
dotnet add package GoRules.ZenEngine
```

## Basic usage

```csharp theme={null}
using GoRules.ZenEngine;

var ruleJson = File.ReadAllBytes("rules/pricing.json");

using var engine = new ZenEngine(loader: null, customNode: null);
var decision = engine.CreateDecision(new JsonBuffer(ruleJson));

var input = new JsonBuffer("""
    {
        "customer": { "tier": "gold", "yearsActive": 3 },
        "order": { "subtotal": 150, "items": 5 }
    }
    """);

var response = await decision.Evaluate(input, null);

Console.WriteLine(response.result);
// => {"discount":0.15,"freeShipping":true}

decision.Dispose();
```

## Loader

The loader pattern enables dynamic decision loading from any storage backend. Implement the `ZenDecisionLoaderCallback` interface with a `Task<JsonBuffer?> Load(string key)` method. Use `ConcurrentDictionary` to cache decisions for optimal performance.

### File system

```csharp theme={null}
using GoRules.ZenEngine;
using System.Collections.Concurrent;

var cache = new ConcurrentDictionary<string, byte[]>();

var loader = new FileLoader(cache);
using var engine = new ZenEngine(loader: loader, customNode: null);

var response = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null);
Console.WriteLine(response.result);

class FileLoader(ConcurrentDictionary<string, byte[]> cache) : ZenDecisionLoaderCallback
{
    public Task<JsonBuffer?> Load(string key)
    {
        var bytes = cache.GetOrAdd(key, k => File.ReadAllBytes(Path.Combine("rules", k)));
        return Task.FromResult<JsonBuffer?>(new JsonBuffer(bytes));
    }
}
```

### AWS S3

```csharp theme={null}
using GoRules.ZenEngine;
using Amazon.S3;
using Amazon.S3.Model;
using System.IO.Compression;
using System.Collections.Concurrent;

var s3 = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);
var rules = new ConcurrentDictionary<string, byte[]>();

var response = await s3.GetObjectAsync(new GetObjectRequest
{
    BucketName = "my-rules-bucket",
    Key = "decisions.zip"
});

using var zip = new ZipArchive(response.ResponseStream, ZipArchiveMode.Read);
foreach (var entry in zip.Entries.Where(e => !string.IsNullOrEmpty(e.Name)))
{
    using var ms = new MemoryStream();
    await using var stream = entry.Open();
    await stream.CopyToAsync(ms);
    rules[entry.FullName] = ms.ToArray();
}

using var engine = new ZenEngine(loader: new DictionaryLoader(rules), customNode: null);
var result = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null);
Console.WriteLine(result.result);

class DictionaryLoader(ConcurrentDictionary<string, byte[]> rules) : ZenDecisionLoaderCallback
{
    public Task<JsonBuffer?> Load(string key) =>
        Task.FromResult(rules.TryGetValue(key, out var bytes) ? new JsonBuffer(bytes) : null);
}
```

### Azure Blob Storage

```csharp theme={null}
using GoRules.ZenEngine;
using Azure.Storage.Blobs;
using System.IO.Compression;
using System.Collections.Concurrent;

var blobService = new BlobServiceClient(Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION"));
var blob = blobService.GetBlobContainerClient("rules").GetBlobClient("decisions.zip");
var download = await blob.DownloadContentAsync();
var rules = new ConcurrentDictionary<string, byte[]>();

using var zip = new ZipArchive(new MemoryStream(download.Value.Content.ToArray()), ZipArchiveMode.Read);
foreach (var entry in zip.Entries.Where(e => !string.IsNullOrEmpty(e.Name)))
{
    using var ms = new MemoryStream();
    await using var stream = entry.Open();
    await stream.CopyToAsync(ms);
    rules[entry.FullName] = ms.ToArray();
}

using var engine = new ZenEngine(loader: new DictionaryLoader(rules), customNode: null);
var result = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null);
Console.WriteLine(result.result);

class DictionaryLoader(ConcurrentDictionary<string, byte[]> rules) : ZenDecisionLoaderCallback
{
    public Task<JsonBuffer?> Load(string key) =>
        Task.FromResult(rules.TryGetValue(key, out var bytes) ? new JsonBuffer(bytes) : null);
}
```

### Google Cloud Storage

```csharp theme={null}
using GoRules.ZenEngine;
using Google.Cloud.Storage.V1;
using System.IO.Compression;
using System.Collections.Concurrent;

var storage = StorageClient.Create();
var rules = new ConcurrentDictionary<string, byte[]>();

using var zipStream = new MemoryStream();
await storage.DownloadObjectAsync("my-rules-bucket", "decisions.zip", zipStream);
zipStream.Position = 0;

using var zip = new ZipArchive(zipStream, ZipArchiveMode.Read);
foreach (var entry in zip.Entries.Where(e => !string.IsNullOrEmpty(e.Name)))
{
    using var ms = new MemoryStream();
    using var stream = entry.Open();
    await stream.CopyToAsync(ms);
    rules[entry.FullName] = ms.ToArray();
}

using var engine = new ZenEngine(new DictionaryLoader(rules), null);
var result = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null);
Console.WriteLine(result.result);

internal class DictionaryLoader(ConcurrentDictionary<string, byte[]> rules) : ZenDecisionLoaderCallback
{
    public Task<JsonBuffer?> Load(string key)
    {
        return Task.FromResult(rules.TryGetValue(key, out var bytes) ? new JsonBuffer(bytes) : null);
    }
}
```

## Async evaluation

Evaluation methods return `Task` for native async/await integration:

```csharp theme={null}
var tasks = inputs.Select(input =>
    decision.Evaluate(new JsonBuffer(input), null)
).ToArray();

var results = await Task.WhenAll(tasks);

foreach (var result in results)
{
    Console.WriteLine(result.result);
}
```

## Error handling

```csharp theme={null}
using GoRules.ZenEngine;

try
{
    var response = await decision.Evaluate(input, null);
    Console.WriteLine(response.result);
}
catch (ZenException.EvaluationException e)
{
    Console.Error.WriteLine($"Evaluation failed: {e.Message}");
}
catch (ZenException e)
{
    Console.Error.WriteLine($"Engine error: {e.Message}");
}
```

## Tracing

Enable tracing to inspect decision execution:

```csharp theme={null}
using GoRules.ZenEngine;

var options = new ZenEvaluateOptions(maxDepth: null, trace: true);

var response = await decision.Evaluate(input, options);

if (response.trace != null)
{
    foreach (var (nodeId, trace) in response.trace)
    {
        Console.WriteLine($"[{trace.order}] {trace.name}: {trace.output}");
    }
}

Console.WriteLine(response.performance);
// Total evaluation time
```

## Expression utilities

Evaluate ZEN expressions outside of a decision context:

```csharp theme={null}
using GoRules.ZenEngine;

// Standard expressions
var result = ZenUniffiMethods.EvaluateExpression(
    "a + b",
    new JsonBuffer("""{ "a": 5, "b": 3 }""")
);
// => 8

var total = ZenUniffiMethods.EvaluateExpression(
    "sum(items)",
    new JsonBuffer("""{ "items": [1, 2, 3, 4] }""")
);
// => 10

// Unary expressions (comparison against $)
var isValid = ZenUniffiMethods.EvaluateUnaryExpression(
    ">= 5",
    new JsonBuffer("""{ "$": 10 }""")
);
// => true

var inList = ZenUniffiMethods.EvaluateUnaryExpression(
    "'US', 'CA', 'MX'",
    new JsonBuffer("""{ "$": "US" }""")
);
// => true

// Compiled expression (reusable, better performance)
using var expr = ZenExpression.Compile("a + b * 2");
var output = expr.Evaluate(new JsonBuffer("""{ "a": 1, "b": 10 }"""));
Console.WriteLine(output);
// => 21
```

## Custom nodes

Extend the engine with custom logic by implementing `ZenCustomNodeCallback`:

```csharp theme={null}
using GoRules.ZenEngine;
using System.Text.Json;

// Register the custom node handler when creating the engine
using var engine = new ZenEngine(loader: new FileLoader(), customNode: new SumCustomNode());

class SumCustomNode : ZenCustomNodeCallback
{
    public Task<ZenEngineHandlerResponse> Handle(ZenEngineHandlerRequest request)
    {
        var input = JsonSerializer.Deserialize<JsonElement>(request.input.ToString());
        var sum = input.EnumerateObject()
            .Where(p => p.Value.ValueKind == JsonValueKind.Number)
            .Sum(p => p.Value.GetDouble());

        return Task.FromResult(new ZenEngineHandlerResponse(
            output: new JsonBuffer(JsonSerializer.Serialize(new { sum })),
            traceData: null
        ));
    }
}

class FileLoader : ZenDecisionLoaderCallback
{
    public Task<JsonBuffer?> Load(string key) =>
        Task.FromResult(File.Exists(key) ? new JsonBuffer(File.ReadAllBytes(key)) : null);
}
```

## Performance note

<Note>
  The C# bindings use UniFFI with P/Invoke for interoperability with the native Rust engine. This introduces some overhead compared to native Rust. Native libraries are bundled for Windows (x64), macOS (x64/ARM), and Linux (x64/ARM).
</Note>

## Best practices

**Use `using` for resource management.** `ZenEngine`, `ZenDecision`, and `ZenExpression` implement `IDisposable` to release native resources.

```csharp theme={null}
using var engine = new ZenEngine(loader: null, customNode: null);
// engine is automatically disposed at end of scope
```

**Initialize the engine once.** Create a single `ZenEngine` instance at application startup and reuse it for all evaluations.

**Implement a loader for dynamic decisions.** The loader pattern centralizes decision loading logic and enables caching with `ConcurrentDictionary`.

**Use `Task.WhenAll` for parallel evaluation.** Evaluate multiple decisions concurrently with async/await.
