Skip to content
WeftKitBeta

.NET / C# Integration

This guide covers connecting to every WeftKit engine from .NET and C#. All examples use standard NuGet packages and widely-used frameworks — no WeftKit-specific SDK is required. Point your existing driver at weftkit-standalone and your existing application code works as-is.

Prerequisites

Add the following NuGet packages to your project. Pick the sections relevant to the engines you are using.

NuGet Package References (.csproj)

xml
<!-- WeftKitRel — Npgsql (PostgreSQL) -->
<PackageReference Include="Npgsql" Version="8.0.3" />

<!-- WeftKitRel — Entity Framework Core + Npgsql provider -->
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design"    Version="8.0.4" />

<!-- WeftKitRel — Dapper -->
<PackageReference Include="Dapper" Version="2.1.35" />

<!-- WeftKitDoc — MongoDB .NET Driver -->
<PackageReference Include="MongoDB.Driver" Version="2.26.0" />

<!-- WeftKitMem — StackExchange.Redis -->
<PackageReference Include="StackExchange.Redis" Version="2.7.33" />

<!-- WeftKitGraph — Neo4j .NET Driver -->
<PackageReference Include="Neo4j.Driver" Version="5.21.0" />

<!-- WeftKitKV — AWS SDK for .NET (DynamoDB) -->
<PackageReference Include="AWSSDK.DynamoDBv2" Version="3.7.303" />

Or install via the .NET CLI:

bash
# PostgreSQL drivers
dotnet add package Npgsql
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Dapper

# MongoDB
dotnet add package MongoDB.Driver

# Redis
dotnet add package StackExchange.Redis

# Neo4j
dotnet add package Neo4j.Driver

# DynamoDB
dotnet add package AWSSDK.DynamoDBv2

WeftKitRel — PostgreSQL via Npgsql

WeftKitRel speaks the PostgreSQL v3 wire protocol, so the standard Npgsql driver connects without any modification.

Default port: 5432

Basic Connection and Query

csharp
using Npgsql;

// Connection string format — identical to standard PostgreSQL
const string connStr = "Host=localhost;Port=5432;Database=mydb;Username=app_user;Password=secret";

// --- INSERT with parameterised command ---
await using var conn = new NpgsqlConnection(connStr);
await conn.OpenAsync();

await using var insertCmd = new NpgsqlCommand(
    "INSERT INTO products (name, price, stock) VALUES (@name, @price, @stock) RETURNING id",
    conn);
insertCmd.Parameters.AddWithValue("name",  "Widget Pro");
insertCmd.Parameters.AddWithValue("price", 29.99);
insertCmd.Parameters.AddWithValue("stock", 250);
long newId = (long)(await insertCmd.ExecuteScalarAsync())!;
Console.WriteLine($"Inserted product id: {newId}");

// --- SELECT and iterate results ---
await using var selectCmd = new NpgsqlCommand(
    "SELECT id, name, price FROM products WHERE price < @max ORDER BY price",
    conn);
selectCmd.Parameters.AddWithValue("max", 50.0);

await using var reader = await selectCmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
    long   id    = reader.GetInt64(0);
    string name  = reader.GetString(1);
    double price = reader.GetDouble(2);
    Console.WriteLine($"  [{id}] {name} — ${price:F2}");
}

Transaction Management

csharp
using Npgsql;

const string connStr = "Host=localhost;Port=5432;Database=mydb;Username=app_user;Password=secret";

static async Task TransferFundsAsync(long fromId, long toId, decimal amount)
{
    await using var conn = new NpgsqlConnection(connStr);
    await conn.OpenAsync();

    await using var tx = await conn.BeginTransactionAsync();
    try
    {
        // Debit source account
        await using var debitCmd = new NpgsqlCommand(
            "UPDATE accounts SET balance = balance - @amount WHERE id = @id", conn, tx);
        debitCmd.Parameters.AddWithValue("amount", amount);
        debitCmd.Parameters.AddWithValue("id",     fromId);
        await debitCmd.ExecuteNonQueryAsync();

        // Credit destination account
        await using var creditCmd = new NpgsqlCommand(
            "UPDATE accounts SET balance = balance + @amount WHERE id = @id", conn, tx);
        creditCmd.Parameters.AddWithValue("amount", amount);
        creditCmd.Parameters.AddWithValue("id",     toId);
        await creditCmd.ExecuteNonQueryAsync();

        await tx.CommitAsync();
        Console.WriteLine("Transfer committed.");
    }
    catch
    {
        await tx.RollbackAsync();
        Console.WriteLine("Transfer rolled back.");
        throw;
    }
}

