A complete .NET library suite for SQL database abstraction and fluent query building, with multi-database support powered by SqlKata.
Working with multiple SQL databases in .NET often means:
- Writing repetitive boilerplate for connection management and disposal.
- Manually wiring up different ADO.NET drivers per database.
- Duplicating transaction handling logic across repositories.
- Losing observability without a consistent logging hook.
- Mixing raw SQL strings with query builder calls inconsistently.
MVFC.SQLCraft solves this by providing a single abstract driver base that every database-specific package extends. You pick the driver for your database, inherit from it, and get a consistent, battle-tested API for querying, executing, and managing transactions — with optional structured logging built in.
All packages follow the same pattern:
SQLCraftDriver(base class) — manages connections, logging lifecycle, and transaction scope.XxxCraftDriver(driver class per database) — provides theDbConnectionfactory and the SqlKataCompilerfor its dialect.IDatabaseLogger(optional interface) — intercepts SQL before/after execution and on errors.IQueryFactory/DefaultQueryFactory— thin wrapper around SqlKata'sQueryFactory.
Once you understand one driver, all others work identically.
| Package | Database | Downloads |
|---|---|---|
| MVFC.SQLCraft | Base abstractions | |
| MVFC.SQLCraft.Mysql | MySQL / MariaDB | |
| MVFC.SQLCraft.MsSQL | Microsoft SQL Server | |
| MVFC.SQLCraft.PostgreSql | PostgreSQL | |
| MVFC.SQLCraft.SQLite | SQLite | |
| MVFC.SQLCraft.Firebird | Firebird | |
| MVFC.SQLCraft.Oracle | Oracle |
Install the base package and the driver for your target database:
# Base abstractions (required)
dotnet add package MVFC.SQLCraft
# Pick your driver
dotnet add package MVFC.SQLCraft.Mysql
dotnet add package MVFC.SQLCraft.MsSQL
dotnet add package MVFC.SQLCraft.PostgreSql
dotnet add package MVFC.SQLCraft.SQLite
dotnet add package MVFC.SQLCraft.Firebird
dotnet add package MVFC.SQLCraft.OracleInherit from the database-specific driver and pass your connection string:
using MVFC.SQLCraft.Mysql;
public class MyAppDatabase : MysqlCraftDriver
{
public MyAppDatabase(string connectionString)
: base(connectionString, logger: null) { }
}builder.Services.AddSingleton<MyAppDatabase>(_ =>
new MyAppDatabase(builder.Configuration.GetConnectionString("Default")!));public class UserRepository
{
private readonly MyAppDatabase _db;
public UserRepository(MyAppDatabase db) => _db = db;
public async Task<IEnumerable<User>> GetActiveUsersAsync(CancellationToken ct = default)
{
var query = new Query("users")
.Where("active", true)
.OrderByDesc("created_at")
.Limit(100);
return await _db.QueryAsync<User>(query, ct: ct);
}
public async Task<User?> GetByIdAsync(int id, CancellationToken ct = default)
{
var query = new Query("users").Where("id", id);
return await _db.QueryFirstOrDefaultAsync<User>(query, ct: ct);
}
}All methods are defined on SQLCraftDriver and available in every driver.
| Method | Description |
|---|---|
| Query(query, tx?) | Returns all matching rows |
| QueryAsync(query, tx?, ct) | Async version of Query |
| QueryFirstOrDefault(query, tx?) | Returns the first matching row or null |
| QueryFirstOrDefaultAsync(query, tx?, ct) | Async version |
| Method | Description |
|---|---|
| Execute(query, tx?) | Executes a SqlKata Query (INSERT/UPDATE/DELETE) |
| Execute(sql, tx?) | Executes a raw SQL string |
| ExecuteAsync(query, tx?, ct) | Async version for Query |
| ExecuteAsync(sql, tx?, ct) | Async version for raw SQL |
| Method | Description |
|---|---|
| ExecuteInTransaction(action, isolation?) | Wraps a synchronous action in a managed transaction |
| ExecuteInTransactionAsync(action, isolation?, ct) | Async version — commits on success, rolls back on exception |
The transaction wrappers manage the full lifecycle (open, commit, rollback, dispose) automatically:
await _db.ExecuteInTransactionAsync(async (driver, tx, ct) =>
{
var insertOrder = new Query("orders").AsInsert(new { CustomerId = 42, Total = 199.90m });
await driver.ExecuteAsync(insertOrder, tx, ct);
var updateStock = new Query("products").Where("id", 7).AsDecrement("stock", 1);
await driver.ExecuteAsync(updateStock, tx, ct);
}, isolation: IsolationLevel.ReadCommitted, ct: cancellationToken);If any operation throws, the transaction is automatically rolled back.
Implement IDatabaseLogger to intercept all SQL executions:
public class MyDatabaseLogger : IDatabaseLogger
{
private readonly ILogger<MyDatabaseLogger> _logger;
public MyDatabaseLogger(ILogger<MyDatabaseLogger> logger) => _logger = logger;
public Task OnBeforeExecuteAsync(string sql, object? bindings, CancellationToken ct = default)
{
_logger.LogDebug("Executing SQL: {Sql} | Bindings: {Bindings}", sql, bindings);
return Task.CompletedTask;
}
public Task OnAfterExecuteAsync(string sql, object? bindings, TimeSpan elapsed, CancellationToken ct = default)
{
_logger.LogInformation("SQL executed in {Elapsed}ms: {Sql}", elapsed.TotalMilliseconds, sql);
return Task.CompletedTask;
}
public Task OnErrorAsync(string sql, object? bindings, Exception ex, CancellationToken ct = default)
{
_logger.LogError(ex, "SQL failed: {Sql} | Bindings: {Bindings}", sql, bindings);
return Task.CompletedTask;
}
}Then wire it up:
public class MyAppDatabase : MysqlCraftDriver
{
public MyAppDatabase(string connectionString, MyDatabaseLogger logger)
: base(connectionString, logger) { }
}src/
MVFC.SQLCraft/ # Base abstractions (SQLCraftDriver, IDatabaseLogger, IQueryFactory)
MVFC.SQLCraft.Firebird/ # Firebird driver
MVFC.SQLCraft.MsSQL/ # SQL Server driver
MVFC.SQLCraft.Mysql/ # MySQL / MariaDB driver
MVFC.SQLCraft.Oracle/ # Oracle driver
MVFC.SQLCraft.PostgreSql/ # PostgreSQL driver
MVFC.SQLCraft.SQLite/ # SQLite driver
tests/
# Unit and integration tests
tools/
# Build tooling (Cake)
.NET 9.0+
SqlKata >= 2.x
The underlying ADO.NET provider for each database (pulled automatically via NuGet)
See CONTRIBUTING.md.