Query and QuerySingle
Query<T> reads many rows into objects; QuerySingle, QuerySingleOrDefault, QueryFirst and QueryFirstOrDefault read one; ExecuteScalar<T> reads one value. Pick by how many rows you expect.
Journeyman · C# · T-SQL · Updated
After this lesson you can
- Read many rows with Query<T>, one row with QuerySingleOrDefault<T>, and one value with ExecuteScalar<T>
- Choose between QuerySingle, QuerySingleOrDefault, QueryFirst and QueryFirstOrDefault by how many rows you expect
Key takeaways
Query<T>reads every row as aT; add.ToList()to get aList<T>. No rows gives an empty list, not an error.QuerySingleOrDefault<T>returnsnullfor no rows and throws for two or more, which makes it the right choice for aGetById.QuerySingle<T>throws when no row comes back, so use it only when the row must exist.QueryFirstandQueryFirstOrDefaultreturn the first of several rows; pair them withORDER BY.ExecuteScalar<T>reads one value, such as a count, already as aT, with no cast.
Dapper's reading methods differ in one thing: how many rows you expect back. Choosing the right one turns a wrong number of rows into a clear error instead of a quiet bug.
All the examples use this model:
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = "";
public decimal? UnitPrice { get; set; }
}
Many rows: Query<T>
Query<T> returns every row, each as a T. It gives back an IEnumerable<T>; add .ToList() to get a List<T>, which is what your DAOs return.
using Dapper;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=Northwind;Integrated Security=true;TrustServerCertificate=true;";
using SqlConnection connection = new SqlConnection(connectionString);
List<Product> sauces = connection.Query<Product>(
"SELECT ProductId, ProductName, UnitPrice FROM Products WHERE ProductName LIKE '%Sauce%' ORDER BY ProductName;").ToList();
Console.WriteLine( quot;{sauces.Count} sauces");
foreach (Product sauce in sauces)
{
Console.WriteLine( quot; {sauce.ProductName}");
}
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = "";
public decimal? UnitPrice { get; set; }
}
2 sauces
Louisiana Fiery Hot Pepper Sauce
Northwoods Cranberry Sauce
No rows is fine: Query returns an empty list.
One row: QuerySingle and friends
When you expect one row, such as looking up a product by its id, there are four methods. They differ in what they do when the number of rows isn't one:
| Method | No rows | One row | Two or more rows |
|---|---|---|---|
QuerySingle<T> |
Throws | Returns it | Throws |
QuerySingleOrDefault<T> |
Returns null |
Returns it | Throws |
QueryFirst<T> |
Throws | Returns it | Returns the first |
QueryFirstOrDefault<T> |
Returns null |
Returns it | Returns the first |
A DAO's GetById uses QuerySingleOrDefault: there may be no such product (return null), but there can never be two with one id (if there were, something is badly wrong, and an exception says so).
using Dapper;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=Northwind;Integrated Security=true;TrustServerCertificate=true;";
using SqlConnection connection = new SqlConnection(connectionString);
string sql = "SELECT ProductId, ProductName, UnitPrice FROM Products WHERE ProductId = @ProductId;";
Product? chai = connection.QuerySingleOrDefault<Product>(sql, new { ProductId = 1 });
Product? nothing = connection.QuerySingleOrDefault<Product>(sql, new { ProductId = 999 });
Console.WriteLine(chai?.ProductName);
Console.WriteLine(nothing is null);
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = "";
public decimal? UnitPrice { get; set; }
}
Chai
True
QuerySingle is stricter: a missing row is an error, for when the row must exist.
using Dapper;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=Northwind;Integrated Security=true;TrustServerCertificate=true;";
using SqlConnection connection = new SqlConnection(connectionString);
Product product = connection.QuerySingle<Product>(
"SELECT ProductId, ProductName, UnitPrice FROM Products WHERE ProductId = @ProductId;", new { ProductId = 999 });
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; } = "";
public decimal? UnitPrice { get; set; }
}
System.InvalidOperationException: Sequence contains no elements
QueryFirst and QueryFirstOrDefault don't mind extra rows; use them with ORDER BY when you want "the top one", such as the newest order.
One value: ExecuteScalar<T>
For a single value, such as a count, use ExecuteScalar<T>:
using Dapper;
using Microsoft.Data.SqlClient;
string connectionString = "Server=localhost;Database=Northwind;Integrated Security=true;TrustServerCertificate=true;";
using SqlConnection connection = new SqlConnection(connectionString);
int count = connection.ExecuteScalar<int>(
"SELECT COUNT(*) FROM Products WHERE CategoryId = @CategoryId;", new { CategoryId = 1 });
Console.WriteLine( quot;{count} beverages");
12 beverages
No casting: ExecuteScalar<int> returns an int. (QuerySingle<int> would work too.)
Check yourself
2 questions to try on the lesson page.
So what
Many rows, Query; one or none, QuerySingleOrDefault; one value, ExecuteScalar. Picking by how many rows you expect means the wrong number is caught where it happens.
Key terms
- Query<T>()
- QuerySingleOrDefault<T>()
- ExecuteScalar<T>()