docs/rules/SER304.md
The same command is queued several times over, and one variadic call does the lot - one round-trip, atomic on the server, no transaction needed.
// flagged
var tran = db.CreateTransaction();
_ = tran.SetAddAsync(key, "a");
_ = tran.SetAddAsync(key, "b");
await tran.ExecuteAsync();
// suggested
long added = await db.SetAddAsync(key, new RedisValue[] { "a", "b" });
One key, many values - every call must be on the same key:
| Repeated | Single call | Server |
|---|---|---|
SetAdd / SetRemove | SetAdd(key, values) / SetRemove(key, values) | any |
SortedSetAdd / SortedSetRemove | SortedSetAdd(key, entries) / SortedSetRemove(key, members) | any |
HashSet / HashDelete | HashSet(key, entries) / HashDelete(key, fields) | any |
ListLeftPush / ListRightPush | ListLeftPush(key, values) / ListRightPush(key, values) | any |
SetContains | SetContains(key, values) (SMISMEMBER) | 6.2 |
Many keys - the calls must be on different keys:
| Repeated | Single call | Server |
|---|---|---|
StringSet | StringSet(KeyValuePair<RedisKey, RedisValue>[]) (MSET) | any |
StringGet | StringGet(keys) (MGET) | any |
KeyDelete | KeyDelete(keys) (DEL) | any |
KeyExists | KeyExists(keys) (EXISTS) | any |
Which direction applies is the whole distinction: SADD takes one key and many values, so calls across
different keys have no single-command form; MSET takes many keys, so calls on one key are not what this is
about. Neither is flagged in the wrong direction.
Most of these variadic forms arrived in Redis 2.4, which predates anything realistically in service, so no version is mentioned. SMISMEMBER at 6.2 is recent enough to say so - see declaring your server version.
This is why it has its own ID rather than sharing SER303: the result changes shape, not just meaning.
bool become one returning a long count. You learn how many were added or removed,
not which ones.StringGet, or bool[] for SetContains).Task<T>s disappear, so anything awaiting them individually needs rewiring.CommandFlags must be carried over verbatim.If your code genuinely needs to know which of the members was new, the per-call form is the right one and this suggestion is not for you - suppress it.
ListLeftPop across keys is not LMPOP. LMPOP pops from the first non-empty key of those given,
not from each of them - a different operation, however similar the argument list looks. Same for ZMPOP.MSET takes one expiry for the whole batch
rather than one per key, and the variadic HashSet has no When, so calls that pass those are left alone -
collapsing them would silently drop the argument, and in the MSET case leave your keys with no expiry at all.Plus everything under when these rules stay quiet.
This rule is a heuristic. It reads your source text - it cannot see your keys, your server, or what you know about the code - so it is deliberately conservative and stays quiet wherever it is unsure. Everything it flags still works, and will keep working: this is a suggestion, not a defect report.
That conservatism is meant to make a false positive rare, not impossible. If you think the rule has flagged something it should not have, please report it, including the transaction as written. A rule that fires on correct code is a bug in the rule - and one that reaches every consumer of the package - so it is worth fixing rather than quietly suppressing.
Reported as a warning, so TreatWarningsAsErrors builds fail until you act on it or turn it down.
<NoWarn>$(NoWarn);SER304</NoWarn>
or locally:
#pragma warning disable SER304