dotnet/orleans · error · ArgumentException

GrainState-Table property not initialized

Error message

GrainState-Table property not initialized

What it means

Thrown by AzureTableGrainStorage.ReadStateAsync when tableDataManager is null. The tableDataManager is only assigned inside Init() (line 498) during the silo lifecycle initialization stage. If Init() never ran (wrong InitStage ordering, lifecycle not subscribed) or Close() was called (which sets it to null at line 513), the field is null and any grain state operation fails.

Source

Thrown at src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs:67

            string name,
            AzureTableStorageOptions options,
            IOptions<ClusterOptions> clusterOptions,
            ILogger<AzureTableGrainStorage> logger,
            IActivatorProvider activatorProvider)
        {
            this.options = options;
            this.clusterOptions = clusterOptions.Value;
            this.name = name;
            this.storageSerializer = options.GrainStorageSerializer;
            this.logger = logger;
            _activatorProvider = activatorProvider;
        }

        /// <summary> Read state data function for this storage provider. </summary>
        /// <see cref="IGrainStorage.ReadStateAsync{T}"/>
        public async Task ReadStateAsync<T>(string grainType, GrainId grainId, IGrainState<T> grainState)
        {
            if (tableDataManager == null) throw new ArgumentException("GrainState-Table property not initialized");

            string pk = GetKeyString(grainId);
            LogTraceReadingGrainState(grainType, pk, grainId, this.options.TableName);
            string partitionKey = pk;
            string rowKey = AzureTableUtils.SanitizeTableProperty(grainType);
            var entity = await tableDataManager.Read(partitionKey, rowKey).ConfigureAwait(false);
            if (entity is not null)
            {
                var loadedState = ConvertFromStorageFormat<T>(entity);
                grainState.RecordExists = loadedState != null;
                grainState.State = loadedState ?? CreateInstance<T>();
                grainState.ETag = entity.ETag.ToString();
            }
            else
            {
                grainState.RecordExists = false;
                grainState.ETag = null;
                grainState.State = CreateInstance<T>();

View on GitHub (pinned to fca799fa70)

Solutions

  1. Check options.InitStage — it must complete before grain activations begin (default is typically ServiceLifecycleStage.ApplicationServices or earlier).
  2. Ensure the storage provider is properly registered via AddAzureTableGrainStorage so Participate(ISiloLifecycle) is wired.
  3. Avoid calling grain methods that trigger storage after silo.StopAsync() or during shutdown.
  4. Verify the Init() method did not throw — check logs for LogErrorInitializationFailed, which would leave tableDataManager null.

Example fix

// before: InitStage too late, grains activate before storage is ready
o.InitStage = ServiceLifecycleStage.Active;

// after: Init at an early stage
o.InitStage = ServiceLifecycleStage.ApplicationServices;
Defensive patterns

Strategy: validation

Validate before calling

// Ensure InitStage is set before grain services activate
builder.AddAzureTableGrainStorage("tableStore", o =>
{
    o.InitStage = ServiceLifecycleStage.ApplicationServices; // before grain activation
});

Try / catch

try { await grain.ReadStateAsync(); }
catch (ArgumentException ex) when (ex.Message.Contains("GrainState-Table property not initialized"))
{
    logger.LogError(ex, "Table storage not initialized — check InitStage and lifecycle wiring.");
    throw;
}

Prevention

When it happens

Trigger: A grain's ReadStateAsync is invoked before the storage provider's Init lifecycle stage has completed, or after the silo has begun shutdown and Close() has set tableDataManager to null. This indicates an InitStage misconfiguration or a storage call outside the active silo lifetime window.

Common situations: InitStage set to a very late stage (e.g., active) while grain activations start earlier. Storage provider was not properly wired into the lifecycle (Participate not called). Calling grain storage operations during or after silo shutdown. A custom lifecycle stage ordering that places grain activation before storage initialization.

Related errors


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