dotnet/orleans · error · ArgumentException

streamId

Error message

streamId

What it means

Thrown by DefaultChannelIdMapper.GetGuidKey when a legacy Guid-keyed subscriber grain (IGrainWithGuidKey or IGrainWithGuidCompoundKey) is implicitly subscribed to a broadcast channel, but the ChannelId's Key bytes cannot be parsed as a Guid in 'N' format (32 hex characters, no dashes). The mapper must convert the channel key into the grain's Guid primary key; if the key is not a valid Guid, mapping fails. This fires inside BroadcastChannelWriter.Publish because the writer must construct a grain reference for every matching implicit subscriber.

Source

Thrown at src/Orleans.BroadcastChannel/IdMapping/DefaultChannelIdMapper.cs:53

                        && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase))
                    {
                        includeNamespaceInGrainId = true;
                    }
                }
            }

            return keyType switch
            {
                nameof(Guid) => GetGuidKey(streamId, includeNamespaceInGrainId),
                nameof(Int64) => GetIntegerKey(streamId, includeNamespaceInGrainId),
                _ => streamId.GetKeyIdSpan(), // null or string
            };
        }

        private static IdSpan GetGuidKey(ChannelId streamId, bool includeNamespaceInGrainId)
        {
            var key = streamId.Key.Span;
            if (!Utf8Parser.TryParse(key, out Guid guidKey, out var len, 'N') || len < key.Length) throw new ArgumentException(nameof(streamId));

            if (!includeNamespaceInGrainId)
                return streamId.GetKeyIdSpan();

            var ns = streamId.Namespace.Span;
            return ns.IsEmpty ? streamId.GetKeyIdSpan() : GrainIdKeyExtensions.CreateGuidKey(guidKey, ns);
        }

        private static IdSpan GetIntegerKey(ChannelId streamId, bool includeNamespaceInGrainId)
        {
            var key = streamId.Key.Span;
            if (!Utf8Parser.TryParse(key, out int intKey, out var len) || len < key.Length) throw new ArgumentException(nameof(streamId));

            return includeNamespaceInGrainId
                ? GrainIdKeyExtensions.CreateIntegerKey(intKey, streamId.Namespace.Span)
                : GrainIdKeyExtensions.CreateIntegerKey(intKey);
        }
    }

View on GitHub (pinned to fca799fa70)

Solutions

  1. Use the ChannelId.Create(string ns, Guid key) overload, which formats the Guid in 'N' format internally and guarantees compatibility with Guid-keyed subscriber grains.
  2. Change the subscriber grain to implement IGrainWithStringKey instead of IGrainWithGuidKey so the default mapper arm uses the key bytes directly without Guid parsing.
  3. Implement a custom IChannelIdMapper that handles the key conversion, register it as a keyed singleton in DI, and reference it via ImplicitChannelSubscription(streamNamespace, channelIdMapper: "your-mapper-name").

Example fix

// before — string key with a Guid-keyed subscriber grain
var channelId = ChannelId.Create("orders", "order-123");
var writer = provider.GetChannelWriter<OrderEvent>(channelId);
await writer.Publish(evt); // throws ArgumentException(nameof(streamId))

// after — Guid key matching the grain's IGrainWithGuidKey interface
var channelId = ChannelId.Create("orders", orderGuid);
var writer = provider.GetChannelWriter<OrderEvent>(channelId);
await writer.Publish(evt);
Defensive patterns

Strategy: validation

Validate before calling

// Validate the channel key is a valid Guid 'N' format before publishing,
// when the subscriber grain implements IGrainWithGuidKey.
static void ValidateGuidChannelKey(ChannelId channelId)
{
    var keyStr = System.Text.Encoding.UTF8.GetString(channelId.Key.Span);
    if (!Guid.TryParseExact(keyStr, "N", out _))
        throw new InvalidOperationException(
            $"ChannelId key '{keyStr}' is not a valid Guid ('N' format). " +
            "The subscriber grain is Guid-keyed; use ChannelId.Create(namespace, guid) instead.");
}

Type guard

// Returns true if the ChannelId key is a valid Guid 'N' format.
static bool HasGuidKey(ChannelId channelId)
{
    var keyStr = System.Text.Encoding.UTF8.GetString(channelId.Key.Span);
    return Guid.TryParseExact(keyStr, "N", out _);
}

Try / catch

try
{
    await writer.Publish(item);
}
catch (ArgumentException ex) when (ex.ParamName == "streamId")
{
    logger.LogWarning(ex, "ChannelId key does not match subscriber grain key type. " +
        "Ensure the channel key is a valid Guid for Guid-keyed grains.");
}

Prevention

When it happens

Trigger: A publisher calls BroadcastChannelWriter.Publish on a ChannelId whose Key is not parseable as a Guid, while the matched subscriber grain implements IGrainWithGuidKey. The grain's legacy-grain-key-type binding is 'Guid', which routes GetGrainKeyId into the GetGuidKey branch where Utf8Parser.TryParse fails. Concretely: ChannelId.Create("ns", "not-a-guid") or ChannelId.Create("ns", someInteger.ToString()) with a Guid-keyed subscriber, or ChannelId.Create("ns", someGuid) where the Guid is formatted with dashes ('D' format) instead of 'N'.

Common situations: The publisher and subscriber teams disagree on the channel key type — the publisher uses a string or integer key while the subscriber grain's interface extends IGrainWithGuidKey. A grain is refactored from IGrainWithStringKey to IGrainWithGuidKey without updating the publishing code. A Guid is formatted with dashes (Guid.ToString() default 'D' format) instead of 'N' before being used as a raw key byte sequence via ChannelId.Create(ns, guidString).

Related errors


AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13). Data as JSON: /api/errors/a24a44c53ffe9bc4. Report an issue: GitHub.