Skip to content

Add NOMKSTREAM support to XADD - #3186

Merged
mgravell merged 2 commits into
StackExchange:mainfrom
HarnageaGabriel:feature/xadd-nomkstream
Aug 20, 2026
Merged

Add NOMKSTREAM support to XADD#3186
mgravell merged 2 commits into
StackExchange:mainfrom
HarnageaGabriel:feature/xadd-nomkstream

Conversation

@HarnageaGabriel

Copy link
Copy Markdown
Contributor

Summary

  • Adds a nomkstream option to StreamAdd/StreamAddAsync (single field/value and NameValueEntry[] shapes, plain and StreamIdempotentId overloads) that maps to the XADD ... NOMKSTREAM flag introduced in Redis 6.2 — when set, XADD will not create the stream if it doesn't already exist and instead returns null.
  • New overloads are additive: existing shipped StreamAdd/StreamAddAsync signatures keep their exact parameter lists for binary compatibility (their optional defaults were removed source-side so calls rebind to the new all-optional overload carrying nomkstream, per the project's back-compat guidance in AGENTS.md).
  • Wired through IDatabase/IDatabaseAsync, RedisDatabase, and the KeyspaceIsolation (KeyPrefixedDatabase/KeyPrefixed) forwarders.
  • Updated PublicAPI.Shipped.txt/PublicAPI.Unshipped.txt accordingly.

Closes #3138.

Test plan

  • dotnet build Build.csproj -c Release /p:CI=true — 0 warnings, 0 errors.
  • dotnet test tests/StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj -f net10.0 --filter "FullyQualifiedName~Stream" — 472 passed, 2 skipped (pre-existing, unrelated), 0 failed, against local docker Redis topology.
  • Added StreamAddNoMkStream integration test in StreamTests.cs (sync/async x single-pair/array) verifying a nonexistent stream stays absent when nomkstream: true, and that adds succeed once the stream exists.
  • Added StreamAddRoundTrip unit test asserting the exact outbound RESP bytes place NOMKSTREAM before MAXLEN/LIMIT, and that it's omitted when nomkstream is false.
  • Added a short doc note in docs/Streams.md.

🤖 Generated with Claude Code

Adds a nomkstream option to StreamAdd/StreamAddAsync so callers can
append to a stream without implicitly creating it, matching the
XADD NOMKSTREAM flag (Redis 6.2+). Existing overloads keep their
signatures for binary compatibility; new all-optional overloads carry
the flag.

Closes StackExchange#3138.
@HarnageaGabriel
HarnageaGabriel force-pushed the feature/xadd-nomkstream branch from 6ccd600 to 9c3822c Compare August 19, 2026 19:33

@mgravell mgravell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. The wire mechanics are right: NOMKSTREAM correctly precedes MAXLEN, both GetStreamAddMessage overloads have their totalLength arithmetic updated, and the round-trip test pins the exact bytes, which is the right way to prove it. The issues are in the API shape, plus one demonstrable source break.

1. nomkstream should not be a wire constant, and we already have this concept

We already expose MKSTREAM, on the sibling command:

bool StreamCreateConsumerGroup(RedisKey key, RedisValue groupName, RedisValue? position = null, bool createStream = true, CommandFlags flags = CommandFlags.None);

So this is the same flag, on the same data type, spelled two different ways with opposite polarity. Please make it bool createStream = true, so XGROUP CREATE and XADD read identically and neither leaks the wire keyword:

db.StreamAdd(key, "field", "value", createStream: false);

(The other candidate is When when = When.Always, with When.Exists meaning NOMKSTREAM, since that is the library-wide vocabulary for NX/XX. I prefer createStream here purely because it matches the sibling stream command, but if you would rather unify both commands on When, say so and we do that instead.)

2. The parameter list is the real problem

Adding one bool costs 4 interface signatures x 2 (sync/async), 4 KeyPrefixed/KeyPrefixedDatabase forwarders, 8 rewritten PublicAPI lines and 8 RS0026/RS0027 suppressions. This signature has already grown twice (int? -> long? plus limit plus trimMode, then the StreamIdempotentId variants), and XADD still has an option we do not expose at all: MINID trimming (we have StreamTrimByMinId for XTRIM, but nothing on XADD), which today's bool useApproximateMaxLength cannot express either. So the next request pays that same multiplier again.

Worth deciding now rather than after this merges, because adding these four overloads and then an options type later leaves us maintaining twelve. Something like:

RedisValue StreamAdd(RedisKey key, RedisValue field, RedisValue value, StreamAddOptions options, CommandFlags flags = CommandFlags.None);
RedisValue StreamAdd(RedisKey key, NameValueEntry[] pairs, StreamAddOptions options, CommandFlags flags = CommandFlags.None);

carrying MessageId / IdempotentId / MaxLength / MinId / approximate / Limit / TrimMode / CreateStream, and every future XADD option lands with no new signatures. StreamConfiguration is the local precedent for that shape, and note it is a class, which matters here: as a struct, default would give CreateStream == false, i.e. the unsafe polarity, so either follow StreamConfiguration or store the negative internally.

I am not asking you to build that speculatively - it is a call for me to make. But I would rather make it before we add the overloads than after.

3. Source break: the new parameter is inserted mid-list

nomkstream goes between useApproximateMaxLength and limit, so positional callers no longer compile. Against this branch:

db.StreamAdd(key, "f", "v", (RedisValue?)"1-1", 5L, true, 10L, StreamTrimMode.DeleteReferences);
// error CS1503: Argument 7: cannot convert from 'long' to 'bool'
// error CS1503: Argument 8: cannot convert from 'StackExchange.Redis.StreamTrimMode' to 'long?'

That compiles on main and does not here, because the old overload lost its defaults (so it needs all nine arguments) and the new one now wants a bool in slot 7.

#3135 did the same strip-the-defaults manoeuvre for StreamReadGroup, but appended maxCount/maxSize at the end, immediately before flags, which is why it did not break positional callers. Please follow that: put createStream after trimMode.

4. Optional: keep the defaults and use [OverloadResolutionPriority]

AGENTS.md points at this, and RedisValue/RedisChannel already use it. Keeping the defaults on both overloads and putting [OverloadResolutionPriority(1)] on the new one compiles cleanly for every call shape I tried, including the bare three-argument call and the positional forms above, and it leaves PublicAPI.Shipped.txt completely untouched (new lines in Unshipped.txt only), which is a nicer diff to review and to trust. I checked this works for exactly this signature pair before suggesting it. Your call whether we adopt it here or stay with the #3135 pattern.

5. Tests

  • KeyPrefixedDatabaseTests and KeyPrefixedTests have one test per overload (StreamAdd_1, StreamAdd_2, StreamAdd_WithTrimMode_1/_2). This adds four forwarders with no corresponding tests; those forwarders are exactly where a copy-paste slip silently drops the prefix.
  • The round-trip test only covers the single-pair, non-idempotent builder. Please add the NameValueEntry[] builder, and a case combining createStream: false with a StreamIdempotentId: the relative order of NOMKSTREAM and the IDMP arguments is currently unverified, and that is precisely the kind of thing a round-trip test exists to pin.

6. Minor

  • Six #pragma warning disable RS0026/RS0027 blocks: prefer one region-level suppression carrying a reason, as at IDatabase.cs:3248.
  • docs/Streams.md: worth saying the returned value is RedisValue.Null (and that this needs 6.2 or later), rather than "a null RedisValue".

Reworks the public surface: instead of inserting a `bool nomkstream` into the
four StreamAdd overload families and their async counterparts, the new options
travel on a required `StreamAddOptions` parameter, adding two overloads per
interface rather than eight.

Because `options` is required it cannot tie with the shipped overloads, so those
keep their signatures *and* their default values - PublicAPI.Shipped.txt is
untouched. That matters: stripping the defaults broke positional callers, e.g.

    db.StreamAdd(key, "f", "v", (RedisValue?)"1-1", 5L, true, 10L, StreamTrimMode.DeleteReferences);

which no longer bound to either overload once `nomkstream` sat mid-list.
[OverloadResolutionPriority] would also have disambiguated, but it is only
honoured at LangVersion 13+, so a C# 12 consumer would get CS0121 instead.

The flag is spelled `CreateStream` (default true), matching the MKSTREAM
parameter we already expose as `StreamCreateConsumerGroup(..., createStream,
...)`, rather than leaking the wire keyword. Since the options type takes future
XADD options for free, MINID trimming is included too - previously reachable
only through XTRIM - so XADD's grammar is now fully expressible.

Notes:
- StreamAddOptions is a readonly struct, passed by value publicly and by `in` to
  the message builders. CreateStream is stored inverted so that `default` means
  "create the stream", matching the command's own default.
- Both builders now share the prefix arithmetic and emission, so NOMKSTREAM,
  MAXLEN|MINID [~], LIMIT, the trim-mode keyword and the idempotency arguments
  can only be ordered one way.
- The options overloads validate up front (MaxLength xor MinId, MessageId xor
  IdempotentId, LIMIT requiring ~ and a threshold); the shipped positional
  overloads deliberately do not, preserving their current behaviour of letting
  the server rule on odd combinations.
@mgravell

Copy link
Copy Markdown
Collaborator

Pushed the rework onto this branch rather than asking you to redo the surface twice - the message-builder work, the wire ordering and the round-trip test were the right parts, and they survive.

What changed, against the review above: the flag now travels on a required StreamAddOptions parameter (two new overloads per interface instead of eight) rather than as a positional bool. Because options is required it can never tie with the shipped overloads, so those keep their signatures and their defaults - PublicAPI.Shipped.txt is back to untouched. The flag is spelled CreateStream (default true), matching the MKSTREAM parameter we already expose on StreamCreateConsumerGroup. MINID trimming came along for free, so XADD is now fully expressible.

Two corrections to what I wrote earlier:

  • Retracting the [OverloadResolutionPriority] suggestion in point 4. It is only honoured at LangVersion 13 and above. I checked it with a two-assembly probe - interfaces compiled at C# 13, consumer at C# 12 - and the consumer gets CS0121 on db.StreamAdd(key, "f", "v"). On a library shipping netstandard2.0 that is not usable for disambiguating equally-applicable overloads, which is presumably why Server 8.10 - LMOVEM, XREAD, XREADGROUP, SDIFFCARD, SUNIONCARD #3135 strips the defaults instead. Please don't adopt it elsewhere on that basis.
  • Point 3's break is confirmed rather than theoretical: db.StreamAdd(key, "f", "v", (RedisValue?)"1-1", 5L, true, 10L, StreamTrimMode.DeleteReferences) compiles on main and did not on this branch (CS1503: Argument 7: cannot convert from 'long' to 'bool'). The options shape sidesteps it entirely.

Tests: the round-trip file now covers both builders, NOMKSTREAM before MAXLEN, MINID exact and approximate, LIMIT, the trim-mode keyword, NOMKSTREAM combined with both IDMP and IDMPAUTO, the nil reply mapping to RedisValue.Null, and the rejected combinations. KeyPrefixedDatabaseTests/KeyPrefixedTests gained cases for the new forwarders. dotnet build Build.csproj -c Release /p:CI=true is clean, and the stream tests pass against the local topology (276 passed, 2 pre-existing skips).

One deliberate asymmetry worth knowing about: the options overloads validate up front (MaxLength xor MinId, MessageId xor IdempotentId, LIMIT needing ~ and a threshold), while the shipped positional overloads still pass odd-but-parseable combinations to the server as they always have. Tightening those would be a behaviour change for existing callers, so it is not in here.

@mgravell
mgravell merged commit cb7f607 into StackExchange:main Aug 20, 2026
7 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support XADD NOMKSTREAM

2 participants