{"record":{"id":"4a75d23b500cc77e","repo":"dotnet/orleans","slug":"channel-binding-for-grain-type-binding-graintype-4a75d2","errorCode":null,"errorMessage":"Channel binding for grain type {binding.GrainType} is missing a \"{WellKnownGrainTypeProperties.ChannelIdMapperKey}\" value","messagePattern":"Channel binding for grain type (.+?) is missing a \"(.+?)\" value","errorType":"exception","errorClass":"KeyNotFoundException","httpStatus":null,"severity":"error","filePath":"src/Orleans.BroadcastChannel/SubscriberTable/ImplicitChannelSubscriberTable.cs","lineNumber":96,"sourceCode":"                           $\"Channel binding for grain type {binding.GrainType} is missing a \\\"{WellKnownGrainTypeProperties.BroadcastChannelBindingPatternKey}\\\" value\");\n                    }\n\n                    IChannelNamespacePredicate? predicate = null;\n                    foreach (var provider in _providers)\n                    {\n                        if (provider.TryGetPredicate(pattern, out predicate)) break;\n                    }\n\n                    if (predicate is null)\n                    {\n                        throw new KeyNotFoundException(\n                            $\"Could not find an {nameof(IChannelNamespacePredicate)} for the pattern \\\"{pattern}\\\".\"\n                            + $\" Ensure that a corresponding {nameof(IChannelNamespacePredicateProvider)} is registered\");\n                    }\n\n                    if (!grainBinding.TryGetValue(WellKnownGrainTypeProperties.ChannelIdMapperKey, out var mapperName))\n                    {\n                        throw new KeyNotFoundException(\n                           $\"Channel binding for grain type {binding.GrainType} is missing a \\\"{WellKnownGrainTypeProperties.ChannelIdMapperKey}\\\" value\");\n                    }\n\n                    var channelIdMapper = _serviceProvider.GetKeyedService<IChannelIdMapper>(string.IsNullOrWhiteSpace(mapperName) ? DefaultChannelIdMapper.Name : mapperName);\n                    var subscriber = new BroadcastChannelSubscriber(binding, channelIdMapper!);\n                    newPredicates.Add(new BroadcastChannelSubscriberPredicate(subscriber, predicate));\n                }\n            }\n\n            return new Cache(version, newPredicates);\n        }\n\n        /// <summary>\n        /// 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.\n        /// </summary>\n        /// <param name=\"channelId\">A channel ID.</param>\n        /// <param name=\"grainFactory\">The grain factory used to get consumer references.</param>\n        /// <returns>A set of references to implicitly subscribed grains. They are expected to support the broadcast channel consumer extension.</returns>","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/src/Orleans.BroadcastChannel/SubscriberTable/ImplicitChannelSubscriberTable.cs#L78-L114","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — manual binding missing channelid-mapper\nvar binding = new Dictionary<string, string?>\n{\n    [WellKnownGrainTypeProperties.BindingTypeKey] = WellKnownGrainTypeProperties.BroadcastChannelBindingTypeValue,\n    [WellKnownGrainTypeProperties.BroadcastChannelBindingPatternKey] = \"namespace:orders\",\n    // missing channelid-mapper\n};\n\n// after — all required keys present\nvar binding = new Dictionary<string, string?>\n{\n    [WellKnownGrainTypeProperties.BindingTypeKey] = WellKnownGrainTypeProperties.BroadcastChannelBindingTypeValue,\n    [WellKnownGrainTypeProperties.BroadcastChannelBindingPatternKey] = \"namespace:orders\",\n    [WellKnownGrainTypeProperties.ChannelIdMapperKey] = DefaultChannelIdMapper.Name,\n};","handlingStrategy":"validation","validationCode":"// Validate a manually constructed broadcast-channel binding has the channelid-mapper key.\nstatic void ValidateBindingHasMapper(Dictionary<string, string?> binding)\n{\n    if (binding.TryGetValue(WellKnownGrainTypeProperties.BindingTypeKey, out var type)\n        && type == WellKnownGrainTypeProperties.BroadcastChannelBindingTypeValue\n        && !binding.ContainsKey(WellKnownGrainTypeProperties.ChannelIdMapperKey))\n    {\n        throw new InvalidOperationException(\n            \"Broadcast-channel binding is missing the required 'channelid-mapper' key. \" +\n            $\"Set it to '{DefaultChannelIdMapper.Name}' for the default mapper.\");\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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'."],"tags":["broadcast-channel","grain-binding","configuration","key-not-found"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}