Skip to the content.

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().

Why this happens so easily

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.

Fixes

Two are offered in the IDE:

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.

Cases that are deliberately not flagged

Suppressing

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.