docs/rules/SER305.md
Commands queued on an ITransaction or IBatch are not sent when you call them - they are buffered, and
go to the server only when you call Execute/ExecuteAsync. The Task you get back therefore cannot complete
until then, so awaiting it at the point of queueing waits for something that can only happen after the line
that is waiting. Your code stops there, permanently.
// flagged - this hangs, for good
var tran = db.CreateTransaction();
var value = await tran.StringGetAsync(key); // SER305: nothing has been sent yet
await tran.ExecuteAsync(); // never reached
// suggested - capture the task, await it after Execute
var tran = db.CreateTransaction();
var pending = tran.StringGetAsync(key);
await tran.ExecuteAsync();
var value = await pending;
// or, if you do not want the result at all
_ = tran.StringSetAsync(key, value);
await tran.ExecuteAsync();
This is the one rule here reported as an error, because unlike the rest of the family it does not describe
code that could be better - it describes code that cannot work. There is no arrangement of the surrounding
lines that rescues it: the wait sits at the queueing site, so even a previous Execute does not help, because
this command was queued after it and will never be sent.
It applies equally to a batch (db.CreateBatch()), which buffers in exactly the same way, and to every way of
waiting - await, .Result, .Wait(), .GetAwaiter().GetResult().
It is the one place where the advice everyone has absorbed - await your tasks, and await them promptly - is
exactly wrong. Every other IDatabaseAsync method returns a task you should await straight away, and
ITransaction offers the same method names on what looks like the same interface. Nothing about the call site
suggests that this one is a buffer rather than a dispatch.
Two are offered in the IDE:
_ = tran.StringSetAsync(...). Right when you only wanted the write.await below the
Execute[Async] call.The second is only offered where it is unambiguously safe: the await form, with an Execute[Async] on the
same transaction later in the same block, and - if the wait declares a variable - nothing between the two
already using that variable, since the declaration is what moves.
var pending = tran.StringGetAsync(key); await pending; is
broken if it is above the Execute and correct if it is below - it is the recommended fix. Telling those
apart means reasoning about statement order, and this rule is an error: it must not guess. The same goes for
await Task.WhenAll(...) over queued tasks.CommandFlags.FireAndForget returns an already-completed task, so awaiting it does not
hang. It is still not giving you an answer, which is SER306 - a warning, not an error.tran.StringGetAsync(key, flags), where flags is a variable, might carry
FireAndForget at runtime, so nothing is reported at all.IDatabaseAsync. A helper taking IDatabaseAsync cannot tell a
transaction from a plain database, and neither can this rule.Reported as an error, so it fails the build. If you are suppressing it, please check first that the code really does complete - this rule fires only on shapes that cannot.
<NoWarn>$(NoWarn);SER305</NoWarn>
or locally:
#pragma warning disable SER305
If you believe it has flagged something that works, that is a bug in the rule and one that reaches every consumer of the package - please report it with the code as written.
See also Transactions and SER306.