RoomSharp.QueryExtensions
0.1.0
dotnet add package RoomSharp.QueryExtensions --version 0.1.0
NuGet\Install-Package RoomSharp.QueryExtensions -Version 0.1.0
<PackageReference Include="RoomSharp.QueryExtensions" Version="0.1.0" />
<PackageVersion Include="RoomSharp.QueryExtensions" Version="0.1.0" />
<PackageReference Include="RoomSharp.QueryExtensions" />
paket add RoomSharp.QueryExtensions --version 0.1.0
#r "nuget: RoomSharp.QueryExtensions, 0.1.0"
#:package RoomSharp.QueryExtensions@0.1.0
#addin nuget:?package=RoomSharp.QueryExtensions&version=0.1.0
#tool nuget:?package=RoomSharp.QueryExtensions&version=0.1.0
RoomSharp.QueryExtensions
Designed for developers who want control, not magic.
RoomSharp.QueryExtensions is a small companion package to RoomSharp that adds a Dapper-like API on top of RoomSharp.Core.RoomDatabase for ad-hoc SQL:
- Query POCOs/DTOs/records without declaring a DAO method
- Fluent Query Builder - Laravel-style
db.From<T>().Where().OrderBy().Get() - Eager Loading - Include related entities with batched queries
- Stream results with
IAsyncEnumerable<T> - Run raw commands and scalars (
Execute*,ExecuteScalar*) - Multi-mapping up to 5 types with
splitOn - Drop down to low-level, allocation-free row access (
RowReader)
It keeps the call-site ergonomic, while caching the expensive parts (parameter binding and result materialization) behind the scenes.
Note: This is a feature-complete preview. APIs may evolve, but the overall design is considered stable.
⚠️ QueryExtensions is not a full ORM. It focuses on querying and mapping, not change tracking or migrations.
When Should I Use QueryExtensions?
Use QueryExtensions when:
- You need ad-hoc queries without creating a DAO
- You want Dapper-like control with RoomSharp integration
- You need streaming or zero-allocation access
Prefer DAOs when:
- Queries are part of your core domain
- You want compile-time SQL ownership
- You rely on migrations and schema coupling
Install
dotnet add package RoomSharp.QueryExtensions
Target frameworks: net8.0, net9.0, net10.0.
Quick Examples
// Fluent query (type-safe)
var users = await db.From<User>()
.Where(u => u.IsActive)
.OrderBy(u => u.Name)
.GetAsync();
// With eager loading
var orders = await db.From<Order>()
.Include(o => o.Customer)
.GetAsync();
// Raw SQL
var count = await db.ExecuteScalarAsync<long>(
"SELECT COUNT(*) FROM users WHERE IsActive = @active",
new { active = true });
// Streaming large results
await foreach (var item in db.QueryStreamAsync<Item>("SELECT * FROM items"))
{
Process(item);
}
Fluent Query Builder
A Laravel-style fluent API for building queries without writing raw SQL. Supports both String-based and Expression-based syntax.
Basic Usage
using RoomSharp.QueryExtensions.FluentQuery;
// String-based
var todos = await db.Table<Todo>("todos")
.Where("IsDone", false)
.Where("CategoryId", ">", 5)
.OrderBy("CreatedAt")
.Take(20)
.GetAsync();
// Expression-based (Type-safe)
var todos = await db.From<Todo>()
.Where(t => t.IsDone == false)
.Where(t => t.CategoryId > 5)
.OrderBy(t => t.CreatedAt)
.Take(20)
.GetAsync();
Where Methods
| Method | Example |
|---|---|
Where(col, val) |
.Where("IsActive", true) |
Where(col, op, val) |
.Where("Age", ">=", 18) |
OrWhere(col, val) |
.OrWhere("Status", "pending") |
WhereIn(col, vals) |
.WhereIn("Status", "active", "pending") |
WhereNotIn(col, vals) |
.WhereNotIn("Status", "deleted") |
WhereNull(col) |
.WhereNull("DeletedAt") |
WhereNotNull(col) |
.WhereNotNull("Email") |
WhereBetween(col, a, b) |
.WhereBetween("Price", 10, 100) |
WhereContains(col, val) |
.WhereContains("Title", "test") |
WhereStartsWith(col, val) |
.WhereStartsWith("Name", "A") |
WhereRaw(sql, params) |
.WhereRaw("LOWER(name) = @p0", "test") |
Expression-based Where
// Comparison operators
.Where(t => t.Price > 100)
.Where(t => t.Status == "active")
// String methods
.Where(t => t.Title.Contains("test"))
.Where(t => t.Name.StartsWith("A"))
// Boolean properties
.Where(t => t.IsActive)
.Where(t => !t.IsDeleted)
// Method calls are supported (evaluated at runtime)
.Where(t => t.CreatedAt > DateTime.Now.AddDays(-7))
.Where(t => t.ExpiresAt < DateTime.UtcNow)
Expression-based Helper Methods
All helper methods also support type-safe Expression syntax:
| Method | Example |
|---|---|
WhereNull(x => x.Prop) |
.WhereNull(x => x.DeletedAt) |
WhereNotNull(x => x.Prop) |
.WhereNotNull(x => x.Email) |
WhereIn(x => x.Prop, vals) |
.WhereIn(x => x.Status, "active", "pending") |
WhereIn(x => x.Prop, list) |
.WhereIn(x => x.Id, userIds) |
WhereNotIn(x => x.Prop, vals) |
.WhereNotIn(x => x.Type, "deleted", "spam") |
WhereBetween(x => x.Prop, a, b) |
.WhereBetween(x => x.Price, 10m, 100m) |
WhereLike(x => x.Prop, pattern) |
.WhereLike(x => x.Name, "A%") |
WhereContains(x => x.Prop, val) |
.WhereContains(x => x.Description, "keyword") |
WhereStartsWith(x => x.Prop, val) |
.WhereStartsWith(x => x.Code, "PRD") |
WhereEndsWith(x => x.Prop, val) |
.WhereEndsWith(x => x.Email, "@gmail.com") |
// Example: Type-safe query with Expression helpers
var orders = await db.From<Order>()
.WhereNotNull(o => o.ShippedAt)
.WhereBetween(o => o.Total, 100m, 1000m)
.WhereIn(o => o.Status, "completed", "shipped")
.OrderByDescending(o => o.CreatedAt)
.GetAsync();
⚠️ Limitation: Only direct properties are supported in Expression-based methods. Nested navigation properties (e.g.,
x => x.Customer.Name) are NOT supported. UseJoin()with string-based methods for filtering on related entities:// ❌ NOT supported .WhereEndsWith(x => x.Customer.Name, "Smith") // ✅ Use Join instead .Join("customers", "orders.customer_id", "customers.id") .WhereEndsWith("customers.name", "Smith")
Ordering & Pagination
.OrderBy("CreatedAt")
.OrderByDescending("Priority")
.OrderBy(t => t.Name)
.Skip(20)
.Take(10)
.ForPage(3, 10) // Page 3, 10 items per page
Terminal Methods
| Method | Description |
|---|---|
Get() |
Returns IEnumerable<T> |
GetAsync() |
Returns List<T> async |
GetStreamAsync() |
Returns IAsyncEnumerable<T> (streaming) |
First() / FirstAsync() |
First row or throws |
FirstOrDefault() / FirstOrDefaultAsync() |
First row or null |
Count() / CountAsync() |
Row count |
Exists() / ExistsAsync() |
Any rows exist? |
Aggregations
// String-based
var total = db.Table<Order>("orders").Sum<decimal>("Amount");
var avg = db.Table<Product>("products").Avg<double>("Price");
var max = db.Table<Order>("orders").Max<DateTime>("CreatedAt");
var min = db.Table<Product>("products").Min<decimal>("Price");
// Expression-based (type-safe)
var total = db.From<Order>().Sum(o => o.Amount);
var avg = db.From<Product>().Avg(p => p.Price);
var max = db.From<Order>().Max(o => o.CreatedAt);
var min = db.From<Product>().Min(p => p.Price);
// Async versions
var totalAsync = await db.From<Order>().SumAsync(o => o.Amount);
var maxAsync = await db.From<Order>().MaxAsync(o => o.CreatedAt);
Debugging & Configuration
// Get generated SQL without executing (for debugging/logging)
var sql = db.From<Order>()
.Where(o => o.Status == "active")
.ToSql();
// "SELECT * FROM orders WHERE Status = @p0"
// Get SQL with parameters
var (sql, parameters) = query.ToSqlWithParameters();
// Set command timeout
var orders = await db.From<Order>()
.WithTimeout(TimeSpan.FromSeconds(60))
.GetAsync();
// Or with seconds directly
db.From<Order>().WithTimeout(120).Get();
Select Projection
// Select specific columns using expression (type-safe)
var orders = db.From<Order>()
.Select(o => new { o.Id, o.Total, o.Status })
.Get();
// Generates: SELECT Id, Total, Status FROM orders
Multi-Provider Support
Auto-detects dialect from RoomDatabase.Dialect or DbSession.Dialect, or specify explicitly:
using RoomSharp.QueryExtensions.FluentQuery.Dialects;
db.Table<Todo>("todos", SqliteDialect.Instance)
db.Table<Todo>("todos", MySqlDialect.Instance)
db.Table<Todo>("todos", PostgreSqlDialect.Instance)
db.Table<Todo>("todos", SqlServerDialect.Instance)
Joins
// INNER JOIN
var results = db.Table<OrderDto>("orders")
.Join("customers", "orders.CustomerId", "customers.Id")
.Select("orders.*", "customers.Name as CustomerName")
.GetAsync();
// LEFT JOIN
db.Table<UserDto>("users")
.LeftJoin("addresses", "users.AddressId", "addresses.Id")
.Select("users.*", "addresses.City")
.Get();
// Multiple JOINs
db.Table<OrderDto>("orders")
.Join("customers", "orders.CustomerId", "customers.Id")
.LeftJoin("products", "orders.ProductId", "products.Id")
.Select("orders.Id", "customers.Name", "products.Title")
.Get();
// With operator
db.Table<T>("a").Join("b", "a.Col", ">", "b.Col"); // a.Col > b.Col
// CROSS JOIN
db.Table<T>("a").CrossJoin("b");
| Method | SQL Output |
|---|---|
.Join(table, col1, col2) |
INNER JOIN table ON col1 = col2 |
.Join(table, col1, op, col2) |
INNER JOIN table ON col1 op col2 |
.LeftJoin(...) |
LEFT JOIN ... |
.RightJoin(...) |
RIGHT JOIN ... |
.CrossJoin(table) |
CROSS JOIN table |
⚠️ Column Ambiguity: When using
Join(), columns with the same name in multiple tables (e.g.,Id) will cause SQL errors. Always use table prefix inSelect():// ✅ Correct - use table prefix to avoid ambiguity db.From<Order>() .Select("orders.id", "orders.total") .Join("customers", "orders.customer_id", "customers.id") .Get(); // ❌ Wrong - "ambiguous column name: Id" error db.From<Order>() .Select(o => new { o.Id, o.Total }) .Join("customers", ...) .Get();
Include (Eager Loading)
Load related entities efficiently with batched queries (solves N+1 problem):
// Expression-based (type-safe, recommended)
var orders = await db.From<Order>()
.Include(o => o.Customer)
.Include(o => o.Items)
.Where(o => o.Status == "active")
.GetAsync();
// String-based (fallback)
var orders = await db.From<Order>()
.Include("customer")
.GetAsync();
// Nested includes (string path)
var orders = await db.From<Order>()
.Include("customer.address")
.GetAsync();
// Nested includes (ThenInclude - type-safe)
var orders = await db.From<Order>()
.Include(o => o.Customer)
.ThenInclude<Customer, Address>(c => c.Address)
.GetAsync();
// Also works with DbSession
await using var session = await db.OpenSessionAsync();
var items = await session.From<Order>()
.Include(o => o.Customer)
.GetAsync();
Note: Related properties must have
[Relation]attribute defined.
// One-to-One / One-to-Many
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
[Relation(Entity = typeof(Customer),
ParentColumn = nameof(CustomerId),
EntityColumn = nameof(Customer.Id))]
public Customer? Customer { get; set; }
[Relation(Entity = typeof(OrderItem),
ParentColumn = nameof(Id),
EntityColumn = "OrderId")]
public List<OrderItem>? Items { get; set; }
}
// Many-to-Many (with junction table)
public class Student
{
public int Id { get; set; }
[Relation(Entity = typeof(Course),
ParentColumn = nameof(Id),
EntityColumn = nameof(Course.Id),
AssociateBy = "student_courses")] // Junction table
public List<Course>? Courses { get; set; }
}
Note: Include executes additional batched queries. It does not generate JOINs or a single large SQL statement.
API Reference
All methods are available as extension methods on both RoomDatabase and DbSession.
Query Methods
Query<T> - Returns all rows as IEnumerable<T>
// Basic usage
var todos = db.Query<Todo>("SELECT * FROM todos").AsList();
// With parameters
var activeTodos = db.Query<Todo>(
"SELECT * FROM todos WHERE IsDone = @isDone AND CategoryId = @categoryId",
new { isDone = false, categoryId = 5 }).AsList();
// With DbSession
await using var session = await db.OpenSessionAsync();
var todos = session.Query<Todo>("SELECT * FROM todos").AsList();
QueryAsync<T> - Returns all rows as List<T>
// Basic usage
var todos = await db.QueryAsync<Todo>("SELECT * FROM todos");
// With parameters
var activeTodos = await db.QueryAsync<Todo>(
"SELECT * FROM todos WHERE IsDone = @isDone",
new { isDone = false });
// With cancellation
var todos = await db.QueryAsync<Todo>(
"SELECT * FROM todos",
param: null,
cancellationToken: cts.Token);
QueryStreamAsync<T> - Streams rows as IAsyncEnumerable<T>
// Stream large result sets without loading all into memory
await foreach (var todo in db.QueryStreamAsync<Todo>("SELECT * FROM todos"))
{
Console.WriteLine(todo.Title);
}
// With parameters
await foreach (var todo in db.QueryStreamAsync<Todo>(
"SELECT * FROM todos WHERE CategoryId = @categoryId",
new { categoryId = 5 }))
{
await ProcessTodoAsync(todo);
}
Single-Row Methods
QueryFirst<T> - Returns first row, throws if empty
// Throws InvalidOperationException if no rows
var todo = db.QueryFirst<Todo>(
"SELECT * FROM todos WHERE Id = @id",
new { id = 1 });
QueryFirstAsync<T> - Async version of QueryFirst
var todo = await db.QueryFirstAsync<Todo>(
"SELECT * FROM todos WHERE Id = @id",
new { id = 1 });
QueryFirstOrDefault<T> - Returns first row or default
// Returns null/default if no rows
var todo = db.QueryFirstOrDefault<Todo>(
"SELECT * FROM todos WHERE Id = @id",
new { id = 999 });
if (todo is null)
Console.WriteLine("Todo not found");
QueryFirstOrDefaultAsync<T> - Async version of QueryFirstOrDefault
var todo = await db.QueryFirstOrDefaultAsync<Todo>(
"SELECT * FROM todos WHERE Email = @email",
new { email = "user@example.com" });
QuerySingle<T> - Returns exactly one row, throws otherwise
// Throws if 0 or 2+ rows
var user = db.QuerySingle<User>(
"SELECT * FROM users WHERE Email = @email",
new { email = "admin@example.com" });
QuerySingleAsync<T> - Async version of QuerySingle
var user = await db.QuerySingleAsync<User>(
"SELECT * FROM users WHERE Id = @id",
new { id = 1 });
QuerySingleOrDefault<T> - Returns one row or default, throws if 2+
// Returns null/default if no rows, throws if 2+ rows
var user = db.QuerySingleOrDefault<User>(
"SELECT * FROM users WHERE Email = @email",
new { email = "unique@example.com" });
QuerySingleOrDefaultAsync<T> - Async version of QuerySingleOrDefault
var user = await db.QuerySingleOrDefaultAsync<User>(
"SELECT * FROM users WHERE Email = @email",
new { email = "unique@example.com" });
Execute Methods
Execute - Executes non-query, returns affected rows
// INSERT
var inserted = db.Execute(
"INSERT INTO todos (Title, IsDone) VALUES (@title, @isDone)",
new { title = "New Todo", isDone = false });
// UPDATE
var updated = db.Execute(
"UPDATE todos SET IsDone = @isDone WHERE Id = @id",
new { isDone = true, id = 5 });
// DELETE
var deleted = db.Execute(
"DELETE FROM todos WHERE IsDone = @isDone",
new { isDone = true });
ExecuteAsync - Async version of Execute
var affected = await db.ExecuteAsync(
"UPDATE todos SET Title = @title WHERE Id = @id",
new { title = "Updated Title", id = 1 });
// With cancellation
var affected = await db.ExecuteAsync(
"DELETE FROM todos WHERE CreatedAt < @date",
new { date = DateTime.UtcNow.AddDays(-30) },
cancellationToken: cts.Token);
Scalar Methods
ExecuteScalar<T> - Returns single scalar value
// COUNT
var count = db.ExecuteScalar<long>("SELECT COUNT(*) FROM todos");
// SUM
var total = db.ExecuteScalar<decimal>(
"SELECT SUM(Amount) FROM orders WHERE UserId = @userId",
new { userId = 1 });
// Single value
var title = db.ExecuteScalar<string>(
"SELECT Title FROM todos WHERE Id = @id",
new { id = 1 });
// Check existence
var exists = db.ExecuteScalar<long>(
"SELECT COUNT(*) FROM users WHERE Email = @email",
new { email = "test@example.com" }) > 0;
ExecuteScalarAsync<T> - Async version of ExecuteScalar
var count = await db.ExecuteScalarAsync<long>("SELECT COUNT(*) FROM todos");
var maxId = await db.ExecuteScalarAsync<long?>("SELECT MAX(Id) FROM todos");
Multi-Mapping Methods
Supports mapping 2 to 5 objects per row. The splitOn parameter specifies where each object starts:
- For 2 types:
splitOn: "col"(single column) - For 3+ types:
splitOn: "col1,col2,..."(comma-separated)
Query<T1, T2, TResult> - Maps two objects per row
public record Order(long Id, decimal Total, long CustomerId);
public record Customer(long Id, string Name);
var orders = db.Query<Order, Customer, (Order, Customer)>(
"""
SELECT o.Id, o.Total, o.CustomerId,
c.Id AS CustId, c.Name
FROM orders o
JOIN customers c ON c.Id = o.CustomerId
""",
map: (order, customer) => (order, customer),
splitOn: "CustId").AsList();
Query<T1, T2, T3, TResult> - Maps three objects per row
public record Order(long Id, decimal Total);
public record Customer(long Id, string Name);
public record Address(long Id, string City);
var rows = db.Query<Order, Customer, Address, (Order, Customer, Address)>(
"""
SELECT o.Id, o.Total,
c.Id AS CustId, c.Name,
a.Id AS AddrId, a.City
FROM orders o
JOIN customers c ON c.Id = o.CustomerId
JOIN addresses a ON a.Id = c.AddressId
""",
map: (o, c, a) => (o, c, a),
splitOn: "CustId,AddrId").AsList();
Query<T1, T2, T3, T4, T5, TResult> - Maps up to 5 objects
var rows = db.Query<T1, T2, T3, T4, T5, TResult>(
sql,
map: (t1, t2, t3, t4, t5) => new Result(t1, t2, t3, t4, t5),
splitOn: "Col2,Col3,Col4,Col5");
Important Multi-Mapping Notes:
splitOnvalues must be explicit column aliases (e.g.,CustId, notId)- Columns must be ordered in the SELECT to match the type parameters (T1, T2, T3...)
- Each
splitOncolumn marks where the next type begins
Transaction Methods
BeginTransaction - Starts a new transaction
using var tx = db.BeginTransaction();
try
{
db.Execute("INSERT INTO logs (Message) VALUES (@msg)", new { msg = "Start" });
db.Execute("UPDATE counters SET Value = Value + 1");
tx.Commit();
}
catch
{
tx.Rollback();
throw;
}
BeginTransactionAsync - Async version of BeginTransaction
await using var tx = await db.BeginTransactionAsync();
// ...
InTransaction - Runs action in transaction with auto commit/rollback
// With return value
var result = db.InTransaction(tx =>
{
db.Execute("INSERT INTO todos (Title) VALUES (@title)", new { title = "New" });
return db.ExecuteScalar<long>("SELECT last_insert_rowid()");
});
// Without return value
db.InTransaction(tx =>
{
db.Execute("UPDATE todos SET IsDone = 1 WHERE Id = @id", new { id = 1 });
db.Execute("INSERT INTO logs (Message) VALUES (@msg)", new { msg = "Completed" });
});
InTransactionAsync - Async version of InTransaction
var newId = await db.InTransactionAsync(async (tx, ct) =>
{
await db.ExecuteAsync("INSERT INTO todos (Title) VALUES (@title)", new { title = "New" });
return await db.ExecuteScalarAsync<long>("SELECT last_insert_rowid()");
});
// Without return value
await db.InTransactionAsync(async (tx, ct) =>
{
await db.ExecuteAsync("DELETE FROM todos WHERE IsDone = 1");
await db.ExecuteAsync("VACUUM");
});
DbSession Methods
All query/execute methods are also available on DbSession for Parallel mode:
// Open session
await using var session = await db.OpenSessionAsync();
// Query methods
var todos = await session.QueryAsync<Todo>("SELECT * FROM todos");
var todo = await session.QueryFirstOrDefaultAsync<Todo>("SELECT * FROM todos WHERE Id = @id", new { id = 1 });
// Execute methods
await session.ExecuteAsync("UPDATE todos SET Title = @title WHERE Id = @id", new { title = "New", id = 1 });
// Scalar methods
var count = await session.ExecuteScalarAsync<long>("SELECT COUNT(*) FROM todos");
// Multi-mapping
var rows = session.Query<Order, Customer, (Order, Customer)>(sql, (o, c) => (o, c), "CustomerId").AsList();
// Transactions
await session.InTransactionAsync(async (tx, ct) =>
{
await session.ExecuteAsync("INSERT INTO todos (Title) VALUES (@title)", new { title = "A" });
await session.ExecuteAsync("INSERT INTO todos (Title) VALUES (@title)", new { title = "B" });
});
Helper Methods
AsList - Materializes IEnumerable to List
var list = db.Query<Todo>("SELECT * FROM todos").AsList();
ToListAsync - Async materialization
var list = await db.QueryStreamAsync<Todo>("SELECT * FROM todos").ToListAsync();
Low-Level Row Access (RowReader)
For zero-allocation row processing, use ReadRaw / ReadRawAsync with RowReader:
using RoomSharp.QueryExtensions.Extensions;
using RoomSharp.QueryExtensions.LowLevel;
// Async version
await db.ReadRawAsync(
"SELECT Id, Title, IsDone FROM todos",
param: null,
(in RowReader r) =>
{
var id = r.Get<long>(0);
var title = r.Get<string>(1);
var isDone = r.Get<bool>(2);
Console.WriteLine($"{id}: {title} ({(isDone ? "done" : "pending")})");
});
// Sync version
db.ReadRaw("SELECT Id, Title FROM todos", null, (in RowReader r) =>
{
Console.WriteLine($"{r.Get<long>(0)}: {r.Get<string>(1)}");
});
When to use RowReader:
- Aggregation without creating objects (counting, summing)
- Streaming to files (CSV/JSON export)
- High-frequency queries where GC pressure matters
- Data transformation/migration between databases
RowReader API:
| Method | Description |
|---|---|
Get<T>(int ordinal) |
Returns value at column index |
IsDbNull(int ordinal) |
Checks if column is NULL |
FieldCount |
Number of columns in row |
Mapping Rules (POCO/DTO)
- Column-to-property matching is case-insensitive by default.
[ColumnInfo(Name="...")]overrides the expected column name.[Ignore]excludes a property from mapping.[TypeConverter(ConverterType=...)]converts the provider value into the property type.- Extra columns are ignored; missing columns keep the property at its default value.
- For non-scalar mapping,
Tmust have a public parameterless constructor, and properties must be public settable. - For scalar mapping (
stringor value types), the first column is read (null/DBNullbecomesdefault).
public sealed class TodoDto
{
public long Id { get; set; }
[ColumnInfo(Name = "Title")]
public required string Name { get; set; }
[Ignore]
public string? ComputedField { get; set; }
[TypeConverter(ConverterType = typeof(JsonConverter<List<string>>))]
public List<string> Tags { get; set; } = [];
}
Parameter Binding
- Parameters are provided as an anonymous object / POCO, and its public readable properties become parameters (cached per parameter type).
- Property names are used as the parameter name (most providers match
@idin SQL to parameter nameid). nullbecomesDBNull.Value.
// Anonymous object
var todos = await db.QueryAsync<Todo>(
"SELECT * FROM todos WHERE CategoryId = @categoryId AND IsDone = @isDone",
new { categoryId = 5, isDone = false });
// POCO parameter object
public record TodoFilter(int CategoryId, bool IsDone);
var filter = new TodoFilter(5, false);
var todos = await db.QueryAsync<Todo>(
"SELECT * FROM todos WHERE CategoryId = @CategoryId AND IsDone = @IsDone",
filter);
Concurrency & Thread-Safety
QueryExtensions is fully integrated with RoomSharp's concurrency model:
- Serialized mode (default): All extension methods automatically acquire the database gate before executing. This ensures thread-safe access to the shared connection.
- Parallel mode: Use
DbSessionoverloads for concurrent operations. Each session has its own connection.
// Serialized mode - automatically thread-safe
var todos = await db.QueryAsync<Todo>("SELECT * FROM todos");
// Parallel mode - use sessions for concurrency
await using var session1 = await db.OpenSessionAsync();
await using var session2 = await db.OpenSessionAsync();
// These can run concurrently
var task1 = session1.QueryAsync<Todo>("SELECT * FROM todos WHERE CategoryId = 1");
var task2 = session2.QueryAsync<Todo>("SELECT * FROM todos WHERE CategoryId = 2");
await Task.WhenAll(task1.AsTask(), task2.AsTask());
Performance Notes
| Aspect | Details |
|---|---|
| Cold path | First call per (T, SQL, column layout) builds an IL materializer; first call per parameter type builds an IL binder |
| Hot path | Reuses cached delegates; no reflection in the steady state |
| Gate overhead | ~100-200ns per call in Serialized mode (negligible compared to SQL execution time) |
| Streaming | QueryStreamAsync holds the command/reader open for the duration of enumeration |
Supported Type Patterns
The materializer supports:
- Classes with parameterless constructor (traditional POCOs)
- Record types with primary constructors:
public record User(long Id, string Name); - Classes with primary constructors (C# 12+):
public class User(long id, string name) - Constructor parameters are matched to column names (case-insensitive)
- Default parameter values are used when columns are missing
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Data.SqlClient (>= 6.1.3)
- RoomSharp (>= 0.4.3)
-
net8.0
- Microsoft.Data.SqlClient (>= 6.1.3)
- RoomSharp (>= 0.4.3)
-
net9.0
- Microsoft.Data.SqlClient (>= 6.1.3)
- RoomSharp (>= 0.4.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 93 | 12/19/2025 |