Skip to main content

C# SDK: Advanced Topics

Error handling, dependency injection, async patterns, and best practices for the Trix .NET SDK.

Overview

This page covers advanced topics for the Trix .NET SDK including error handling, dependency injection, async patterns, logging, and best practices for production applications.

Error Handling

The SDK provides typed exception classes for precise error handling:

ErrorHandling.cs
using Trix.Exceptions;

try
{
    var memory = await client.Memories.GetAsync("mem_invalid");
}
catch (NotFoundException ex)
{
    Console.WriteLine($"Memory not found: {ex.Message}");
}
catch (AuthenticationException ex)
{
    Console.WriteLine($"Invalid API key: {ex.Message}");
}
catch (RateLimitException ex)
{
    Console.WriteLine($"Rate limited. Retry after {ex.RetryAfterSeconds}s");
}
catch (ValidationException ex)
{
    Console.WriteLine($"Validation failed: {ex.Message}");
    foreach (var (field, errors) in ex.Errors ?? new Dictionary<string, string[]>())
    {
        Console.WriteLine($"  {field}: {string.Join(", ", errors)}");
    }
}
catch (PermissionException ex)
{
    Console.WriteLine($"Insufficient permissions: {ex.Message}");
}
catch (NetworkException ex)
{
    Console.WriteLine($"Network error: {ex.Message}");
}
catch (TrixTimeoutException ex)
{
    Console.WriteLine($"Request timed out: {ex.Message}");
}
catch (ServerException ex)
{
    Console.WriteLine($"Server error: {ex.StatusCode}");
}
catch (TrixException ex)
{
    Console.WriteLine($"Trix error: {ex.Message}");
    Console.WriteLine($"  Status: {ex.StatusCode}");
    Console.WriteLine($"  Request ID: {ex.RequestId}");
}

Exception Hierarchy

Exception TypeHTTP StatusDescription
AuthenticationException401Invalid or missing API key
PermissionException403Insufficient permissions for the operation
NotFoundException404Resource not found
ValidationException400/422Request validation failed
RateLimitException429Rate limit exceeded
ServerException5xxServer-side error
NetworkException-Network connectivity issues
TrixTimeoutException-Request timeout

Error Response Details

ErrorDetails.cs
catch (TrixException ex)
{
    // Access error details
    Console.WriteLine($"Error: {ex.Message}");
    Console.WriteLine($"Status Code: {ex.StatusCode}");
    Console.WriteLine($"Request ID: {ex.RequestId}");  // For support tickets

    // For validation errors
    if (ex is ValidationException validationEx)
    {
        foreach (var (field, errors) in validationEx.Errors ?? new())
        {
            Console.WriteLine($"  {field}: {string.Join(", ", errors)}");
        }
    }

    // For rate limits
    if (ex is RateLimitException rateLimitEx)
    {
        await Task.Delay(TimeSpan.FromSeconds(rateLimitEx.RetryAfterSeconds));
        // Retry the request
    }
}

Dependency Injection

Register the Trix client with the .NET dependency injection container:

Basic Registration

BasicDI.cs
// In Program.cs
builder.Services.AddSingleton(sp =>
{
    var configuration = sp.GetRequiredService<IConfiguration>();
    var loggerFactory = sp.GetRequiredService<ILoggerFactory>();

    var options = new TrixClientOptions
    {
        ApiKey = configuration["Trix:ApiKey"]!,
        LoggerFactory = loggerFactory
    };

    return new TrixClient(options);
});

Options Pattern

OptionsDI.cs
// In Program.cs
builder.Services.Configure<TrixClientOptions>(
    builder.Configuration.GetSection("Trix"));

builder.Services.AddSingleton(sp =>
{
    var options = sp.GetRequiredService<IOptions<TrixClientOptions>>().Value;
    options.LoggerFactory = sp.GetRequiredService<ILoggerFactory>();
    return new TrixClient(options);
});

// In appsettings.json
{
    "Trix": {
        "ApiKey": "gdb_your_api_key",
        "BaseUrl": "https://api.trixdb.com",
        "Timeout": "00:00:30",
        "MaxRetries": 3
    }
}

Using in Services

ServiceUsage.cs
public class MemoryService
{
    private readonly TrixClient _client;
    private readonly ILogger<MemoryService> _logger;

    public MemoryService(TrixClient client, ILogger<MemoryService> logger)
    {
        _client = client;
        _logger = logger;
    }

    public async Task<Memory> StoreUserPreference(string userId, string preference)
    {
        _logger.LogInformation("Storing preference for user {UserId}", userId);

        return await _client.Memories.CreateAsync(new CreateMemoryRequest
        {
            Content = preference,
            Tags = new List<string> { "preference", $"user:{userId}" },
            Metadata = new Dictionary<string, object>
            {
                ["user_id"] = userId
            }
        });
    }
}

Scoped Client for Multi-Tenancy