Bulk Copy with NpgsqlBinaryImporter

csharp
using Npgsql;

static async Task BulkImportProductsAsync(IEnumerable<(string Name, double Price, int Stock)> rows)
{
    await using var conn = new NpgsqlConnection(connStr);
    await conn.OpenAsync();

    await using var writer = await conn.BeginBinaryImportAsync(
        "COPY products (name, price, stock) FROM STDIN (FORMAT BINARY)");

    foreach (var (name, price, stock) in rows)
    {
        await writer.StartRowAsync();
        await writer.WriteAsync(name,  NpgsqlTypes.NpgsqlDbType.Text);
        await writer.WriteAsync(price, NpgsqlTypes.NpgsqlDbType.Double);
        await writer.WriteAsync(stock, NpgsqlTypes.NpgsqlDbType.Integer);
    }

    ulong rowsWritten = await writer.CompleteAsync();
    Console.WriteLine($"Bulk imported {rowsWritten} rows.");
}

WeftKitRel — Entity Framework Core + Npgsql

appsettings.json

json
{
  "ConnectionStrings": {
    "WeftKitRel": "Host=localhost;Port=5432;Database=mydb;Username=app_user;Password=secret"
  }
}

Model Class

csharp
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

[Table("orders")]
public class Order
{
    [Key]
    [Column("id")]
    public long Id { get; set; }

    [Required]
    [Column("customer_id")]
    public long CustomerId { get; set; }

    [Required]
    [Column("total_amount")]
    public decimal TotalAmount { get; set; }

    [Column("status")]
    [MaxLength(32)]
    public string Status { get; set; } = "pending";

    [Column("created_at")]
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

DbContext

csharp
using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Order>   Orders   { get; set; } = null!;
    public DbSet<Product> Products { get; set; } = null!;

    protected override void OnModelCreating(ModelBuilder builder)
    {
        builder.Entity<Order>()
            .HasIndex(o => o.CustomerId);

        builder.Entity<Order>()
            .Property(o => o.CreatedAt)
            .HasDefaultValueSql("NOW()");
    }
}

ASP.NET Core DI Registration

csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("WeftKitRel")));

// Or use NpgsqlDataSource directly (recommended for .NET 7+)
builder.Services.AddNpgsqlDataSource(
    builder.Configuration.GetConnectionString("WeftKitRel")!);

var app = builder.Build();

LINQ Queries and CRUD

csharp
using Microsoft.EntityFrameworkCore;

public class OrderService
{
    private readonly AppDbContext _db;

    public OrderService(AppDbContext db) => _db = db;

    // --- Read: async list query ---
    public async Task<List<Order>> GetPendingOrdersAsync() =>
        await _db.Orders
            .Where(o => o.Status == "pending")
            .OrderBy(o => o.CreatedAt)
            .ToListAsync();

    // --- Read: single item ---
    public async Task<Order?> GetOrderAsync(long id) =>
        await _db.Orders.FirstOrDefaultAsync(o => o.Id == id);

    // --- Create ---
    public async Task<Order> PlaceOrderAsync(long customerId, decimal amount)
    {
        var order = new Order { CustomerId = customerId, TotalAmount = amount };
        await _db.Orders.AddAsync(order);
        await _db.SaveChangesAsync();
        return order;
    }

    // --- Update ---
    public async Task ShipOrderAsync(long orderId)
    {
        var order = await _db.Orders.FindAsync(orderId)
            ?? throw new KeyNotFoundException($"Order {orderId} not found.");
        order.Status = "shipped";
        await _db.SaveChangesAsync();
    }

    // --- Delete ---
    public async Task CancelOrderAsync(long orderId)
    {
        var order = await _db.Orders.FindAsync(orderId);
        if (order is not null)
        {
            _db.Orders.Remove(order);
            await _db.SaveChangesAsync();
        }
    }
}

Migrations

bash
# Add a new migration (run from the project directory)
dotnet ef migrations add InitialSchema

# Apply migrations to the running WeftKit instance
dotnet ef database update

