diff --git a/Akka.Persistence.Redis.sln b/Akka.Persistence.Redis.sln index c244b8b..015da47 100644 --- a/Akka.Persistence.Redis.sln +++ b/Akka.Persistence.Redis.sln @@ -32,6 +32,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.Persistence.Redis.Benchmark.DockerTests", "src\benchmarks\Akka.Persistence.Redis.Benchmark.DockerTests\Akka.Persistence.Redis.Benchmark.DockerTests.csproj", "{BFBB3933-E8FC-4574-8D60-E6FA6BEABAE9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Redis.Hosting", "src\Akka.Persistence.Redis.Hosting\Akka.Persistence.Redis.Hosting.csproj", "{80BD65FD-3AB6-4AF1-B0CD-573C9D50DB1E}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build-system", "build-system", "{D118A7A7-A89D-450D-8218-E2AE580DEF5D}" ProjectSection(SolutionItems) = preProject build-system\azure-pipeline.template.yaml = build-system\azure-pipeline.template.yaml @@ -70,6 +72,10 @@ Global {BFBB3933-E8FC-4574-8D60-E6FA6BEABAE9}.Debug|Any CPU.Build.0 = Debug|Any CPU {BFBB3933-E8FC-4574-8D60-E6FA6BEABAE9}.Release|Any CPU.ActiveCfg = Release|Any CPU {BFBB3933-E8FC-4574-8D60-E6FA6BEABAE9}.Release|Any CPU.Build.0 = Release|Any CPU + {80BD65FD-3AB6-4AF1-B0CD-573C9D50DB1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {80BD65FD-3AB6-4AF1-B0CD-573C9D50DB1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {80BD65FD-3AB6-4AF1-B0CD-573C9D50DB1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {80BD65FD-3AB6-4AF1-B0CD-573C9D50DB1E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Akka.Persistence.Redis.Hosting/Akka.Persistence.Redis.Hosting.csproj b/src/Akka.Persistence.Redis.Hosting/Akka.Persistence.Redis.Hosting.csproj new file mode 100644 index 0000000..27d8a46 --- /dev/null +++ b/src/Akka.Persistence.Redis.Hosting/Akka.Persistence.Redis.Hosting.csproj @@ -0,0 +1,19 @@ + + + + $(NetStandardVersion) + README.md + + + + + + + + + + + + + + diff --git a/src/Akka.Persistence.Redis.Hosting/AkkaPersistenceRedisHostingExtensions.cs b/src/Akka.Persistence.Redis.Hosting/AkkaPersistenceRedisHostingExtensions.cs new file mode 100644 index 0000000..90ad7b3 --- /dev/null +++ b/src/Akka.Persistence.Redis.Hosting/AkkaPersistenceRedisHostingExtensions.cs @@ -0,0 +1,216 @@ +using System; +using System.Text; +using Akka.Actor; +using Akka.Configuration; +using Akka.Hosting; +using Akka.Persistence.Hosting; + +#nullable enable +namespace Akka.Persistence.Redis.Hosting; + +public static class AkkaPersistenceRedisHostingExtensions +{ + /// + /// Adds Akka.Persistence.Redis support to this . + /// + /// + /// The builder instance being configured. + /// + /// + /// Connection string as described here: https://stackexchange.github.io/StackExchange.Redis/Configuration#basic-configuration-strings. + /// + /// + /// + /// Determines which settings should be added by this method call. + /// + /// Default: + /// + /// + /// + /// Should the redis store table be initialized automatically. + /// + /// Default: false + /// + /// + /// + /// An used to configure an instance. + /// + /// Default: null + /// + /// + /// + /// The configuration identifier for the plugins + /// + /// Default: "redis" + /// + /// + /// + /// A bool flag to set the plugin as the default persistence plugin for the + /// + /// Default: true + /// + /// + /// The same instance originally passed in. + /// + /// + /// Thrown when is set and is set to + /// + /// + public static AkkaConfigurationBuilder WithRedisPersistence( + this AkkaConfigurationBuilder builder, + string configurationString, + PersistenceMode mode = PersistenceMode.Both, + bool autoInitialize = true, + Action? journalBuilder = null, + string pluginIdentifier = "redis", + bool isDefaultPlugin = true) + { + if (mode == PersistenceMode.SnapshotStore && journalBuilder is { }) + throw new Exception( + $"{nameof(journalBuilder)} can only be set when {nameof(mode)} is set to either {PersistenceMode.Both} or {PersistenceMode.Journal}"); + + var journalOpt = new RedisJournalOptions(isDefaultPlugin, pluginIdentifier) + { + ConfigurationString = configurationString, + AutoInitialize = autoInitialize, + }; + + var adapters = new AkkaPersistenceJournalBuilder(journalOpt.Identifier, builder); + journalBuilder?.Invoke(adapters); + journalOpt.Adapters = adapters; + + var snapshotOpt = new RedisSnapshotOptions(isDefaultPlugin, pluginIdentifier) + { + ConfigurationString = configurationString, + AutoInitialize = autoInitialize, + }; + + return mode switch + { + PersistenceMode.Journal => builder.WithRedisPersistence(journalOpt, null), + PersistenceMode.SnapshotStore => builder.WithRedisPersistence(null, snapshotOpt), + PersistenceMode.Both => builder.WithRedisPersistence(journalOpt, snapshotOpt), + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Invalid PersistenceMode defined.") + }; + } + + /// + /// Adds Akka.Persistence.Redis support to this . At least one of the + /// configurator delegate needs to be populated else this method will throw an exception. + /// + /// + /// The builder instance being configured. + /// + /// + /// + /// An that modifies an instance of , + /// used to configure the journal plugin + /// + /// Default: null + /// + /// + /// + /// An that modifies an instance of , + /// used to configure the snapshot store plugin + /// + /// Default: null + /// + /// + /// + /// A bool flag to set the plugin as the default persistence plugin for the + /// + /// Default: true + /// + /// + /// The same instance originally passed in. + /// + /// + /// Thrown when both and are null. + /// + public static AkkaConfigurationBuilder WithRedisPersistence( + this AkkaConfigurationBuilder builder, + Action? journalOptionConfigurator = null, + Action? snapshotOptionConfigurator = null, + bool isDefaultPlugin = true) + { + if (journalOptionConfigurator is null && snapshotOptionConfigurator is null) + throw new ArgumentException( + $"{nameof(journalOptionConfigurator)} and {nameof(snapshotOptionConfigurator)} could not both be null"); + + RedisJournalOptions? journalOptions = null; + if (journalOptionConfigurator is { }) + { + journalOptions = new RedisJournalOptions(isDefaultPlugin); + journalOptionConfigurator(journalOptions); + } + + RedisSnapshotOptions? snapshotOptions = null; + if (snapshotOptionConfigurator is { }) + { + snapshotOptions = new RedisSnapshotOptions(isDefaultPlugin); + snapshotOptionConfigurator(snapshotOptions); + } + + return builder.WithRedisPersistence(journalOptions, snapshotOptions); + } + + /// + /// Adds Akka.Persistence.Redis support to this . At least one of the options + /// have to be populated else this method will throw an exception. + /// + /// + /// The builder instance being configured. + /// + /// + /// + /// An instance of , used to configure the journal plugin + /// + /// Default: null + /// + /// + /// + /// An instance of , used to configure the snapshot store plugin + /// + /// Default: null + /// + /// + /// The same instance originally passed in. + /// + /// + /// Thrown when both and are null. + /// + public static AkkaConfigurationBuilder WithRedisPersistence( + this AkkaConfigurationBuilder builder, + RedisJournalOptions? journalOptions = null, + RedisSnapshotOptions? snapshotOptions = null) + { + if (journalOptions is null && snapshotOptions is null) + throw new ArgumentException($"{nameof(journalOptions)} and {nameof(snapshotOptions)} could not both be null"); + + return (journalOptions, snapshotOptions) switch + { + (null, null) => + throw new ArgumentException( + $"{nameof(journalOptions)} and {nameof(snapshotOptions)} could not both be null"), + + (_, null) => + builder + .AddHocon(journalOptions.ToConfig(), HoconAddMode.Prepend) + .AddHocon(journalOptions.DefaultConfig, HoconAddMode.Append) + .AddHocon(RedisPersistence.DefaultConfig(), HoconAddMode.Append), + + (null, _) => + builder + .AddHocon(snapshotOptions.ToConfig(), HoconAddMode.Prepend) + .AddHocon(snapshotOptions.DefaultConfig, HoconAddMode.Append), + + (_, _) => + builder + .AddHocon(journalOptions.ToConfig(), HoconAddMode.Prepend) + .AddHocon(snapshotOptions.ToConfig(), HoconAddMode.Prepend) + .AddHocon(journalOptions.DefaultConfig, HoconAddMode.Append) + .AddHocon(snapshotOptions.DefaultConfig, HoconAddMode.Append) + .AddHocon(RedisPersistence.DefaultConfig(), HoconAddMode.Append), + }; + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.Redis.Hosting/README.md b/src/Akka.Persistence.Redis.Hosting/README.md new file mode 100644 index 0000000..7dd3990 --- /dev/null +++ b/src/Akka.Persistence.Redis.Hosting/README.md @@ -0,0 +1,87 @@ +# Akka.Persistence.Redis.Hosting + +Akka.Hosting extension methods to add Akka.Persistence.Redis to an ActorSystem + +# Akka.Persistence.Redis Extension Methods + +## WithRedisPersistence() Method + +```csharp +public static AkkaConfigurationBuilder WithRedisPersistence( + this AkkaConfigurationBuilder builder, + string configurationString, + PersistenceMode mode = PersistenceMode.Both, + bool autoInitialize = true, + Action? journalBuilder = null, + string pluginIdentifier = "Redis", + bool isDefaultPlugin = true); +``` + +```csharp +public static AkkaConfigurationBuilder WithRedisPersistence( + this AkkaConfigurationBuilder builder, + Action? journalOptionConfigurator = null, + Action? snapshotOptionConfigurator = null, + bool isDefaultPlugin = true) +``` + +```csharp +public static AkkaConfigurationBuilder WithRedisPersistence( + this AkkaConfigurationBuilder builder, + RedisJournalOptions? journalOptions = null, + RedisSnapshotOptions? snapshotOptions = null) +``` + +### Parameters + +* `configurationString` __string__ + + Connection string used for database access. Connection string as described here: https://stackexchange.github.io/StackExchange.Redis/Configuration#basic-configuration-strings. + +* `mode` __PersistenceMode__ + + Determines which settings should be added by this method call. __Default__: `PersistenceMode.Both` + + * `PersistenceMode.Journal`: Only add the journal settings + * `PersistenceMode.SnapshotStore`: Only add the snapshot store settings + * `PersistenceMode.Both`: Add both journal and snapshot store settings + +* `autoInitialize` __bool__ + + Should the Redis store collection be initialized automatically. __Default__: `false` + +* `journalBuilder` __Action\__ + + An Action delegate used to configure an `AkkaPersistenceJournalBuilder` instance. Used to configure [Event Adapters](https://getakka.net/articles/persistence/event-adapters.html) + +* `journalConfigurator` __Action\__ + + An Action delegate to configure a `RedisJournalOptions` instance. + +* `snapshotConfigurator` __Action\__ + + An Action delegate to configure a `RedisSnapshotOptions` instance. + +* `journalOptions` __RedisJournalOptions__ + + An `RedisJournalOptions` instance to configure the Redis journal store. + +* `snapshotOptions` __RedisSnapshotOptions__ + + An `RedisSnapshotOptions` instance to configure the Redis snapshot store. + +## Example + +```csharp +using var host = new HostBuilder() + .ConfigureServices((context, services) => + { + services.AddAkka("redisDemo", (builder, provider) => + { + builder + .WithRedisPersistence("your-redis-connection-string"); + }); + }).Build(); + +await host.RunAsync(); +``` \ No newline at end of file diff --git a/src/Akka.Persistence.Redis.Hosting/RedisJournalOptions.cs b/src/Akka.Persistence.Redis.Hosting/RedisJournalOptions.cs new file mode 100644 index 0000000..577f33a --- /dev/null +++ b/src/Akka.Persistence.Redis.Hosting/RedisJournalOptions.cs @@ -0,0 +1,60 @@ +using System.Text; +using Akka.Configuration; +using Akka.Hosting; +using Akka.Persistence.Hosting; + +#nullable enable +namespace Akka.Persistence.Redis.Hosting; + +public class RedisJournalOptions : JournalOptions +{ + private static readonly Config Default = RedisPersistence.DefaultConfig().GetConfig(RedisPersistence.JournalConfigPath); + + public RedisJournalOptions() : this(true) + { + } + + public RedisJournalOptions(bool isDefault, string identifier = "redis") : base(isDefault) + { + Identifier = identifier; + } + + /// + /// Connection string, as described here: https://stackexchange.github.io/StackExchange.Redis/Configuration#basic-configuration-strings + /// + public string ConfigurationString { get; set; } = string.Empty; + + /// + /// Redis journals key prefixes. Leave it for default or change it to appropriate value. WARNING: don't change it on production instances. + /// + public string? KeyPrefix { get; set; } + + /// + /// Set the Redis default database to use. If you added defaultDatabase to the connection-strings, you have to set database to the value of defaultDatabase. + /// + public int? Database { get; set; } + + /// + /// Determines redis database precedence when a user adds defaultDatabase to the connection-strings. For Redis Cluster, the defaultDatabase is 0 + /// + public bool? UseDatabaseFromConnectionString { get; set; } + + public override string Identifier { get; set; } + protected override Config InternalDefaultConfig { get; } = Default; + + protected override StringBuilder Build(StringBuilder sb) + { + sb.AppendLine($"configuration-string = {ConfigurationString.ToHocon()}"); + + if (KeyPrefix is not null) + sb.AppendLine($"key-prefix = {KeyPrefix.ToHocon()}"); + + if (Database is not null) + sb.AppendLine($"database = {Database.ToHocon()}"); + + if (UseDatabaseFromConnectionString is not null) + sb.AppendLine($"use-database-number-from-connection-string = {UseDatabaseFromConnectionString.Value.ToHocon()}"); + + return base.Build(sb); + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.Redis.Hosting/RedisSnapshotOptions.cs b/src/Akka.Persistence.Redis.Hosting/RedisSnapshotOptions.cs new file mode 100644 index 0000000..c47e352 --- /dev/null +++ b/src/Akka.Persistence.Redis.Hosting/RedisSnapshotOptions.cs @@ -0,0 +1,61 @@ +using System.Text; +using Akka.Configuration; +using Akka.Hosting; +using Akka.Persistence.Hosting; + +#nullable enable +namespace Akka.Persistence.Redis.Hosting; + +public class RedisSnapshotOptions : SnapshotOptions +{ + private static readonly Config Default = RedisPersistence.DefaultConfig() + .GetConfig(RedisPersistence.SnapshotConfigPath); + + public RedisSnapshotOptions() : this(true) + { + } + + public RedisSnapshotOptions(bool isDefault, string identifier = "redis") : base(isDefault) + { + Identifier = identifier; + } + + /// + /// Connection string, as described here: https://stackexchange.github.io/StackExchange.Redis/Configuration#basic-configuration-strings + /// + public string ConfigurationString { get; set; } = string.Empty; + + /// + /// Redis journals key prefixes. Leave it for default or change it to appropriate value. WARNING: don't change it on production instances. + /// + public string? KeyPrefix { get; set; } + + /// + /// Set the Redis default database to use. If you added defaultDatabase to the connection-strings, you have to set database to the value of defaultDatabase. + /// + public int? Database { get; set; } + + /// + /// Determines redis database precedence when a user adds defaultDatabase to the connection-strings. For Redis Cluster, the defaultDatabase is 0 + /// + public bool? UseDatabaseFromConnectionString { get; set; } + + public override string Identifier { get; set; } + protected override Config InternalDefaultConfig { get; } = Default; + + protected override StringBuilder Build(StringBuilder sb) + { + sb.AppendLine($"configuration-string = {ConfigurationString.ToHocon()}"); + + if (KeyPrefix is not null) + sb.AppendLine($"key-prefix = {KeyPrefix.ToHocon()}"); + + if (Database is not null) + sb.AppendLine($"database = {Database.ToHocon()}"); + + if (UseDatabaseFromConnectionString is not null) + sb.AppendLine($"use-database-number-from-connection-string = {UseDatabaseFromConnectionString.Value.ToHocon()}"); + + return base.Build(sb); + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.Redis.Tests/Akka.Persistence.Redis.Tests.csproj b/src/Akka.Persistence.Redis.Tests/Akka.Persistence.Redis.Tests.csproj index 80fd921..e95d95d 100644 --- a/src/Akka.Persistence.Redis.Tests/Akka.Persistence.Redis.Tests.csproj +++ b/src/Akka.Persistence.Redis.Tests/Akka.Persistence.Redis.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Akka.Persistence.Redis.Tests/Hosting/RedisJournalOptionsSpec.cs b/src/Akka.Persistence.Redis.Tests/Hosting/RedisJournalOptionsSpec.cs new file mode 100644 index 0000000..0e12436 --- /dev/null +++ b/src/Akka.Persistence.Redis.Tests/Hosting/RedisJournalOptionsSpec.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using System.Text; +using Akka.Configuration; +using Akka.Persistence.Redis.Hosting; +using FluentAssertions; +using FluentAssertions.Extensions; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Akka.Persistence.Redis.Tests.Hosting +{ + public class RedisJournalOptionsSpec + { + [Fact(DisplayName = "RedisJournalOptions as default plugin should generate plugin setting")] + public void DefaultPluginJournalOptionsTest() + { + var options = new RedisJournalOptions(true); + var config = options.ToConfig(); + + config.GetString("akka.persistence.journal.plugin").Should().Be("akka.persistence.journal.redis"); + config.HasPath("akka.persistence.journal.redis").Should().BeTrue(); + } + + [Fact(DisplayName = "Empty RedisJournalOptions should equal empty config with default fallback")] + public void DefaultJournalOptionsTest() + { + var options = new RedisJournalOptions(false); + var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); + var baseRootConfig = Config.Empty + .WithFallback(RedisPersistence.DefaultConfig()); + + emptyRootConfig.GetString("akka.persistence.journal.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.journal.plugin")); + + var config = emptyRootConfig.GetConfig("akka.persistence.journal.redis"); + var baseConfig = baseRootConfig.GetConfig("akka.persistence.journal.redis"); + config.Should().NotBeNull(); + baseConfig.Should().NotBeNull(); + + config.GetString("class").Should().Be(baseConfig.GetString("class")); + config.GetString("configuration-string").Should().Be(baseConfig.GetString("configuration-string")); + config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); + config.GetString("key-prefix").Should().Be(baseConfig.GetString("key-prefix")); + config.GetInt("database").Should().Be(baseConfig.GetInt("database")); + config.GetBoolean("use-database-number-from-connection-string").Should().Be(baseConfig.GetBoolean("use-database-number-from-connection-string")); + } + + [Fact(DisplayName = "Empty RedisJournalOptions with custom identifier should equal empty config with default fallback")] + public void CustomIdJournalOptionsTest() + { + var options = new RedisJournalOptions(false, "custom"); + var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); + var baseRootConfig = Config.Empty + .WithFallback(RedisPersistence.DefaultConfig()); + + emptyRootConfig.GetString("akka.persistence.journal.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.journal.plugin")); + + var config = emptyRootConfig.GetConfig("akka.persistence.journal.custom"); + var baseConfig = baseRootConfig.GetConfig("akka.persistence.journal.redis"); + config.Should().NotBeNull(); + baseConfig.Should().NotBeNull(); + + config.GetString("class").Should().Be(baseConfig.GetString("class")); + config.GetString("configuration-string").Should().Be(baseConfig.GetString("configuration-string")); + config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); + config.GetString("key-prefix").Should().Be(baseConfig.GetString("key-prefix")); + config.GetInt("database").Should().Be(baseConfig.GetInt("database")); + config.GetBoolean("use-database-number-from-connection-string").Should().Be(baseConfig.GetBoolean("use-database-number-from-connection-string")); + } + + [Fact(DisplayName = "RedisJournalOptions should generate proper config")] + public void JournalOptionsTest() + { + var options = new RedisJournalOptions(true) + { + Identifier = "custom", + AutoInitialize = true, + ConfigurationString = "testConfigurationString", + KeyPrefix = "testKeyPrefix", + Database = 999123, + UseDatabaseFromConnectionString = true + }; + + var baseConfig = options.ToConfig(); + + baseConfig.GetString("akka.persistence.journal.plugin").Should().Be("akka.persistence.journal.custom"); + + var config = baseConfig.GetConfig("akka.persistence.journal.custom"); + config.Should().NotBeNull(); + config.GetBoolean("auto-initialize").Should().Be(options.AutoInitialize); + config.GetString("configuration-string").Should().Be(options.ConfigurationString); + config.GetString("key-prefix").Should().Be(options.KeyPrefix); + config.GetInt("database").Should().Be(options.Database); + config.GetBoolean("use-database-number-from-connection-string").Should().Be(options.UseDatabaseFromConnectionString.Value); + } + + const string Json = @" + { + ""Logging"": { + ""LogLevel"": { + ""Default"": ""Information"", + ""Microsoft.AspNetCore"": ""Warning"" + } + }, + ""Akka"": { + ""JournalOptions"": { + ""Identifier"": ""customRedis"", + ""AutoInitialize"": true, + ""IsDefaultPlugin"": false, + ""ConfigurationString"": ""ConfigurationStringFromConfigJson"", + ""KeyPrefix"": ""KeyPrefixFromConfigJson"", + ""Database"": 123456, + ""UseDatabaseFromConnectionString"": true, + ""Serializer"": ""TestSerializer"", + } + } + }"; + + [Fact(DisplayName = "RedisJournalOptions should be bindable to IConfiguration")] + public void JournalOptionsIConfigurationBindingTest() + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(Json)); + var jsonConfig = new ConfigurationBuilder().AddJsonStream(stream).Build(); + + var options = jsonConfig.GetSection("Akka:JournalOptions").Get(); + options.Identifier.Should().Be("customRedis"); + options.AutoInitialize.Should().BeTrue(); + options.IsDefaultPlugin.Should().BeFalse(); + options.ConfigurationString.Should().Be("ConfigurationStringFromConfigJson"); + options.KeyPrefix.Should().Be("KeyPrefixFromConfigJson"); + options.Database.Should().Be(123456); + options.UseDatabaseFromConnectionString.Should().BeTrue(); + options.Serializer.Should().Be("TestSerializer"); + } + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.Redis.Tests/Hosting/RedisSnapshotOptionsSpec.cs b/src/Akka.Persistence.Redis.Tests/Hosting/RedisSnapshotOptionsSpec.cs new file mode 100644 index 0000000..70a9dda --- /dev/null +++ b/src/Akka.Persistence.Redis.Tests/Hosting/RedisSnapshotOptionsSpec.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using System.Text; +using Akka.Configuration; +using Akka.Persistence.Redis.Hosting; +using FluentAssertions; +using FluentAssertions.Extensions; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Akka.Persistence.Redis.Tests.Hosting +{ + public class RedisSnapshotOptionsSpec + { + [Fact(DisplayName = "RedisSnapshotOptions as default plugin should generate plugin setting")] + public void DefaultPluginSnapshotOptionsTest() + { + var options = new RedisSnapshotOptions(true); + var config = options.ToConfig(); + + config.GetString("akka.persistence.snapshot-store.plugin").Should().Be("akka.persistence.snapshot-store.redis"); + config.HasPath("akka.persistence.snapshot-store.redis").Should().BeTrue(); + } + + [Fact(DisplayName = "Empty RedisSnapshotOptions should equal empty config with default fallback")] + public void DefaultSnapshotOptionsTest() + { + var options = new RedisSnapshotOptions(false); + var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); + var baseRootConfig = Config.Empty + .WithFallback(RedisPersistence.DefaultConfig()); + + emptyRootConfig.GetString("akka.persistence.snapshot-store.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.snapshot-store.plugin")); + + var config = emptyRootConfig.GetConfig("akka.persistence.snapshot-store.redis"); + var baseConfig = baseRootConfig.GetConfig("akka.persistence.snapshot-store.redis"); + config.Should().NotBeNull(); + baseConfig.Should().NotBeNull(); + + config.GetString("class").Should().Be(baseConfig.GetString("class")); + config.GetString("configuration-string").Should().Be(baseConfig.GetString("configuration-string")); + config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); + config.GetString("key-prefix").Should().Be(baseConfig.GetString("key-prefix")); + config.GetInt("database").Should().Be(baseConfig.GetInt("database")); + config.GetBoolean("use-database-number-from-connection-string").Should().Be(baseConfig.GetBoolean("use-database-number-from-connection-string")); + } + + [Fact(DisplayName = "Empty RedisSnapshotOptions with custom identifier should equal empty config with default fallback")] + public void CustomIdSnapshotOptionsTest() + { + var options = new RedisSnapshotOptions(false, "custom"); + var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); + var baseRootConfig = Config.Empty + .WithFallback(RedisPersistence.DefaultConfig()); + + emptyRootConfig.GetString("akka.persistence.snapshot-store.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.snapshot-store.plugin")); + + var config = emptyRootConfig.GetConfig("akka.persistence.snapshot-store.custom"); + var baseConfig = baseRootConfig.GetConfig("akka.persistence.snapshot-store.redis"); + config.Should().NotBeNull(); + baseConfig.Should().NotBeNull(); + + config.GetString("class").Should().Be(baseConfig.GetString("class")); + config.GetString("configuration-string").Should().Be(baseConfig.GetString("configuration-string")); + config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); + config.GetString("key-prefix").Should().Be(baseConfig.GetString("key-prefix")); + config.GetInt("database").Should().Be(baseConfig.GetInt("database")); + config.GetBoolean("use-database-number-from-connection-string").Should().Be(baseConfig.GetBoolean("use-database-number-from-connection-string")); + } + + [Fact(DisplayName = "RedisSnapshotOptions should generate proper config")] + public void SnapshotOptionsTest() + { + var options = new RedisSnapshotOptions(true) + { + Identifier = "custom", + AutoInitialize = true, + ConfigurationString = "testConfigurationString", + KeyPrefix = "testKeyPrefix", + Database = 999123, + UseDatabaseFromConnectionString = true + }; + + var baseConfig = options.ToConfig(); + + baseConfig.GetString("akka.persistence.snapshot-store.plugin").Should().Be("akka.persistence.snapshot-store.custom"); + + var config = baseConfig.GetConfig("akka.persistence.snapshot-store.custom"); + config.Should().NotBeNull(); + config.GetBoolean("auto-initialize").Should().Be(options.AutoInitialize); + config.GetString("configuration-string").Should().Be(options.ConfigurationString); + config.GetString("key-prefix").Should().Be(options.KeyPrefix); + config.GetInt("database").Should().Be(options.Database); + config.GetBoolean("use-database-number-from-connection-string").Should().Be(options.UseDatabaseFromConnectionString.Value); + } + + const string Json = @" + { + ""Logging"": { + ""LogLevel"": { + ""Default"": ""Information"", + ""Microsoft.AspNetCore"": ""Warning"" + } + }, + ""Akka"": { + ""SnapshotOptions"": { + ""Identifier"": ""customRedis"", + ""AutoInitialize"": true, + ""IsDefaultPlugin"": false, + ""ConfigurationString"": ""ConfigurationStringFromConfigJson"", + ""KeyPrefix"": ""KeyPrefixFromConfigJson"", + ""Database"": 123456, + ""UseDatabaseFromConnectionString"": true, + ""Serializer"": ""TestSerializer"", + } + } + }"; + + [Fact(DisplayName = "RedisSnapshotOptions should be bindable to IConfiguration")] + public void SnapshotOptionsIConfigurationBindingTest() + { + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(Json)); + var jsonConfig = new ConfigurationBuilder().AddJsonStream(stream).Build(); + + var options = jsonConfig.GetSection("Akka:SnapshotOptions").Get(); + options.Identifier.Should().Be("customRedis"); + options.AutoInitialize.Should().BeTrue(); + options.IsDefaultPlugin.Should().BeFalse(); + options.ConfigurationString.Should().Be("ConfigurationStringFromConfigJson"); + options.KeyPrefix.Should().Be("KeyPrefixFromConfigJson"); + options.Database.Should().Be(123456); + options.UseDatabaseFromConnectionString.Should().BeTrue(); + options.Serializer.Should().Be("TestSerializer"); + } + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.Redis/Akka.Persistence.Redis.csproj b/src/Akka.Persistence.Redis/Akka.Persistence.Redis.csproj index a381829..ae09970 100644 --- a/src/Akka.Persistence.Redis/Akka.Persistence.Redis.csproj +++ b/src/Akka.Persistence.Redis/Akka.Persistence.Redis.csproj @@ -4,7 +4,6 @@ false $(NetStandardVersion) README.md - true False @@ -19,4 +18,8 @@ + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 0a5377c..567a613 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -19,8 +19,7 @@ - - + @@ -32,7 +31,6 @@ snupkg akka;actors;actor model;Akka;concurrency;persistence;eventsource;redis icon.png - README.md https://github.com/akkadotnet/Akka.Persistence.Redis Apache-2.0 Upgraded to [Akka.NET 1.5.0](https://github.com/akkadotnet/akka.net/releases/tag/1.5.0) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 27ae5af..6031410 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -7,6 +7,7 @@ +