ScopedDI.cs
// Factory for tenant-scoped clients
builder.Services.AddScoped<TrixClient>(sp =>
{
    var httpContext = sp.GetRequiredService<IHttpContextAccessor>().HttpContext;
    var configuration = sp.GetRequiredService<IConfiguration>();
    var loggerFactory = sp.GetRequiredService<ILoggerFactory>();

    // Get tenant API key from context
    var tenantApiKey = httpContext?.Items["TenantApiKey"]?.ToString()
        ?? configuration["Trix:DefaultApiKey"]!;

    return new TrixClient(new TrixClientOptions
    {
        ApiKey = tenantApiKey,
        LoggerFactory = loggerFactory
    });
});

Cancellation

All async methods support cancellation tokens for cooperative cancellation:

Cancellation.cs
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

try
{
    var memories = await client.Memories.ListAsync(
        cancellationToken: cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation was cancelled");
}

// Cancel on demand
public async Task ProcessMemories(CancellationToken cancellationToken)
{
    await foreach (var memory in client.Memories.ListAllAsync(
        cancellationToken: cancellationToken))
    {
        if (cancellationToken.IsCancellationRequested)
            break;

        await ProcessMemory(memory, cancellationToken);
    }
}

// Link tokens
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    requestCancellationToken,
    applicationStoppingToken);

await client.Memories.CreateAsync(request, linkedCts.Token);

Logging

Configure logging via Microsoft.Extensions.Logging:

Logging.cs
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
    builder
        .AddConsole()
        .AddDebug()
        .SetMinimumLevel(LogLevel.Debug));

var options = new TrixClientOptions
{
    ApiKey = "gdb_your_api_key",
    LoggerFactory = loggerFactory
};

using var client = new TrixClient(options);

Debug logging outputs request/response details including URLs, headers, and timing information. Sensitive data like API keys is automatically redacted.

Structured Logging

StructuredLogging.cs
// In ASP.NET Core with Serilog
builder.Host.UseSerilog((context, config) =>
    config.ReadFrom.Configuration(context.Configuration));

// Log entries include:
// - Request method and URL
// - Response status code
// - Request duration
// - Request ID for tracing

Retry & Backoff

The SDK automatically retries failed requests with exponential backoff:

DefaultRetry.cs
// Default retry configuration:
// - MaxRetries: 3
// - Initial delay: 1 second
// - Max delay: 30 seconds
// - Exponential backoff with jitter

// Retryable scenarios:
// - RateLimitException (429)
// - NetworkException
// - TrixTimeoutException
// - ServerException (5xx)

Custom Retry Configuration

CustomRetry.cs
var options = new TrixClientOptions
{
    ApiKey = "gdb_your_api_key",
    MaxRetries = 5,  // Increase retry attempts
    Timeout = TimeSpan.FromSeconds(60)  // Increase timeout
};

using var client = new TrixClient(options);

Async Patterns

Parallel Operations

ParallelOps.cs
// Process multiple memories in parallel
var memoryIds = new[] { "mem_1", "mem_2", "mem_3" };

var tasks = memoryIds.Select(id => client.Memories.GetAsync(id));
var memories = await Task.WhenAll(tasks);

// Bounded parallelism
var semaphore = new SemaphoreSlim(5);  // Max 5 concurrent

await Parallel.ForEachAsync(memoryIds,
    new ParallelOptions { MaxDegreeOfParallelism = 5 },
    async (id, ct) =>
    {
        await semaphore.WaitAsync(ct);
        try
        {
            var memory = await client.Memories.GetAsync(id, ct);
            ProcessMemory(memory);
        }
        finally
        {
            semaphore.Release();
        }
    });

Streaming with IAsyncEnumerable

Streaming.cs
// Process all memories without loading into memory
await foreach (var memory in client.Memories.ListAllAsync())
{
    await ProcessAsync(memory);
}

// With LINQ
var important = client.Memories.ListAllAsync()
    .Where(m => m.Priority > 5)
    .Take(100);

await foreach (var memory in important)
{
    Console.WriteLine(memory.Content);
}

// Collect to list if needed
var allMemories = await client.Memories.ListAllAsync().ToListAsync();

Background Processing

BackgroundService.cs
public class MemoryIngestionService : BackgroundService
{
    private readonly TrixClient _client;
    private readonly ILogger<MemoryIngestionService> _logger;
    private readonly Channel<string> _queue;

    public MemoryIngestionService(
        TrixClient client,
        ILogger<MemoryIngestionService> logger)
    {
        _client = client;
        _logger = logger;
        _queue = Channel.CreateBounded<string>(1000);
    }

    public async ValueTask QueueAsync(string content)
    {
        await _queue.Writer.WriteAsync(content);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var content in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                await _client.Memories.CreateAsync(new CreateMemoryRequest
                {
                    Content = content
                }, stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to create memory");
            }
        }
    }
}

Best Practices

1. Reuse the Client

