> ## 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` argument accepts a `ZenLoader` that resolves decisions by key. Use `ZenLoader.Callback` to load from any storage backend, or a configuration variant (`Static`, `Filesystem`, `Zip`) to pre-load and pre-compile all decisions when you create the engine.

### Loader configurations

Prefer a configuration when your decisions are known up front. The engine compiles them once, so evaluations skip loading and parsing entirely.

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

// Static: decisions provided as an in-memory map
using var staticEngine = new ZenEngine(loader: new ZenLoader.Static(new Dictionary<string, JsonBuffer>
{
    ["pricing.json"] = new JsonBuffer(File.ReadAllBytes("rules/pricing.json"))
}));

// Filesystem: decisions resolved relative to a directory
using var fsEngine = new ZenEngine(loader: new ZenLoader.Filesystem("rules"));

// Zip: decisions extracted from a zip archive
using var zipEngine = new ZenEngine(loader: new ZenLoader.Zip(File.ReadAllBytes("decisions.zip")));

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

### File system

Implement the `ZenDecisionLoaderCallback` interface with a `Task<JsonBuffer?> Load(string key)` method and wrap it in `ZenLoader.Callback`. Return `null` when the decision does not exist. Use `ConcurrentDictionary` to cache decisions for optimal performance.

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

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

using var engine = new ZenEngine(loader: new ZenLoader.Callback(new FileLoader(cache)), 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;

var s3 = new AmazonS3Client(Amazon.RegionEndpoint.USEast1);

// Download all decisions as a zip archive at startup
var response = await s3.GetObjectAsync(new GetObjectRequest
{
    BucketName = "my-rules-bucket",
    Key = "decisions.zip"
});

using var ms = new MemoryStream();
await response.ResponseStream.CopyToAsync(ms);

// ZenLoader.Zip extracts and pre-compiles every decision in the archive
using var engine = new ZenEngine(loader: new ZenLoader.Zip(ms.ToArray()), customNode: null);
var result = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null);
Console.WriteLine(result.Result);
```

### Azure Blob Storage

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

var blobService = new BlobServiceClient(Environment.GetEnvironmentVariable("AZURE_STORAGE_CONNECTION"));
var blob = blobService.GetBlobContainerClient("rules").GetBlobClient("decisions.zip");

// Download all decisions as a zip archive at startup
var download = await blob.DownloadContentAsync();

// ZenLoader.Zip extracts and pre-compiles every decision in the archive
using var engine = new ZenEngine(loader: new ZenLoader.Zip(download.Value.Content.ToArray()), customNode: null);
var result = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null);
Console.WriteLine(result.Result);
```

### Google Cloud Storage

```csharp theme={null}
using GoRules.ZenEngine;
using Google.Cloud.Storage.V1;

var storage = StorageClient.Create();

// Download all decisions as a zip archive at startup
using var zipStream = new MemoryStream();
await storage.DownloadObjectAsync("my-rules-bucket", "decisions.zip", zipStream);

// ZenLoader.Zip extracts and pre-compiles every decision in the archive
using var engine = new ZenEngine(loader: new ZenLoader.Zip(zipStream.ToArray()), customNode: null);
var result = await engine.Evaluate("pricing.json", new JsonBuffer("{}"), null);
Console.WriteLine(result.Result);
```

## 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);
}
```

## Batch evaluation

Use `EvaluateBatch` to evaluate many contexts in a single call. Each request pairs a decision key with a context, and each result reports its own success or error:

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

var requests = new List<ZenBatchRequest>
{
    new ZenBatchRequest("pricing.json", new JsonBuffer("""{ "order": { "subtotal": 150 } }""")),
    new ZenBatchRequest("pricing.json", new JsonBuffer("""{ "order": { "subtotal": 40 } }"""))
};

var results = await engine.EvaluateBatch(requests, null);

foreach (var result in results)
{
    if (result.Success)
    {
        Console.WriteLine(result.Data!.Result);
    }
    else
    {
        Console.Error.WriteLine($"Evaluation failed: {result.Error}");
    }
}
```

## 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 ZenLoader.Callback(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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.