using System.Net.Http.Json; using System.Text.Json; using System.Web; namespace Synor.Sdk.Database; /// /// Synor Database SDK client for C#. /// /// Provides multi-model database with Key-Value, Document, Vector, and Time Series stores. /// /// /// /// var db = new SynorDatabase(new DatabaseConfig { ApiKey = "your-api-key" }); /// /// // Key-Value operations /// await db.Kv.SetAsync("mykey", "myvalue"); /// var value = await db.Kv.GetAsync("mykey"); /// /// // Document operations /// var id = await db.Documents.CreateAsync("users", new { name = "Alice", age = 30 }); /// var doc = await db.Documents.GetAsync("users", id); /// /// db.Dispose(); /// /// public class SynorDatabase : IDisposable { private readonly DatabaseConfig _config; private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonOptions; private bool _disposed; public KeyValueStore Kv { get; } public DocumentStore Documents { get; } public VectorStore Vectors { get; } public TimeSeriesStore TimeSeries { get; } public SynorDatabase(DatabaseConfig config) { _config = config; _httpClient = new HttpClient { BaseAddress = new Uri(config.Endpoint), Timeout = config.Timeout }; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {config.ApiKey}"); _httpClient.DefaultRequestHeaders.Add("X-SDK-Version", "csharp/0.1.0"); _jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true }; Kv = new KeyValueStore(this); Documents = new DocumentStore(this); Vectors = new VectorStore(this); TimeSeries = new TimeSeriesStore(this); } /// /// Key-Value store operations. /// public class KeyValueStore { private readonly SynorDatabase _db; internal KeyValueStore(SynorDatabase db) => _db = db; public async Task GetAsync(string key, CancellationToken ct = default) { var response = await _db.GetAsync($"/kv/{Uri.EscapeDataString(key)}", ct); return response.Value; } public async Task SetAsync(string key, object value, int? ttl = null, CancellationToken ct = default) { var body = new Dictionary { ["key"] = key, ["value"] = value }; if (ttl.HasValue) body["ttl"] = ttl.Value; await _db.PutAsync($"/kv/{Uri.EscapeDataString(key)}", body, ct); } public async Task DeleteAsync(string key, CancellationToken ct = default) { await _db.DeleteAsync($"/kv/{Uri.EscapeDataString(key)}", ct); } public async Task> ListAsync(string prefix, CancellationToken ct = default) { var response = await _db.GetAsync($"/kv?prefix={Uri.EscapeDataString(prefix)}", ct); return response.Items ?? new List(); } } /// /// Document store operations. /// public class DocumentStore { private readonly SynorDatabase _db; internal DocumentStore(SynorDatabase db) => _db = db; public async Task CreateAsync(string collection, object document, CancellationToken ct = default) { var response = await _db.PostAsync( $"/collections/{Uri.EscapeDataString(collection)}/documents", document, ct); return response.Id; } public async Task GetAsync(string collection, string id, CancellationToken ct = default) { return await _db.GetAsync( $"/collections/{Uri.EscapeDataString(collection)}/documents/{Uri.EscapeDataString(id)}", ct); } public async Task UpdateAsync(string collection, string id, object update, CancellationToken ct = default) { await _db.PatchAsync( $"/collections/{Uri.EscapeDataString(collection)}/documents/{Uri.EscapeDataString(id)}", update, ct); } public async Task DeleteAsync(string collection, string id, CancellationToken ct = default) { await _db.DeleteAsync( $"/collections/{Uri.EscapeDataString(collection)}/documents/{Uri.EscapeDataString(id)}", ct); } public async Task> QueryAsync(string collection, DocumentQuery query, CancellationToken ct = default) { var response = await _db.PostAsync( $"/collections/{Uri.EscapeDataString(collection)}/query", query, ct); return response.Documents ?? new List(); } } /// /// Vector store operations. /// public class VectorStore { private readonly SynorDatabase _db; internal VectorStore(SynorDatabase db) => _db = db; public async Task UpsertAsync(string collection, IEnumerable vectors, CancellationToken ct = default) { await _db.PostAsync( $"/vectors/{Uri.EscapeDataString(collection)}/upsert", new { vectors = vectors }, ct); } public async Task> SearchAsync(string collection, double[] vector, int k, CancellationToken ct = default) { var response = await _db.PostAsync( $"/vectors/{Uri.EscapeDataString(collection)}/search", new { vector, k }, ct); return response.Results ?? new List(); } public async Task DeleteAsync(string collection, IEnumerable ids, CancellationToken ct = default) { await _db.DeleteAsync($"/vectors/{Uri.EscapeDataString(collection)}", new { ids = ids }, ct); } } /// /// Time series store operations. /// public class TimeSeriesStore { private readonly SynorDatabase _db; internal TimeSeriesStore(SynorDatabase db) => _db = db; public async Task WriteAsync(string series, IEnumerable points, CancellationToken ct = default) { await _db.PostAsync( $"/timeseries/{Uri.EscapeDataString(series)}/write", new { points = points }, ct); } public async Task> QueryAsync( string series, TimeRange range, Aggregation? aggregation = null, CancellationToken ct = default) { var body = new Dictionary { ["range"] = range }; if (aggregation != null) body["aggregation"] = aggregation; var response = await _db.PostAsync( $"/timeseries/{Uri.EscapeDataString(series)}/query", body, ct); return response.Points ?? new List(); } } /// /// Perform a health check. /// public async Task HealthCheckAsync(CancellationToken ct = default) { try { var response = await GetAsync("/health", ct); return response.Status == "healthy"; } catch { return false; } } public bool IsClosed => _disposed; public void Dispose() { if (!_disposed) { _httpClient.Dispose(); _disposed = true; } GC.SuppressFinalize(this); } // Private HTTP methods internal async Task GetAsync(string path, CancellationToken ct) { return await ExecuteWithRetryAsync(async () => { var response = await _httpClient.GetAsync(path, ct); await EnsureSuccessAsync(response); return await response.Content.ReadFromJsonAsync(_jsonOptions, ct) ?? throw new DatabaseException("Failed to deserialize response"); }); } internal async Task PostAsync(string path, object body, CancellationToken ct) { return await ExecuteWithRetryAsync(async () => { var response = await _httpClient.PostAsJsonAsync(path, body, _jsonOptions, ct); await EnsureSuccessAsync(response); var content = await response.Content.ReadAsStringAsync(ct); if (string.IsNullOrEmpty(content)) return default!; return JsonSerializer.Deserialize(content, _jsonOptions)!; }); } internal async Task PutAsync(string path, object body, CancellationToken ct) { return await ExecuteWithRetryAsync(async () => { var response = await _httpClient.PutAsJsonAsync(path, body, _jsonOptions, ct); await EnsureSuccessAsync(response); var content = await response.Content.ReadAsStringAsync(ct); if (string.IsNullOrEmpty(content)) return default!; return JsonSerializer.Deserialize(content, _jsonOptions)!; }); } internal async Task PatchAsync(string path, object body, CancellationToken ct) { return await ExecuteWithRetryAsync(async () => { var request = new HttpRequestMessage(HttpMethod.Patch, path) { Content = JsonContent.Create(body, options: _jsonOptions) }; var response = await _httpClient.SendAsync(request, ct); await EnsureSuccessAsync(response); var content = await response.Content.ReadAsStringAsync(ct); if (string.IsNullOrEmpty(content)) return default!; return JsonSerializer.Deserialize(content, _jsonOptions)!; }); } internal async Task DeleteAsync(string path, CancellationToken ct) { await ExecuteWithRetryAsync(async () => { var response = await _httpClient.DeleteAsync(path, ct); await EnsureSuccessAsync(response); return true; }); } internal async Task DeleteAsync(string path, object body, CancellationToken ct) { await ExecuteWithRetryAsync(async () => { var request = new HttpRequestMessage(HttpMethod.Delete, path) { Content = JsonContent.Create(body, options: _jsonOptions) }; var response = await _httpClient.SendAsync(request, ct); await EnsureSuccessAsync(response); return true; }); } private async Task ExecuteWithRetryAsync(Func> operation) { Exception? lastError = null; for (int attempt = 0; attempt < _config.Retries; attempt++) { try { return await operation(); } catch (Exception ex) { lastError = ex; if (_config.Debug) Console.WriteLine($"Attempt {attempt + 1} failed: {ex.Message}"); if (attempt < _config.Retries - 1) { await Task.Delay(TimeSpan.FromSeconds(1 << attempt)); } } } throw lastError ?? new DatabaseException("Unknown error"); } private async Task EnsureSuccessAsync(HttpResponseMessage response) { if (!response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); var error = JsonSerializer.Deserialize>(content, _jsonOptions); var message = error?.GetValueOrDefault("message")?.ToString() ?? $"HTTP {(int)response.StatusCode}"; var code = error?.GetValueOrDefault("code")?.ToString(); throw new DatabaseException(message, code, (int)response.StatusCode); } } }