Zero-Config Orleans Persistence — Auto-Provisioning SQL Server Tables
One of the most common friction points when deploying Orleans-based systems to production is the SQL Server setup. Orleans requires specific tables for clustering, grain persistence, reminders, and streaming — and missing any of them means the silo fails to start. FabrCore's OrleansSqlServerInitializer eliminates this entirely by auto-provisioning every required table at startup.
The Manual Setup Problem
Orleans ships SQL scripts for each provider subsystem — clustering, persistence, reminders, and streaming. In a typical deployment, an operator would need to run these scripts against the database before the silo could start. Miss one table, and the silo throws an opaque error about missing schema objects. This was especially painful for developers running locally for the first time, or teams spinning up new environments.
The scripts themselves are well-documented in the Orleans repository, but the workflow of "create database, run four scripts, then start your app" adds unnecessary steps to what should be a single-command experience.
FabrCore takes the position that if you provide a connection string and set ClusteringMode to SqlServer, the framework should handle the rest.
How OrleansSqlServerInitializer Works
When AddFabrCoreServer detects SqlServer clustering mode, it registers the OrleansSqlServerInitializer as a hosted service that runs before the Orleans silo starts. The initializer carries embedded SQL scripts for all four subsystems and executes them idempotently against the configured database.
All Orleans tables are created under a dedicated orlns schema, keeping them cleanly separated from application tables. The four table groups are:
| Subsystem | Schema | Purpose |
|---|---|---|
| Clustering | orlns | Silo membership, liveness, and cluster coordination |
| Persistence | orlns | Grain state storage for agents, clients, and management data |
| Reminders | orlns | Persistent timers that survive grain deactivation and silo restarts |
| Streaming | orlns | Pub/sub state for agent-to-agent and agent-to-client messaging |
The scripts are embedded as resources in the FabrCore.Host assembly, so there is nothing to download, copy, or reference externally. Each script uses IF NOT EXISTS guards, meaning the initializer is safe to run on every startup — it creates what is missing and skips what is already there.
Configuration
The only configuration required is what you already provide for Orleans itself. Set the clustering mode to SqlServer and provide a connection string:
{
"Orleans": {
"ClusterId": "prod",
"ServiceId": "fabrcore",
"ClusteringMode": "SqlServer",
"ConnectionString": "Server=localhost;Database=FabrCore;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
That is the complete configuration. On the first startup, FabrCore creates the orlns schema if it does not exist, then runs each embedded script to provision the clustering, persistence, reminders, and streaming tables. On subsequent startups, the idempotency guards skip all CREATE statements.
If you need separate databases for clustering and storage, use the optional StorageConnectionString:
{
"Orleans": {
"ClusterId": "prod",
"ServiceId": "fabrcore",
"ClusteringMode": "SqlServer",
"ConnectionString": "Server=localhost;Database=FabrCore;Trusted_Connection=True;TrustServerCertificate=True;",
"StorageConnectionString": "Server=localhost;Database=FabrCoreStorage;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
When StorageConnectionString is provided, the initializer runs the persistence and streaming scripts against that connection, while clustering and reminders use the primary ConnectionString. When omitted, all tables go to the same database.
The server setup code remains minimal:
using FabrCore.Host;
var builder = WebApplication.CreateBuilder(args);
builder.AddFabrCoreServer(new FabrCoreServerOptions
{
AdditionalAssemblies = [typeof(MyAgent).Assembly]
});
var app = builder.Build();
app.UseFabrCoreServer();
app.Run();
No SQL scripts to run. No schema to create manually. Point at a database and start the application.
What Gets Provisioned
Behind the scenes, FabrCore registers Orleans providers with specific named constants. Understanding these names is useful if you ever need to inspect the tables directly or configure the advanced path with UseOrleans:
| Constant | Value | Purpose |
|---|---|---|
FabrCoreOrleansConstants.StorageProviderName | "fabrcoreStorage" | Grain state for agents, clients, and management |
FabrCoreOrleansConstants.PubSubStoreName | "PubSubStore" | Orleans streaming pub/sub state |
FabrCoreOrleansConstants.StreamProviderName | "fabrcoreStreams" | Agent-to-agent and agent-to-client messaging |
| Reminder service | (any) | Agent timers and persistent reminders |
If you use the advanced path — calling AddFabrCoreServices and UseOrleans separately for full control over providers — you are responsible for ensuring these named providers are registered. The auto-provisioning only applies to the simple AddFabrCoreServer path.
builder.AddFabrCoreServices(new FabrCoreServerOptions
{
AdditionalAssemblies = [typeof(MyAgent).Assembly]
});
builder.UseOrleans(siloBuilder =>
{
// Storage — must register these named providers
siloBuilder.AddAdoNetGrainStorage(
FabrCoreOrleansConstants.StorageProviderName, o =>
o.ConnectionString = connStr);
siloBuilder.AddAdoNetGrainStorage(
FabrCoreOrleansConstants.PubSubStoreName, o =>
o.ConnectionString = connStr);
// Streams — must use this name
siloBuilder.AddMemoryStreams(
FabrCoreOrleansConstants.StreamProviderName);
// Register FabrCore grains
siloBuilder.AddFabrCore([typeof(MyAgent).Assembly]);
});
Built with FabrCore on .NET 10.
Builder of FabrCore and OpenCaddis.