RepositoryBase methods for SqlCommand initialization - lobodava/artisan-orm GitHub Wiki

The RepositoryBase(string connectionString) constructor creates a closed Microsoft.Data.SqlClient.SqlConnection from the supplied connection string and exposes it through Connection. It does not discover application configuration by itself.

In the source being prepared for 4.1.0, RepositoryBase(transaction, connectionString) uses the supplied active transaction's existing connection. The caller retains ownership of both resources: disposing this repository does not commit, roll back, close, or dispose them. A completed transaction is rejected, and transaction-start helpers reject an already attached transaction. See Transactions for the ownership and commit rules.

The following methods create a command, attach the repository's current transaction when present, invoke a callback, and dispose the command on exit:

Sync signature What the callback does
T GetByCommand<T>(Func<SqlCommand, T> func) Configures and executes SQL, returning its result
void RunCommand(Action<SqlCommand> action) Configures and executes SQL, without returning a result
int ExecuteCommand(Action<SqlCommand> action) Configures SQL only; the repository executes it afterward
Async signature Callback contract
Task<T> GetByCommandAsync<T>(Func<SqlCommand, Task<T>> func) Asynchronous work, no supplied token
Task<T> GetByCommandAsync<T>(Func<SqlCommand, CancellationToken, Task<T>> func, CancellationToken cancellationToken) Asynchronous work receiving the token
Task RunCommandAsync(Func<SqlCommand, Task> action) Asynchronous work, no supplied token
Task RunCommandAsync(Func<SqlCommand, CancellationToken, Task> action, CancellationToken cancellationToken) Asynchronous work receiving the token
Task<int> ExecuteCommandAsync(Action<SqlCommand> action, CancellationToken cancellationToken = default) Synchronous configuration; asynchronous execution by the repository

GetByCommand

The callback must execute the command, usually through an Artisan read extension. Merely assigning CommandText does not execute SQL. The read helper opens a closed connection and closes it afterward; if the caller already opened it, it stays open.

The following method belongs to a repository derived from RepositoryBase. User has a registered mapper matching the procedure's SELECT list:

public User? GetUserById(int id)
{
    return GetByCommand(cmd =>
    {
        cmd.UseProcedure("dbo.GetUserById");
        cmd.AddIntParam("@Id", id);
        return cmd.ReadTo<User>();
    });
}

Pass a cancellation token into both layers of the async form:

public Task<User?> GetUserByIdAsync(int id, CancellationToken cancellationToken)
{
    return GetByCommandAsync((cmd, token) =>
    {
        cmd.UseProcedure("dbo.GetUserById");
        cmd.AddIntParam("@Id", id);
        return cmd.ReadToAsync<User>(token);
    }, cancellationToken);
}

The wrapper forwards the token to its callback; it does not automatically pass it to operations performed by that callback. In 4.1.0, async read helpers forward the supplied token through connection opening, command execution, and library-controlled asynchronous row/result traversal. Synchronous callbacks still control their own work and cancellation checks. See Read Methods Understanding.

RunCommand

This callback also executes the command itself. One use is assigning a result to an outer variable. In this example, the procedure returns a user followed by tinyint role IDs; both sets are consumed even if no user exists:

public User? GetById(int id)
{
    User? user = null;
    byte[] roleIds = Array.Empty<byte>();

    RunCommand(cmd =>
    {
        cmd.UseProcedure("dbo.GetUserById");
        cmd.AddIntParam("@Id", id);
        cmd.ExecuteReader(reader =>
        {
            user = reader.ReadTo<User>();
            roleIds = reader.ReadToArray<byte>();
        });
    });

    if (user != null)
        user.RoleIds = roleIds;
    return user;
}

ExecuteReader(Action<SqlDataReader>) above is an Artisan extension that owns and disposes its reader. RunCommand owns and disposes the command. Neither wrapper disposes the repository's connection object; dispose the repository at the end of its lifetime.

ExecuteCommand

Use this for configuration followed by non-query execution. It opens a closed connection, executes the SQL, closes the connection if it opened it, and returns the T-SQL RETURN value. The returned integer is not the affected-row count.

public bool DeleteUser(int userId)
{
    var returnValue = ExecuteCommand(cmd =>
    {
        cmd.UseProcedure("dbo.DeleteUser");
        cmd.AddIntParam("@UserId", userId);
    });
    return returnValue == 0;
}

Do not execute the command inside this configuration callback, or it would execute twice. ExecuteCommandAsync also takes Action<SqlCommand>: do not pass an async lambda that becomes async void. Use RunCommandAsync / GetByCommandAsync when the callback itself must await work.

For transaction commit/rollback behavior, see Transactions. Keep the same repository for commands that must participate in its active transaction.

Source: RepositoryBase.cs.


See also:

⚠️ **GitHub.com Fallback** ⚠️