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:
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 Type | HTTP Status | Description |
|---|---|---|
AuthenticationException | 401 | Invalid or missing API key |
PermissionException | 403 | Insufficient permissions for the operation |
NotFoundException | 404 | Resource not found |
ValidationException | 400/422 | Request validation failed |
RateLimitException | 429 | Rate limit exceeded |
ServerException | 5xx | Server-side error |
NetworkException | - | Network connectivity issues |
TrixTimeoutException | - | Request timeout |
Error Response Details
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
// 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
// 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
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
// 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:
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:
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
// 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 tracingRetry & Backoff
The SDK automatically retries failed requests with exponential backoff:
// 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
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
// 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
// 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
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
// 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
// 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
// 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
// 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
// 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:
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
- REST API - Direct HTTP access for any language
- Best Practices Guide - General memory management tips
- GitHub Repository - Source code and issues