Repository navigation
Support batch SQL updates from IEnumerable parameter sets #62
Description
Problem
Dapper.ExecuteAsync(sql, IEnumerable<object>, transaction) accepts a sequence of parameter objects and executes the update once for each object within the supplied transaction. Activout.DatabaseClient currently has no equivalent for an annotated DAO method.
How this was discovered
While migrating fork-order-processing-system to Activout.DatabaseClient 5.0.0-rc4, BookRepository.AddAsync and UpdateAsync needed to insert all book authors after the book row was created or updated.
The previous Dapper implementation used:
var authors = book.Authors.Select(author => new { book.ISBN, author.AuthorName });
await connection.ExecuteAsync(authorSql, authors, transaction);A DAO method currently builds only one SqlStatement: MethodHandler.AddSqlStatementParameters adds one value for each method argument, and DapperGateway.GetDynamicParameters converts that to one DynamicParameters instance. Passing IEnumerable<Author> as a normal argument therefore does not express Dapper's batch-execution overload.
Current workaround
Keep the operation in the repository transaction and issue one DAO call per author:
using var transaction = database.BeginTransaction();
await database.InsertBookAsync(/* ... */, transaction);
foreach (var author in book.Authors)
await database.InsertAuthorAsync(book.ISBN, author.AuthorName, transaction);
transaction.Commit();[SqlUpdate("INSERT INTO BookAuthor (ISBN, AuthorName) VALUES (@isbn, @authorName)")]
Task InsertAuthorAsync(string isbn, string authorName, IDbTransaction transaction);This preserves correctness and atomicity, and Dapper itself also performs the batch as repeated executions. However, it adds a generated-client dispatch/reflection path for each item and makes the intent less direct.
Suggested API
Add explicit batch binding for SqlUpdate, for example:
[SqlUpdate("INSERT INTO BookAuthor (ISBN, AuthorName) VALUES (@isbn, @authorName)")]
Task<int> InsertAuthorsAsync(
[BindMany] IEnumerable<BookAuthorInsert> authors,
IDbTransaction transaction);Possible implementation:
- Add a
[BindMany]parameter attribute rather than overloading[BindProperties], whose current semantics are one object with properties. - Have
MethodHandlerdetect it only forSqlUpdatemethods and produce multiple parameter sets, preserving the transaction parameter separately. - Extend
IDatabaseGatewaywith a bulk update method, for exampleTask<int> ExecuteManyAsync(SqlStatement statement, IEnumerable<IReadOnlyList<QueryParameter>> parameterSets). - In
DapperGateway, materialize each set as an object/dictionary orDynamicParametersand call Dapper'sExecuteAsync(sql, parameterSets, transaction), returning Dapper's aggregate affected-row count. - Add tests for an empty sequence, multiple rows,
[Bind("...")]property aliases,TaskandTask<int>returns, rollback behavior, and use withIWithTransactions.
An alternative is an overload accepting IEnumerable<T> plus a mapper, but an explicit attribute preserves the declarative DAO style and avoids ambiguous treatment of enumerable query parameters such as WHERE id = ANY(@ids).
Activity