ReuseClient.cs
// DO: Create once, reuse everywhere
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<TrixClient>();
    }
}

// DON'T: Create new clients for each request
public async Task HandleRequest()
{
    using var client = new TrixClient(apiKey);  // Bad!
    await client.Memories.CreateAsync(...);
}

2. Use Bulk Operations

BulkOps.cs
// DO: Use bulk operations for multiple items
var memories = await client.Memories.BulkCreateAsync(items);

// DON'T: Create one at a time
foreach (var item in items)
{
    await client.Memories.CreateAsync(item);  // Slow!
}

3. Handle Rate Limits Gracefully

RateLimits.cs
// Implement retry with backoff
async Task<T> WithRetry<T>(Func<Task<T>> operation)
{
    for (int attempt = 0; attempt < 3; attempt++)
    {
        try
        {
            return await operation();
        }
        catch (RateLimitException ex)
        {
            if (attempt == 2) throw;
            await Task.Delay(TimeSpan.FromSeconds(ex.RetryAfterSeconds));
        }
    }
    throw new InvalidOperationException("Unreachable");
}

4. Use Cancellation Tokens

CancellationTokens.cs
// Always pass cancellation tokens through
public async Task<Memory> GetMemory(
    string id,
    CancellationToken cancellationToken = default)
{
    return await _client.Memories.GetAsync(id, cancellationToken);
}

5. Structure Your Metadata

Metadata.cs
// DO: Use consistent, structured metadata
var memory = await client.Memories.CreateAsync(new CreateMemoryRequest
{
    Content = content,
    Metadata = new Dictionary<string, object>
    {
        ["source"] = "api",
        ["version"] = "1.0",
        ["user_id"] = userId,
        ["created_by"] = "system"
    }
});

// Use tags for searchable categories
Tags = new List<string> { "user-pref", $"user:{userId}", "v1" }

Complete Example

Here's a complete example demonstrating production-ready patterns:

ProductionExample.cs
using Microsoft.Extensions.Logging;
using Trix;
using Trix.Models;
using Trix.Exceptions;

public class KnowledgeService
{
    private readonly TrixClient _client;
    private readonly ILogger<KnowledgeService> _logger;

    public KnowledgeService(TrixClient client, ILogger<KnowledgeService> logger)
    {
        _client = client;
        _logger = logger;
    }

    public async Task<List<Memory>> SearchWithContext(
        string query,
        string? spaceId = null,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Searching for: {Query}", query);

        try
        {
            // Search for relevant memories
            var results = await _client.Memories.ListAsync(new ListMemoriesRequest
            {
                Q = query,
                Mode = SearchMode.Hybrid,
                SpaceId = spaceId,
                Limit = 10,
                Threshold = 0.5
            }, cancellationToken);

            if (results.Data.Count == 0)
            {
                _logger.LogInformation("No results found");
                return new List<Memory>();
            }

            // Get context for top result
            var context = await _client.Graph.GetContextAsync(new ContextRequest
            {
                MemoryIds = new List<string> { results.Data[0].Id },
                ContextSize = 5,
                RelevanceThreshold = 0.6
            }, cancellationToken);

            // Combine results
            var allMemories = results.Data
                .Concat(context.Context?.RelatedMemories?
                    .Select(m => new Memory
                    {
                        Id = m.Id,
                        Content = m.Content
                    }) ?? Enumerable.Empty<Memory>())
                .DistinctBy(m => m.Id)
                .ToList();

            _logger.LogInformation("Found {Count} memories with context", allMemories.Count);
            return allMemories;
        }
        catch (RateLimitException ex)
        {
            _logger.LogWarning("Rate limited, retry after {Seconds}s", ex.RetryAfterSeconds);
            throw;
        }
        catch (TrixException ex)
        {
            _logger.LogError(ex, "Trix error during search: {RequestId}", ex.RequestId);
            throw;
        }
    }

    public async Task IngestBatch(
        IEnumerable<string> contents,
        string spaceId,
        CancellationToken cancellationToken = default)
    {
        var items = contents.Select(c => new CreateMemoryRequest
        {
            Content = c,
            SpaceId = spaceId
        }).ToList();

        if (items.Count == 0) return;

        _logger.LogInformation("Ingesting batch of {Count} memories", items.Count);

        // Process in batches of 100
        foreach (var batch in items.Chunk(100))
        {
            cancellationToken.ThrowIfCancellationRequested();

            var result = await _client.Memories.BulkCreateAsync(
                batch.ToList(),
                cancellationToken);

            _logger.LogInformation(
                "Batch complete: {Success} created, {Failed} failed",
                result.Success,
                result.Failed);

            if (result.Failed > 0 && result.Errors != null)
            {
                foreach (var error in result.Errors)
                {
                    _logger.LogWarning(
                        "Failed at index {Index}: {Message}",
                        error.Index,
                        error.Message);
                }
            }
        }
    }
}

Next Steps