# Generate SQL script instead of applying directly
dotnet ef migrations script --output migrations.sql

WeftKitRel — Dapper

Dapper provides lightweight object mapping on top of any IDbConnection.

csharp
using Dapper;
using Npgsql;
using System.Data;

public class ProductRepository
{
    private readonly string _connStr;

    public ProductRepository(string connectionString) => _connStr = connectionString;

    private IDbConnection Connection() => new NpgsqlConnection(_connStr);

    // --- QueryAsync<T>: map rows to typed POCOs ---
    public async Task<IEnumerable<Product>> FindByMaxPriceAsync(double maxPrice)
    {
        using var conn = Connection();
        return await conn.QueryAsync<Product>(
            "SELECT id, name, price, stock FROM products WHERE price < @MaxPrice ORDER BY price",
            new { MaxPrice = maxPrice }
        );
    }

    // --- QueryFirstOrDefaultAsync: single row ---
    public async Task<Product?> FindByIdAsync(long id)
    {
        using var conn = Connection();
        return await conn.QueryFirstOrDefaultAsync<Product>(
            "SELECT id, name, price, stock FROM products WHERE id = @Id",
            new { Id = id }
        );
    }

    // --- ExecuteAsync: INSERT / UPDATE / DELETE ---
    public async Task<int> CreateProductAsync(string name, double price, int stock)
    {
        using var conn = Connection();
        return await conn.ExecuteAsync(
            "INSERT INTO products (name, price, stock) VALUES (@Name, @Price, @Stock)",
            new { Name = name, Price = price, Stock = stock }
        );
    }

    public async Task<int> UpdatePriceAsync(long id, double newPrice)
    {
        using var conn = Connection();
        return await conn.ExecuteAsync(
            "UPDATE products SET price = @Price WHERE id = @Id",
            new { Price = newPrice, Id = id }
        );
    }

    // --- Multi-mapping: JOIN two tables into two POCOs ---
    public async Task<IEnumerable<(Order Order, Customer Customer)>> GetOrdersWithCustomersAsync()
    {
        using var conn = Connection();
        var results = await conn.QueryAsync<Order, Customer, (Order, Customer)>(
            @"SELECT o.id, o.customer_id, o.total_amount, o.status,
                     c.id, c.name, c.email
              FROM orders o
              JOIN customers c ON c.id = o.customer_id
              ORDER BY o.created_at DESC",
            (order, customer) => (order, customer),
            splitOn: "id"   // column name where the second type starts
        );
        return results;
    }
}

ASP.NET Core DI Registration Patterns

Minimal API with Npgsql

csharp
// Program.cs — Minimal API pattern
var builder = WebApplication.CreateBuilder(args);

// Read connection string from appsettings.json
string connStr = builder.Configuration.GetConnectionString("WeftKitRel")!;

// Register NpgsqlDataSource (thread-safe, connection-pooling built in)
builder.Services.AddNpgsqlDataSource(connStr);

// Register EF Core alongside if needed
builder.Services.AddDbContext<AppDbContext>(opt => opt.UseNpgsql(connStr));

// Register application services
builder.Services.AddScoped<OrderService>();
builder.Services.AddScoped<ProductRepository>();

var app = builder.Build();

// Map endpoints
app.MapGet("/orders/{id:long}", async (long id, OrderService svc) =>
{
    var order = await svc.GetOrderAsync(id);
    return order is null ? Results.NotFound() : Results.Ok(order);
});

app.MapPost("/orders", async (PlaceOrderRequest req, OrderService svc) =>
{
    var order = await svc.PlaceOrderAsync(req.CustomerId, req.Amount);
    return Results.Created($"/orders/{order.Id}", order);
});

app.Run();

record PlaceOrderRequest(long CustomerId, decimal Amount);

Controller-Based API with EF Core

csharp
// Controllers/OrdersController.cs
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly OrderService _orderService;

    public OrdersController(OrderService orderService) =>
        _orderService = orderService;

    [HttpGet("{id:long}")]
    public async Task<IActionResult> Get(long id)
    {
        var order = await _orderService.GetOrderAsync(id);
        return order is null ? NotFound() : Ok(order);
    }

    [HttpGet("pending")]
    public async Task<IActionResult> GetPending() =>
        Ok(await _orderService.GetPendingOrdersAsync());

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] PlaceOrderRequest req)
    {
        var order = await _orderService.PlaceOrderAsync(req.CustomerId, req.Amount);
        return CreatedAtAction(nameof(Get), new { id = order.Id }, order);
    }

    [HttpPatch("{id:long}/ship")]
    public async Task<IActionResult> Ship(long id)
    {
        await _orderService.ShipOrderAsync(id);
        return NoContent();
    }
}

