dotnet/orleans · error · ArgumentNullException

Value cannot be null. (Parameter 'clusterId')

Error message

Value cannot be null. (Parameter 'clusterId')

What it means

Thrown by OrleansSiloInstanceManager.DeleteTableEntries(string clusterId) when clusterId is null. The cluster/deployment id is the Azure Table partition key for membership entries; a null cannot scope the deletion, so the call is rejected with ArgumentNullException(nameof(clusterId)). This is a defensive guard before ReadAllTableEntriesForPartitionAsync.

Source

Thrown at src/Azure/Orleans.Clustering.AzureStorage/OrleansSiloInstanceManager.cs:208

                sb.AppendLine(string.Format("[IP {0}:{1}:{2}, {3}, Instance={4}, Status={5}]", entry.Address, entry.Port, entry.Generation,
                    entry.HostName, entry.SiloName, entry.Status));
            }
            return sb.ToString();
        }

        internal Task<string> MergeTableEntryAsync(SiloInstanceTableEntry data)
        {
            return storage.MergeTableEntryAsync(data, AzureTableUtils.ANY_ETAG); // we merge this without checking eTags.
        }

        internal Task<(SiloInstanceTableEntry? Entity, string? ETag)> ReadSingleTableEntryAsync(string partitionKey, string rowKey)
        {
            return storage.ReadSingleTableEntryAsync(partitionKey, rowKey);
        }

        internal async Task<int> DeleteTableEntries(string clusterId)
        {
            if (clusterId == null) throw new ArgumentNullException(nameof(clusterId));

            var entries = await storage.ReadAllTableEntriesForPartitionAsync(clusterId);

            await DeleteEntriesBatch(entries);

            return entries.Count;
        }

        public async Task CleanupDefunctSiloEntries(DateTimeOffset beforeDate)
        {
            var entriesList = (await FindAllSiloEntries())
                .Where(entry => !SiloInstanceTableEntry.IsVersionRow(entry.Entity.RowKey)
                    && entry.Item1.Status != INSTANCE_STATUS_ACTIVE
                    && entry.Item1.Timestamp < beforeDate)
                .ToList();

            // Defunct-row cleanup intentionally does not advance the membership snapshot fence.
            await DeleteEntriesBatch(entriesList);

View on GitHub (pinned to fca799fa70)

Solutions

  1. Pass the concrete cluster id (the DeploymentId used when the cluster was created).
  2. Bind clusterId from configuration and validate non-null before teardown.
  3. Use `clusterId ?? throw new InvalidOperationException("ClusterId not configured.")` for a clearer upstream error.
  4. Add a startup check that the deployment-id option is set on all silos.

Example fix

// before
await manager.DeleteTableEntries(clusterId: null);

// after
var cid = options.ClusterId ?? throw new InvalidOperationException("ClusterId missing.");
await manager.DeleteTableEntries(cid);
Defensive patterns

Strategy: validation

Validate before calling

if (string.IsNullOrWhiteSpace(clusterId))
    throw new InvalidOperationException("ClusterId/DeploymentId is not configured.");

Type guard

static bool HasClusterId(string? s) => !string.IsNullOrWhiteSpace(s);

Try / catch

try { await manager.DeleteTableEntries(clusterId); }
catch (ArgumentNullException ex) when (ex.ParamName == nameof(clusterId)) { /* bind option */ }

Prevention

When it happens

Trigger: Calling DeleteTableEntries(null), e.g. an admin/cleanup tool that read the cluster id from a missing config value, or an automated teardown script with an unbound option.

Common situations: Cluster teardown tooling with an unbound ClusterId/DeploymentId option; misconfigured test harness; refactor that renamed the option but left the binding stale.

Related errors


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