Table of Contents

Context Definition

A QuarryContext subclass is the entry point for all queries and modifications.

Basic Context

[QuarryContext(Dialect = SqlDialect.SQLite)]
public partial class AppDb : QuarryContext
{
    public partial IEntityAccessor<User> Users();
    public partial IEntityAccessor<Order> Orders();
}

The class must be partial. The source generator emits a companion partial class containing:

  • Constructors -- one accepting IDbConnection, one accepting IDbConnection and bool ownsConnection, and a full constructor accepting IDbConnection, bool ownsConnection, TimeSpan? defaultTimeout, and IsolationLevel? defaultIsolation.
  • Entity accessor methods -- each partial IEntityAccessor<T> declaration gets a generated body that returns a carrier instance wired to the context's connection and dialect.
  • Insert / Update / Delete -- accessed through the entity accessor (db.Users().Insert(entity), db.Users().Update(), db.Users().Delete()). The generator intercepts each call site and emits pre-built SQL.
  • MigrateAsync -- generated when migration classes are present in the project. Accepts a DbConnection and optional MigrationOptions, then delegates to the migration runner with the correct dialect and an ordered list of discovered migrations.

All of these generated method bodies throw NotSupportedException at the carrier base class level. The interceptor emitter replaces every call site with a concrete implementation at compile time, so the throw path is never reached at runtime.

Dialect Selection

Available dialects: SQLite, PostgreSQL, MySQL, SqlServer.

[QuarryContext(Dialect = SqlDialect.PostgreSQL)]
public partial class PgDb : QuarryContext
{
    public partial IEntityAccessor<User> Users();
}

The Dialect property is required. It determines quoting style, parameter formatting, boolean literals, pagination syntax, and RETURNING/OUTPUT clauses for the entire context.

Schema Property

The optional Schema property on QuarryContextAttribute qualifies all table references with a database schema name:

[QuarryContext(Dialect = SqlDialect.PostgreSQL, Schema = "public")]
public partial class PgDb : QuarryContext
{
    public partial IEntityAccessor<User> Users();
}

When set, generated SQL uses qualified table names:

Dialect Schema value Generated SQL
PostgreSQL "public" "public"."users"
SqlServer "dbo" [dbo].[users]
MySQL "mydb" `mydb`.`users`
SQLite (ignored) "users"

If Schema is omitted or null, tables are referenced without schema qualification. This property allows the same schema classes to be reused across multiple contexts targeting different database schemas -- for example, a multi-tenant application where each tenant maps to a separate PostgreSQL schema.

MySqlBackslashEscapes (MySQL)

The optional MySqlBackslashEscapes property (default true) controls how Quarry emits LIKE patterns on MySQL. The default matches stock MySQL where backslash inside string literals is an escape character; the generator emits doubled backslashes in literal patterns and ESCAPE '\\' so the resulting SQL parses to a literal \.

// Default: matches stock MySQL sql_mode
[QuarryContext(Dialect = SqlDialect.MySQL)]
public partial class MyDb : QuarryContext { ... }

// Opt out for sessions/servers running NO_BACKSLASH_ESCAPES in sql_mode
[QuarryContext(Dialect = SqlDialect.MySQL, MySqlBackslashEscapes = false)]
public partial class MyAnsiDb : QuarryContext { ... }

Set the flag to false only when the consuming process configures MySQL's sql_mode to include NO_BACKSLASH_ESCAPES. The property has no effect on PostgreSQL, SQLite, or SQL Server. Mismatching the flag against the actual sql_mode produces either a 1064 syntax error (default mode without the flag) or doubled backslashes in matched data (NO_BACKSLASH_ESCAPES with the flag enabled).

Typed Accessor Chains (QuarryContext<TSelf>)

To chain entity accessors after a CTE-producing With<TDto>() call (db.With<Dto>(…).Users()…), derive from the generic base class instead:

[QuarryContext(Dialect = SqlDialect.SQLite)]
public partial class AppDb : QuarryContext<AppDb>
{
    public partial IEntityAccessor<User> Users();
    public partial IEntityAccessor<Order> Orders();
}

This opt-in form gives With<TDto>() the typed receiver it needs to return a builder whose follow-on accessor calls (.Users(), .Orders()) resolve correctly on the same context. The non-generic QuarryContext base class continues to work for all other queries; adopt QuarryContext<TSelf> only when you intend to use post-With accessor chains. See Querying → Common Table Expressions for the surrounding syntax.

Multiple Contexts

Multiple contexts with different dialects can coexist in the same project. Each generates its own interceptor file with dialect-correct SQL. The generator resolves the correct context by walking the receiver chain at each call site.

[QuarryContext(Dialect = SqlDialect.SQLite)]
public partial class CacheDb : QuarryContext
{
    public partial IEntityAccessor<CachedItem> Items();
}