WeftKitDoc — MongoDB .NET Driver

WeftKitDoc speaks the MongoDB Wire protocol. The standard MongoDB .NET Driver connects directly.

Default port: 27017

Typed POCO and Collection Setup

csharp
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.Driver;

// --- POCO mapped to a MongoDB document ---
[BsonIgnoreExtraElements]
public class Product
{
    [BsonId]
    [BsonRepresentation(BsonType.ObjectId)]
    public string? Id { get; set; }

    [BsonElement("name")]
    public string Name { get; set; } = string.Empty;

    [BsonElement("category")]
    public string Category { get; set; } = string.Empty;

    [BsonElement("price")]
    public double Price { get; set; }

    [BsonElement("stock")]
    public int Stock { get; set; }

    [BsonElement("tags")]
    public List<string> Tags { get; set; } = new();
}

CRUD Operations

csharp
using MongoDB.Driver;

const string connectionString = "mongodb://app_user:secret@localhost:27017/catalog";

var client     = new MongoClient(connectionString);
var database   = client.GetDatabase("catalog");
IMongoCollection<Product> products = database.GetCollection<Product>("products");

// --- InsertOne ---
var newProduct = new Product
{
    Name     = "Widget Pro",
    Category = "widgets",
    Price    = 29.99,
    Stock    = 250,
    Tags     = new List<string> { "sale", "featured" }
};
await products.InsertOneAsync(newProduct);
Console.WriteLine($"Inserted: {newProduct.Id}");

// --- InsertMany ---
var batch = new List<Product>
{
    new() { Name = "Gadget A", Category = "gadgets", Price = 9.99,  Stock = 100 },
    new() { Name = "Gadget B", Category = "gadgets", Price = 14.99, Stock = 50  }
};
await products.InsertManyAsync(batch);

// --- FindAsync with FilterDefinitionBuilder ---
var filter = Builders<Product>.Filter.And(
    Builders<Product>.Filter.Eq(p => p.Category, "widgets"),
    Builders<Product>.Filter.Gt(p => p.Price, 10.0)
);
var sort = Builders<Product>.Sort.Ascending(p => p.Price);

using var cursor = await products.FindAsync(filter,
    new FindOptions<Product> { Sort = sort, Limit = 20 });

await cursor.ForEachAsync(p =>
    Console.WriteLine($"  {p.Name} — ${p.Price:F2}"));

// --- UpdateOne ---
var updateFilter = Builders<Product>.Filter.Eq(p => p.Name, "Widget Pro");
var update = Builders<Product>.Update
    .Set(p => p.Price, 24.99)
    .Inc(p => p.Stock, -10)
    .AddToSet(p => p.Tags, "clearance");

UpdateResult updateResult = await products.UpdateOneAsync(updateFilter, update);
Console.WriteLine($"Matched: {updateResult.MatchedCount}, Modified: {updateResult.ModifiedCount}");

// --- DeleteOne ---
DeleteResult deleteResult = await products.DeleteOneAsync(
    Builders<Product>.Filter.Eq(p => p.Name, "Gadget A"));
Console.WriteLine($"Deleted: {deleteResult.DeletedCount}");

Aggregation Pipeline

csharp
using MongoDB.Driver;

static async Task SummarizeByCategory(IMongoCollection<Product> products)
{
    var pipeline = products.Aggregate()
        .Match(Builders<Product>.Filter.Gt(p => p.Stock, 0))
        .Group(
            p => p.Category,
            g => new
            {
                Category   = g.Key,
                TotalStock = g.Sum(p => p.Stock),
                AvgPrice   = g.Average(p => p.Price),
                Count      = g.Count()
            }
        )
        .SortByDescending(r => r.Count);

    await pipeline.ForEachAsync(r =>
        Console.WriteLine($"Category: {r.Category} | count={r.Count} | avgPrice=${r.AvgPrice:F2}"));
}

WeftKitDoc — ASP.NET Core DI Registration

