Multi-row mapping
List inside a mapped value
public record Album(int Id, string Title) : IDbReadable;
public record Artist(int Id, string Name, List<Album> Albums);
static readonly QueryCommand GetArtists = new("SELECT ar.ArtistId AS Id, ar.Name, al.AlbumId AS AlbumsId, al.Title AS AlbumsTitle FROM artists ar JOIN albums al ON al.ArtistId = ar.ArtistId ORDER BY ar.ArtistId");
List<Artist> artists = GetArtists.Query<List<Artist>>(cnn);
List<Album> folds consecutive rows while Album keeps its mapping.
Rows for one parent group stay consecutive.
Name adaptation on the same path
public record Album(int Id, string Title) : IDbReadable;
public record Artist(int Id, string Name, [Alt("Album")] List<Album> Albums);
List<Artist> artists = cnn.Query<List<Artist>>("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");
// AlbumId reaches Albums.Id through Alt("Album").
Left join with no child
public record Album([AbortOnNull] int Id, string Title) : IDbReadable;
public record Artist(int Id, string Name, List<Album> Albums);
List<Artist> artists = cnn.Query<List<Artist>>("SELECT ar.ArtistId AS Id, ar.Name, al.AlbumId AS AlbumsId, al.Title AS AlbumsTitle FROM artists ar LEFT JOIN albums al ON al.ArtistId = ar.ArtistId ORDER BY ar.ArtistId");
// NULL AlbumsId keeps the Artist and contributes no Album.
Scalar elements
public record Palette(int Id, List<string> Colors);
Palette palette = cnn.Query<Palette>("SELECT PaletteId AS Id, Color AS Colors FROM palette_colors WHERE PaletteId = @paletteId ORDER BY SortOrder", new { paletteId = 3 });
// Database NULL Colors elements are skipped.
public record Palette(int Id, [KeepNullElements] List<string?> Colors);
// Database NULL Colors elements remain in the collection.
Another multi-row mapping inside the element shape
public record Track(int Id, string Name) : IDbReadable;
public record Album(int Id, string Title, List<Track> Tracks) : IDbReadable;
public record Artist(int Id, string Name, List<Album> Albums);
static readonly QueryCommand GetArtists = new("SELECT ar.ArtistId AS Id, ar.Name, al.AlbumId AS AlbumsId, al.Title AS AlbumsTitle, tr.TrackId AS AlbumsTracksId, tr.Name AS AlbumsTracksName FROM artists ar JOIN albums al ON al.ArtistId = ar.ArtistId JOIN tracks tr ON tr.AlbumId = al.AlbumId ORDER BY ar.ArtistId, al.AlbumId");
List<Artist> artists = GetArtists.Query<List<Artist>>(cnn);
// Artist.Albums folds rows at the Artist level.
// Album.Tracks folds rows while each Album is mapped.
Each multi-row mapping negotiates its own boundary at the point where it appears in the type shape.
Side by side multi-row mappings
public record OrderItem([AbortOnNull] int Id, decimal Price) : IDbReadable;
public record OrderNote([AbortOnNull] int Id, string Text) : IDbReadable;
public record Order(int Id, List<OrderItem> Items, List<OrderNote> Notes);
List<Order> orders = cnn.Query<List<Order>>("SELECT o.OrderId AS Id, i.ItemId AS ItemsId, i.Price AS ItemsPrice, n.NoteId AS NotesId, n.Text AS NotesText FROM orders o LEFT JOIN order_items i ON i.OrderId = o.OrderId LEFT JOIN order_notes n ON n.OrderId = o.OrderId ORDER BY o.OrderId");
// Each row is offered to both multi-row mappings.
// AbortOnNull lets either side contribute no element on that row.
The two mappings still use their own element mapping and grouping behavior.
Built in collection result shapes
const string sql = "SELECT AlbumId AS Id, Title FROM albums ORDER BY AlbumId";
List<Album> list = cnn.Query<List<Album>>(sql);
Album[] array = cnn.Query<Album[]>(sql);
IEnumerable<Album> stream = cnn.Query<IEnumerable<Album>>(sql);
List<T>, arrays, and IEnumerable<T> have built in multi-row mappings.