[QuarryContext(Dialect = SqlDialect.PostgreSQL, Schema = "public")]
public partial class MainDb : QuarryContext
{
    public partial IEntityAccessor<User> Users();
    public partial IEntityAccessor<Order> Orders();
}

A practical example: an application that uses SQLite for a local offline cache and PostgreSQL for the primary database. Each context operates independently with its own connection:

// Local cache -- SQLite file on disk
await using var cache = new CacheDb(new SqliteConnection("Data Source=cache.db"));
await cache.MigrateAsync(cacheConnection);

var cached = await cache.Items()
    .Where(i => i.ExpiresAt > DateTime.UtcNow)
    .Select(i => i)
    .ExecuteFetchAllAsync();

// Main database -- PostgreSQL
await using var main = new MainDb(new NpgsqlConnection(connectionString));

var users = await main.Users()
    .Where(u => u.IsActive)
    .Select(u => (u.UserId, u.UserName))
    .ExecuteFetchAllAsync();

Both contexts can be registered in a DI container and injected where needed. The generated interceptors are fully independent -- each file contains only the SQL variants for its own dialect.

Connection Management and the Disposable Pattern

QuarryContext implements both IAsyncDisposable and IDisposable. Use await using to ensure the connection is cleaned up:

await using var db = new AppDb(connection);

var users = await db.Users()
    .Select(u => u)
    .ExecuteFetchAllAsync();
// connection is restored to its original state when db goes out of scope

Key behaviors:

  • The context accepts any IDbConnection, but it must be a DbConnection underneath (required for async support). Passing a non-DbConnection throws ArgumentException.
  • If the connection was already open when the context was created, the context leaves it open on dispose.
  • If the connection was closed, the context opens it on first query and closes it on dispose.
  • The default query timeout is 30 seconds. Override it via the constructor:
await using var db = new AppDb(
    connection,
    ownsConnection: false,
    defaultTimeout: TimeSpan.FromSeconds(10),
    defaultIsolation: IsolationLevel.ReadCommitted);

Avoid sharing a single context across concurrent operations. Create a new context per unit of work (per request, per background job, etc.).

Connection Ownership

By default, the context borrows the connection -- it may close it on dispose but never disposes it. When ownsConnection is true, the context takes full ownership and disposes the connection when the context is disposed:

// Context owns the connection -- disposes it when done
await using var db = new AppDb(
    new SqliteConnection("Data Source=app.db"),
    ownsConnection: true);
ownsConnection Connection was closed Connection was open
false (default) Closes on dispose Left open
true Disposes on dispose Disposes on dispose

This is primarily useful for dependency injection scenarios where the context should manage the entire connection lifecycle.

Dependency Injection

Register the context as a scoped service so each request gets its own context and connection. The DI container handles disposal at the end of the scope, which disposes the owned connection and returns it to the pool:

// Program.cs
services.AddScoped<AppDb>(_ =>
    new AppDb(new SqliteConnection(connectionString), ownsConnection: true));

Consumers inject the context directly -- no connection knowledge required:

public class UserService(AppDb db)
{
    public async Task<List<User>> GetActiveUsers()
    {
        return await db.Users()
            .Where(u => u.IsActive)
            .Select(u => u)
            .ExecuteFetchAllAsync();
    }
}

Usage

await using var db = new AppDb(connection);

// Query
var users = await db.Users()
    .Select(u => u)
    .ExecuteFetchAllAsync();

// Insert
await db.Users()
    .Insert(new User { UserName = "Alice" })
    .ExecuteNonQueryAsync();

// Update
await db.Users()
    .Update()
    .Set(u => u.UserName = "Bob")
    .Where(u => u.UserId == 1)
    .ExecuteNonQueryAsync();

// Delete
await db.Users()
    .Delete()
    .Where(u => u.UserId == 1)
    .ExecuteNonQueryAsync();

// Migrations
await db.MigrateAsync(connection);

InterceptorsNamespaces

Your consumer .csproj must register the context's namespace for interceptors:

<PropertyGroup>
  <InterceptorsNamespaces>$(InterceptorsNamespaces);MyApp.Data</InterceptorsNamespaces>
</PropertyGroup>

If your context has no namespace, use Quarry.Generated.

For multiple contexts in different namespaces, add each one:

<PropertyGroup>
  <InterceptorsNamespaces>$(InterceptorsNamespaces);MyApp.Data;MyApp.Cache</InterceptorsNamespaces>
</PropertyGroup>

Quarry's NuGet build/Quarry.targets auto-registers the generator's internal Quarry.Generated namespace, so you only need to list your own context namespaces.

If you forget this property or use the wrong namespace, the QRY044 analyzer emits a warning at each [QuarryContext] class whose namespace is missing, with the exact csproj line to paste. If the warning is suppressed or ignored, the build then fails with CS9137 (C# 12 interceptors for an unregistered namespace). Add the printed entry to resolve both.