Configuration Class and appsettings.json

json
{
  "MongoSettings": {
    "ConnectionString": "mongodb://app_user:secret@localhost:27017",
    "DatabaseName": "catalog"
  }
}
csharp
// MongoSettings.cs
public class MongoSettings
{
    public string ConnectionString { get; set; } = string.Empty;
    public string DatabaseName     { get; set; } = string.Empty;
}

Registration and Injection

csharp
// Program.cs
builder.Services.Configure<MongoSettings>(
    builder.Configuration.GetSection("MongoSettings"));

builder.Services.AddSingleton<IMongoClient>(sp =>
{
    var settings = sp.GetRequiredService<IOptions<MongoSettings>>().Value;
    return new MongoClient(settings.ConnectionString);
});

builder.Services.AddScoped<IMongoDatabase>(sp =>
{
    var settings = sp.GetRequiredService<IOptions<MongoSettings>>().Value;
    var client   = sp.GetRequiredService<IMongoClient>();
    return client.GetDatabase(settings.DatabaseName);
});

builder.Services.AddScoped<ProductRepository>();
csharp
// ProductRepository.cs — injected via DI
using Microsoft.Extensions.Options;
using MongoDB.Driver;

public class ProductRepository
{
    private readonly IMongoCollection<Product> _collection;

    public ProductRepository(IMongoDatabase database)
    {
        _collection = database.GetCollection<Product>("products");
    }

    public async Task<List<Product>> GetByCategoryAsync(string category) =>
        await _collection
            .Find(Builders<Product>.Filter.Eq(p => p.Category, category))
            .ToListAsync();

    public async Task<Product?> GetByIdAsync(string id) =>
        await _collection
            .Find(Builders<Product>.Filter.Eq(p => p.Id, id))
            .FirstOrDefaultAsync();

    public async Task CreateAsync(Product product) =>
        await _collection.InsertOneAsync(product);
}

WeftKitMem — StackExchange.Redis

WeftKitMem speaks the Redis RESP3 protocol. StackExchange.Redis is the standard high-performance .NET client.

Default port: 6379

Connection Setup

csharp
using StackExchange.Redis;

// Create once and share — ConnectionMultiplexer is thread-safe
var configOptions = new ConfigurationOptions
{
    EndPoints    = { { "localhost", 6379 } },
    Password     = "secret",
    AbortOnConnectFail = false,
    ConnectRetry = 3,
    ReconnectRetryPolicy = new ExponentialRetry(5000)
};

IConnectionMultiplexer multiplexer = await ConnectionMultiplexer.ConnectAsync(configOptions);
IDatabase db = multiplexer.GetDatabase(0);

String, Hash, List, and Sorted Set Operations

csharp
// --- String (value) operations ---
await db.StringSetAsync("session:abc123", "user-id:42", TimeSpan.FromHours(1));
RedisValue sessionValue = await db.StringGetAsync("session:abc123");
Console.WriteLine($"Session: {sessionValue}");

bool existed = await db.KeyDeleteAsync("session:abc123");

// Atomic increment
long views = await db.StringIncrementAsync("stats:page_views");

// --- Hash operations ---
await db.HashSetAsync("user:42", new HashEntry[]
{
    new("name",  "Alice"),
    new("email", "alice@example.com"),
    new("role",  "admin")
});
RedisValue email = await db.HashGetAsync("user:42", "email");
HashEntry[] allFields = await db.HashGetAllAsync("user:42");
Console.WriteLine($"Email: {email}");

// --- List operations ---
await db.ListLeftPushAsync("queue:jobs", "job-001");
await db.ListLeftPushAsync("queue:jobs", "job-002");
RedisValue[] jobs = await db.ListRangeAsync("queue:jobs", 0, -1);
long queueLength = await db.ListLengthAsync("queue:jobs");

// --- Sorted Set operations ---
await db.SortedSetAddAsync("leaderboard", new SortedSetEntry[]
{
    new("alice", 1500),
    new("bob",   1250),
    new("carol", 1750)
});

SortedSetEntry[] topPlayers = await db.SortedSetRangeByRankWithScoresAsync(
    "leaderboard", 0, 9, Order.Descending);

foreach (var entry in topPlayers)
    Console.WriteLine($"  {entry.Element}: {entry.Score}");

Pub/Sub

