Rinku overview
Query
public record Album(int Id, string Title);
static readonly QueryCommand GetAlbums = new("SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = @artistId");
List<Album> albums = GetAlbums.Query<List<Album>>(cnn, new { artistId = 7 });
The SQL string form accesses a cached QueryCommand through the exact SQL string.
List<Album> albums = cnn.Query<List<Album>>("SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = @artistId", new { artistId = 7 });
Result shapes
Album first = GetAlbums.Query<Album>(cnn, new { artistId = 7 });
Optional<Album> maybe = GetAlbums.Query<Optional<Album>>(cnn, new { artistId = 7 });
Single<Album> exactlyOne = GetAlbums.Query<Single<Album>>(cnn, new { artistId = 7 });
List<Album> list = GetAlbums.Query<List<Album>>(cnn, new { artistId = 7 });
Album[] array = GetAlbums.Query<Album[]>(cnn, new { artistId = 7 });
IEnumerable<Album> stream = GetAlbums.Query<IEnumerable<Album>>(cnn, new { artistId = 7 });
The requested result type controls how complete mapped values are consumed.
Map the boundary
public record Customer(int Id, string Name);
static readonly QueryCommand GetCustomers = new("SELECT customer_id AS Id, display_name AS Name FROM customers");
List<Customer> customers = GetCustomers.Query<List<Customer>>(cnn);
// SQL aliases adapt the database names.
public record Customer([Alt("customer_id")] int Id, [Alt("display_name")] string Name);
static readonly QueryCommand GetCustomers = new("SELECT customer_id, display_name FROM customers");
List<Customer> customers = GetCustomers.Query<List<Customer>>(cnn);
// Alt adapts the .NET mapping name.
public record Customer(int Id, string Name);
TypeParsingInfo.GetOrAdd<Customer>().UpdateAltName(names =>
names.GetDefaultName() switch
{
"Id" => new NameComparer("customer_id"),
"Name" => new NameComparer("display_name"),
_ => null
});
// The mapping is registered outside SQL and the model.
Recursive mapping
public record Country(int Id, string Name) : IDbReadable;
public record Address(string Street, Country Country) : IDbReadable;
public record Customer(int Id, string Name, Address Address);
static readonly QueryCommand GetCustomer = new("SELECT c.CustomerId AS Id, c.Name, a.Street AS AddressStreet, co.CountryId AS AddressCountryId, co.Name AS AddressCountryName FROM customers c JOIN addresses a ON a.AddressId = c.AddressId JOIN countries co ON co.CountryId = a.CountryId WHERE c.CustomerId = @customerId");
Customer customer = GetCustomer.Query<Customer>(cnn, new { customerId = 12 });
// AddressCountryId fills customer.Address.Country.Id.
// The same mapping process continues through each nested type.
public record Employee(int Id, string Name, [Alt("Boss")] Employee? Manager = null) : IDbReadable;
static readonly QueryCommand GetEmployee = new("SELECT e.EmployeeId AS Id, e.Name, m.EmployeeId AS ManagerId, m.Name AS ManagerName, b.EmployeeId AS ManagerBossId, b.Name AS ManagerBossName FROM employees e LEFT JOIN employees m ON m.EmployeeId = e.ManagerId LEFT JOIN employees b ON b.EmployeeId = m.ManagerId WHERE e.EmployeeId = @employeeId");
Employee employee = GetEmployee.Query<Employee>(cnn, new { employeeId = 12 });
// ManagerId fills employee.Manager.Id.
// ManagerBossId fills employee.Manager.Manager.Id through the Boss alternate name.
// The default lets this constructor finish when no deeper Manager path exists.
Objects · Nested paths · Construction
Multi-row folding
public record Album(int Id, string Title) : IDbReadable;
public record ArtistWithAlbums(int Id, string Name, [Alt("Album")] List<Album> Albums);
static readonly QueryCommand GetArtists = new("SELECT ar.ArtistId AS Id, ar.Name, al.AlbumId AS AlbumId, al.Title AS AlbumTitle FROM artists ar JOIN albums al ON al.ArtistId = ar.ArtistId ORDER BY ar.ArtistId");
List<ArtistWithAlbums> artists = GetArtists.Query<List<ArtistWithAlbums>>(cnn);
// List<Album> folds consecutive rows inside one ArtistWithAlbums.
// Album itself is mapped through the same recursive mapping process.
Collections · Grouping · Custom multi-row mappings
Conditional SQL
static readonly QueryCommand SearchAlbums = new("SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = ?@artistId AND Title LIKE ?@title");
List<Album> albums = SearchAlbums.Query<List<Album>>(cnn, new { artistId = 7 });
// SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = @artistId
albums = SearchAlbums.Query<List<Album>>(cnn);
// SELECT AlbumId AS Id, Title FROM albums
Collections can expand into database parameters.
static readonly QueryCommand GetAlbumsByIds = new("SELECT AlbumId AS Id, Title FROM albums WHERE AlbumId IN (@ids_X)");
int[] ids = [2, 5, 9];
List<Album> albums = GetAlbumsByIds.Query<List<Album>>(cnn, new { ids });
// SELECT AlbumId AS Id, Title FROM albums WHERE AlbumId IN (@ids_0, @ids_1, @ids_2)
Conditional variables · Collection expansion · Markers
Builder state
var search = SearchAlbums.StartBuilder();
if (artistId is int id)
search.Use("@artistId", id);
if (!string.IsNullOrWhiteSpace(title))
search.Use("@title", title);
List<Album> albums = search.Query<List<Album>>(cnn);
The builder holds values for this execution flow. The referenced QueryCommand does not hold that per-call state.
Execution
static readonly QueryCommand RenameAlbum = new("UPDATE albums SET Title = @title WHERE AlbumId = @albumId");
static readonly QueryCommand CountAlbums = new("SELECT COUNT(*) FROM albums WHERE ArtistId = @artistId");
static readonly QueryCommand UpdateArtist = new("UPDATE artists SET ModifiedAt = @modifiedAt WHERE ArtistId = @artistId");
int affected = RenameAlbum.Execute(cnn, new { albumId = 12, title = "Kind of Blue" });
int count = CountAlbums.Query<int>(cnn, new { artistId = 7 });
List<Album> albums = await GetAlbums.QueryAsync<List<Album>>(cnn, new { artistId = 7 }, ct: cancellationToken);
await foreach (Album album in GetAlbums.StreamQueryAsync<Album>(cnn, new { artistId = 7 }, ct: cancellationToken))
Console.WriteLine(album.Title);
using DbTransaction transaction = cnn.BeginTransaction();
RenameAlbum.Execute(cnn, new { albumId = 12, title = "Kind of Blue" }, transaction: transaction);
UpdateArtist.Execute(cnn, new { artistId = 7, modifiedAt = DateTime.UtcNow }, transaction: transaction);
transaction.Commit();
Execution · Async · Streaming · Transactions
Several result sets
static readonly QueryCommand GetDashboard = new("SELECT ArtistId AS Id, Name FROM artists WHERE ArtistId = @artistId; SELECT AlbumId AS Id, Title FROM albums WHERE ArtistId = @artistId");
using MultiReader results = GetDashboard.ExecuteMultiReader(cnn, new { artistId = 7 });
Artist artist = results.Query<Artist>();
List<Album> albums = results.Query<List<Album>>();
Stored procedures
static readonly QueryCommand GetAlbumsForArtist = new("GetAlbumsForArtist", ["artistId"]);
List<Album> albums = GetAlbumsForArtist.Query<List<Album>>(cnn, new { artistId = 7 });
QueryCommand getAlbumsForArtist = QueryCommand.FromProc("GetAlbumsForArtist", cnn);
List<Album> albums = getAlbumsForArtist.Query<List<Album>>(cnn, new { artistId = 7 });
Existing ADO.NET commands
static readonly CachedTypeParser<Album> AlbumParser = new();
using DbCommand command = cnn.CreateCommand();
command.CommandText = "SELECT AlbumId AS Id, Title FROM albums WHERE AlbumId = 12";
Album album = AlbumParser.Query(command);
DbDataReader reader = GetAlbums.ExecuteReader(cnn, out DbCommand command, new { artistId = 7 });
using (command)
using (reader)
{
while (reader.Read())
Console.WriteLine(reader.GetString(1));
}
Existing DbCommand · Raw readers
Code generation
static readonly CachedTypeParser<List<GetAlbumsByArtistResult>> AlbumParser = new();
List<GetAlbumsByArtistResult> albums = AlbumParser.Query(cnn.GetAlbumsByArtist(artistId: 7));
Generated methods return DbCommand instances.
using DbCommand command = cnn.GetAlbumsByArtist(artistId: 7);
using DbDataReader reader = command.ExecuteReader();
Code generation · Generated code · Analyzers
Tracking
Album original = new(12, "Blue");
IRuntimeTrackingItem<Album> edit = RuntimeTracking.Default<Album>().Create(original);
edit.Set(nameof(Album.Title), "Kind of Blue");
foreach (TrackingChange change in edit.GetChanges())
Console.WriteLine($"{change.Name} {change.OriginalValue} -> {change.Value}");
The same generated item can expose a typed contract.
public interface IAlbumEdit : IRuntimeTrackingItem<Album>
{
string Title { get; set; }
}
RuntimeTrackingOptions<Album> options = RuntimeTracking.CreateOptions<Album, IAlbumEdit>();
IAlbumEdit edit = options.GetRegistration<IAlbumEdit>().Create(original);
edit.Title = "Kind of Blue";
Tracking · Runtime tracking · Lists
Customization
public readonly record struct PositionalValue<T>(T Value);
TypeParsingInfo.AddOrSet(typeof(PositionalValue<>), CtorTypeInfo.Instance);
static readonly QueryCommand CountAlbums = new("SELECT COUNT(*) FROM albums");
PositionalValue<int> count = CountAlbums.Query<PositionalValue<int>>(cnn);
Type registration · Complete result parsers
Custom multi-row mappings · Parameter binding · Method caller · Cache control · Advanced customization