diff --git a/Orleans.slnx b/Orleans.slnx
index ec2dd21f42..178450a979 100644
--- a/Orleans.slnx
+++ b/Orleans.slnx
@@ -33,6 +33,7 @@
+
@@ -65,6 +66,7 @@
+
@@ -72,6 +74,7 @@
+
@@ -79,6 +82,8 @@
+
+
@@ -98,6 +103,7 @@
+
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/DynamoDBRemindersProviderBuilder.cs b/src/AWS/Orleans.AdvancedReminders.DynamoDB/DynamoDBRemindersProviderBuilder.cs
new file mode 100644
index 0000000000..806304bfdf
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/DynamoDBRemindersProviderBuilder.cs
@@ -0,0 +1,79 @@
+using Microsoft.Extensions.Configuration;
+using Orleans;
+using Orleans.Hosting;
+using Orleans.Providers;
+using Orleans.AdvancedReminders.DynamoDB;
+
+[assembly: RegisterProvider("DynamoDB", "AdvancedReminders", "Silo", typeof(AdvancedDynamoDBRemindersProviderBuilder))]
+
+namespace Orleans.Hosting;
+
+internal sealed class AdvancedDynamoDBRemindersProviderBuilder : IProviderBuilder
+{
+ public void Configure(ISiloBuilder builder, string name, IConfigurationSection configurationSection)
+ {
+ builder.UseDynamoDBAdvancedReminderService(options =>
+ {
+ var accessKey = configurationSection[nameof(options.AccessKey)];
+ if (!string.IsNullOrEmpty(accessKey))
+ {
+ options.AccessKey = accessKey;
+ }
+
+ var secretKey = configurationSection[nameof(options.SecretKey)];
+ if (!string.IsNullOrEmpty(secretKey))
+ {
+ options.SecretKey = secretKey;
+ }
+
+ var region = configurationSection[nameof(options.Service)] ?? configurationSection["Region"];
+ if (!string.IsNullOrEmpty(region))
+ {
+ options.Service = region;
+ }
+
+ var token = configurationSection[nameof(options.Token)];
+ if (!string.IsNullOrEmpty(token))
+ {
+ options.Token = token;
+ }
+
+ var profileName = configurationSection[nameof(options.ProfileName)];
+ if (!string.IsNullOrEmpty(profileName))
+ {
+ options.ProfileName = profileName;
+ }
+
+ var tableName = configurationSection[nameof(options.TableName)];
+ if (!string.IsNullOrEmpty(tableName))
+ {
+ options.TableName = tableName;
+ }
+
+ if (int.TryParse(configurationSection[nameof(options.ReadCapacityUnits)], out var rcu))
+ {
+ options.ReadCapacityUnits = rcu;
+ }
+
+ if (int.TryParse(configurationSection[nameof(options.WriteCapacityUnits)], out var wcu))
+ {
+ options.WriteCapacityUnits = wcu;
+ }
+
+ if (bool.TryParse(configurationSection[nameof(options.UseProvisionedThroughput)], out var upt))
+ {
+ options.UseProvisionedThroughput = upt;
+ }
+
+ if (bool.TryParse(configurationSection[nameof(options.CreateIfNotExists)], out var cine))
+ {
+ options.CreateIfNotExists = cine;
+ }
+
+ if (bool.TryParse(configurationSection[nameof(options.UpdateIfExists)], out var uie))
+ {
+ options.UpdateIfExists = uie;
+ }
+ });
+ }
+}
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/DynamoDBServiceCollectionReminderExtensions.cs b/src/AWS/Orleans.AdvancedReminders.DynamoDB/DynamoDBServiceCollectionReminderExtensions.cs
new file mode 100644
index 0000000000..896cab56d4
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/DynamoDBServiceCollectionReminderExtensions.cs
@@ -0,0 +1,35 @@
+using Microsoft.Extensions.DependencyInjection;
+using Orleans.Configuration;
+using Orleans.Configuration.Internal;
+using Orleans.AdvancedReminders.DynamoDB;
+using System;
+
+namespace Orleans.Hosting
+{
+ ///
+ /// extensions.
+ ///
+ public static class DynamoDBServiceCollectionReminderExtensions
+ {
+ ///
+ /// Adds reminder storage backed by Amazon DynamoDB.
+ ///
+ ///
+ /// The service collection.
+ ///
+ ///
+ /// The delegate used to configure the reminder store.
+ ///
+ ///
+ /// The provided , for chaining.
+ ///
+ public static IServiceCollection UseDynamoDBAdvancedReminderService(this IServiceCollection services, Action configure)
+ {
+ services.AddAdvancedReminders();
+ services.AddSingleton();
+ services.Configure(configure);
+ services.ConfigureFormatter();
+ return services;
+ }
+ }
+}
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/DynamoDBSiloBuilderReminderExtensions.cs b/src/AWS/Orleans.AdvancedReminders.DynamoDB/DynamoDBSiloBuilderReminderExtensions.cs
new file mode 100644
index 0000000000..5c7b9a7c2d
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/DynamoDBSiloBuilderReminderExtensions.cs
@@ -0,0 +1,29 @@
+using System;
+using Orleans.AdvancedReminders.DynamoDB;
+
+namespace Orleans.Hosting
+{
+ ///
+ /// Silo host builder extensions.
+ ///
+ public static class DynamoDBSiloBuilderReminderExtensions
+ {
+ ///
+ /// Adds reminder storage backed by Amazon DynamoDB.
+ ///
+ ///
+ /// The builder.
+ ///
+ ///
+ /// The delegate used to configure the reminder store.
+ ///
+ ///
+ /// The provided , for chaining.
+ ///
+ public static ISiloBuilder UseDynamoDBAdvancedReminderService(this ISiloBuilder builder, Action configure)
+ {
+ builder.ConfigureServices(services => services.UseDynamoDBAdvancedReminderService(configure));
+ return builder;
+ }
+ }
+}
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/GlobalUsings.cs b/src/AWS/Orleans.AdvancedReminders.DynamoDB/GlobalUsings.cs
new file mode 100644
index 0000000000..cc91a7365d
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/GlobalUsings.cs
@@ -0,0 +1,2 @@
+global using Orleans.AdvancedReminders.Runtime;
+global using Orleans.AdvancedReminders.DynamoDB;
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/Orleans.AdvancedReminders.DynamoDB.csproj b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Orleans.AdvancedReminders.DynamoDB.csproj
new file mode 100644
index 0000000000..817e672d8f
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Orleans.AdvancedReminders.DynamoDB.csproj
@@ -0,0 +1,39 @@
+
+
+ README.md
+ Microsoft.Orleans.AdvancedReminders.DynamoDB
+ Microsoft Orleans Advanced Reminders for DynamoDB
+ AWS DynamoDB provider for Microsoft Orleans Advanced Reminders.
+ $(PackageTags) AWS DynamoDB
+ $(DefaultTargetFrameworks)
+ true
+
+
+
+ Orleans.AdvancedReminders.DynamoDB
+ Orleans.AdvancedReminders.DynamoDB
+ $(DefineConstants);ADVANCED_REMINDERS_DYNAMODB
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/README.md b/src/AWS/Orleans.AdvancedReminders.DynamoDB/README.md
new file mode 100644
index 0000000000..6538cb0b24
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/README.md
@@ -0,0 +1,119 @@
+# Microsoft Orleans Advanced Reminders for DynamoDB
+
+## Introduction
+Microsoft Orleans Advanced Reminders for DynamoDB stores reminder definitions in Amazon DynamoDB.
+
+This package does not include a DynamoDB-backed durable jobs implementation. You must also configure a durable jobs backend, for example `UseInMemoryDurableJobs()` for local development or `UseAzureBlobDurableJobs(...)` for persisted execution.
+
+## Getting Started
+To use this package, install it via NuGet:
+
+```shell
+dotnet add package Microsoft.Orleans.AdvancedReminders.DynamoDB
+```
+
+## Example - Configuring DynamoDB Advanced Reminders
+```csharp
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Orleans.Configuration;
+using Orleans.AdvancedReminders;
+using Orleans.Hosting;
+using Orleans.DurableJobs;
+
+var builder = Host.CreateApplicationBuilder(args)
+ .UseOrleans(siloBuilder =>
+ {
+ siloBuilder
+ .UseLocalhostClustering()
+ .UseInMemoryDurableJobs()
+ // Configure DynamoDB for reminder definitions
+ .UseDynamoDBAdvancedReminderService(options =>
+ {
+ options.AccessKey = "YOUR_AWS_ACCESS_KEY";
+ options.SecretKey = "YOUR_AWS_SECRET_KEY";
+ options.Service = "us-east-1";
+ options.TableName = "OrleansAdvancedReminders";
+ options.CreateIfNotExists = true;
+ });
+ });
+
+// Run the host
+var host = builder.Build();
+await host.StartAsync();
+
+// Get a reference to the grain
+var reminderGrain = host.Services.GetRequiredService()
+ .GetGrain("my-reminder-grain");
+
+// Start the reminder
+await reminderGrain.StartReminder("ExampleReminder");
+Console.WriteLine("Reminder started!");
+
+// Keep the host running until the application is shut down
+await host.WaitForShutdownAsync();
+```
+
+## Example - Using Reminders in a Grain
+```csharp
+using System;
+using System.Threading.Tasks;
+using Orleans;
+using Orleans.AdvancedReminders;
+using Orleans.AdvancedReminders.Runtime;
+
+namespace ReminderExample;
+
+public interface IReminderGrain : IGrainWithStringKey
+{
+ Task StartReminder(string reminderName);
+ Task StopReminder();
+}
+
+public class ReminderGrain : Grain, IReminderGrain, IRemindable
+{
+ private string _reminderName = "MyReminder";
+
+ public async Task StartReminder(string reminderName)
+ {
+ _reminderName = reminderName;
+
+ // Register a persistent reminder
+ await RegisterOrUpdateReminder(
+ reminderName,
+ TimeSpan.FromMinutes(2), // Time to delay before the first tick (must be > 1 minute)
+ TimeSpan.FromMinutes(5)); // Period of the reminder (must be > 1 minute)
+ }
+
+ public async Task StopReminder()
+ {
+ // Find and unregister the reminder
+ var reminder = await GetReminder(_reminderName);
+ if (reminder != null)
+ {
+ await UnregisterReminder(reminder);
+ }
+ }
+
+ public Task ReceiveReminder(string reminderName, TickStatus status)
+ {
+ // This method is called when the reminder ticks
+ Console.WriteLine($"Reminder {reminderName} triggered at {DateTime.UtcNow}. Status: {status}");
+ return Task.CompletedTask;
+ }
+}
+```
+
+## Documentation
+For more comprehensive documentation, please refer to:
+- [Microsoft Orleans Documentation](https://learn.microsoft.com/dotnet/orleans/)
+- [Reminders and Timers](https://learn.microsoft.com/en-us/dotnet/orleans/grains/timers-and-reminders)
+- [Reminder Services](https://learn.microsoft.com/en-us/dotnet/orleans/implementation/reminder-services)
+- [AWS SDK for .NET Documentation](https://docs.aws.amazon.com/sdk-for-net/index.html)
+
+## Feedback & Contributing
+- If you have any issues or would like to provide feedback, please [open an issue on GitHub](https://github.com/dotnet/orleans/issues)
+- Join our community on [Discord](https://aka.ms/orleans-discord)
+- Follow the [@msftorleans](https://twitter.com/msftorleans) Twitter account for Orleans announcements
+- Contributions are welcome! Please review our [contribution guidelines](https://github.com/dotnet/orleans/blob/main/CONTRIBUTING.md)
+- This project is licensed under the [MIT license](https://github.com/dotnet/orleans/blob/main/LICENSE)
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDBReminderTable.cs b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDBReminderTable.cs
new file mode 100644
index 0000000000..b5eb507079
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDBReminderTable.cs
@@ -0,0 +1,484 @@
+using Amazon.DynamoDBv2;
+using Amazon.DynamoDBv2.Model;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Orleans.Configuration;
+using Orleans.Runtime;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading.Tasks;
+
+namespace Orleans.AdvancedReminders.DynamoDB
+{
+ ///
+ /// Implementation for IReminderTable using DynamoDB as underlying storage.
+ ///
+ internal sealed partial class DynamoDBReminderTable : IReminderTable
+ {
+ private const string GRAIN_REFERENCE_PROPERTY_NAME = "GrainReference";
+ private const string REMINDER_NAME_PROPERTY_NAME = "ReminderName";
+ private const string SERVICE_ID_PROPERTY_NAME = "ServiceId";
+ private const string START_TIME_PROPERTY_NAME = "StartTime";
+ private const string PERIOD_PROPERTY_NAME = "Period";
+ private const string CRON_EXPRESSION_PROPERTY_NAME = "CronExpression";
+ private const string CRON_TIME_ZONE_ID_PROPERTY_NAME = "CronTimeZoneId";
+ private const string NEXT_DUE_UTC_PROPERTY_NAME = "NextDueUtc";
+ private const string LAST_FIRE_UTC_PROPERTY_NAME = "LastFireUtc";
+ private const string PRIORITY_PROPERTY_NAME = "Priority";
+ private const string ACTION_PROPERTY_NAME = "Action";
+ private const string GRAIN_HASH_PROPERTY_NAME = "GrainHash";
+ private const string REMINDER_ID_PROPERTY_NAME = "ReminderId";
+ private const string ETAG_PROPERTY_NAME = "ETag";
+ private const string CURRENT_ETAG_ALIAS = ":currentETag";
+ private const string SERVICE_ID_GRAIN_HASH_INDEX = "ServiceIdIndex";
+ private const string SERVICE_ID_GRAIN_REFERENCE_INDEX = "ServiceIdGrainReferenceIndex";
+
+ private readonly ILogger logger;
+ private readonly DynamoDBReminderStorageOptions options;
+ private readonly string serviceId;
+
+ private DynamoDBStorage storage;
+
+ /// Initializes a new instance of the class.
+ /// logger factory to use
+ ///
+ ///
+ public DynamoDBReminderTable(
+ ILoggerFactory loggerFactory,
+ IOptions clusterOptions,
+ IOptions storageOptions)
+ {
+ this.logger = loggerFactory.CreateLogger();
+ this.serviceId = clusterOptions.Value.ServiceId;
+ this.options = storageOptions.Value;
+ }
+
+ /// Initialize current instance with specific global configuration and logger
+ public Task Init()
+ {
+ this.storage = new DynamoDBStorage(
+ this.logger,
+ this.options.Service,
+ this.options.AccessKey,
+ this.options.SecretKey,
+ this.options.Token,
+ this.options.ProfileName,
+ this.options.ReadCapacityUnits,
+ this.options.WriteCapacityUnits,
+ this.options.UseProvisionedThroughput,
+ this.options.CreateIfNotExists,
+ this.options.UpdateIfExists);
+
+ LogInformationInitializingDynamoDBRemindersTable(logger);
+
+ var serviceIdGrainHashGlobalSecondaryIndex = new GlobalSecondaryIndex
+ {
+ IndexName = SERVICE_ID_GRAIN_HASH_INDEX,
+ Projection = new Projection { ProjectionType = ProjectionType.ALL },
+ KeySchema = new List
+ {
+ new KeySchemaElement { AttributeName = SERVICE_ID_PROPERTY_NAME, KeyType = KeyType.HASH},
+ new KeySchemaElement { AttributeName = GRAIN_HASH_PROPERTY_NAME, KeyType = KeyType.RANGE }
+ }
+ };
+
+ var serviceIdGrainReferenceGlobalSecondaryIndex = new GlobalSecondaryIndex
+ {
+ IndexName = SERVICE_ID_GRAIN_REFERENCE_INDEX,
+ Projection = new Projection { ProjectionType = ProjectionType.ALL },
+ KeySchema = new List
+ {
+ new KeySchemaElement { AttributeName = SERVICE_ID_PROPERTY_NAME, KeyType = KeyType.HASH},
+ new KeySchemaElement { AttributeName = GRAIN_REFERENCE_PROPERTY_NAME, KeyType = KeyType.RANGE }
+ }
+ };
+
+ return this.storage.InitializeTable(this.options.TableName,
+ new List
+ {
+ new KeySchemaElement { AttributeName = REMINDER_ID_PROPERTY_NAME, KeyType = KeyType.HASH },
+ new KeySchemaElement { AttributeName = GRAIN_HASH_PROPERTY_NAME, KeyType = KeyType.RANGE }
+ },
+ new List
+ {
+ new AttributeDefinition { AttributeName = REMINDER_ID_PROPERTY_NAME, AttributeType = ScalarAttributeType.S },
+ new AttributeDefinition { AttributeName = GRAIN_HASH_PROPERTY_NAME, AttributeType = ScalarAttributeType.N },
+ new AttributeDefinition { AttributeName = SERVICE_ID_PROPERTY_NAME, AttributeType = ScalarAttributeType.S },
+ new AttributeDefinition { AttributeName = GRAIN_REFERENCE_PROPERTY_NAME, AttributeType = ScalarAttributeType.S }
+ },
+ new List { serviceIdGrainHashGlobalSecondaryIndex, serviceIdGrainReferenceGlobalSecondaryIndex });
+ }
+
+ ///
+ /// Reads a reminder for a grain reference by reminder name.
+ /// Read a row from the reminder table
+ ///
+ /// grain ref to locate the row
+ /// reminder name to locate the row
+ /// Return the ReminderTableData if the rows were read successfully
+ public async Task ReadRow(GrainId grainId, string reminderName)
+ {
+ var reminderId = ConstructReminderId(this.serviceId, grainId, reminderName);
+
+ var keys = new Dictionary
+ {
+ { $"{REMINDER_ID_PROPERTY_NAME}", new AttributeValue(reminderId) },
+ { $"{GRAIN_HASH_PROPERTY_NAME}", new AttributeValue { N = grainId.GetUniformHashCode().ToString() } }
+ };
+
+ try
+ {
+ return await this.storage.ReadSingleEntryAsync(this.options.TableName, keys, this.Resolve).ConfigureAwait(false);
+ }
+ catch (Exception exc)
+ {
+ LogWarningReadReminderEntry(logger, exc, new(keys), this.options.TableName);
+ throw;
+ }
+ }
+
+ ///
+ /// Read one row from the reminder table
+ ///
+ /// grain ref to locate the row
+ /// Return the ReminderTableData if the rows were read successfully
+ public async Task ReadRows(GrainId grainId)
+ {
+ var expressionValues = new Dictionary
+ {
+ { $":{SERVICE_ID_PROPERTY_NAME}", new AttributeValue(this.serviceId) },
+ { $":{GRAIN_REFERENCE_PROPERTY_NAME}", new AttributeValue(grainId.ToString()) }
+ };
+
+ try
+ {
+ var expression = $"{SERVICE_ID_PROPERTY_NAME} = :{SERVICE_ID_PROPERTY_NAME} AND {GRAIN_REFERENCE_PROPERTY_NAME} = :{GRAIN_REFERENCE_PROPERTY_NAME}";
+ var records = await this.storage.QueryAllAsync(this.options.TableName, expressionValues, expression, this.Resolve, SERVICE_ID_GRAIN_REFERENCE_INDEX, consistentRead: false).ConfigureAwait(false);
+
+ return new ReminderTableData(records);
+ }
+ catch (Exception exc)
+ {
+ LogWarningReadReminderEntries(logger, exc, new(expressionValues), this.options.TableName);
+ throw;
+ }
+ }
+
+ ///
+ /// Reads reminder table data for a given hash range.
+ ///
+ ///
+ ///
+ /// Return the RemiderTableData if the rows were read successfully
+ public async Task ReadRows(uint begin, uint end)
+ {
+ Dictionary expressionValues = null;
+
+ try
+ {
+ string expression = string.Empty;
+ List records;
+
+ if (begin < end)
+ {
+ expressionValues = new Dictionary
+ {
+ { $":{SERVICE_ID_PROPERTY_NAME}", new AttributeValue(this.serviceId) },
+ { $":Begin{GRAIN_HASH_PROPERTY_NAME}", new AttributeValue { N = (begin + 1).ToString() } },
+ { $":End{GRAIN_HASH_PROPERTY_NAME}", new AttributeValue { N = end.ToString() } }
+ };
+ expression = $"{SERVICE_ID_PROPERTY_NAME} = :{SERVICE_ID_PROPERTY_NAME} AND {GRAIN_HASH_PROPERTY_NAME} BETWEEN :Begin{GRAIN_HASH_PROPERTY_NAME} AND :End{GRAIN_HASH_PROPERTY_NAME}";
+ records = await this.storage.QueryAllAsync(this.options.TableName, expressionValues, expression, this.Resolve, SERVICE_ID_GRAIN_HASH_INDEX, consistentRead: false).ConfigureAwait(false);
+ }
+ else
+ {
+ expressionValues = new Dictionary
+ {
+ { $":{SERVICE_ID_PROPERTY_NAME}", new AttributeValue(this.serviceId) },
+ { $":End{GRAIN_HASH_PROPERTY_NAME}", new AttributeValue { N = end.ToString() } }
+ };
+ expression = $"{SERVICE_ID_PROPERTY_NAME} = :{SERVICE_ID_PROPERTY_NAME} AND {GRAIN_HASH_PROPERTY_NAME} <= :End{GRAIN_HASH_PROPERTY_NAME}";
+ records = await this.storage.QueryAllAsync(this.options.TableName, expressionValues, expression, this.Resolve, SERVICE_ID_GRAIN_HASH_INDEX, consistentRead: false).ConfigureAwait(false);
+
+ expressionValues = new Dictionary
+ {
+ { $":{SERVICE_ID_PROPERTY_NAME}", new AttributeValue(this.serviceId) },
+ { $":Begin{GRAIN_HASH_PROPERTY_NAME}", new AttributeValue { N = begin.ToString() } }
+ };
+ expression = $"{SERVICE_ID_PROPERTY_NAME} = :{SERVICE_ID_PROPERTY_NAME} AND {GRAIN_HASH_PROPERTY_NAME} > :Begin{GRAIN_HASH_PROPERTY_NAME}";
+ records.AddRange(await this.storage.QueryAllAsync(this.options.TableName, expressionValues, expression, this.Resolve, SERVICE_ID_GRAIN_HASH_INDEX, consistentRead: false).ConfigureAwait(false));
+
+ }
+
+ return new ReminderTableData(records);
+ }
+ catch (Exception exc)
+ {
+ LogWarningReadReminderEntryRange(logger, exc, new(expressionValues), this.options.TableName);
+ throw;
+ }
+ }
+
+ private ReminderEntry Resolve(Dictionary item)
+ {
+ return new ReminderEntry
+ {
+ ETag = item[ETAG_PROPERTY_NAME].N,
+ GrainId = GrainId.Parse(item[GRAIN_REFERENCE_PROPERTY_NAME].S),
+ Period = TimeSpan.Parse(item[PERIOD_PROPERTY_NAME].S, CultureInfo.InvariantCulture),
+ CronExpression = ReadOptionalString(item, CRON_EXPRESSION_PROPERTY_NAME),
+ CronTimeZoneId = ReadOptionalString(item, CRON_TIME_ZONE_ID_PROPERTY_NAME),
+ NextDueUtc = ReadOptionalDateTime(item, NEXT_DUE_UTC_PROPERTY_NAME),
+ LastFireUtc = ReadOptionalDateTime(item, LAST_FIRE_UTC_PROPERTY_NAME),
+ Priority = ReadPriority(item),
+ Action = ReadAction(item),
+ ReminderName = item[REMINDER_NAME_PROPERTY_NAME].S,
+ StartAt = DateTime.Parse(item[START_TIME_PROPERTY_NAME].S, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind)
+ };
+ }
+
+ private static string ReadOptionalString(Dictionary item, string propertyName)
+ => item.TryGetValue(propertyName, out var value) ? value.S : null;
+
+ private static DateTime? ReadOptionalDateTime(Dictionary item, string propertyName)
+ {
+ if (!item.TryGetValue(propertyName, out var value) || string.IsNullOrWhiteSpace(value.S))
+ {
+ return null;
+ }
+
+ return DateTime.Parse(value.S, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
+ }
+
+ private static ReminderPriority ReadPriority(Dictionary item)
+ {
+ if (!TryReadInt32(item, PRIORITY_PROPERTY_NAME, out var value))
+ {
+ return ReminderPriority.Normal;
+ }
+
+ return ParsePriority(value);
+ }
+
+ private static MissedReminderAction ReadAction(Dictionary item)
+ {
+ if (!TryReadInt32(item, ACTION_PROPERTY_NAME, out var value))
+ {
+ return MissedReminderAction.Skip;
+ }
+
+ return ParseAction(value);
+ }
+
+ private static bool TryReadInt32(Dictionary item, string propertyName, out int value)
+ {
+ value = default;
+ return item.TryGetValue(propertyName, out var attributeValue)
+ && !string.IsNullOrWhiteSpace(attributeValue.N)
+ && int.TryParse(attributeValue.N, NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
+ }
+
+ private static ReminderPriority ParsePriority(int value) => value switch
+ {
+ (int)ReminderPriority.High => ReminderPriority.High,
+ (int)ReminderPriority.Normal => ReminderPriority.Normal,
+ _ => ReminderPriority.Normal,
+ };
+
+ private static MissedReminderAction ParseAction(int value) => value switch
+ {
+ (int)MissedReminderAction.FireImmediately => MissedReminderAction.FireImmediately,
+ (int)MissedReminderAction.Skip => MissedReminderAction.Skip,
+ (int)MissedReminderAction.Notify => MissedReminderAction.Notify,
+ _ => MissedReminderAction.Skip,
+ };
+
+ ///
+ /// Remove one row from the reminder table
+ ///
+ /// specific grain ref to locate the row
+ /// reminder name to locate the row
+ /// e tag
+ /// Return true if the row was removed
+ public async Task RemoveRow(GrainId grainId, string reminderName, string eTag)
+ {
+ var reminderId = ConstructReminderId(this.serviceId, grainId, reminderName);
+
+ var keys = new Dictionary
+ {
+ { $"{REMINDER_ID_PROPERTY_NAME}", new AttributeValue(reminderId) },
+ { $"{GRAIN_HASH_PROPERTY_NAME}", new AttributeValue { N = grainId.GetUniformHashCode().ToString() } }
+ };
+
+ try
+ {
+ var conditionalValues = new Dictionary { { CURRENT_ETAG_ALIAS, new AttributeValue { N = eTag } } };
+ var expression = $"{ETAG_PROPERTY_NAME} = {CURRENT_ETAG_ALIAS}";
+
+ await this.storage.DeleteEntryAsync(this.options.TableName, keys, expression, conditionalValues).ConfigureAwait(false);
+ return true;
+ }
+ catch (ConditionalCheckFailedException)
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Test hook to clear reminder table data.
+ ///
+ ///
+ public async Task TestOnlyClearTable()
+ {
+ var expressionValues = new Dictionary
+ {
+ { $":{SERVICE_ID_PROPERTY_NAME}", new AttributeValue(this.serviceId) }
+ };
+
+ try
+ {
+ var expression = $"{SERVICE_ID_PROPERTY_NAME} = :{SERVICE_ID_PROPERTY_NAME}";
+ var records = await this.storage.ScanAsync(this.options.TableName, expressionValues, expression,
+ item => new Dictionary
+ {
+ { REMINDER_ID_PROPERTY_NAME, item[REMINDER_ID_PROPERTY_NAME] },
+ { GRAIN_HASH_PROPERTY_NAME, item[GRAIN_HASH_PROPERTY_NAME] }
+ }).ConfigureAwait(false);
+
+ if (records.Count <= 25)
+ {
+ await this.storage.DeleteEntriesAsync(this.options.TableName, records);
+ }
+ else
+ {
+ List tasks = new List();
+ foreach (var batch in records.BatchIEnumerable(25))
+ {
+ tasks.Add(this.storage.DeleteEntriesAsync(this.options.TableName, batch));
+ }
+ await Task.WhenAll(tasks);
+ }
+ }
+ catch (Exception exc)
+ {
+ LogWarningRemoveReminderEntries(logger, exc, new(expressionValues), this.options.TableName);
+ throw;
+ }
+ }
+
+ ///
+ /// Async method to put an entry into the reminder table
+ ///
+ /// The entry to put
+ /// Return the entry ETag if entry was upsert successfully
+ public async Task UpsertRow(ReminderEntry entry)
+ {
+ var reminderId = ConstructReminderId(this.serviceId, entry.GrainId, entry.ReminderName);
+
+ var fields = new Dictionary
+ {
+ { REMINDER_ID_PROPERTY_NAME, new AttributeValue(reminderId) },
+ { GRAIN_HASH_PROPERTY_NAME, new AttributeValue { N = entry.GrainId.GetUniformHashCode().ToString() } },
+ { SERVICE_ID_PROPERTY_NAME, new AttributeValue(this.serviceId) },
+ { GRAIN_REFERENCE_PROPERTY_NAME, new AttributeValue( entry.GrainId.ToString()) },
+ { PERIOD_PROPERTY_NAME, new AttributeValue(entry.Period.ToString("c", CultureInfo.InvariantCulture)) },
+ { START_TIME_PROPERTY_NAME, new AttributeValue(entry.StartAt.ToString("O", CultureInfo.InvariantCulture)) },
+ { REMINDER_NAME_PROPERTY_NAME, new AttributeValue(entry.ReminderName) },
+ { PRIORITY_PROPERTY_NAME, new AttributeValue { N = ((int)entry.Priority).ToString(CultureInfo.InvariantCulture) } },
+ { ACTION_PROPERTY_NAME, new AttributeValue { N = ((int)entry.Action).ToString(CultureInfo.InvariantCulture) } },
+ { ETAG_PROPERTY_NAME, new AttributeValue { N = Random.Shared.Next().ToString(CultureInfo.InvariantCulture) } }
+ };
+
+ if (!string.IsNullOrWhiteSpace(entry.CronExpression))
+ {
+ fields[CRON_EXPRESSION_PROPERTY_NAME] = new AttributeValue(entry.CronExpression);
+ }
+
+ if (!string.IsNullOrWhiteSpace(entry.CronTimeZoneId))
+ {
+ fields[CRON_TIME_ZONE_ID_PROPERTY_NAME] = new AttributeValue(entry.CronTimeZoneId);
+ }
+
+ if (entry.NextDueUtc is { } nextDueUtc)
+ {
+ fields[NEXT_DUE_UTC_PROPERTY_NAME] = new AttributeValue(nextDueUtc.ToString("O"));
+ }
+
+ if (entry.LastFireUtc is { } lastFireUtc)
+ {
+ fields[LAST_FIRE_UTC_PROPERTY_NAME] = new AttributeValue(lastFireUtc.ToString("O"));
+ }
+
+ try
+ {
+ LogDebugUpsertRow(logger, entry, entry.ETag);
+
+ await this.storage.PutEntryAsync(this.options.TableName, fields);
+
+ entry.ETag = fields[ETAG_PROPERTY_NAME].N;
+ return entry.ETag;
+ }
+ catch (Exception exc)
+ {
+ LogWarningUpdateReminderEntry(logger, exc, entry, options.TableName);
+ throw;
+ }
+ }
+
+ private static string ConstructReminderId(string serviceId, GrainId grainId, string reminderName) => $"{serviceId}_{grainId}_{reminderName}";
+
+ [LoggerMessage(
+ EventId = (int)ErrorCode.ReminderServiceBase,
+ Level = LogLevel.Information,
+ Message = "Initializing AWS DynamoDB Reminders Table"
+ )]
+ private static partial void LogInformationInitializingDynamoDBRemindersTable(ILogger logger);
+
+ private readonly struct DictionaryLogRecord(Dictionary keys)
+ {
+ public override string ToString() => Utils.DictionaryToString(keys);
+ }
+
+ [LoggerMessage(
+ EventId = (int)ErrorCode.ReminderServiceBase,
+ Level = LogLevel.Warning,
+ Message = "Intermediate error reading reminder entry {Keys} from table {TableName}."
+ )]
+ private static partial void LogWarningReadReminderEntry(ILogger logger, Exception exception, DictionaryLogRecord keys, string tableName);
+
+ [LoggerMessage(
+ EventId = (int)ErrorCode.ReminderServiceBase,
+ Level = LogLevel.Warning,
+ Message = "Intermediate error reading reminder entry {Entries} from table {TableName}."
+ )]
+ private static partial void LogWarningReadReminderEntries(ILogger logger, Exception exception, DictionaryLogRecord entries, string tableName);
+
+ [LoggerMessage(
+ EventId = (int)ErrorCode.ReminderServiceBase,
+ Level = LogLevel.Warning,
+ Message = "Intermediate error reading reminder entry {ExpressionValues} from table {TableName}."
+ )]
+ private static partial void LogWarningReadReminderEntryRange(ILogger logger, Exception exception, DictionaryLogRecord expressionValues, string tableName);
+
+ [LoggerMessage(
+ EventId = (int)ErrorCode.ReminderServiceBase,
+ Level = LogLevel.Warning,
+ Message = "Intermediate error removing reminder entries {Entries} from table {TableName}."
+ )]
+ private static partial void LogWarningRemoveReminderEntries(ILogger logger, Exception exception, DictionaryLogRecord entries, string tableName);
+
+ [LoggerMessage(
+ Level = LogLevel.Debug,
+ Message = "UpsertRow entry = {Entry}, etag = {ETag}"
+ )]
+ private static partial void LogDebugUpsertRow(ILogger logger, ReminderEntry entry, string eTag);
+
+ [LoggerMessage(
+ EventId = (int)ErrorCode.ReminderServiceBase,
+ Level = LogLevel.Warning,
+ Message = "Intermediate error updating entry {Entry} to the table {TableName}."
+ )]
+ private static partial void LogWarningUpdateReminderEntry(ILogger logger, Exception exception, ReminderEntry entry, string tableName);
+ }
+}
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDbReminderServiceOptions.cs b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDbReminderServiceOptions.cs
new file mode 100644
index 0000000000..7b19820064
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDbReminderServiceOptions.cs
@@ -0,0 +1,14 @@
+namespace Orleans.AdvancedReminders.DynamoDB
+{
+ ///
+ /// Configuration for Amazon DynamoDB reminder storage.
+ ///
+ public class DynamoDBReminderTableOptions
+ {
+ ///
+ /// Gets or sets the connection string.
+ ///
+ [RedactConnectionString]
+ public string ConnectionString { get; set; }
+ }
+}
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDbReminderStorageOptions.cs b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDbReminderStorageOptions.cs
new file mode 100644
index 0000000000..a4782f97fd
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDbReminderStorageOptions.cs
@@ -0,0 +1,41 @@
+using Orleans.AdvancedReminders.DynamoDB;
+
+namespace Orleans.AdvancedReminders.DynamoDB
+{
+ ///
+ /// Configuration for Amazon DynamoDB reminder storage.
+ ///
+ public class DynamoDBReminderStorageOptions : DynamoDBClientOptions
+ {
+ ///
+ /// Read capacity unit for DynamoDB storage
+ ///
+ public int ReadCapacityUnits { get; set; } = DynamoDBStorage.DefaultReadCapacityUnits;
+
+ ///
+ /// Write capacity unit for DynamoDB storage
+ ///
+ public int WriteCapacityUnits { get; set; } = DynamoDBStorage.DefaultWriteCapacityUnits;
+
+ ///
+ /// Use Provisioned Throughput for tables
+ ///
+ public bool UseProvisionedThroughput { get; set; } = true;
+
+ ///
+ /// Create the table if it doesn't exist
+ ///
+ public bool CreateIfNotExists { get; set; } = true;
+
+ ///
+ /// Update the table if it exists
+ ///
+ public bool UpdateIfExists { get; set; } = true;
+
+ ///
+ /// DynamoDB table name.
+ /// Defaults to 'OrleansAdvancedReminders'.
+ ///
+ public string TableName { get; set; } = "OrleansAdvancedReminders";
+ }
+}
diff --git a/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDbReminderStorageOptionsExtensions.cs b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDbReminderStorageOptionsExtensions.cs
new file mode 100644
index 0000000000..5cb52b980f
--- /dev/null
+++ b/src/AWS/Orleans.AdvancedReminders.DynamoDB/Reminders/DynamoDbReminderStorageOptionsExtensions.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Linq;
+
+namespace Orleans.AdvancedReminders.DynamoDB
+{
+ ///
+ /// Configuration for Amazon DynamoDB reminder storage.
+ ///
+ public static class DynamoDBReminderStorageOptionsExtensions
+ {
+ private const string AccessKeyPropertyName = "AccessKey";
+ private const string SecretKeyPropertyName = "SecretKey";
+ private const string ServicePropertyName = "Service";
+ private const string ReadCapacityUnitsPropertyName = "ReadCapacityUnits";
+ private const string WriteCapacityUnitsPropertyName = "WriteCapacityUnits";
+ private const string UseProvisionedThroughputPropertyName = "UseProvisionedThroughput";
+ private const string CreateIfNotExistsPropertyName = "CreateIfNotExists";
+ private const string UpdateIfExistsPropertyName = "UpdateIfExists";
+
+ ///
+ /// Configures this instance using the provided connection string.
+ ///
+ public static void ParseConnectionString(this DynamoDBReminderStorageOptions options, string connectionString)
+ {
+ var parameters = connectionString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
+
+ var serviceConfig = Array.Find(parameters, p => p.Contains(ServicePropertyName));
+ if (!string.IsNullOrWhiteSpace(serviceConfig))
+ {
+ var value = serviceConfig.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
+ if (value.Length == 2 && !string.IsNullOrWhiteSpace(value[1]))
+ options.Service = value[1];
+ }
+
+ var secretKeyConfig = Array.Find(parameters, p => p.Contains(SecretKeyPropertyName));
+ if (!string.IsNullOrWhiteSpace(secretKeyConfig))
+ {
+ var value = secretKeyConfig.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
+ if (value.Length == 2 && !string.IsNullOrWhiteSpace(value[1]))
+ options.SecretKey = value[1];
+ }
+
+ var accessKeyConfig = Array.Find(parameters, p => p.Contains(AccessKeyPropertyName));
+ if (!string.IsNullOrWhiteSpace(accessKeyConfig))
+ {
+ var value = accessKeyConfig.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
+ if (value.Length == 2 && !string.IsNullOrWhiteSpace(value[1]))
+ options.AccessKey = value[1];
+ }
+
+ var readCapacityUnitsConfig = Array.Find(parameters, p => p.Contains(ReadCapacityUnitsPropertyName));
+ if (!string.IsNullOrWhiteSpace(readCapacityUnitsConfig))
+ {
+ var value = readCapacityUnitsConfig.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
+ if (value.Length == 2 && !string.IsNullOrWhiteSpace(value[1]))
+ options.ReadCapacityUnits = int.Parse(value[1]);
+ }
+
+ var writeCapacityUnitsConfig = Array.Find(parameters, p => p.Contains(WriteCapacityUnitsPropertyName));
+ if (!string.IsNullOrWhiteSpace(writeCapacityUnitsConfig))
+ {
+ var value = writeCapacityUnitsConfig.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
+ if (value.Length == 2 && !string.IsNullOrWhiteSpace(value[1]))
+ options.WriteCapacityUnits = int.Parse(value[1]);
+ }
+
+ var useProvisionedThroughputConfig = Array.Find(parameters, p => p.Contains(UseProvisionedThroughputPropertyName));
+ if (!string.IsNullOrWhiteSpace(useProvisionedThroughputConfig))
+ {
+ var value = useProvisionedThroughputConfig.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
+ if (value.Length == 2 && !string.IsNullOrWhiteSpace(value[1]))
+ options.UseProvisionedThroughput = bool.Parse(value[1]);
+ }
+
+ var createIfNotExistsPropertyNameConfig = Array.Find(parameters, p => p.Contains(CreateIfNotExistsPropertyName));
+ if (!string.IsNullOrWhiteSpace(createIfNotExistsPropertyNameConfig))
+ {
+ var value = createIfNotExistsPropertyNameConfig.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
+ if (value.Length == 2 && !string.IsNullOrWhiteSpace(value[1]))
+ options.CreateIfNotExists = bool.Parse(value[1]);
+ }
+
+ var updateIfExistsPropertyNameConfig = Array.Find(parameters, p => p.Contains(UpdateIfExistsPropertyName));
+ if (!string.IsNullOrWhiteSpace(updateIfExistsPropertyNameConfig))
+ {
+ var value = updateIfExistsPropertyNameConfig.Split(new[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
+ if (value.Length == 2 && !string.IsNullOrWhiteSpace(value[1]))
+ options.UpdateIfExists = bool.Parse(value[1]);
+ }
+ }
+ }
+}
diff --git a/src/AWS/Shared/AWSUtils.cs b/src/AWS/Shared/AWSUtils.cs
index 269935ce81..16398db8fe 100644
--- a/src/AWS/Shared/AWSUtils.cs
+++ b/src/AWS/Shared/AWSUtils.cs
@@ -3,6 +3,8 @@
#if CLUSTERING_DYNAMODB
namespace Orleans.Clustering.DynamoDB
+#elif ADVANCED_REMINDERS_DYNAMODB
+namespace Orleans.AdvancedReminders.DynamoDB
#elif PERSISTENCE_DYNAMODB
namespace Orleans.Persistence.DynamoDB
#elif REMINDERS_DYNAMODB
diff --git a/src/AWS/Shared/Storage/DynamoDBClientOptions.cs b/src/AWS/Shared/Storage/DynamoDBClientOptions.cs
index ddec8065ea..389460af4d 100644
--- a/src/AWS/Shared/Storage/DynamoDBClientOptions.cs
+++ b/src/AWS/Shared/Storage/DynamoDBClientOptions.cs
@@ -1,5 +1,7 @@
#if CLUSTERING_DYNAMODB
namespace Orleans.Clustering.DynamoDB
+#elif ADVANCED_REMINDERS_DYNAMODB
+namespace Orleans.AdvancedReminders.DynamoDB
#elif PERSISTENCE_DYNAMODB
namespace Orleans.Persistence.DynamoDB
#elif REMINDERS_DYNAMODB
@@ -37,4 +39,4 @@ public class DynamoDBClientOptions
///
public string ProfileName { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/src/AWS/Shared/Storage/DynamoDBStorage.cs b/src/AWS/Shared/Storage/DynamoDBStorage.cs
index 593a9f4e5c..6b9373c387 100755
--- a/src/AWS/Shared/Storage/DynamoDBStorage.cs
+++ b/src/AWS/Shared/Storage/DynamoDBStorage.cs
@@ -13,6 +13,8 @@
#if CLUSTERING_DYNAMODB
namespace Orleans.Clustering.DynamoDB
+#elif ADVANCED_REMINDERS_DYNAMODB
+namespace Orleans.AdvancedReminders.DynamoDB
#elif PERSISTENCE_DYNAMODB
namespace Orleans.Persistence.DynamoDB
#elif REMINDERS_DYNAMODB
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/AdoNetRemindersProviderBuilder.cs b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/AdoNetRemindersProviderBuilder.cs
new file mode 100644
index 0000000000..104dfb577c
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/AdoNetRemindersProviderBuilder.cs
@@ -0,0 +1,39 @@
+using System;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Orleans;
+using Orleans.Hosting;
+using Orleans.Providers;
+using Orleans.AdvancedReminders.AdoNet;
+
+[assembly: RegisterProvider("AdoNet", "AdvancedReminders", "Silo", typeof(AdvancedAdoNetRemindersProviderBuilder))]
+
+namespace Orleans.Hosting;
+
+internal sealed class AdvancedAdoNetRemindersProviderBuilder : IProviderBuilder
+{
+ public void Configure(ISiloBuilder builder, string name, IConfigurationSection configurationSection)
+ {
+ builder.UseAdoNetAdvancedReminderService((OptionsBuilder optionsBuilder) => optionsBuilder.Configure((options, services) =>
+ {
+ var invariant = configurationSection[nameof(options.Invariant)];
+ if (!string.IsNullOrEmpty(invariant))
+ {
+ options.Invariant = invariant;
+ }
+
+ var connectionString = configurationSection[nameof(options.ConnectionString)];
+ var connectionName = configurationSection["ConnectionName"];
+ if (string.IsNullOrEmpty(connectionString) && !string.IsNullOrEmpty(connectionName))
+ {
+ connectionString = services.GetRequiredService().GetConnectionString(connectionName);
+ }
+
+ if (!string.IsNullOrEmpty(connectionString))
+ {
+ options.ConnectionString = connectionString;
+ }
+ }));
+ }
+}
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/GlobalUsings.cs b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/GlobalUsings.cs
new file mode 100644
index 0000000000..d59a9bde38
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/GlobalUsings.cs
@@ -0,0 +1 @@
+global using Orleans.AdvancedReminders.Runtime;
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/MySQL-Reminders-10.0.0.sql b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/MySQL-Reminders-10.0.0.sql
new file mode 100644
index 0000000000..36f98241a1
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/MySQL-Reminders-10.0.0.sql
@@ -0,0 +1,150 @@
+-- Run this migration for upgrading MySQL reminder tables created before 10.0.0.
+
+ALTER TABLE OrleansAdvancedRemindersTable
+ ADD COLUMN CronExpression NVARCHAR(200) NULL,
+ ADD COLUMN CronTimeZoneId NVARCHAR(200) NULL,
+ ADD COLUMN NextDueUtc DATETIME NULL,
+ ADD COLUMN LastFireUtc DATETIME NULL,
+ ADD COLUMN Priority TINYINT NOT NULL DEFAULT 0,
+ ADD COLUMN Action TINYINT NOT NULL DEFAULT 0;
+
+CREATE INDEX IX_RemindersTable_NextDueUtc_Priority ON OrleansAdvancedRemindersTable(ServiceId, NextDueUtc, Priority);
+
+UPDATE OrleansQuery
+SET QueryText = '
+ INSERT INTO OrleansAdvancedRemindersTable
+ (
+ ServiceId,
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ GrainHash,
+ Version
+ )
+ VALUES
+ (
+ @ServiceId,
+ @GrainId,
+ @ReminderName,
+ @StartTime,
+ @Period,
+ @CronExpression,
+ @CronTimeZoneId,
+ @NextDueUtc,
+ @LastFireUtc,
+ @Priority,
+ @Action,
+ @GrainHash,
+ last_insert_id(0)
+ )
+ ON DUPLICATE KEY
+ UPDATE
+ StartTime = @StartTime,
+ Period = @Period,
+ CronExpression = @CronExpression,
+ CronTimeZoneId = @CronTimeZoneId,
+ NextDueUtc = @NextDueUtc,
+ LastFireUtc = @LastFireUtc,
+ Priority = @Priority,
+ Action = @Action,
+ GrainHash = @GrainHash,
+ Version = last_insert_id(Version+1);
+
+
+ SELECT last_insert_id() AS Version;
+'
+WHERE QueryKey = 'UpsertReminderRowKey';
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL;
+'
+WHERE QueryKey = 'ReadReminderRowsKey';
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL;
+'
+WHERE QueryKey = 'ReadReminderRowKey';
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainHash > @BeginHash AND @BeginHash IS NOT NULL
+ AND GrainHash <= @EndHash AND @EndHash IS NOT NULL;
+'
+WHERE QueryKey = 'ReadRangeRows1Key';
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND ((GrainHash > @BeginHash AND @BeginHash IS NOT NULL)
+ OR (GrainHash <= @EndHash AND @EndHash IS NOT NULL));
+'
+WHERE QueryKey = 'ReadRangeRows2Key';
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/Oracle-Reminders-10.0.0.sql b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/Oracle-Reminders-10.0.0.sql
new file mode 100644
index 0000000000..19ef4c1763
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/Oracle-Reminders-10.0.0.sql
@@ -0,0 +1,126 @@
+-- Run this migration for upgrading Oracle reminder tables created before 10.0.0.
+
+ALTER TABLE OrleansAdvancedRemindersTable ADD (CronExpression NVARCHAR2(200) NULL);
+/
+ALTER TABLE OrleansAdvancedRemindersTable ADD (CronTimeZoneId NVARCHAR2(200) NULL);
+/
+ALTER TABLE OrleansAdvancedRemindersTable ADD (NextDueUtc TIMESTAMP(6) NULL);
+/
+ALTER TABLE OrleansAdvancedRemindersTable ADD (LastFireUtc TIMESTAMP(6) NULL);
+/
+ALTER TABLE OrleansAdvancedRemindersTable ADD (Priority NUMBER(3,0) DEFAULT 0 NOT NULL);
+/
+ALTER TABLE OrleansAdvancedRemindersTable ADD (Action NUMBER(3,0) DEFAULT 0 NOT NULL);
+/
+
+CREATE INDEX IX_REMINDERS_NEXTDUE_PRIORITY ON OrleansAdvancedRemindersTable(SERVICEID, NEXTDUEUTC, PRIORITY);
+/
+
+CREATE OR REPLACE FUNCTION UpsertReminderRow(PARAM_SERVICEID IN NVARCHAR2, PARAM_GRAINHASH IN INT, PARAM_GRAINID IN VARCHAR2, PARAM_REMINDERNAME IN NVARCHAR2,
+ PARAM_STARTTIME IN TIMESTAMP, PARAM_PERIOD IN NUMBER, PARAM_CRONEXPRESSION IN NVARCHAR2,
+ PARAM_CRONTIMEZONEID IN NVARCHAR2, PARAM_NEXTDUEUTC IN TIMESTAMP, PARAM_LASTFIREUTC IN TIMESTAMP, PARAM_PRIORITY IN NUMBER, PARAM_ACTION IN NUMBER)
+RETURN NUMBER IS
+ rowcount NUMBER;
+ currentVersion NUMBER := 0;
+ PRAGMA AUTONOMOUS_TRANSACTION;
+ BEGIN
+ MERGE INTO OrleansAdvancedRemindersTable ort
+ USING (
+ SELECT PARAM_SERVICEID as SERVICEID,
+ PARAM_GRAINID as GRAINID,
+ PARAM_REMINDERNAME as REMINDERNAME,
+ PARAM_STARTTIME as STARTTIME,
+ PARAM_PERIOD as PERIOD,
+ PARAM_CRONEXPRESSION as CRONEXPRESSION,
+ PARAM_CRONTIMEZONEID as CRONTIMEZONEID,
+ PARAM_NEXTDUEUTC as NEXTDUEUTC,
+ PARAM_LASTFIREUTC as LASTFIREUTC,
+ PARAM_PRIORITY as PRIORITY,
+ PARAM_ACTION as ACTION,
+ PARAM_GRAINHASH GRAINHASH
+ FROM dual
+ ) n_ort
+ ON (ort.ServiceId = n_ort.SERVICEID AND
+ ort.GrainId = n_ort.GRAINID AND
+ ort.ReminderName = n_ort.REMINDERNAME
+ )
+ WHEN MATCHED THEN
+ UPDATE SET
+ ort.StartTime = n_ort.STARTTIME,
+ ort.Period = n_ort.PERIOD,
+ ort.CronExpression = n_ort.CRONEXPRESSION,
+ ort.CronTimeZoneId = n_ort.CRONTIMEZONEID,
+ ort.NextDueUtc = n_ort.NEXTDUEUTC,
+ ort.LastFireUtc = n_ort.LASTFIREUTC,
+ ort.Priority = n_ort.PRIORITY,
+ ort.Action = n_ort.ACTION,
+ ort.GrainHash = n_ort.GRAINHASH,
+ ort.Version = ort.Version+1
+ WHEN NOT MATCHED THEN
+ INSERT (ort.ServiceId, ort.GrainId, ort.ReminderName, ort.StartTime, ort.Period, ort.CronExpression, ort.CronTimeZoneId, ort.NextDueUtc, ort.LastFireUtc, ort.Priority, ort.Action, ort.GrainHash, ort.Version)
+ VALUES (n_ort.SERVICEID, n_ort.GRAINID, n_ort.REMINDERNAME, n_ort.STARTTIME, n_ort.PERIOD, n_ort.CRONEXPRESSION, n_ort.CRONTIMEZONEID, n_ort.NEXTDUEUTC, n_ort.LASTFIREUTC, n_ort.PRIORITY, n_ort.ACTION, n_ort.GRAINHASH, 0);
+
+ SELECT Version INTO currentVersion FROM OrleansAdvancedRemindersTable
+ WHERE ServiceId = PARAM_SERVICEID AND PARAM_SERVICEID IS NOT NULL
+ AND GrainId = PARAM_GRAINID AND PARAM_GRAINID IS NOT NULL
+ AND ReminderName = PARAM_REMINDERNAME AND PARAM_REMINDERNAME IS NOT NULL;
+ COMMIT;
+ RETURN(currentVersion);
+ END;
+/
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT UpsertReminderRow(:ServiceId, :GrainHash, :GrainId, :ReminderName, :StartTime, :Period, :CronExpression, :CronTimeZoneId, :NextDueUtc, :LastFireUtc, :Priority, :Action) AS Version FROM DUAL
+'
+WHERE QueryKey = 'UpsertReminderRowKey';
+/
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT GrainId, ReminderName, StartTime, Period, CronExpression, CronTimeZoneId, NextDueUtc, LastFireUtc, Priority, Action, Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = :ServiceId AND :ServiceId IS NOT NULL
+ AND GrainId = :GrainId AND :GrainId IS NOT NULL
+'
+WHERE QueryKey = 'ReadReminderRowsKey';
+/
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT GrainId, ReminderName, StartTime, Period, CronExpression, CronTimeZoneId, NextDueUtc, LastFireUtc, Priority, Action, Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = :ServiceId AND :ServiceId IS NOT NULL
+ AND GrainId = :GrainId AND :GrainId IS NOT NULL
+ AND ReminderName = :ReminderName AND :ReminderName IS NOT NULL
+'
+WHERE QueryKey = 'ReadReminderRowKey';
+/
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT GrainId, ReminderName, StartTime, Period, CronExpression, CronTimeZoneId, NextDueUtc, LastFireUtc, Priority, Action, Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = :ServiceId AND :ServiceId IS NOT NULL
+ AND GrainHash > :BeginHash AND :BeginHash IS NOT NULL
+ AND GrainHash <= :EndHash AND :EndHash IS NOT NULL
+'
+WHERE QueryKey = 'ReadRangeRows1Key';
+/
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT GrainId, ReminderName, StartTime, Period, CronExpression, CronTimeZoneId, NextDueUtc, LastFireUtc, Priority, Action, Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = :ServiceId AND :ServiceId IS NOT NULL
+ AND ((GrainHash > :BeginHash AND :BeginHash IS NOT NULL)
+ OR (GrainHash <= :EndHash AND :EndHash IS NOT NULL))
+'
+WHERE QueryKey = 'ReadRangeRows2Key';
+/
+
+COMMIT;
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/PostgreSQL-Reminders-10.0.0.sql b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/PostgreSQL-Reminders-10.0.0.sql
new file mode 100644
index 0000000000..51caf30658
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/PostgreSQL-Reminders-10.0.0.sql
@@ -0,0 +1,188 @@
+-- Run this migration for upgrading PostgreSQL reminder tables created before 10.0.0.
+
+ALTER TABLE OrleansAdvancedRemindersTable
+ ADD COLUMN CronExpression varchar(200),
+ ADD COLUMN CronTimeZoneId varchar(200),
+ ADD COLUMN NextDueUtc timestamptz(3),
+ ADD COLUMN LastFireUtc timestamptz(3),
+ ADD COLUMN Priority smallint NOT NULL DEFAULT 0,
+ ADD COLUMN Action smallint NOT NULL DEFAULT 0;
+
+CREATE INDEX IX_RemindersTable_NextDueUtc_Priority
+ ON OrleansAdvancedRemindersTable(ServiceId, NextDueUtc, Priority);
+
+CREATE OR REPLACE FUNCTION upsert_reminder_row(
+ ServiceIdArg OrleansAdvancedRemindersTable.ServiceId%TYPE,
+ GrainIdArg OrleansAdvancedRemindersTable.GrainId%TYPE,
+ ReminderNameArg OrleansAdvancedRemindersTable.ReminderName%TYPE,
+ StartTimeArg OrleansAdvancedRemindersTable.StartTime%TYPE,
+ PeriodArg OrleansAdvancedRemindersTable.Period%TYPE,
+ CronExpressionArg OrleansAdvancedRemindersTable.CronExpression%TYPE,
+ CronTimeZoneIdArg OrleansAdvancedRemindersTable.CronTimeZoneId%TYPE,
+ NextDueUtcArg OrleansAdvancedRemindersTable.NextDueUtc%TYPE,
+ LastFireUtcArg OrleansAdvancedRemindersTable.LastFireUtc%TYPE,
+ PriorityArg OrleansAdvancedRemindersTable.Priority%TYPE,
+ ActionArg OrleansAdvancedRemindersTable.Action%TYPE,
+ GrainHashArg OrleansAdvancedRemindersTable.GrainHash%TYPE
+ )
+ RETURNS TABLE(version integer) AS
+$func$
+DECLARE
+ VersionVar int := 0;
+BEGIN
+
+ INSERT INTO OrleansAdvancedRemindersTable
+ (
+ ServiceId,
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ GrainHash,
+ Version
+ )
+ SELECT
+ ServiceIdArg,
+ GrainIdArg,
+ ReminderNameArg,
+ StartTimeArg,
+ PeriodArg,
+ CronExpressionArg,
+ CronTimeZoneIdArg,
+ NextDueUtcArg,
+ LastFireUtcArg,
+ PriorityArg,
+ ActionArg,
+ GrainHashArg,
+ 0
+ ON CONFLICT (ServiceId, GrainId, ReminderName)
+ DO UPDATE SET
+ StartTime = excluded.StartTime,
+ Period = excluded.Period,
+ CronExpression = excluded.CronExpression,
+ CronTimeZoneId = excluded.CronTimeZoneId,
+ NextDueUtc = excluded.NextDueUtc,
+ LastFireUtc = excluded.LastFireUtc,
+ Priority = excluded.Priority,
+ Action = excluded.Action,
+ GrainHash = excluded.GrainHash,
+ Version = OrleansAdvancedRemindersTable.Version + 1
+ RETURNING
+ OrleansAdvancedRemindersTable.Version INTO STRICT VersionVar;
+
+ RETURN QUERY SELECT VersionVar AS version;
+
+END
+$func$ LANGUAGE plpgsql;
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT * FROM upsert_reminder_row(
+ @ServiceId,
+ @GrainId,
+ @ReminderName,
+ @StartTime,
+ @Period::bigint,
+ @CronExpression,
+ @CronTimeZoneId,
+ @NextDueUtc,
+ @LastFireUtc,
+ @Priority::smallint,
+ @Action::smallint,
+ @GrainHash
+ );
+'
+WHERE QueryKey = 'UpsertReminderRowKey';
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL;
+'
+WHERE QueryKey = 'ReadReminderRowsKey';
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL;
+'
+WHERE QueryKey = 'ReadReminderRowKey';
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainHash > @BeginHash AND @BeginHash IS NOT NULL
+ AND GrainHash <= @EndHash AND @EndHash IS NOT NULL;
+'
+WHERE QueryKey = 'ReadRangeRows1Key';
+
+UPDATE OrleansQuery
+SET QueryText = '
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND ((GrainHash > @BeginHash AND @BeginHash IS NOT NULL)
+ OR (GrainHash <= @EndHash AND @EndHash IS NOT NULL));
+'
+WHERE QueryKey = 'ReadRangeRows2Key';
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/PostgreSQL-Reminders-3.6.0.sql b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/PostgreSQL-Reminders-3.6.0.sql
new file mode 100644
index 0000000000..176f2da4b6
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/PostgreSQL-Reminders-3.6.0.sql
@@ -0,0 +1,58 @@
+-- Run this migration for upgrading the PostgreSQL reminder table and routines for deployments created before 3.6.0
+
+BEGIN;
+
+-- Change date type
+
+ALTER TABLE OrleansAdvancedRemindersTable
+ALTER COLUMN StartTime TYPE TIMESTAMPTZ(3) USING StartTime AT TIME ZONE 'UTC';
+
+-- Recreate routines
+
+CREATE OR REPLACE FUNCTION upsert_reminder_row(
+ ServiceIdArg OrleansAdvancedRemindersTable.ServiceId%TYPE,
+ GrainIdArg OrleansAdvancedRemindersTable.GrainId%TYPE,
+ ReminderNameArg OrleansAdvancedRemindersTable.ReminderName%TYPE,
+ StartTimeArg OrleansAdvancedRemindersTable.StartTime%TYPE,
+ PeriodArg OrleansAdvancedRemindersTable.Period%TYPE,
+ GrainHashArg OrleansAdvancedRemindersTable.GrainHash%TYPE
+ )
+ RETURNS TABLE(version integer) AS
+$func$
+DECLARE
+ VersionVar int := 0;
+BEGIN
+
+ INSERT INTO OrleansAdvancedRemindersTable
+ (
+ ServiceId,
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ GrainHash,
+ Version
+ )
+ SELECT
+ ServiceIdArg,
+ GrainIdArg,
+ ReminderNameArg,
+ StartTimeArg,
+ PeriodArg,
+ GrainHashArg,
+ 0
+ ON CONFLICT (ServiceId, GrainId, ReminderName)
+ DO UPDATE SET
+ StartTime = excluded.StartTime,
+ Period = excluded.Period,
+ GrainHash = excluded.GrainHash,
+ Version = OrleansAdvancedRemindersTable.Version + 1
+ RETURNING
+ OrleansAdvancedRemindersTable.Version INTO STRICT VersionVar;
+
+ RETURN QUERY SELECT VersionVar AS versionr;
+
+END
+$func$ LANGUAGE plpgsql;
+
+COMMIT;
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/SQLServer-Reminders-10.0.0.sql b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/SQLServer-Reminders-10.0.0.sql
new file mode 100644
index 0000000000..e9ba84614f
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Migrations/SQLServer-Reminders-10.0.0.sql
@@ -0,0 +1,152 @@
+-- Run this migration for upgrading SQL Server reminder tables created before 10.0.0.
+
+ALTER TABLE OrleansAdvancedRemindersTable ADD CronExpression NVARCHAR(200) NULL;
+ALTER TABLE OrleansAdvancedRemindersTable ADD CronTimeZoneId NVARCHAR(200) NULL;
+ALTER TABLE OrleansAdvancedRemindersTable ADD NextDueUtc DATETIME2(3) NULL;
+ALTER TABLE OrleansAdvancedRemindersTable ADD LastFireUtc DATETIME2(3) NULL;
+ALTER TABLE OrleansAdvancedRemindersTable ADD Priority TINYINT NOT NULL CONSTRAINT DF_OrleansAdvancedRemindersTable_Priority DEFAULT (0);
+ALTER TABLE OrleansAdvancedRemindersTable ADD Action TINYINT NOT NULL CONSTRAINT DF_OrleansAdvancedRemindersTable_Action DEFAULT (0);
+
+CREATE INDEX IX_RemindersTable_NextDueUtc_Priority
+ON OrleansAdvancedRemindersTable(ServiceId, NextDueUtc, Priority);
+
+UPDATE OrleansQuery
+SET QueryText = 'DECLARE @Version AS INT = 0;
+ SET XACT_ABORT, NOCOUNT ON;
+ BEGIN TRANSACTION;
+ UPDATE OrleansAdvancedRemindersTable WITH(UPDLOCK, ROWLOCK, HOLDLOCK)
+ SET
+ StartTime = @StartTime,
+ Period = @Period,
+ CronExpression = @CronExpression,
+ CronTimeZoneId = @CronTimeZoneId,
+ NextDueUtc = @NextDueUtc,
+ LastFireUtc = @LastFireUtc,
+ Priority = @Priority,
+ Action = @Action,
+ GrainHash = @GrainHash,
+ @Version = Version = Version + 1
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL;
+
+ INSERT INTO OrleansAdvancedRemindersTable
+ (
+ ServiceId,
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ GrainHash,
+ Version
+ )
+ SELECT
+ @ServiceId,
+ @GrainId,
+ @ReminderName,
+ @StartTime,
+ @Period,
+ @CronExpression,
+ @CronTimeZoneId,
+ @NextDueUtc,
+ @LastFireUtc,
+ @Priority,
+ @Action,
+ @GrainHash,
+ 0
+ WHERE
+ @@ROWCOUNT=0;
+ SELECT @Version AS Version;
+ COMMIT TRANSACTION;
+ '
+WHERE QueryKey = 'UpsertReminderRowKey';
+
+UPDATE OrleansQuery
+SET QueryText = 'SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL;
+ '
+WHERE QueryKey = 'ReadReminderRowsKey';
+
+UPDATE OrleansQuery
+SET QueryText = 'SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL;
+ '
+WHERE QueryKey = 'ReadReminderRowKey';
+
+UPDATE OrleansQuery
+SET QueryText = 'SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainHash > @BeginHash AND @BeginHash IS NOT NULL
+ AND GrainHash <= @EndHash AND @EndHash IS NOT NULL;
+ '
+WHERE QueryKey = 'ReadRangeRows1Key';
+
+UPDATE OrleansQuery
+SET QueryText = 'SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND ((GrainHash > @BeginHash AND @BeginHash IS NOT NULL)
+ OR (GrainHash <= @EndHash AND @EndHash IS NOT NULL));
+ '
+WHERE QueryKey = 'ReadRangeRows2Key';
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/MySQL-Reminders.sql b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/MySQL-Reminders.sql
new file mode 100644
index 0000000000..12ea9841a4
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/MySQL-Reminders.sql
@@ -0,0 +1,188 @@
+-- Orleans Reminders table - https://learn.microsoft.com/dotnet/orleans/grains/timers-and-reminders
+CREATE TABLE OrleansAdvancedRemindersTable
+(
+ ServiceId NVARCHAR(150) NOT NULL,
+ GrainId VARCHAR(150) NOT NULL,
+ ReminderName NVARCHAR(150) NOT NULL,
+ StartTime DATETIME NOT NULL,
+ Period BIGINT NOT NULL,
+ CronExpression NVARCHAR(200) NULL,
+ CronTimeZoneId NVARCHAR(200) NULL,
+ NextDueUtc DATETIME NULL,
+ LastFireUtc DATETIME NULL,
+ Priority TINYINT NOT NULL DEFAULT 0,
+ Action TINYINT NOT NULL DEFAULT 0,
+ GrainHash INT NOT NULL,
+ Version INT NOT NULL,
+
+ CONSTRAINT PK_RemindersTable_ServiceId_GrainId_ReminderName PRIMARY KEY(ServiceId, GrainId, ReminderName)
+);
+
+CREATE INDEX IX_RemindersTable_NextDueUtc_Priority
+ON OrleansAdvancedRemindersTable(ServiceId, NextDueUtc, Priority);
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'UpsertReminderRowKey','
+ INSERT INTO OrleansAdvancedRemindersTable
+ (
+ ServiceId,
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ GrainHash,
+ Version
+ )
+ VALUES
+ (
+ @ServiceId,
+ @GrainId,
+ @ReminderName,
+ @StartTime,
+ @Period,
+ @CronExpression,
+ @CronTimeZoneId,
+ @NextDueUtc,
+ @LastFireUtc,
+ @Priority,
+ @Action,
+ @GrainHash,
+ last_insert_id(0)
+ )
+ ON DUPLICATE KEY
+ UPDATE
+ StartTime = @StartTime,
+ Period = @Period,
+ CronExpression = @CronExpression,
+ CronTimeZoneId = @CronTimeZoneId,
+ NextDueUtc = @NextDueUtc,
+ LastFireUtc = @LastFireUtc,
+ Priority = @Priority,
+ Action = @Action,
+ GrainHash = @GrainHash,
+ Version = last_insert_id(Version+1);
+
+
+ SELECT last_insert_id() AS Version;
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadReminderRowsKey','
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL;
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadReminderRowKey','
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL;
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadRangeRows1Key','
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainHash > @BeginHash AND @BeginHash IS NOT NULL
+ AND GrainHash <= @EndHash AND @EndHash IS NOT NULL;
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadRangeRows2Key','
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND ((GrainHash > @BeginHash AND @BeginHash IS NOT NULL)
+ OR (GrainHash <= @EndHash AND @EndHash IS NOT NULL));
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'DeleteReminderRowKey','
+ DELETE FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL
+ AND Version = @Version AND @Version IS NOT NULL;
+ SELECT ROW_COUNT();
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'DeleteReminderRowsKey','
+ DELETE FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL;
+');
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Oracle-Reminders.sql b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Oracle-Reminders.sql
new file mode 100644
index 0000000000..662a195cd3
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Oracle-Reminders.sql
@@ -0,0 +1,172 @@
+-- Orleans Reminders table - https://learn.microsoft.com/dotnet/orleans/grains/timers-and-reminders
+CREATE TABLE "ORLEANSREMINDERSTABLE"
+(
+ "SERVICEID" NVARCHAR2(150) NOT NULL ENABLE,
+ "GRAINID" VARCHAR2(150) NOT NULL,
+ "REMINDERNAME" NVARCHAR2(150) NOT NULL,
+ "STARTTIME" TIMESTAMP(6) NOT NULL ENABLE,
+ "PERIOD" NUMBER(19,0) NULL,
+ "CRONEXPRESSION" NVARCHAR2(200) NULL,
+ "CRONTIMEZONEID" NVARCHAR2(200) NULL,
+ "NEXTDUEUTC" TIMESTAMP(6) NULL,
+ "LASTFIREUTC" TIMESTAMP(6) NULL,
+ "PRIORITY" NUMBER(3,0) DEFAULT 0 NOT NULL,
+ "ACTION" NUMBER(3,0) DEFAULT 0 NOT NULL,
+ "GRAINHASH" INT NOT NULL,
+ "VERSION" INT NOT NULL,
+
+ CONSTRAINT PK_REMINDERSTABLE PRIMARY KEY(SERVICEID, GRAINID, REMINDERNAME)
+);
+/
+CREATE INDEX IX_REMINDERS_NEXTDUE_PRIORITY ON OrleansAdvancedRemindersTable(SERVICEID, NEXTDUEUTC, PRIORITY);
+/
+
+CREATE OR REPLACE FUNCTION UpsertReminderRow(PARAM_SERVICEID IN NVARCHAR2, PARAM_GRAINHASH IN INT, PARAM_GRAINID IN VARCHAR2, PARAM_REMINDERNAME IN NVARCHAR2,
+ PARAM_STARTTIME IN TIMESTAMP, PARAM_PERIOD IN NUMBER, PARAM_CRONEXPRESSION IN NVARCHAR2,
+ PARAM_CRONTIMEZONEID IN NVARCHAR2, PARAM_NEXTDUEUTC IN TIMESTAMP, PARAM_LASTFIREUTC IN TIMESTAMP, PARAM_PRIORITY IN NUMBER, PARAM_ACTION IN NUMBER)
+RETURN NUMBER IS
+ rowcount NUMBER;
+ currentVersion NUMBER := 0;
+ PRAGMA AUTONOMOUS_TRANSACTION;
+ BEGIN
+ MERGE INTO OrleansAdvancedRemindersTable ort
+ USING (
+ SELECT PARAM_SERVICEID as SERVICEID,
+ PARAM_GRAINID as GRAINID,
+ PARAM_REMINDERNAME as REMINDERNAME,
+ PARAM_STARTTIME as STARTTIME,
+ PARAM_PERIOD as PERIOD,
+ PARAM_CRONEXPRESSION as CRONEXPRESSION,
+ PARAM_CRONTIMEZONEID as CRONTIMEZONEID,
+ PARAM_NEXTDUEUTC as NEXTDUEUTC,
+ PARAM_LASTFIREUTC as LASTFIREUTC,
+ PARAM_PRIORITY as PRIORITY,
+ PARAM_ACTION as ACTION,
+ PARAM_GRAINHASH GRAINHASH
+ FROM dual
+ ) n_ort
+ ON (ort.ServiceId = n_ort.SERVICEID AND
+ ort.GrainId = n_ort.GRAINID AND
+ ort.ReminderName = n_ort.REMINDERNAME
+ )
+ WHEN MATCHED THEN
+ UPDATE SET
+ ort.StartTime = n_ort.STARTTIME,
+ ort.Period = n_ort.PERIOD,
+ ort.CronExpression = n_ort.CRONEXPRESSION,
+ ort.CronTimeZoneId = n_ort.CRONTIMEZONEID,
+ ort.NextDueUtc = n_ort.NEXTDUEUTC,
+ ort.LastFireUtc = n_ort.LASTFIREUTC,
+ ort.Priority = n_ort.PRIORITY,
+ ort.Action = n_ort.ACTION,
+ ort.GrainHash = n_ort.GRAINHASH,
+ ort.Version = ort.Version+1
+ WHEN NOT MATCHED THEN
+ INSERT (ort.ServiceId, ort.GrainId, ort.ReminderName, ort.StartTime, ort.Period, ort.CronExpression, ort.CronTimeZoneId, ort.NextDueUtc, ort.LastFireUtc, ort.Priority, ort.Action, ort.GrainHash, ort.Version)
+ VALUES (n_ort.SERVICEID, n_ort.GRAINID, n_ort.REMINDERNAME, n_ort.STARTTIME, n_ort.PERIOD, n_ort.CRONEXPRESSION, n_ort.CRONTIMEZONEID, n_ort.NEXTDUEUTC, n_ort.LASTFIREUTC, n_ort.PRIORITY, n_ort.ACTION, n_ort.GRAINHASH, 0);
+
+ SELECT Version INTO currentVersion FROM OrleansAdvancedRemindersTable
+ WHERE ServiceId = PARAM_SERVICEID AND PARAM_SERVICEID IS NOT NULL
+ AND GrainId = PARAM_GRAINID AND PARAM_GRAINID IS NOT NULL
+ AND ReminderName = PARAM_REMINDERNAME AND PARAM_REMINDERNAME IS NOT NULL;
+ COMMIT;
+ RETURN(currentVersion);
+ END;
+/
+
+CREATE OR REPLACE FUNCTION DeleteReminderRow(PARAM_SERVICEID IN NVARCHAR2, PARAM_GRAINID IN VARCHAR2, PARAM_REMINDERNAME IN NVARCHAR2,
+ PARAM_VERSION IN NUMBER)
+RETURN NUMBER IS
+ rowcount NUMBER;
+ PRAGMA AUTONOMOUS_TRANSACTION;
+ BEGIN
+ DELETE FROM OrleansAdvancedRemindersTable
+ WHERE ServiceId = PARAM_SERVICEID AND PARAM_SERVICEID IS NOT NULL
+ AND GrainId = PARAM_GRAINID AND PARAM_GRAINID IS NOT NULL
+ AND ReminderName = PARAM_REMINDERNAME AND PARAM_REMINDERNAME IS NOT NULL
+ AND Version = PARAM_VERSION AND PARAM_VERSION IS NOT NULL;
+
+ rowcount := SQL%ROWCOUNT;
+
+ COMMIT;
+ RETURN(rowcount);
+ END;
+/
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'UpsertReminderRowKey','
+ SELECT UpsertReminderRow(:ServiceId, :GrainHash, :GrainId, :ReminderName, :StartTime, :Period, :CronExpression, :CronTimeZoneId, :NextDueUtc, :LastFireUtc, :Priority, :Action) AS Version FROM DUAL
+');
+/
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadReminderRowsKey','
+ SELECT GrainId, ReminderName, StartTime, Period, CronExpression, CronTimeZoneId, NextDueUtc, LastFireUtc, Priority, Action, Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = :ServiceId AND :ServiceId IS NOT NULL
+ AND GrainId = :GrainId AND :GrainId IS NOT NULL
+');
+/
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadReminderRowKey','
+ SELECT GrainId, ReminderName, StartTime, Period, CronExpression, CronTimeZoneId, NextDueUtc, LastFireUtc, Priority, Action, Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = :ServiceId AND :ServiceId IS NOT NULL
+ AND GrainId = :GrainId AND :GrainId IS NOT NULL
+ AND ReminderName = :ReminderName AND :ReminderName IS NOT NULL
+');
+/
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadRangeRows1Key','
+ SELECT GrainId, ReminderName, StartTime, Period, CronExpression, CronTimeZoneId, NextDueUtc, LastFireUtc, Priority, Action, Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = :ServiceId AND :ServiceId IS NOT NULL
+ AND GrainHash > :BeginHash AND :BeginHash IS NOT NULL
+ AND GrainHash <= :EndHash AND :EndHash IS NOT NULL
+');
+/
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadRangeRows2Key','
+ SELECT GrainId, ReminderName, StartTime, Period, CronExpression, CronTimeZoneId, NextDueUtc, LastFireUtc, Priority, Action, Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = :ServiceId AND :ServiceId IS NOT NULL
+ AND ((GrainHash > :BeginHash AND :BeginHash IS NOT NULL)
+ OR (GrainHash <= :EndHash AND :EndHash IS NOT NULL))
+');
+/
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'DeleteReminderRowKey','
+ SELECT DeleteReminderRow(:ServiceId, :GrainId, :ReminderName, :Version) AS RESULT FROM DUAL
+');
+/
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'DeleteReminderRowsKey','
+ DELETE FROM OrleansAdvancedRemindersTable
+ WHERE ServiceId = :ServiceId AND :ServiceId IS NOT NULL
+');
+/
+
+COMMIT;
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Orleans.AdvancedReminders.AdoNet.csproj b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Orleans.AdvancedReminders.AdoNet.csproj
new file mode 100644
index 0000000000..18f7f74a9a
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/Orleans.AdvancedReminders.AdoNet.csproj
@@ -0,0 +1,34 @@
+
+
+ README.md
+ Microsoft.Orleans.AdvancedReminders.AdoNet
+ Microsoft Orleans Advanced Reminders for ADO.NET
+ ADO.NET provider for Microsoft Orleans Advanced Reminders.
+ $(PackageTags) ADO.NET SQL MySQL PostgreSQL Oracle
+ $(DefaultTargetFrameworks)
+ true
+
+
+
+ Orleans.AdvancedReminders.AdoNet
+ Orleans.AdvancedReminders.AdoNet
+ $(DefineConstants);REMINDERS_ADONET;ADVANCED_REMINDERS_ADONET
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/PostgreSQL-Reminders.sql b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/PostgreSQL-Reminders.sql
new file mode 100644
index 0000000000..15e2c6d76c
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/PostgreSQL-Reminders.sql
@@ -0,0 +1,251 @@
+-- Orleans Reminders table - https://learn.microsoft.com/dotnet/orleans/grains/timers-and-reminders
+CREATE TABLE OrleansAdvancedRemindersTable
+(
+ ServiceId varchar(150) NOT NULL,
+ GrainId varchar(150) NOT NULL,
+ ReminderName varchar(150) NOT NULL,
+ StartTime timestamptz(3) NOT NULL,
+ Period bigint NOT NULL,
+ CronExpression varchar(200) NULL,
+ CronTimeZoneId varchar(200) NULL,
+ NextDueUtc timestamptz(3) NULL,
+ LastFireUtc timestamptz(3) NULL,
+ Priority smallint NOT NULL DEFAULT 0,
+ Action smallint NOT NULL DEFAULT 0,
+ GrainHash integer NOT NULL,
+ Version integer NOT NULL,
+
+ CONSTRAINT PK_RemindersTable_ServiceId_GrainId_ReminderName PRIMARY KEY(ServiceId, GrainId, ReminderName)
+);
+
+CREATE INDEX IX_RemindersTable_NextDueUtc_Priority
+ ON OrleansAdvancedRemindersTable(ServiceId, NextDueUtc, Priority);
+
+CREATE FUNCTION upsert_reminder_row(
+ ServiceIdArg OrleansAdvancedRemindersTable.ServiceId%TYPE,
+ GrainIdArg OrleansAdvancedRemindersTable.GrainId%TYPE,
+ ReminderNameArg OrleansAdvancedRemindersTable.ReminderName%TYPE,
+ StartTimeArg OrleansAdvancedRemindersTable.StartTime%TYPE,
+ PeriodArg OrleansAdvancedRemindersTable.Period%TYPE,
+ CronExpressionArg OrleansAdvancedRemindersTable.CronExpression%TYPE,
+ CronTimeZoneIdArg OrleansAdvancedRemindersTable.CronTimeZoneId%TYPE,
+ NextDueUtcArg OrleansAdvancedRemindersTable.NextDueUtc%TYPE,
+ LastFireUtcArg OrleansAdvancedRemindersTable.LastFireUtc%TYPE,
+ PriorityArg OrleansAdvancedRemindersTable.Priority%TYPE,
+ ActionArg OrleansAdvancedRemindersTable.Action%TYPE,
+ GrainHashArg OrleansAdvancedRemindersTable.GrainHash%TYPE
+ )
+ RETURNS TABLE(version integer) AS
+$func$
+DECLARE
+ VersionVar int := 0;
+BEGIN
+
+ INSERT INTO OrleansAdvancedRemindersTable
+ (
+ ServiceId,
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ GrainHash,
+ Version
+ )
+ SELECT
+ ServiceIdArg,
+ GrainIdArg,
+ ReminderNameArg,
+ StartTimeArg,
+ PeriodArg,
+ CronExpressionArg,
+ CronTimeZoneIdArg,
+ NextDueUtcArg,
+ LastFireUtcArg,
+ PriorityArg,
+ ActionArg,
+ GrainHashArg,
+ 0
+ ON CONFLICT (ServiceId, GrainId, ReminderName)
+ DO UPDATE SET
+ StartTime = excluded.StartTime,
+ Period = excluded.Period,
+ CronExpression = excluded.CronExpression,
+ CronTimeZoneId = excluded.CronTimeZoneId,
+ NextDueUtc = excluded.NextDueUtc,
+ LastFireUtc = excluded.LastFireUtc,
+ Priority = excluded.Priority,
+ Action = excluded.Action,
+ GrainHash = excluded.GrainHash,
+ Version = OrleansAdvancedRemindersTable.Version + 1
+ RETURNING
+ OrleansAdvancedRemindersTable.Version INTO STRICT VersionVar;
+
+ RETURN QUERY SELECT VersionVar AS version;
+
+END
+$func$ LANGUAGE plpgsql;
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'UpsertReminderRowKey','
+ SELECT * FROM upsert_reminder_row(
+ @ServiceId,
+ @GrainId,
+ @ReminderName,
+ @StartTime,
+ @Period::bigint,
+ @CronExpression,
+ @CronTimeZoneId,
+ @NextDueUtc,
+ @LastFireUtc,
+ @Priority::smallint,
+ @Action::smallint,
+ @GrainHash
+ );
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadReminderRowsKey','
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL;
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadReminderRowKey','
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL;
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadRangeRows1Key','
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainHash > @BeginHash AND @BeginHash IS NOT NULL
+ AND GrainHash <= @EndHash AND @EndHash IS NOT NULL;
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'ReadRangeRows2Key','
+ SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND ((GrainHash > @BeginHash AND @BeginHash IS NOT NULL)
+ OR (GrainHash <= @EndHash AND @EndHash IS NOT NULL));
+');
+
+CREATE FUNCTION delete_reminder_row(
+ ServiceIdArg OrleansAdvancedRemindersTable.ServiceId%TYPE,
+ GrainIdArg OrleansAdvancedRemindersTable.GrainId%TYPE,
+ ReminderNameArg OrleansAdvancedRemindersTable.ReminderName%TYPE,
+ VersionArg OrleansAdvancedRemindersTable.Version%TYPE
+)
+ RETURNS TABLE(row_count integer) AS
+$func$
+DECLARE
+ RowCountVar int := 0;
+BEGIN
+
+
+ DELETE FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = ServiceIdArg AND ServiceIdArg IS NOT NULL
+ AND GrainId = GrainIdArg AND GrainIdArg IS NOT NULL
+ AND ReminderName = ReminderNameArg AND ReminderNameArg IS NOT NULL
+ AND Version = VersionArg AND VersionArg IS NOT NULL;
+
+ GET DIAGNOSTICS RowCountVar = ROW_COUNT;
+
+ RETURN QUERY SELECT RowCountVar;
+
+END
+$func$ LANGUAGE plpgsql;
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'DeleteReminderRowKey','
+ SELECT * FROM delete_reminder_row(
+ @ServiceId,
+ @GrainId,
+ @ReminderName,
+ @Version
+ );
+');
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+VALUES
+(
+ 'DeleteReminderRowsKey','
+ DELETE FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL;
+');
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/README.md b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/README.md
new file mode 100644
index 0000000000..2b85effce7
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/README.md
@@ -0,0 +1,128 @@
+# Microsoft Orleans Advanced Reminders for ADO.NET
+
+## Introduction
+Microsoft Orleans Advanced Reminders for ADO.NET stores reminder definitions in ADO.NET-compatible databases.
+
+This package does not include an ADO.NET-backed durable jobs implementation. You must also configure a durable jobs backend, for example `UseInMemoryDurableJobs()` for local development or `UseAzureBlobDurableJobs(...)` for persisted execution.
+
+## Getting Started
+To use this package, install it via NuGet:
+
+```shell
+dotnet add package Microsoft.Orleans.AdvancedReminders.AdoNet
+```
+
+You will also need to install the appropriate ADO.NET provider for your database:
+
+```shell
+# For SQL Server
+dotnet add package Microsoft.Data.SqlClient
+
+# For MySQL
+dotnet add package MySql.Data
+
+# For PostgreSQL
+dotnet add package Npgsql
+```
+
+## Example - Configuring ADO.NET Advanced Reminders
+```csharp
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Orleans.Configuration;
+using Orleans.AdvancedReminders;
+using Orleans.Hosting;
+using Orleans.DurableJobs;
+using Example;
+
+// Create a host builder
+var builder = Host.CreateApplicationBuilder(args);
+builder.UseOrleans(siloBuilder =>
+{
+ siloBuilder
+ .UseLocalhostClustering()
+ .UseInMemoryDurableJobs()
+ // Configure ADO.NET for reminder definitions
+ .UseAdoNetAdvancedReminderService(options =>
+ {
+ options.Invariant = "Microsoft.Data.SqlClient"; // For SQL Server
+ options.ConnectionString = "Server=localhost;Database=OrleansAdvancedReminders;User ID=orleans;******;";
+ });
+});
+
+// Build and start the host
+var host = builder.Build();
+await host.StartAsync();
+
+// Get a grain reference and use it
+var grain = host.Services.GetRequiredService().GetGrain("my-reminder-grain");
+await grain.StartReminder("DailyReport");
+Console.WriteLine("Reminder started successfully!");
+
+// Keep the host running until the application is shut down
+await host.WaitForShutdownAsync();
+```
+
+## Example - Using Reminders in a Grain
+```csharp
+using System;
+using System.Threading.Tasks;
+using Orleans;
+using Orleans.AdvancedReminders;
+using Orleans.AdvancedReminders.Runtime;
+
+namespace Example;
+
+public interface IReminderGrain : IGrainWithStringKey
+{
+ Task StartReminder(string reminderName);
+ Task StopReminder();
+}
+
+public class ReminderGrain : Grain, IReminderGrain, IRemindable
+{
+ private string _reminderName = "MyReminder";
+
+ public async Task StartReminder(string reminderName)
+ {
+ _reminderName = reminderName;
+
+ // Register a persistent reminder
+ await RegisterOrUpdateReminder(
+ reminderName,
+ TimeSpan.FromMinutes(2), // Time to delay before the first tick (must be > 1 minute)
+ TimeSpan.FromMinutes(5)); // Period of the reminder (must be > 1 minute)
+ }
+
+ public async Task StopReminder()
+ {
+ // Find and unregister the reminder
+ var reminder = await GetReminder(_reminderName);
+ if (reminder != null)
+ {
+ await UnregisterReminder(reminder);
+ }
+ }
+
+ public Task ReceiveReminder(string reminderName, TickStatus status)
+ {
+ // This method is called when the reminder ticks
+ Console.WriteLine($"Reminder {reminderName} triggered at {DateTime.UtcNow}. Status: {status}");
+ return Task.CompletedTask;
+ }
+}
+```
+
+## Documentation
+For more comprehensive documentation, please refer to:
+- [Microsoft Orleans Documentation](https://learn.microsoft.com/dotnet/orleans/)
+- [Reminders and Timers](https://learn.microsoft.com/en-us/dotnet/orleans/grains/timers-and-reminders)
+- [Reminder Services](https://learn.microsoft.com/en-us/dotnet/orleans/implementation/reminder-services)
+- [ADO.NET Database Setup](https://learn.microsoft.com/en-us/dotnet/orleans/host/configuration-guide/adonet-configuration)
+
+## Feedback & Contributing
+- If you have any issues or would like to provide feedback, please [open an issue on GitHub](https://github.com/dotnet/orleans/issues)
+- Join our community on [Discord](https://aka.ms/orleans-discord)
+- Follow the [@msftorleans](https://twitter.com/msftorleans) Twitter account for Orleans announcements
+- Contributions are welcome! Please review our [contribution guidelines](https://github.com/dotnet/orleans/blob/main/CONTRIBUTING.md)
+- This project is licensed under the [MIT license](https://github.com/dotnet/orleans/blob/main/LICENSE)
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/ReminderService/AdoNetReminderTable.cs b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/ReminderService/AdoNetReminderTable.cs
new file mode 100644
index 0000000000..0619a1d461
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/ReminderService/AdoNetReminderTable.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+using Orleans.Configuration;
+using Orleans.AdvancedReminders.AdoNet;
+using Orleans.Reminders.AdoNet.Storage;
+
+namespace Orleans.AdvancedReminders.Runtime.ReminderService
+{
+ internal sealed class AdoNetReminderTable : IReminderTable
+ {
+ private readonly AdoNetReminderTableOptions options;
+ private readonly string serviceId;
+ private RelationalOrleansQueries orleansQueries;
+
+ public AdoNetReminderTable(
+ IOptions clusterOptions,
+ IOptions storageOptions)
+ {
+ this.serviceId = clusterOptions.Value.ServiceId;
+ this.options = storageOptions.Value;
+ }
+
+ public async Task Init()
+ {
+ this.orleansQueries = await RelationalOrleansQueries.CreateInstance(this.options.Invariant, this.options.ConnectionString);
+ }
+
+ public Task ReadRows(GrainId grainId)
+ {
+ return this.orleansQueries.ReadReminderRowsAsync(this.serviceId, grainId);
+ }
+
+ public Task ReadRows(uint beginHash, uint endHash)
+ {
+ return this.orleansQueries.ReadReminderRowsAsync(this.serviceId, beginHash, endHash);
+ }
+
+ public Task ReadRow(GrainId grainId, string reminderName)
+ {
+ return this.orleansQueries.ReadReminderRowAsync(this.serviceId, grainId, reminderName);
+ }
+
+ public Task UpsertRow(ReminderEntry entry)
+ {
+ if (entry.StartAt.Kind is DateTimeKind.Unspecified)
+ {
+ entry.StartAt = new DateTime(entry.StartAt.Ticks, DateTimeKind.Utc);
+ }
+
+ return this.orleansQueries.UpsertReminderRowAsync(
+ this.serviceId,
+ entry.GrainId,
+ entry.ReminderName,
+ entry.StartAt,
+ entry.Period,
+ entry.CronExpression,
+ entry.CronTimeZoneId,
+ entry.NextDueUtc,
+ entry.LastFireUtc,
+ entry.Priority,
+ entry.Action);
+ }
+
+ public Task RemoveRow(GrainId grainId, string reminderName, string eTag)
+ {
+ return this.orleansQueries.DeleteReminderRowAsync(this.serviceId, grainId, reminderName, eTag);
+ }
+
+ public Task TestOnlyClearTable()
+ {
+ return this.orleansQueries.DeleteReminderRowsAsync(this.serviceId);
+ }
+ }
+}
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/ReminderService/AdoNetReminderTableOptions.cs b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/ReminderService/AdoNetReminderTableOptions.cs
new file mode 100644
index 0000000000..24627e06c2
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/ReminderService/AdoNetReminderTableOptions.cs
@@ -0,0 +1,19 @@
+namespace Orleans.AdvancedReminders.AdoNet
+{
+ ///
+ /// Options for ADO.NET reminder storage.
+ ///
+ public class AdoNetReminderTableOptions
+ {
+ ///
+ /// Gets or sets the ADO.NET invariant.
+ ///
+ public string Invariant { get; set; }
+
+ ///
+ /// Gets or sets the connection string.
+ ///
+ [Redact]
+ public string ConnectionString { get; set; }
+ }
+}
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/ReminderService/AdoNetReminderTableOptionsValidator.cs b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/ReminderService/AdoNetReminderTableOptionsValidator.cs
new file mode 100644
index 0000000000..7cdcec37d2
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/ReminderService/AdoNetReminderTableOptionsValidator.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.Options;
+using Orleans.Runtime;
+using Orleans.AdvancedReminders.Runtime.ReminderService;
+
+namespace Orleans.AdvancedReminders.AdoNet
+{
+ ///
+ /// Validates configuration.
+ ///
+ public class AdoNetReminderTableOptionsValidator : IConfigurationValidator
+ {
+ private readonly AdoNetReminderTableOptions options;
+
+ public AdoNetReminderTableOptionsValidator(IOptions options)
+ {
+ this.options = options.Value;
+ }
+
+ ///
+ public void ValidateConfiguration()
+ {
+ if (string.IsNullOrWhiteSpace(this.options.Invariant))
+ {
+ throw new OrleansConfigurationException($"Invalid {nameof(AdoNetReminderTableOptions)} values for {nameof(AdoNetReminderTable)}. {nameof(options.Invariant)} is required.");
+ }
+
+ if (string.IsNullOrWhiteSpace(this.options.ConnectionString))
+ {
+ throw new OrleansConfigurationException($"Invalid {nameof(AdoNetReminderTableOptions)} values for {nameof(AdoNetReminderTable)}. {nameof(options.ConnectionString)} is required.");
+ }
+ }
+ }
+}
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/SQLServer-Reminders.sql b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/SQLServer-Reminders.sql
new file mode 100644
index 0000000000..b581f50e87
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/SQLServer-Reminders.sql
@@ -0,0 +1,239 @@
+-- Orleans Reminders table - https://learn.microsoft.com/dotnet/orleans/grains/timers-and-reminders
+IF OBJECT_ID(N'[OrleansAdvancedRemindersTable]', 'U') IS NULL
+CREATE TABLE OrleansAdvancedRemindersTable
+(
+ ServiceId NVARCHAR(150) NOT NULL,
+ GrainId VARCHAR(150) NOT NULL,
+ ReminderName NVARCHAR(150) NOT NULL,
+ StartTime DATETIME2(3) NOT NULL,
+ Period BIGINT NOT NULL,
+ CronExpression NVARCHAR(200) NULL,
+ CronTimeZoneId NVARCHAR(200) NULL,
+ NextDueUtc DATETIME2(3) NULL,
+ LastFireUtc DATETIME2(3) NULL,
+ Priority TINYINT NOT NULL CONSTRAINT DF_OrleansAdvancedRemindersTable_Priority DEFAULT (0),
+ Action TINYINT NOT NULL CONSTRAINT DF_OrleansAdvancedRemindersTable_Action DEFAULT (0),
+ GrainHash INT NOT NULL,
+ Version INT NOT NULL,
+
+ CONSTRAINT PK_RemindersTable_ServiceId_GrainId_ReminderName PRIMARY KEY(ServiceId, GrainId, ReminderName)
+);
+
+IF NOT EXISTS (
+ SELECT 1
+ FROM sys.indexes
+ WHERE name = 'IX_RemindersTable_NextDueUtc_Priority'
+ AND object_id = OBJECT_ID('OrleansAdvancedRemindersTable')
+)
+BEGIN
+ CREATE INDEX IX_RemindersTable_NextDueUtc_Priority
+ ON OrleansAdvancedRemindersTable(ServiceId, NextDueUtc, Priority);
+END;
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+SELECT
+ 'UpsertReminderRowKey',
+ 'DECLARE @Version AS INT = 0;
+ SET XACT_ABORT, NOCOUNT ON;
+ BEGIN TRANSACTION;
+ UPDATE OrleansAdvancedRemindersTable WITH(UPDLOCK, ROWLOCK, HOLDLOCK)
+ SET
+ StartTime = @StartTime,
+ Period = @Period,
+ CronExpression = @CronExpression,
+ CronTimeZoneId = @CronTimeZoneId,
+ NextDueUtc = @NextDueUtc,
+ LastFireUtc = @LastFireUtc,
+ Priority = @Priority,
+ Action = @Action,
+ GrainHash = @GrainHash,
+ @Version = Version = Version + 1
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL;
+
+ INSERT INTO OrleansAdvancedRemindersTable
+ (
+ ServiceId,
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ GrainHash,
+ Version
+ )
+ SELECT
+ @ServiceId,
+ @GrainId,
+ @ReminderName,
+ @StartTime,
+ @Period,
+ @CronExpression,
+ @CronTimeZoneId,
+ @NextDueUtc,
+ @LastFireUtc,
+ @Priority,
+ @Action,
+ @GrainHash,
+ 0
+ WHERE
+ @@ROWCOUNT=0;
+ SELECT @Version AS Version;
+ COMMIT TRANSACTION;
+ '
+WHERE NOT EXISTS
+(
+ SELECT 1
+ FROM OrleansQuery oqt
+ WHERE oqt.[QueryKey] = 'UpsertReminderRowKey'
+);
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+SELECT
+ 'ReadReminderRowsKey',
+ 'SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL;
+ '
+WHERE NOT EXISTS
+(
+ SELECT 1
+ FROM OrleansQuery oqt
+ WHERE oqt.[QueryKey] = 'ReadReminderRowsKey'
+);
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+SELECT
+ 'ReadReminderRowKey',
+ 'SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL;
+ '
+WHERE NOT EXISTS
+(
+ SELECT 1
+ FROM OrleansQuery oqt
+ WHERE oqt.[QueryKey] = 'ReadReminderRowKey'
+);
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+SELECT
+ 'ReadRangeRows1Key',
+ 'SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainHash > @BeginHash AND @BeginHash IS NOT NULL
+ AND GrainHash <= @EndHash AND @EndHash IS NOT NULL;
+ '
+WHERE NOT EXISTS
+(
+ SELECT 1
+ FROM OrleansQuery oqt
+ WHERE oqt.[QueryKey] = 'ReadRangeRows1Key'
+);
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+SELECT
+ 'ReadRangeRows2Key',
+ 'SELECT
+ GrainId,
+ ReminderName,
+ StartTime,
+ Period,
+ CronExpression,
+ CronTimeZoneId,
+ NextDueUtc,
+ LastFireUtc,
+ Priority,
+ Action,
+ Version
+ FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND ((GrainHash > @BeginHash AND @BeginHash IS NOT NULL)
+ OR (GrainHash <= @EndHash AND @EndHash IS NOT NULL));
+ '
+WHERE NOT EXISTS
+(
+ SELECT 1
+ FROM OrleansQuery oqt
+ WHERE oqt.[QueryKey] = 'ReadRangeRows2Key'
+);
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+SELECT
+ 'DeleteReminderRowKey',
+ 'DELETE FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL
+ AND GrainId = @GrainId AND @GrainId IS NOT NULL
+ AND ReminderName = @ReminderName AND @ReminderName IS NOT NULL
+ AND Version = @Version AND @Version IS NOT NULL;
+ SELECT @@ROWCOUNT;
+ '
+WHERE NOT EXISTS
+(
+ SELECT 1
+ FROM OrleansQuery oqt
+ WHERE oqt.[QueryKey] = 'DeleteReminderRowKey'
+);
+
+INSERT INTO OrleansQuery(QueryKey, QueryText)
+SELECT
+ 'DeleteReminderRowsKey',
+ 'DELETE FROM OrleansAdvancedRemindersTable
+ WHERE
+ ServiceId = @ServiceId AND @ServiceId IS NOT NULL;
+ '
+WHERE NOT EXISTS
+(
+ SELECT 1
+ FROM OrleansQuery oqt
+ WHERE oqt.[QueryKey] = 'DeleteReminderRowsKey'
+);
diff --git a/src/AdoNet/Orleans.AdvancedReminders.AdoNet/SiloBuilderReminderExtensions.cs b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/SiloBuilderReminderExtensions.cs
new file mode 100644
index 0000000000..0fa23ada51
--- /dev/null
+++ b/src/AdoNet/Orleans.AdvancedReminders.AdoNet/SiloBuilderReminderExtensions.cs
@@ -0,0 +1,63 @@
+using System;
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Orleans.Configuration;
+using Orleans.Configuration.Internal;
+using Orleans.Hosting;
+using Orleans.AdvancedReminders.Runtime.ReminderService;
+using Orleans.AdvancedReminders.AdoNet;
+
+namespace Orleans.Hosting
+{
+ ///
+ /// Silo host builder extensions.
+ ///
+ public static class SiloBuilderReminderExtensions
+ {
+ /// Adds reminder storage using ADO.NET. Instructions on configuring your database are available at .
+ /// The builder.
+ /// Configuration delegate.
+ /// The provided , for chaining.
+ ///
+ /// Instructions on configuring your database are available at .
+ ///
+ public static ISiloBuilder UseAdoNetAdvancedReminderService(
+ this ISiloBuilder builder,
+ Action configureOptions)
+ {
+ return builder.UseAdoNetAdvancedReminderService(ob => ob.Configure(configureOptions));
+ }
+
+ /// Adds reminder storage using ADO.NET. Instructions on configuring your database are available at .
+ /// The builder.
+ /// Configuration delegate.
+ /// The provided , for chaining.
+ ///
+ /// Instructions on configuring your database are available at .
+ ///
+ public static ISiloBuilder UseAdoNetAdvancedReminderService(
+ this ISiloBuilder builder,
+ Action> configureOptions)
+ {
+ return builder.ConfigureServices(services => services.UseAdoNetAdvancedReminderService(configureOptions));
+ }
+
+ /// Adds reminder storage using ADO.NET. Instructions on configuring your database are available at .
+ /// The service collection.
+ /// Configuration delegate.
+ /// The provided , for chaining.
+ ///
+ /// Instructions on configuring your database are available at .
+ ///
+ public static IServiceCollection UseAdoNetAdvancedReminderService(this IServiceCollection services, Action> configureOptions)
+ {
+ services.AddAdvancedReminders();
+ services.AddSingleton();
+ services.ConfigureFormatter();
+ services.AddSingleton();
+ configureOptions(services.AddOptions());
+ return services;
+ }
+ }
+}
diff --git a/src/AdoNet/Shared/Storage/DbExtensions.cs b/src/AdoNet/Shared/Storage/DbExtensions.cs
index 48c615dc2a..52a6cd17d2 100644
--- a/src/AdoNet/Shared/Storage/DbExtensions.cs
+++ b/src/AdoNet/Shared/Storage/DbExtensions.cs
@@ -3,6 +3,7 @@
using System.Collections.ObjectModel;
using System.Data;
using System.Data.Common;
+using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
@@ -273,7 +274,7 @@ public static int GetInt32(this IDataRecord record, string fieldName)
try
{
var ordinal = record.GetOrdinal(fieldName);
- return record.GetInt32(ordinal);
+ return Convert.ToInt32(record.GetValue(ordinal), CultureInfo.InvariantCulture);
}
catch (IndexOutOfRangeException e)
{
@@ -318,7 +319,7 @@ public static long GetInt64(this IDataRecord record, string fieldName)
if (value == DBNull.Value)
return null;
- return Convert.ToInt32(value);
+ return Convert.ToInt32(value, CultureInfo.InvariantCulture);
}
catch (IndexOutOfRangeException e)
{
diff --git a/src/AdoNet/Shared/Storage/DbStoredQueries.cs b/src/AdoNet/Shared/Storage/DbStoredQueries.cs
index f1c9de17e1..63bcb56296 100644
--- a/src/AdoNet/Shared/Storage/DbStoredQueries.cs
+++ b/src/AdoNet/Shared/Storage/DbStoredQueries.cs
@@ -265,7 +265,7 @@ private static string TryGetSiloName(IDataRecord record)
internal static int GetVersion(IDataRecord record)
{
- return Convert.ToInt32(record.GetValue