csharp
ISubscriber subscriber = multiplexer.GetSubscriber();

// Subscribe to a channel
await subscriber.SubscribeAsync(RedisChannel.Literal("notifications:orders"), (channel, message) =>
{
    Console.WriteLine($"Received on {channel}: {message}");
});

// Publish a message from another part of the application
await subscriber.PublishAsync(RedisChannel.Literal("notifications:orders"), "order-shipped:99");

WeftKitMem — ASP.NET Core DI Registration

appsettings.json

json
{
  "Redis": {
    "ConnectionString": "localhost:6379,password=secret,abortConnect=false"
  }
}

Registration

csharp
// Program.cs
builder.Services.AddSingleton<IConnectionMultiplexer>(_ =>
    ConnectionMultiplexer.Connect(
        builder.Configuration["Redis:ConnectionString"]!));

// Expose IDatabase as a scoped service
builder.Services.AddScoped<IDatabase>(sp =>
    sp.GetRequiredService<IConnectionMultiplexer>().GetDatabase(0));

builder.Services.AddScoped<CacheService>();
csharp
// CacheService.cs
using StackExchange.Redis;

public class CacheService
{
    private readonly IDatabase _db;

    public CacheService(IDatabase db) => _db = db;

    public async Task SetAsync<T>(string key, T value, TimeSpan expiry)
    {
        string json = System.Text.Json.JsonSerializer.Serialize(value);
        await _db.StringSetAsync(key, json, expiry);
    }

    public async Task<T?> GetAsync<T>(string key)
    {
        RedisValue value = await _db.StringGetAsync(key);
        if (value.IsNullOrEmpty) return default;
        return System.Text.Json.JsonSerializer.Deserialize<T>(value!);
    }

    public async Task InvalidateAsync(string key) =>
        await _db.KeyDeleteAsync(key);
}

WeftKitGraph — Neo4j .NET Driver

WeftKitGraph speaks the Bolt protocol. The standard Neo4j .NET Driver connects without modification.

Default port: 7687

Driver Setup and Session Usage

csharp
using Neo4j.Driver;

// Create once and share across the application lifetime
IDriver driver = GraphDatabase.Driver(
    "bolt://localhost:7687",
    AuthTokens.Basic("neo4j", "secret"),
    config => config.WithMaxConnectionPoolSize(50));

// Verify connectivity at startup
await driver.VerifyConnectivityAsync();

Write and Read Transactions

csharp
using Neo4j.Driver;

// --- Write: create nodes and relationships ---
static async Task CreateUserFollowsAsync(IDriver driver, string fromName, string toName)
{
    await using IAsyncSession session = driver.AsyncSession();
    await session.ExecuteWriteAsync(async tx =>
    {
        await tx.RunAsync(
            "MERGE (a:User {name: $from}) " +
            "MERGE (b:User {name: $to}) "  +
            "MERGE (a)-[:FOLLOWS]->(b)",
            new { from = fromName, to = toName });
    });
}

// --- Read: find followers ---
static async Task<List<string>> GetFollowersAsync(IDriver driver, string userName)
{
    await using IAsyncSession session = driver.AsyncSession();
    return await session.ExecuteReadAsync(async tx =>
    {
        var cursor = await tx.RunAsync(
            "MATCH (follower:User)-[:FOLLOWS]->(u:User {name: $name}) " +
            "RETURN follower.name AS followerName " +
            "ORDER BY followerName",
            new { name = userName });

        var records = await cursor.ToListAsync();
        return records.Select(r => r["followerName"].As<string>()).ToList();
    });
}

// --- Read: shortest path ---
static async Task<int> ShortestPathLengthAsync(IDriver driver, string fromName, string toName)
{
    await using IAsyncSession session = driver.AsyncSession();
    return await session.ExecuteReadAsync(async tx =>
    {
        var cursor = await tx.RunAsync(
            "MATCH p = shortestPath(" +
            "  (a:User {name: $from})-[:FOLLOWS*]-(b:User {name: $to})" +
            ") RETURN length(p) AS hops",
            new { from = fromName, to = toName });

        return await cursor.PeekAsync() is not null
            ? (await cursor.SingleAsync())["hops"].As<int>()
            : -1;
    });
}

