dotnet/orleans · error · KeyNotFoundException
Channel binding for grain type {binding.GrainType} is missin
Error message
Channel binding for grain type {binding.GrainType} is missing a "{WellKnownGrainTypeProperties.ChannelIdMapperKey}" value What it means
Thrown during ImplicitChannelSubscriberTable.BuildCache when a grain binding is marked as type 'broadcast-channel' and has a channel-pattern, but its dictionary does not contain the 'channelid-mapper' key (WellKnownGrainTypeProperties.ChannelIdMapperKey). This key names the IChannelIdMapper keyed service used to map channel keys to grain keys. Under normal usage via [ImplicitChannelSubscription], this key is always populated with DefaultChannelIdMapper.Name or a user-specified mapper name. Even a null/whitespace value is tolerated (line 100 falls back to the default mapper), so the key must be entirely absent to trigger this.
Source
Thrown at src/Orleans.BroadcastChannel/SubscriberTable/ImplicitChannelSubscriberTable.cs:96
$"Channel binding for grain type {binding.GrainType} is missing a \"{WellKnownGrainTypeProperties.BroadcastChannelBindingPatternKey}\" value");
}
IChannelNamespacePredicate? predicate = null;
foreach (var provider in _providers)
{
if (provider.TryGetPredicate(pattern, out predicate)) break;
}
if (predicate is null)
{
throw new KeyNotFoundException(
$"Could not find an {nameof(IChannelNamespacePredicate)} for the pattern \"{pattern}\"."
+ $" Ensure that a corresponding {nameof(IChannelNamespacePredicateProvider)} is registered");
}
if (!grainBinding.TryGetValue(WellKnownGrainTypeProperties.ChannelIdMapperKey, out var mapperName))
{
throw new KeyNotFoundException(
$"Channel binding for grain type {binding.GrainType} is missing a \"{WellKnownGrainTypeProperties.ChannelIdMapperKey}\" value");
}
var channelIdMapper = _serviceProvider.GetKeyedService<IChannelIdMapper>(string.IsNullOrWhiteSpace(mapperName) ? DefaultChannelIdMapper.Name : mapperName);
var subscriber = new BroadcastChannelSubscriber(binding, channelIdMapper!);
newPredicates.Add(new BroadcastChannelSubscriberPredicate(subscriber, predicate));
}
}
return new Cache(version, newPredicates);
}
/// <summary>
/// Retrieve a map of implicit subscriptionsIds to implicit subscribers, given a channel ID. This method throws an exception if there's no namespace associated with the channel ID.
/// </summary>
/// <param name="channelId">A channel ID.</param>
/// <param name="grainFactory">The grain factory used to get consumer references.</param>
/// <returns>A set of references to implicitly subscribed grains. They are expected to support the broadcast channel consumer extension.</returns>View on GitHub (pinned to fca799fa70)
Solutions
- Replace manual binding construction with the [ImplicitChannelSubscription] attribute, which always sets ChannelIdMapperKey to DefaultChannelIdMapper.Name.
- If constructing bindings programmatically, include WellKnownGrainTypeProperties.ChannelIdMapperKey in the binding dictionary with the mapper name (use DefaultChannelIdMapper.Name, i.e. "default", for the standard mapper).
- Audit any custom IGrainBindingsProviderAttribute implementations to ensure they emit all three required keys: type, channel-pattern, and channelid-mapper.
Example fix
// before — manual binding missing channelid-mapper
var binding = new Dictionary<string, string?>
{
[WellKnownGrainTypeProperties.BindingTypeKey] = WellKnownGrainTypeProperties.BroadcastChannelBindingTypeValue,
[WellKnownGrainTypeProperties.BroadcastChannelBindingPatternKey] = "namespace:orders",
// missing channelid-mapper
};
// after — all required keys present
var binding = new Dictionary<string, string?>
{
[WellKnownGrainTypeProperties.BindingTypeKey] = WellKnownGrainTypeProperties.BroadcastChannelBindingTypeValue,
[WellKnownGrainTypeProperties.BroadcastChannelBindingPatternKey] = "namespace:orders",
[WellKnownGrainTypeProperties.ChannelIdMapperKey] = DefaultChannelIdMapper.Name,
}; Defensive patterns
Strategy: validation
Validate before calling
// Validate a manually constructed broadcast-channel binding has the channelid-mapper key.
static void ValidateBindingHasMapper(Dictionary<string, string?> binding)
{
if (binding.TryGetValue(WellKnownGrainTypeProperties.BindingTypeKey, out var type)
&& type == WellKnownGrainTypeProperties.BroadcastChannelBindingTypeValue
&& !binding.ContainsKey(WellKnownGrainTypeProperties.ChannelIdMapperKey))
{
throw new InvalidOperationException(
"Broadcast-channel binding is missing the required 'channelid-mapper' key. " +
$"Set it to '{DefaultChannelIdMapper.Name}' for the default mapper.");
}
} Prevention
- Prefer the [ImplicitChannelSubscription] attribute which always sets ChannelIdMapperKey.
- If building bindings programmatically, always include WellKnownGrainTypeProperties.ChannelIdMapperKey set to DefaultChannelIdMapper.Name.
- When migrating from Orleans.Streams, remember the key is 'channelid-mapper', not 'streamid-mapper'.
When it happens
Trigger: Grain bindings are constructed programmatically or via a custom IGrainBindingsProviderAttribute that sets BindingTypeKey to 'broadcast-channel' and BroadcastChannelBindingPatternKey but omits ChannelIdMapperKey. This fires at silo startup or on first cache refresh when BuildCache iterates all broadcast-channel bindings. It never occurs with the built-in [ImplicitChannelSubscription] attribute, whose GetBindings always emits all three required keys.
Common situations: A custom grain binding provider includes the pattern but forgets the mapper name. Programmatic manifest generation that partially copies binding dictionaries. A migration from streaming (which uses 'streamid-mapper') to broadcast-channel (which uses 'channelid-mapper') where the key name was not translated.
Related errors
- Channel binding for grain type {binding.GrainType} is missin
- streamId
- Could not find an {nameof(IChannelNamespacePredicate)} for t
- Unknown Playground:Storage:Provider value '{storageProvider}
- WEBSITE_PRIVATE_PORTS must contain at least one TCP port.
AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13).
Data as JSON: /api/errors/4a75d23b500cc77e.
Report an issue: GitHub.