Skip to content

Mise Readers

SteffenBlake edited this page Sep 28, 2026 · 1 revision

Readers

DbReader executes query builders on one connection scope. Choose a terminal based on whether the application needs all rows, one row, one value, or an existence check.

Choose a terminal

Method Return shape No matching rows
ListAsync<T> Result<IReadOnlyList<T>> Successful empty list
FirstOrNotFoundAsync<T> Result<T> NotFound
ScalarAsync<T> Result<T?> Successful default(T)
ExistsAsync Result<bool> Successful false
StreamAsync<T> IAsyncEnumerable<T> No yielded items

Row terminals require T : IRow<T>. See Result rows. Scalar and existence reads do not require a mapped row.

Read a list

var query = new PostgreSqlQueryBuilder()
    .Select($"{CourseTbl.CourseIdCol:raw}")
    .Select($"{CourseTbl.TitleCol:raw}")
    .Select($"{CourseTbl.CapacityCol:raw}")
    .From($"{CourseTbl.Table:raw}");

var result = await reader.ListAsync<CourseRow>(query, cancellationToken);

if (result.IsSuccess(out var courses))
{
    foreach (var course in courses)
    {
        Console.WriteLine(course.Title);
    }
}

ListAsync buffers the mapped rows before returning. The command and database reader are disposed by the terminal. Use Streaming when the caller should process rows incrementally.

Read one row

query.Where($"{CourseTbl.CourseIdCol:raw} = {courseId}");

Result<CourseRow> course = await reader.FirstOrNotFoundAsync<CourseRow>(
    query,
    cancellationToken
);

The terminal reads the first row and ignores later rows. It does not enforce uniqueness. Use a unique predicate or deliberate ordering when several matches are possible. It requests ADO.NET SingleRow behavior, which is a provider hint rather than a SQL uniqueness check.

Scalar reads

var titleQuery = new PostgreSqlQueryBuilder()
    .Select($"{CourseTbl.TitleCol:raw}")
    .From($"{CourseTbl.Table:raw}")
    .Where($"{CourseTbl.CourseIdCol:raw} = {courseId}");

Result<string?> title = await reader.ScalarAsync<string>(titleQuery, cancellationToken);

Only the first cell is returned. No row and SQL NULL both produce default(T). For reference types this is null. For value types, pass a nullable type such as long? if you need null rather than zero for an absent value.

The non-null value is cast to T, not generally converted. Match the provider's returned type. For example, PostgreSQL COUNT(*) returns long; requesting int can fail with a cast exception.

Existence checks

var existsQuery = new PostgreSqlQueryBuilder()
    .Select($"1")
    .From($"{CourseTbl.Table:raw}")
    .Where($"{CourseTbl.CourseIdCol:raw} = {courseId}");

Result<bool> exists = await reader.ExistsAsync(existsQuery, cancellationToken);

ExistsAsync checks whether the query yields a row. A query selecting COUNT(*) yields a row even when the count is zero, so it is not an appropriate existence query.

Cancellation and failures

Pass the operation's token to each terminal. Provider, mapping, cancellation, and disposal exceptions propagate to your caller; Mise does not automatically wrap them in an Error result. Expected empty-result behavior follows the table above.

Each reader allows one active operation. Finish a read before starting another on the same instance. See Resource lifetimes for connection ownership and Custom SQL and procedures for reader behavior flags.

Clone this wiki locally