// --- Write: remove a relationship ---
static async Task UnfollowAsync(IDriver driver, string fromName, string toName)
{
    await using IAsyncSession session = driver.AsyncSession();
    await session.ExecuteWriteAsync(async tx =>
    {
        var cursor = await tx.RunAsync(
            "MATCH (a:User {name: $from})-[r:FOLLOWS]->(b:User {name: $to}) DELETE r",
            new { from = fromName, to = toName });
        var summary = await cursor.ConsumeAsync();
        Console.WriteLine($"Relationships deleted: {summary.Counters.RelationshipsDeleted}");
    });
}

// Example usage
await CreateUserFollowsAsync(driver, "Alice", "Bob");
await CreateUserFollowsAsync(driver, "Alice", "Carol");
await CreateUserFollowsAsync(driver, "Bob",   "Carol");

var followers = await GetFollowersAsync(driver, "Carol");
Console.WriteLine($"Carol's followers: {string.Join(", ", followers)}");

int hops = await ShortestPathLengthAsync(driver, "Alice", "Carol");
Console.WriteLine($"Shortest path Alice -> Carol: {hops} hop(s)");

// Dispose driver when the application shuts down
await driver.DisposeAsync();

ASP.NET Core DI Registration

csharp
// Program.cs
builder.Services.AddSingleton<IDriver>(_ =>
    GraphDatabase.Driver(
        "bolt://localhost:7687",
        AuthTokens.Basic("neo4j", "secret")));

// Expose a scoped session factory
builder.Services.AddScoped(sp =>
    sp.GetRequiredService<IDriver>().AsyncSession());

builder.Services.AddScoped<GraphService>();
csharp
// GraphService.cs
public class GraphService
{
    private readonly IDriver _driver;

    public GraphService(IDriver driver) => _driver = driver;

    public async Task<List<string>> GetFollowersAsync(string userName)
    {
        await using var session = _driver.AsyncSession();
        return await session.ExecuteReadAsync(async tx =>
        {
            var cursor = await tx.RunAsync(
                "MATCH (f:User)-[:FOLLOWS]->(u:User {name: $name}) RETURN f.name AS name",
                new { name = userName });
            return (await cursor.ToListAsync())
                .Select(r => r["name"].As<string>())
                .ToList();
        });
    }
}

WeftKitKV — AWS SDK for .NET

WeftKitKV exposes a DynamoDB REST API. Point the AWS SDK at WeftKit's endpoint instead of AWS.

Default port: 8000

Client Setup and Low-Level Item Operations

csharp
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.Model;
using Amazon.Runtime;

// Point the SDK at WeftKit instead of AWS
var config = new AmazonDynamoDBConfig
{
    ServiceURL = "http://localhost:8000"
};
var credentials = new BasicAWSCredentials("weftkit", "secret");
var dynamo      = new AmazonDynamoDBClient(credentials, config);

// --- PutItem (INSERT or full replace) ---
static async Task PutProductAsync(IAmazonDynamoDB dynamo,
    string productId, string name, double price, int stock)
{
    await dynamo.PutItemAsync(new PutItemRequest
    {
        TableName = "products",
        Item = new Dictionary<string, AttributeValue>
        {
            ["productId"] = new AttributeValue { S = productId },
            ["name"]      = new AttributeValue { S = name },
            ["price"]     = new AttributeValue { N = price.ToString() },
            ["stock"]     = new AttributeValue { N = stock.ToString() }
        }
    });
    Console.WriteLine($"PutItem succeeded for: {productId}");
}

// --- GetItem ---
static async Task<Dictionary<string, AttributeValue>?> GetProductAsync(
    IAmazonDynamoDB dynamo, string productId)
{
    var response = await dynamo.GetItemAsync(new GetItemRequest
    {
        TableName = "products",
        Key = new Dictionary<string, AttributeValue>
        {
            ["productId"] = new AttributeValue { S = productId }
        }
    });

    if (!response.IsItemSet)
    {
        Console.WriteLine($"Item not found: {productId}");
        return null;
    }

    Console.WriteLine($"Found: {response.Item["name"].S} | " +
                      $"price={response.Item["price"].N} | " +
                      $"stock={response.Item["stock"].N}");
    return response.Item;
}

