Marc's answer works, but in my case it caused a decent amount of code bloat (and it's easy to forget to do it that way), so I came up with an abstraction that imposes a pattern.
Here's how you use it:
await db.TransactAsync(commands => commands .Enqueue(tran => tran.SomeCommandAsync(...)) .Enqueue(tran => tran.SomeCommandAsync(...)) .Enqueue(tran => tran.SomeCommandAsync(...)));
Here's the implementation:
public static class RedisExtensions { public static async Task TransactAsync(this IDatabase db, Action<RedisCommandQueue> addCommands) { var tran = db.CreateTransaction(); var q = new RedisCommandQueue(tran); addCommands(q); if (await tran.ExecuteAsync()) await q.CompleteAsync(); } } public class RedisCommandQueue { private readonly ITransaction _tran; private readonly IList<Task> _tasks = new List<Task>(); public RedisCommandQueue Enqueue(Func<ITransaction, Task> cmd) { _tasks.Add(cmd(_tran)); return this; } internal RedisCommandQueue(ITransaction tran) => _tran = tran; internal Task CompleteAsync() => Task.WhenAll(_tasks); }
One caveat: this does not provide an easy way to get the result of any of the commands. In my case (and OP) this is normal - I always use transactions for a series of records. I found that it really helped truncate my code, and only exposing the tran inside Enqueue (which requires you to return the Task), I am less likely to forget that I should not await these commands at the time of input call them.
source share