A modern, production-grade .NET/C# client library for SurrealDB with connection pooling, typed exception handling, and flexible protocol support (HTTP and WebSocket).
For package users, start here:
| Document | Purpose |
|---|---|
| docs/consumer/README.md | Main entry point - What is SurrealDB.Client? |
| docs/consumer/GETTING_STARTED.md | Installation, connection, first queries |
| docs/consumer/API_REFERENCE.md | Complete API documentation |
| docs/consumer/EXAMPLES.md | Real-world code examples |
| docs/consumer/SECURITY.md | Security best practices |
| docs/consumer/CHANGELOG.md | Version history and upgrade guide |
| Document | Focus |
|---|---|
| ARCHITECTURE.md | Architectural design and EF Core comparison |
| DESIGN_DECISIONS.md | Key design choices and rationale |
| STATE_MANAGEMENT.md | Entity states, change tracking, session lifecycle |
| QUERY_COMPOSITION.md | IQueryable API, query building, expression translation |
| LOADING_PATTERNS.md | Include/Lazy/Explicit loading patterns |
| INTERCEPTORS.md | Middleware and query interception |
| QUERY_CACHING.md | Multi-level caching strategies |
| DIAGNOSTICS.md | Monitoring, profiling, observability |
| Document | Focus |
|---|---|
| MIGRATIONS.md | Schema versioning, migrations, rollback |
| PLUGINS.md | Plugin architecture and extensibility |
| DATALOADER.md | Batch loading, N+1 prevention |
| EVENT_SOURCING.md | Event sourcing, event replay, snapshots |
- EF Core-Inspired State Management: ISurrealDbSession with automatic change tracking
- Entity Change Tracking: Snapshot-based detection with property-level granularity
- Composable Queries: IQueryable API with deferred execution
- Protocol Flexibility: HTTP and WebSocket support with runtime selection
- Real-Time Subscriptions: Live queries with change notifications
- Transactions: ACID-compliant with isolation level control
- Optimistic Concurrency: Version tokens for conflict detection
- Typed Exceptions: Database-agnostic error hierarchy
- Connection Pooling: Intelligent pooling with health checking
- Multi-Serializer: System.Text.Json, Newtonsoft.Json, or custom
Core Features:
- β Unit of Work pattern (ISurrealDbSession)
- β Automatic change tracking with snapshots
- β IQueryable composition (deferred execution)
- β Include/Lazy/Explicit loading patterns
- β Advanced interceptors and middleware
- β Multi-level caching (plan/compiled/result)
- β Optimistic concurrency tokens
- β Typed exceptions
- β Protocol abstraction (HTTP/WebSocket)
- β Real-time subscriptions
- β Comprehensive diagnostics
Enterprise Features:
- β Migrations: Schema versioning with rollback
- β Security: RLS, field encryption, audit trails, compliance
- β Plugins: Extensible plugin architecture
- β DataLoader: Batch loading, N+1 prevention
- β Event Sourcing: Event store, event replay, snapshots
Unique Competitive Advantages:
- π Real-time subscriptions (not in EF Core)
- π Protocol abstraction (HTTP/WebSocket)
- π Multi-serializer support
- π 99% bandwidth efficiency (change tracking)
- SurrealDB 3.0 or newer (required - 1.x and 2.x not supported)
- Features like UPSERT syntax depend on SurrealDB 3.0+ enhancements
- Version validation occurs on
ConnectAsync()with clear error messaging - Update your SurrealDB deployment to 3.0+ before using this client
dotnet add package SurrealDB.Clientusing SurrealDB.Client;
// Create and connect client
var client = new SurrealDbClient("surreal://localhost:8000");
await client.ConnectAsync();
// Authenticate
await client.AuthenticateAsync(new UsernamePasswordAuth("user", "password"));
// Create session (Unit of Work)
using var session = client.CreateSession();
// Create entity
var user = new User { Name = "John", Email = "john@example.com" };
session.Add(user);
// Query with composition
var adults = await session.Set<User>()
.Where(u => u.Age >= 18)
.OrderBy(u => u.Name)
.ToListAsync();
// Modify (automatic change tracking)
user.Email = "newemail@example.com";
await session.SaveChangesAsync(); // Only Email property sent to server
// Real-time subscriptions
using var subscription = await client.SubscribeAsync<User>(
q => q.Where(u => u.Status == "online")
);
await foreach (var change in subscription.GetChangesAsync())
{
Console.WriteLine($"Change: {change.Action} - {change.Record.Name}");
}
// Cleanup
await client.DisconnectAsync();A bounded context managing a coherent set of changes:
using var session = client.CreateSession();
// Track changes
var user = await session.FindAsync<User>("user:1");
user.Email = "new@example.com"; // Automatically tracked
// Atomic commit
await session.SaveChangesAsync();
// Only changed properties sent β bandwidth efficient| State | Meaning | SaveChangesAsync() Action |
|---|---|---|
| Detached | Not tracked | None |
| Added | New entity | INSERT |
| Unchanged | Loaded, no changes | None |
| Modified | Loaded, then changed | UPDATE (only changed properties) |
| Deleted | Marked for deletion | DELETE |
Automatic snapshot-based change detection:
var user = await session.FindAsync<User>("user:1");
// Snapshot captured
user.Email = "new@test.com"; // Property changed
user.UpdatedAt = DateTime.UtcNow;
var entry = session.ChangeTracker.Entry(user);
var changed = entry.GetModifiedProperties(); // ["Email", "UpdatedAt"]
await session.SaveChangesAsync();
// UPDATE users:1 SET Email = ..., UpdatedAt = ...
// (Only changed properties sent, not entire object)Build complex queries step-by-step without execution:
// Composable extensions
public static IQueryable<User> Active(this IQueryable<User> query)
=> query.Where(u => u.Status == "active");
public static IQueryable<User> Adults(this IQueryable<User> query)
=> query.Where(u => u.Age >= 18);
// Fluent usage - single query to server
var results = await session.Set<User>()
.Active()
.Adults()
.OrderBy(u => u.Name)
.ToListAsync();Prevent silent overwrites:
public class User
{
public string Id { get; set; }
[ConcurrencyToken] // Auto-managed by server
public long Version { get; set; }
public string Email { get; set; }
}
try
{
user.Email = "new@example.com";
await session.SaveChangesAsync();
}
catch (ConcurrencyException)
{
// Handle conflict - reload and retry
await session.Entry(user).ReloadAsync();
}Scenario: Update 1 property of 20-property object
Without change tracking:
Request size: 20KB (entire object)
With change tracking:
Request size: 200B (only changed property)
Efficiency: 99% bandwidth reduction
| Operation | Time | Notes |
|---|---|---|
| Connection setup | <1s | Pooled connections |
| Authentication | <500ms | Token validation |
| Simple query | <50ms | 100 records |
| Update (1 property) | <100ms | With change tracking |
| Batch (100 records) | <200ms | Single transaction |
var mockSession = new Mock<ISurrealDbSession>();
var repository = new UserRepository(mockSession.Object);
var user = await repository.GetUserAsync("user:1");
Assert.NotNull(user);[Collection("Database")]
public class UserRepositoryTests : IAsyncLifetime
{
private ISurrealDbSession _session;
[Fact]
public async Task CreateAndRetrieve_Succeeds()
{
var user = new User { Name = "Test" };
_session.Add(user);
await _session.SaveChangesAsync();
var retrieved = await _session.FindAsync<User>(user.Id);
Assert.NotNull(retrieved);
}
}var client = new SurrealDbClient("surreal://user:password@localhost:8000");
await client.ConnectAsync();
using var session = client.CreateSession();
// Use session for Unit of Workservices.AddSurrealDbClient(options =>
{
options.ConnectionString = configuration["Database:ConnectionString"];
options.EnableLogging = true;
options.PoolSize = 10;
});using var session = client.CreateSession();
var user = await session.FindAsync<User>("user:1");
user.Email = "newemail@example.com";
await session.SaveChangesAsync(); // Efficient: only Email sentusing var session = client.CreateSession();
var user = new User { Name = "John", Email = "john@example.com" };
session.Add(user);
var order = new Order { UserId = user.Id, Amount = 100 };
session.Add(order);
await session.SaveChangesAsync(); // Atomic transactionusing var session = client.CreateSession();
var users = await session.Set<User>()
.Where(u => u.Status == "inactive")
.ToListAsync();
foreach (var user in users)
user.Status = "active";
await session.SaveChangesAsync(); // Single transaction for allusing var subscription = await client.SubscribeAsync<User>(
q => q.Where(u => u.Status == "online")
);
await foreach (var change in subscription.GetChangesAsync())
{
if (change.Action == ChangeAction.Create)
Console.WriteLine($"New: {change.Record.Name}");
}// EF Core
using var context = new AppDbContext();
var users = await context.Users.Where(u => u.Active).ToListAsync();
// SurrealDB.Client - Nearly identical API!
using var session = client.CreateSession();
var users = await session.Set<User>()
.Where(u => u.Status == "active")
.ToListAsync();// Raw queries still supported
var results = await client.QueryAsync<User>(
"SELECT * FROM users WHERE age >= $age",
new { age = 18 }
);try
{
await session.SaveChangesAsync();
}
catch (UniqueConstraintException ex)
{
logger.LogWarning($"Duplicate: {ex.Details}");
}
catch (ConcurrencyException ex)
{
await session.Entry(ex.Entity).ReloadAsync();
}
catch (SurrealDbException ex)
{
logger.LogError($"Error: {ex.Message}");
}- ARCHITECTURE.md - Architectural design and EF Core comparison
- STATE_MANAGEMENT.md - Entity states, change tracking, session lifecycle, performance
- QUERY_COMPOSITION.md - IQueryable API, expression translation, optimization patterns
- DESIGN_DECISIONS.md - Key architectural decisions and rationale
MIT License - see LICENSE for details
- Official: surrealdb.com
- Docs: surrealdb.com/docs
- Discord: discord.gg/surrealdb
Architecture inspired by Entity Framework Core's proven patterns, adapted for SurrealDB's unique capabilities.