Table of Contents

Running queries

Define a command once, run it however you need. Each block below is a complete example.

Define once, run

A QueryCommand is built from a SQL string and reused for the life of the app. Execution methods sit directly on it.

using RinkuLib.Queries;
using RinkuLib.Commands;

public record Track(int Id, string Name, decimal UnitPrice);

static readonly QueryCommand GetTracks = new(
    "SELECT TrackId AS Id, Name, UnitPrice FROM tracks WHERE AlbumId = @albumId");

using DbConnection cnn = GetConnection();

List<Track> tracks = GetTracks.Query<List<Track>>(cnn, new { albumId = 1 });

The command holds no per-call state and is safe to share across threads. Per-call values travel in the arguments.

The type argument picks the shape

Same command, different T, different result.

List<Track> all       = GetTracks.Query<List<Track>>(cnn, new { albumId = 1 });        // buffered
IEnumerable<Track> it = GetTracks.Query<IEnumerable<Track>>(cnn, new { albumId = 1 }); // streamed
Track track           = GetTrackById.Query<Track>(cnn, new { id = 10 });               // one, throws if absent
Optional<Track> maybe = GetTrackById.Query<Optional<Track>>(cnn, new { id = 99 });     // one or empty

Every shape and its zero-row behavior is on result shapes.

Parameters

Pass an object whose members match the parameter names, case-insensitive. Anonymous objects, records, and DTOs all work, and unmatched members are ignored.

static readonly QueryCommand ByComposer = new(
    "SELECT TrackId AS Id, Name, UnitPrice FROM tracks WHERE Composer = @composer AND UnitPrice >= @minPrice");

var tracks = ByComposer.Query<List<Track>>(cnn, new { composer = "AC/DC", minPrice = 0.99m });

When C# logic should set the values instead, use a builder. Both roads are covered on supplying values.

var b = ByComposer.StartBuilder();
b.Use("@composer", "AC/DC");
b.Use("@minPrice", 0.99m);
var tracks = b.Query<List<Track>>(cnn);

Writes and scalars

Execute returns the affected-row count, ExecuteScalar<T> a single value.

static readonly QueryCommand UpdatePrice = new(
    "UPDATE tracks SET UnitPrice = @price WHERE TrackId = @id");
static readonly QueryCommand CountTracks = new("SELECT COUNT(*) FROM tracks");

int affected = UpdatePrice.Execute(cnn, new { id = 10, price = 1.29m });
int total    = CountTracks.ExecuteScalar<int>(cnn);

Async

Every method has an async version. StreamQueryAsync returns an IAsyncEnumerable<T>.

List<Track> tracks = await GetTracks.QueryAsync<List<Track>>(cnn, new { albumId = 1 }, ct: token);

await foreach (Track t in GetTracks.StreamQueryAsync<Track>(cnn, new { albumId = 1 }, ct: token))
    Process(t);

Transactions, timeouts, cancellation

The optional context arguments come after the parameter object.

using var trans = cnn.BeginTransaction();
UpdatePrice.Execute(cnn, new { id = 10, price = 1.29m }, transaction: trans);
trans.Commit();

var slow = GetTracks.Query<List<Track>>(cnn, new { albumId = 1 }, timeout: 60);

var rows = await GetTracks.QueryAsync<List<Track>>(cnn, new { albumId = 1 }, ct: token);

Reuse one DbCommand across a batch

Bind a builder to a single DbCommand and a loop stops rebuilding it each pass.

static readonly QueryCommand Insert = new("INSERT INTO playlists (Name) VALUES (@name)");

using var sqlCmd = new SqlCommand();
var batch = Insert.StartBuilder(sqlCmd);
foreach (var name in names) {
    batch.Use("@name", name);
    batch.Execute(cnn);
}

Several result sets

One command, several selects, read in order.

static readonly QueryCommand Dashboard = new(
    "SELECT * FROM artists WHERE ArtistId = @id; SELECT * FROM albums WHERE ArtistId = @id");

using var multi = Dashboard.ExecuteMultiReader(cnn, out DbCommand cmd, new { id = 1 });
using (cmd) {
    Artist artist      = multi.Query<Artist>();
    List<Album> albums = multi.Query<List<Album>>();
}

See multiple result sets.

The SQL string on the connection

Skip declaring a QueryCommand and hand the SQL to the connection. It caches the command by the string. More on the SQL string.

List<Track> tracks = cnn.Query<List<Track>>(
    "SELECT TrackId AS Id, Name, UnitPrice FROM tracks WHERE AlbumId = @albumId",
    new { albumId = 1 });

A DbCommand you already have

The mapping side also runs on a command you built yourself.

static readonly CachedTypeParser<Track> Tracks = new();

using var cmd = cnn.CreateCommand();
cmd.CommandText = "SELECT TrackId AS Id, Name, UnitPrice FROM tracks WHERE TrackId = @id";
cmd.Parameters.Add(new SqlParameter("@id", 10));

Track track = Tracks.Query(cmd);

See any DbCommand.

One command that adapts to its input

The template can mark parts optional, so the values you pass decide the SQL. ?@ marks a variable optional, _X spreads a collection.

static readonly QueryCommand Search = new(
    "SELECT TrackId AS Id, Name, UnitPrice FROM tracks WHERE AlbumId = ?@albumId AND GenreId IN (?@genreIds_X)");

Search.Query<List<Track>>(cnn, new { albumId = 1 });
// SELECT TrackId AS Id, Name, UnitPrice FROM tracks WHERE AlbumId = @albumId

Search.Query<List<Track>>(cnn, new { genreIds = new[] { 1, 2, 3 } });
// SELECT TrackId AS Id, Name, UnitPrice FROM tracks WHERE GenreId IN (@genreIds_1, @genreIds_2, @genreIds_3)

static readonly QueryCommand UpdateTrack = new(
    "UPDATE tracks SET Name = ?@name, UnitPrice = ?@price WHERE TrackId = @trackId");

UpdateTrack.Execute(cnn, new { trackId = 10, name = "Remastered" });
// UPDATE tracks SET Name = @name WHERE TrackId = @trackId

The ?@ toggle is a structural rule the engine applies to every keyword section alike, the WHERE and the SET list above, and just as well a projected column, a join, a group-by, or an order-by. When several queries are really one with parts switched on and off, one command replaces them all. The template syntax is its own section, conditional SQL.

Cheatsheet

Goal Method Sync return Async return
Insert / Update / Delete Execute int Task<int>
Single value ExecuteScalar<T> T Task<T>
One row (throws if none) Query<T> T Task<T>
One row or empty Query<Optional<T>> Optional<T> Task<Optional<T>>
Exactly one row Query<Single<T>> Single<T> Task<Single<T>>
Many (buffered) Query<List<T>> List<T> Task<List<T>>
Many (streamed) Query<IEnumerable<T>> IEnumerable<T> Task<IEnumerable<T>>
Many (async stream) StreamQueryAsync<T> n/a IAsyncEnumerable<T>
A raw reader ExecuteReader DbDataReader Task<DbDataReader>
Several result sets ExecuteMultiReader MultiReader Task<MultiReader>

The T in Query<T> is open, not a fixed menu. New result shapes plug in the same way the built-in ones do.