A production-quality .NET library for CockroachDB that provides retry handling, circuit breaker patterns, CDC (Change Data Capture) webhooks, follower reads, multi-region support, AI/vector search (pgvector), RAG pipelines, and concurrency testing -- all through a composable middleware pipeline.
This is the .NET equivalent of the Java cockroachdb-spring-boot-starter, following idiomatic C# and ASP.NET Core patterns.
Packages are published to this repository's GitHub Pages NuGet feed — no token required. Add the source, then install the packages you need:
<!-- NuGet.config -->
<configuration>
<packageSources>
<add key="cockroachlabs" value="https://cockroachlabs.github.io/cockroachdb-dotnet-library/nuget/index.json" />
</packageSources>
</configuration>dotnet add package CockroachDB.DotNet.Core --version 1.0.1API reference: https://cockroachlabs.github.io/cockroachdb-dotnet-library/
The library uses a middleware pipeline pattern (similar to ASP.NET Core middleware or MediatR behaviors). Each concern -- retry, circuit breaker, metrics, region routing, follower reads -- is a separate middleware step that users compose declaratively.
Metrics -> CircuitBreaker -> Retry -> Region -> FollowerRead -> [operation]
| Package | Purpose |
|---|---|
CockroachDB.DotNet.Abstractions |
Interfaces, attributes, configuration models (zero external deps) |
CockroachDB.DotNet.Core |
Middleware pipeline, retry engine, circuit breaker, region context, follower reads |
CockroachDB.DotNet.EntityFramework |
EF Core interceptors, entity attributes, DbContext extensions |
CockroachDB.DotNet.Cdc |
Changefeed creation via SQL, webhook listener endpoint |
CockroachDB.DotNet.AI |
AI/vector search, RAG pipelines, semantic search, chat integration, pgvector support |
CockroachDB.DotNet.Testing |
Testcontainers fixture, concurrency runner, retry/follower-read simulators |
CockroachDB.DotNet.Demo |
ASP.NET Core sample app demonstrating all features |
Abstractions (zero deps)
|
Core (Npgsql, Polly)
| | |
EntityFramework Cdc AI (pgvector, RAG)
|
Testing (Testcontainers)
|
Demo (ASP.NET Core)
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("CockroachDB")
?? "Host=localhost;Port=26257;Database=defaultdb;Username=root;SSL Mode=Disable;";
builder.Services.AddSingleton(NpgsqlDataSource.Create(connectionString));
builder.Services.AddCockroachDB(builder.Configuration, pipeline =>
{
pipeline.UseMetrics();
pipeline.UseCircuitBreaker();
pipeline.UseRetry();
pipeline.UseRegion();
pipeline.UseFollowerRead();
});public class OrderService(ICockroachDBTemplate crdb, AppDbContext db)
{
public async Task CreateOrderAsync(Order order)
{
await crdb.ExecuteAsync(async () =>
{
db.Orders.Add(order);
await db.SaveChangesAsync();
});
}
public async Task<List<Order>> GetOrdersStaleAsync()
{
return await crdb.ExecuteFollowerReadAsync(async () =>
await db.Orders.AsNoTracking().ToListAsync());
}
public async Task CreateInRegionAsync(Order order, string region)
{
await crdb.InRegion(region).ExecuteAsync(async () =>
{
db.Orders.Add(order);
await db.SaveChangesAsync();
});
}
}builder.Services.AddDbContext<AppDbContext>(options =>
options.UseCockroachDB(connectionString));The CockroachDBCommandInterceptor automatically:
- Detects
FollowerReadContextand prependsSET TRANSACTION AS OF SYSTEM TIME follower_read_timestamp() - Detects
RegionContextand prependsSET crdb_region = '<region>'
CockroachDB uses serializable isolation by default, which can produce 40001 errors under contention. The retry middleware catches these and retries with exponential backoff + jitter.
// Via template
await crdb.ExecuteAsync(async () => { /* retried automatically */ });
// Via attribute (on middleware context)
[CockroachDB(MaxRetries = 5, DelayMs = 200)]Configuration in appsettings.json:
{
"CockroachDB": {
"Retry": {
"MaxRetries": 3,
"DelayMs": 100,
"Multiplier": 2.0,
"MaxDelayMs": 5000,
"JitterMs": 50
}
}
}Read from the nearest replica for lower latency at the cost of slight staleness. Three activation methods:
1. Ambient Context (EF Core)
using (FollowerReadContext.Scoped())
{
return await db.Orders.AsNoTracking().ToListAsync();
}2. Template API
var orders = await crdb.ExecuteFollowerReadAsync(async () =>
await db.Orders.AsNoTracking().ToListAsync());3. Attribute
[FollowerRead]
public class StaleReadService { /* ... */ }Opens the circuit after consecutive failures, preventing cascading failures:
pipeline.UseCircuitBreaker();Route queries to specific CockroachDB regions:
// Via template
await crdb.InRegion("us-east1").ExecuteAsync(() => /* ... */);
// Via ambient context
using (RegionContext.Scoped("eu-west1"))
{
await db.Orders.ToListAsync();
}Entity attributes for multi-region tables:
[RegionalByRow]
public class Order
{
[CrdbRegion]
public string Region { get; set; } = "";
}
[GlobalTable]
public class Product { /* ... */ }Listen for changefeed events via webhooks:
// Register CDC
builder.Services.AddCockroachDBCdc();
builder.Services.AddChangefeedHandler<Order, OrderChangefeedHandler>();
// Map webhook endpoint
app.MapCockroachDBChangefeed<Order>("/webhooks/orders");Implement a handler:
public class OrderChangefeedHandler : IChangefeedHandler<Order>
{
public Task HandleAsync(ChangefeedPayload<Order> payload, CancellationToken ct)
{
foreach (var evt in payload.Events)
Console.WriteLine($"Order {evt.After.Id} changed");
return Task.CompletedTask;
}
}Built-in metrics recording for transactions, retries, failures, and follower reads:
pipeline.UseMetrics();
// Access programmatically
var metrics = serviceProvider.GetRequiredService<ICockroachDBMetrics>();
Console.WriteLine($"Transactions: {metrics.TransactionCount}");
Console.WriteLine($"Retries: {metrics.RetryCount}");
Console.WriteLine($"Follower Reads: {metrics.FollowerReadCount}");CockroachDB supports pgvector for vector similarity search with C-SPANN distributed indexing. The CockroachDB.DotNet.AI package provides a complete AI integration layer.
builder.Services.AddCockroachDBAI(builder.Configuration);
builder.Services.AddCockroachDBVectorStore();
builder.Services.AddCockroachDBRagChat();Configuration in appsettings.json:
{
"CockroachDB": {
"AI": {
"Enabled": true,
"VectorSearch": {
"DefaultMetric": "Cosine",
"DefaultLimit": 10,
"DefaultTable": "vector_store",
"DefaultVectorColumn": "embedding",
"InitializeSchema": true,
"DefaultBeamSize": 32
},
"Embedding": {
"Provider": "local",
"Dimensions": 1536
},
"Chat": {
"MaxContextDocuments": 5,
"SimilarityThreshold": 0.7,
"SystemPrompt": "You are a helpful assistant."
}
}
}
}Direct SQL-level vector search with full control:
var template = serviceProvider.GetRequiredService<VectorSearchTemplate>();
// Search for similar vectors
var results = await template.SearchAsync(
table: "documents",
embedding: queryVector,
limit: 10,
metric: DistanceMetric.Cosine);
// Store an embedding
await template.StoreEmbeddingAsync("documents", "doc-1", embedding);
// Store with metadata (JSONB)
await template.StoreEmbeddingWithMetadataAsync("documents", "doc-1", embedding,
new Dictionary<string, object?> { ["category"] = "science", ["year"] = 2024 });Higher-level API for document management with automatic embedding generation:
var store = serviceProvider.GetRequiredService<CockroachDBVectorStore>();
// Initialize schema (creates table + vector index)
await store.InitializeSchemaAsync();
// Add documents
await store.AddDocumentsAsync(new[]
{
new VectorDocument { Id = "1", Text = "CockroachDB is distributed SQL" },
new VectorDocument { Id = "2", Text = "Vector search uses pgvector" }
});
// Similarity search
var results = await store.SimilaritySearchAsync("distributed database", topK: 5);Filter vector search results using JSONB metadata:
var filter = new FilterGroup
{
Logic = FilterLogic.And,
Expressions =
[
new() { Key = "category", Operator = FilterOperator.Eq, Value = "tech" },
new() { Key = "year", Operator = FilterOperator.Gte, Value = 2023 }
]
};
// Generates: metadata->>'category' = 'tech' AND (metadata->>'year')::numeric >= 2023
var sql = FilterExpressionConverter.Convert(filter);Supported operators: Eq, Ne, Gt, Gte, Lt, Lte, In, NotIn.
| Metric | pgvector Operator | Use Case |
|---|---|---|
| L2 (Euclidean) | <-> |
General purpose, absolute distance |
| Cosine | <=> |
Text embeddings, direction similarity |
| Inner Product | <#> |
Pre-normalized vectors, maximum dot product |
CockroachDB's C-SPANN distributed vector indexing supports beam_size for accuracy/speed tradeoff:
// Higher beam_size = more accurate but slower
await template.SearchAsync("docs", embedding, limit: 10,
metric: DistanceMetric.Cosine, beamSize: 32);Complete RAG pipeline with provider-agnostic LLM integration:
// Implement IChatCompletionProvider for your LLM (OpenAI, Anthropic, etc.)
public class OpenAIChatProvider : IChatCompletionProvider
{
public async Task<ChatResponse> CompleteAsync(
IReadOnlyList<ChatMessage> messages, ChatOptions? options, CancellationToken ct)
{
// Call your LLM API
}
}
// Register and use
builder.Services.AddSingleton<IChatCompletionProvider, OpenAIChatProvider>();
builder.Services.AddCockroachDBRagChat();
// The RAG client automatically:
// 1. Generates embedding for the user query
// 2. Searches the vector store for relevant context
// 3. Builds the prompt with system message + context documents + history
// 4. Delegates to IChatCompletionProvider for completion
var chat = serviceProvider.GetRequiredService<IChatClient>();
var response = await chat.CompleteAsync("What is CockroachDB?");Declarative annotations for vector search behavior:
// Mark a method for vector search
[VectorSearch(Table = "products", Metric = DistanceMetric.Cosine, Limit = 5, BeamSize = 32)]
public Task<List<Product>> FindSimilarAsync(float[] queryVector) { /* ... */ }
// Semantic search with metadata filtering
[SemanticSearch(Table = "articles", Filter = "category = :category", MaxDistance = 0.5)]
public Task<List<Article>> SearchAsync(float[] queryVector) { /* ... */ }
// Mark a property as a vector column
public class Document
{
[VectorColumn(Dimensions = 1536)]
public float[]? Embedding { get; set; }
}Thread-safe metrics for vector search, embeddings, and chat:
var metrics = serviceProvider.GetRequiredService<IAIMetrics>();
Console.WriteLine($"Searches: {metrics.VectorSearchCount}");
Console.WriteLine($"Failures: {metrics.VectorSearchFailureCount}");
Console.WriteLine($"Avg Duration: {metrics.AverageVectorSearchDurationMs}ms");
Console.WriteLine($"Embeddings: {metrics.EmbeddingGenerationCount}");
Console.WriteLine($"Chat Completions: {metrics.ChatCompletionCount}");// Read Committed (relaxed isolation)
await crdb.ExecuteReadCommittedAsync(async () => { /* ... */ });
// Default: Serializable
await crdb.ExecuteAsync(async () => { /* ... */ });The CockroachDBMigrationsSqlGenerator extends Npgsql's generator with CockroachDB-specific DDL handling:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString)
.UseCockroachDB()); // Registers migration generator + NoopLock + interceptorsHandles:
- Computed columns — Enforces
STORED(CockroachDB doesn't support virtual), emitsGENERATED ALWAYS AS ((expr)::type) STORED - JSON defaults — Fixes EF Core's empty-string default for JSONB columns to
{} - Serial columns — Maps
unique_rowid()correctly for INT8 primary keys
CockroachDB lacks PostgreSQL's advisory locks (pg_advisory_lock). The CockroachDBHistoryRepository provides a no-op lock so EF Core 10+ migrations work:
// Automatically registered when UseCockroachDB() is called — no manual config needed
// Migration coordination should happen at the deployment level (single-pod deploy, migration job)using CockroachDB.DotNet.Abstractions.Attributes;
public class Order
{
public Guid Id { get; set; }
public int Price { get; set; }
public int Quantity { get; set; }
[CockroachDBComputed("price * quantity")]
public int Total { get; set; }
}// Before (old library)
options.UseNpgsql(conn).UseCockroach();
// After (new library — includes migration gen + NoopLock + interceptors)
builder.Services.AddCockroachDB();
options.UseNpgsql(conn).UseCockroachDB();The new library provides everything efcore.pg.cockroach did plus: automatic retry on SQLSTATE 40001, circuit breaker (Polly v8), multi-region routing, follower reads, CDC webhooks, vector search, and OpenTelemetry metrics.
The CockroachDB.DotNet.Testing package provides utilities for testing CockroachDB-specific behavior without a live cluster.
Retry Simulator -- force 40001 serialization errors:
var result = await RetrySimulator.SimulateConflictAsync(
failCount: 2,
operation: attempt => Task.FromResult($"attempt-{attempt}"));
Assert.Equal(3, result.TotalAttempts);
Assert.True(result.Succeeded);Follower Read Simulator -- measure write-to-visible staleness:
var result = await FollowerReadSimulator.MeasureStalenessAsync(
writeOperation: () => WriteToDatabase(),
followerReadCheck: () => IsVisibleOnFollower(),
maxWaitMs: 5000);
Assert.True(result.WasVisible);
Console.WriteLine($"Visible after {result.WriteToVisibleMs}ms");Concurrency Runner -- stress test under contention:
var result = await ConcurrencyRunner.RunAsync(
threads: 10,
iterationsPerThread: 100,
action: () => TransferMoney(from, to, 1.00m));
Assert.Equal(0, result.Failures);Testcontainers Fixture -- real CockroachDB in Docker:
public class IntegrationTests(CockroachDBFixture fixture)
: IClassFixture<CockroachDBFixture>
{
[Fact]
public async Task RealDatabaseTest()
{
await using var db = CreateDbContext(fixture.ConnectionString);
// ...
}
}dotnet testThe CockroachDB.DotNet.Demo project is a minimal ASP.NET Core app demonstrating all features:
| Endpoint | Method | Description |
|---|---|---|
/ |
GET | Health check / welcome |
/health |
GET | ASP.NET Core health check |
/orders |
GET | List all orders |
/orders |
POST | Create an order (with retry) |
/orders/stale |
GET | Follower read via FollowerReadContext.Scoped() |
/orders/stale/template |
GET | Follower read via ICockroachDBTemplate |
/webhooks/orders |
POST | CDC webhook endpoint |
/ai/init |
POST | Initialize vector store schema |
/ai/documents |
POST | Ingest documents into vector store |
/ai/documents |
DELETE | Delete documents by ID |
/ai/search |
POST | Similarity search (text query) |
/ai/chat |
POST | RAG chat (context-aware Q&A) |
/ai/metrics |
GET | AI metrics (search counts, durations) |
Run the demo:
cd src/CockroachDB.DotNet.Demo
dotnet runFull appsettings.json configuration:
{
"ConnectionStrings": {
"CockroachDB": "Host=localhost;Port=26257;Database=defaultdb;Username=root;SSL Mode=Disable;"
},
"CockroachDB": {
"Retry": {
"MaxRetries": 3,
"DelayMs": 100,
"Multiplier": 2.0,
"MaxDelayMs": 5000,
"JitterMs": 50
},
"CircuitBreaker": {
"FailureRatio": 0.5,
"MinimumThroughput": 10,
"SamplingDurationSeconds": 30,
"BreakDurationSeconds": 60
},
"Region": {
"Default": "",
"Validate": false,
"Allowed": [],
"Resolver": "Context",
"HeaderName": "X-CockroachDB-Region"
},
"Metrics": {
"Enabled": true
},
"AI": {
"Enabled": true,
"VectorSearch": {
"DefaultMetric": "Cosine",
"DefaultLimit": 10,
"DefaultBeamSize": 0,
"DefaultTable": "vector_store",
"DefaultVectorColumn": "embedding",
"InitializeSchema": false
},
"Embedding": {
"Provider": "local",
"Dimensions": 1536
},
"Chat": {
"MaxContextDocuments": 5,
"SimilarityThreshold": 0.7,
"SystemPrompt": "You are a helpful assistant."
}
}
}
}| Java Concept | .NET Equivalent |
|---|---|
@CockroachDB annotation |
[CockroachDB] attribute |
@FollowerRead annotation |
[FollowerRead] attribute + FollowerReadContext |
Spring AOP @Around |
ICockroachDBMiddleware pipeline |
CockroachDBTemplate |
ICockroachDBTemplate |
ThreadLocal<String> |
AsyncLocal<string> |
@ConfigurationProperties |
IOptions<CockroachDBOptions> |
@AutoConfiguration |
IServiceCollection.AddCockroachDB() |
| Micrometer counters | ICockroachDBMetrics / IAIMetrics |
@VectorSearch annotation |
[VectorSearch] attribute |
@SemanticSearch annotation |
[SemanticSearch] attribute |
@VectorColumn annotation |
[VectorColumn] attribute |
VectorSearchTemplate (JDBC) |
VectorSearchTemplate (Npgsql) |
CockroachDBVectorStore (Spring AI) |
CockroachDBVectorStore |
CockroachDBFilterExpressionConverter |
FilterExpressionConverter |
ExecutorService + CountDownLatch |
ConcurrencyRunner (Task.WhenAll + Barrier) |
| Testcontainers (Java) | Testcontainers (dotnet) via CockroachDBFixture |
- .NET 10+ / C# 13
- CockroachDB v23.1+ (for follower reads and multi-region features)
- CockroachDB v24.3+ (for pgvector / C-SPANN vector indexing)
- Docker (for Testcontainers-based integration tests)
Optional surfaces for Cockroach Continuum, separate from the core pipeline. Clients fall back to an in-memory simulated backend when no API key/endpoint is configured, so code runs without a Continuum organization. The HTTP clients take an injectable HttpClient for tests.
using CockroachDB.DotNet.Continuum.Cloud;
using CockroachDB.DotNet.Continuum.Mcp;
using CockroachDB.DotNet.Continuum.Aegis;
// --- Cloud control plane ---
var cloud = HttpCloudClient.Create(HttpCloudClient.DefaultBaseUrl, apiKey);
var clusters = await cloud.ListClustersAsync();
await cloud.ConfigureMetricExportAsync(clusterId, MetricExport.Datadog(ddKey, "US1")); // typed builders
// Readiness: ContinuumCloudHealthCheck (IHealthCheck) registered via DI.
// --- Virtual clusters + routing ---
var vm = new VirtualClusterManager(sqlRunner); // ISqlRunner (Npgsql default)
await vm.CreateAsync("analytics");
var cs = VirtualClusterManager.RoutingConnString(baseConnString, "analytics"); // Options=-ccluster=
// --- MCP client (real 2025-06-18 wire protocol) + Aegis over MCP ---
var mcp = HttpMcpClient.Create("https://cockroachlabs.cloud/mcp", token);
var tools = await mcp.ListToolsAsync();
var aegis = new McpAegisClient(mcp, new McpToolNames(Findings: "list_findings")); // discover via ListToolsAsync
var findings = await aegis.GetFindingsAsync();MCP is a public, versioned protocol, so the client speaks the real wire format (initialize handshake, tools/list, tools/call, SSE + session handling). The Aegis REST API is not yet public, so Aegis is reached read-only over MCP with caller-supplied tool names rather than an invented wire protocol.
Also in the Continuum package: a standalone Aegis client (findings/recommendations + app "exhaust"), Plenum storage insights, and vCluster workload tagging.
// Aegis — simulated by default:
IAegisClient aegis = new SimulatedAegisClient();
await aegis.GetFindingsAsync();
// Plenum — read-only, customer-safe per-table storage insights:
IStorageInsights storage = new StorageInsights(dataSource);
// vCluster — workload tagging / QoS:
using (VirtualClusterContext.Scoped("analytics", WorkloadPriority.Low)) { /* run report */ }MIT