Yesterday, we introduced grain persistence in Microsoft Orleans, using the volatile MemoryStorage
provider to quickly and easily learn how to set up and work with storage providers.
Today, we will use the ADO .NET Storage Provider in order to save our grain state to a database. Currently, we can use this with either SQL Server or MySQL.
Note: MariaDB is a drop-in replacement for MySQL. Thus, the ADO .NET Storage Provider works with it too.
Note: While there is a partial script for PostgreSQL, it is not yet supported.
Update 9th June 2017: Some material from this article was contributed towards the official Grain Persistence documentation.
Example Scenario
Like in yesterday’s article, we’ll use a very simple setup based on the Orleans Dev/Test Host project. We’ll use the same classes and interface:
public interface IPersonGrain : IGrainWithStringKey
{
Task SayHelloAsync();
}
public class PersonGrainState
{
public bool SaidHello { get; set; }
}
[StorageProvider(ProviderName = "OrleansStorage")]
public class PersonGrain : Grain<PersonGrainState>, IPersonGrain
{
public async Task SayHelloAsync()
{
string primaryKey = this.GetPrimaryKeyString();
bool saidHelloBefore = this.State.SaidHello;
string saidHelloBeforeStr = saidHelloBefore ? " already" : null;
Console.WriteLine($"{primaryKey}{saidHelloBeforeStr} said hello!");
this.State.SaidHello = true;
await this.WriteStateAsync();
}
}
If we give our PersonGrain the name of “Joe” and call SayHelloAsync()
, then it will write “Joe said hello!” to the console. After that, it sets the SaidHello
property in its grain state to true
, and saves the updated state to the database via the configured storage provider. The next time SayHelloAsync()
is called, it will find that the value of SaidHello
is true
, so the output will be “Joe already said hello!” instead.
In the main program, let’s add the following code (same as in yesterday’s article) to invoke SayHelloAsync()
on a grain. This needs to go in the place of the TODO comment that was generated as part of the project template.
var joe = GrainClient.GrainFactory.GetGrain<IPersonGrain>("Joe");
joe.SayHelloAsync();
Now, let’s move on to configure our storage provider.
Setting up the ADO .NET Storage Provider
Install the following package:
Install-Package Microsoft.Orleans.OrleansSqlUtils
Navigate to the folder where the package was installed alongside the project, and you’ll find folders in there for different databases.
In there you’ll find scripts you’ll need to run in order to set up the Orleans database, for different database vendors. Each script contains a table for storage and several others for cluster membership and other internal Orleans operations. For the scope of this article, we’re only concerned with storage.
Note: while there is a script for PostgreSQL, at the time of writing this article, it does not support storage.
The steps to configure an ADO .NET Storage Provider are as follows:
- Create the database specific for the database vendor you want.
- Install a NuGet package specific for the database vendor you want.
- Set up the storage provider in code or XML configuration. Set the
AdoInvariant
setting (optional only if you’re using SQL Server).
Using SQL Server for Grain Persistence
Setting Up the Database
When using SQL Server, you’ll want to use the SQLServer\CreateOrleansTables_SqlServer.sql script.
Open SQL Server Management Studio. Right click on Databases, and create a new one.
Call it whatever you like, e.g. OrleansStorage.
Open a query window based on your new database. Copy in the aforementioned script, and run it.
Installing the NuGet Package
Next, install the following package:
Install-Package System.Data.SqlClient
Configuring via Code
Finally, we need to configure the storage provider itself. We can do this in code by replacing the following line in OrleansHostWrapper.cs:
config.AddMemoryStorageProvider();
…with code that sets up an ADO .NET storage provider. First, we declare a connection string:
const string connStr = "Server=.\SQLEXPRESS;Database=OrleansStorage;Integrated Security=True";
Then, we can set up the ADO .NET storage provider using either a registration call that takes the storage provider’s type name as a string:
var typeName = "Orleans.Storage.AdoNetStorageProvider";
var properties = new Dictionary<string, string>()
{
["DataConnectionString"] = connStr,
["AdoInvariant"] = "System.Data.SqlClient"
};
config.Globals.RegisterStorageProvider(typeName, "OrleansStorage", properties);
…or its generic equivalent:
config.Globals.RegisterStorageProvider<AdoNetStorageProvider>("OrleansStorage", properties);
Note that there is also a generic extension method we could use as a shortcut:
config.AddAdoNetStorageProvider("OrleansStorage", connStr);
However, I recommend against using this method. As per an issue I just opened, there is no way to set the AdoInvariant
(which is optional for SQL Server but necessary when working with other database vendors), and the default serialization format is XML (whereas the default is usually binary). Thus, if you switch between this method and the earlier ones while relying on defaults, you will get deserialization errors.
Configuring via XML
On the other hand, to configure the storage provider using an XML configuration file (which is generally a better approach), first we have to add a file named OrleansConfiguration.xml and set it to copy to the output directory.
Add the following to the file:
<?xml version="1.0" encoding="utf-8"?>
<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<StorageProviders>
<Provider Type="Orleans.Storage.AdoNetStorageProvider"
Name="OrleansStorage"
AdoInvariant="System.Data.SqlClient"
DataConnectionString="Server=.\SQLEXPRESS;Database=OrleansStorage;Integrated Security=True"/>
</StorageProviders>
<SeedNode Address="localhost" Port="22222" />
</Globals>
<Defaults>
<Networking Address="localhost" Port="22222" />
<ProxyingGateway Address="localhost" Port="40000" />
</Defaults>
</OrleansConfiguration>
Then, replace the following code in OrleansHostWrapper.cs:
var config = ClusterConfiguration.LocalhostPrimarySilo();
config.AddMemoryStorageProvider();
siloHost = new SiloHost(siloName, config);
…with this:
siloHost = new SiloHost(siloName);
siloHost.ConfigFileName = "OrleansConfiguration.xml";
Testing It Out
Once you have configured everything, run the program:
It says “Joe said hello!” Remember that the state should have been updated at the end of the method that writes that message. Let’s verify it by closing the program, and running it again:
This time, it says “Joe already said hello!” That means that the state was correctly read from the database.
Using MySQL for Grain Persistence
Note: this also works for MariaDB, which is a drop-in replacement for MySQL.
Setting Up the Database
Deep beneath the folder where the Microsoft.Orleans.SqlUtils package gets installed, you’ll find a MySql\CreateOrleansTables_MySql.sql script. We’ll use this for MySQL or MariaDB.
Use your favourite administrative tool to create a new database, and give it a name (e.g. “orleansstorage”). Paste the script in a query window, and run it against the new database.
Installing the NuGet Package
Install the following package:
Install-Package MySql.Data
Configuring via Code
We can replace the AddMemoryStorageProvider()
call in OrleansHostWrapper.cs with code to set up our ADO .NET provider. First, let’s put our connection string in a variable to keep the rest of the setup code concise:
const string connStr = "Server=localhost;Database=orleansstorage;uid=xxx;pwd=xxx";
We can now configure the storage provider, using either a named type:
var typeName = "Orleans.Storage.AdoNetStorageProvider";
var properties = new Dictionary<string, string>()
{
["DataConnectionString"] = connStr,
["AdoInvariant"] = "MySql.Data.MySqlClient"
};
config.Globals.RegisterStorageProvider(typeName, "OrleansStorage", properties);
…or a generic method based on the type itself:
config.Globals.RegisterStorageProvider<AdoNetStorageProvider>("OrleansStorage", properties);
As you can see, this is almost identical to the configuration for SQL Server. We’re using the exact same ADO .NET storage provider, and changing just the connection string and the AdoInvariant
which identifies the underlying database vendor.
Configuring via XML
Instead of hardcoding our configuration, we have the option to read it from an XML file. Let’s create a file named OrleansConfiguration.xml, set it to copy to the output directory, and give it the following content:
<?xml version="1.0" encoding="utf-8"?>
<OrleansConfiguration xmlns="urn:orleans">
<Globals>
<StorageProviders>
<Provider Type="Orleans.Storage.AdoNetStorageProvider"
Name="OrleansStorage"
AdoInvariant="MySql.Data.MySqlClient"
DataConnectionString="Server=localhost;Database=orleansstorage;Uid=xxx;Pwd=xxx"/>
</StorageProviders>
<SeedNode Address="localhost" Port="22222" />
</Globals>
<Defaults>
<Networking Address="localhost" Port="22222" />
<ProxyingGateway Address="localhost" Port="40000" />
</Defaults>
</OrleansConfiguration>
Then, as we did before in the SQL Server section, replace the following code in OrleansHostWrapper.cs:
var config = ClusterConfiguration.LocalhostPrimarySilo();
config.AddMemoryStorageProvider();
siloHost = new SiloHost(siloName, config);
…with this:
siloHost = new SiloHost(siloName);
siloHost.ConfigFileName = "OrleansConfiguration.xml";
This allows us to bypass the default localhost silo configuration and read the configuration from our file instead.
A Word On Formats
ADO .NET Storage providers can save data in one of three formats: JSON, XML, or a compact binary format.
The default format is binary, and while it is compact, it means the data is opaque and you can’t really work with it directly. This could be a problem if you need to patch, migrate, or troubleshoot the data. XML is bloated, so I generally recommend using JSON. It’s lightweight (although not as much as the binary format) and is easy to work with.
You can switch between them by setting the appropriate property in either code or XML configuration:
UseJsonFormat="true"
uses JSON.
UseXmlFormat="true"
uses XML.
UseBinaryFormat="true"
uses binary, which is the default setting.
Ideally you should set this only the first time. If you change this setting when you already have data, you’ll have a mess of formats and will not be able to read old data.
Summary
Install the base package for the ADO .NET Storage Provider:
Install-Package Microsoft.Orleans.OrleansSqlUtils
Then set up as follows depending on the database vendor:
|
SQL Server |
MySQL |
Script |
SQLServer\CreateOrleansTables_SqlServer.sql |
MySql\CreateOrleansTables_MySql.sql |
NuGet Package |
System.Data.SqlClient |
MySql.Data |
AdoInvariant |
System.Data.SqlClient |
MySql.Data.MySqlClient |
You can set the following propeties:
Name |
Type |
Description |
Name |
String |
Arbitrary name that persistent grains will use to refer to this storage provider |
Type |
String |
Set to Orleans.Storage.AdoNetStorageProvider |
AdoInvariant |
String |
Identifies the database vendor (see above table for values; default is System.Data.SqlClient) |
DataConnectionString |
String |
Vendor-specific database connection string (required) |
UseJsonFormat |
Boolean |
Use JSON format |
UseXmlFormat |
Boolean |
Use XML format |
UseBinaryFormat |
Boolean |
Use compact binary format (default) |
When configuring via code, prefer the generic registration call, as it avoids magic strings and potential runtime errors by using a type provided at compile time. Avoid the shortcut extension method since it does not let you configure the AdoInvariant
and has a different default format from the other API methods.