// --- UpdateItem (partial update with condition) ---
static async Task DecrementStockAsync(IAmazonDynamoDB dynamo, string productId, int quantity)
{
    await dynamo.UpdateItemAsync(new UpdateItemRequest
    {
        TableName        = "products",
        Key              = new Dictionary<string, AttributeValue>
        {
            ["productId"] = new AttributeValue { S = productId }
        },
        UpdateExpression    = "SET stock = stock - :qty",
        ConditionExpression = "stock >= :qty",
        ExpressionAttributeValues = new Dictionary<string, AttributeValue>
        {
            [":qty"] = new AttributeValue { N = quantity.ToString() }
        }
    });
}

// --- DeleteItem ---
static async Task DeleteProductAsync(IAmazonDynamoDB dynamo, string productId)
{
    await dynamo.DeleteItemAsync(new DeleteItemRequest
    {
        TableName = "products",
        Key = new Dictionary<string, AttributeValue>
        {
            ["productId"] = new AttributeValue { S = productId }
        }
    });
    Console.WriteLine($"Deleted: {productId}");
}

// --- Query with KeyConditionExpression ---
static async Task<List<Dictionary<string, AttributeValue>>> QueryByCategoryAsync(
    IAmazonDynamoDB dynamo, string category)
{
    var response = await dynamo.QueryAsync(new QueryRequest
    {
        TableName              = "products",
        KeyConditionExpression = "category = :cat",
        ExpressionAttributeValues = new Dictionary<string, AttributeValue>
        {
            [":cat"] = new AttributeValue { S = category }
        },
        Limit = 100
    });

    Console.WriteLine($"Items found: {response.Count}");
    return response.Items;
}

High-Level DynamoDBContext with Attributes

csharp
using Amazon.DynamoDBv2;
using Amazon.DynamoDBv2.DataModel;

// --- POCO annotated for the high-level client ---
[DynamoDBTable("products")]
public class ProductItem
{
    [DynamoDBHashKey("productId")]
    public string ProductId { get; set; } = string.Empty;

    [DynamoDBProperty("name")]
    public string Name { get; set; } = string.Empty;

    [DynamoDBProperty("price")]
    public double Price { get; set; }

    [DynamoDBProperty("stock")]
    public int Stock { get; set; }
}

// --- Usage with DynamoDBContext ---
static async Task HighLevelExampleAsync(IAmazonDynamoDB dynamo)
{
    var context = new DynamoDBContext(dynamo);

    // Save (upsert)
    var product = new ProductItem
    {
        ProductId = "prod-001",
        Name      = "Widget Pro",
        Price     = 29.99,
        Stock     = 250
    };
    await context.SaveAsync(product);

    // Load by primary key
    var loaded = await context.LoadAsync<ProductItem>("prod-001");
    Console.WriteLine($"Loaded: {loaded?.Name}");

    // Update and save
    if (loaded is not null)
    {
        loaded.Price = 24.99;
        await context.SaveAsync(loaded);
    }

    // Delete
    await context.DeleteAsync<ProductItem>("prod-001");
}

ASP.NET Core DI Registration

csharp
// Program.cs
builder.Services.AddSingleton<IAmazonDynamoDB>(_ =>
{
    var config = new AmazonDynamoDBConfig
    {
        ServiceURL = builder.Configuration["WeftKitKV:Endpoint"] ?? "http://localhost:8000"
    };
    var creds = new BasicAWSCredentials("weftkit", "secret");
    return new AmazonDynamoDBClient(creds, config);
});

builder.Services.AddScoped<IDynamoDBContext>(sp =>
    new DynamoDBContext(sp.GetRequiredService<IAmazonDynamoDB>()));
json
// appsettings.json
{
  "WeftKitKV": {
    "Endpoint": "http://localhost:8000"
  }
}

Connection String Reference

| Engine | Driver | Connection Detail | |---|---|---| | WeftKitRel | Npgsql / EF Core / Dapper | Host=localhost;Port=5432;Database=mydb;Username=app_user;Password=secret | | WeftKitDoc | MongoDB .NET Driver | mongodb://app_user:secret@localhost:27017/catalog | | WeftKitMem | StackExchange.Redis | localhost:6379,password=secret | | WeftKitGraph | Neo4j .NET Driver | bolt://localhost:7687 | | WeftKitKV | AWS SDK for .NET | ServiceURL = "http://localhost:8000" |

All ports are configurable in weftkit.toml. See the Integration Guides overview for TLS/SSL and authentication details.