diff --git a/Akka.Persistence.MongoDb.sln b/Akka.Persistence.MongoDb.sln index b3f311a..7ff99fb 100644 --- a/Akka.Persistence.MongoDb.sln +++ b/Akka.Persistence.MongoDb.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29306.81 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33723.286 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.Persistence.MongoDb", "src\Akka.Persistence.MongoDb\Akka.Persistence.MongoDb.csproj", "{E945AABA-2779-41E8-9B43-8898FFD64F22}" EndProject @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{BE1178E1 build.sh = build.sh EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Akka.Persistence.MongoDb.Hosting", "src\Akka.Persistence.MongoDb.Hosting\Akka.Persistence.MongoDb.Hosting.csproj", "{72B8C165-FE00-465F-A2E9-60B4B79F81AF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -29,6 +31,10 @@ Global {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Debug|Any CPU.Build.0 = Debug|Any CPU {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Release|Any CPU.ActiveCfg = Release|Any CPU {0F9B9BC6-9F86-40E8-BA9B-D27BF3AC7970}.Release|Any CPU.Build.0 = Release|Any CPU + {72B8C165-FE00-465F-A2E9-60B4B79F81AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72B8C165-FE00-465F-A2E9-60B4B79F81AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72B8C165-FE00-465F-A2E9-60B4B79F81AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72B8C165-FE00-465F-A2E9-60B4B79F81AF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Akka.Persistence.MongoDb.Hosting/Akka.Persistence.MongoDb.Hosting.csproj b/src/Akka.Persistence.MongoDb.Hosting/Akka.Persistence.MongoDb.Hosting.csproj new file mode 100644 index 0000000..9a02ec8 --- /dev/null +++ b/src/Akka.Persistence.MongoDb.Hosting/Akka.Persistence.MongoDb.Hosting.csproj @@ -0,0 +1,16 @@ + + + + + $(NetStandardLibVersion) + latest + + + + + + + + + + \ No newline at end of file diff --git a/src/Akka.Persistence.MongoDb.Hosting/AkkaPersistenceMongoDbHostingExtensions.cs b/src/Akka.Persistence.MongoDb.Hosting/AkkaPersistenceMongoDbHostingExtensions.cs new file mode 100644 index 0000000..b4b6ae5 --- /dev/null +++ b/src/Akka.Persistence.MongoDb.Hosting/AkkaPersistenceMongoDbHostingExtensions.cs @@ -0,0 +1,215 @@ +using System; +using Akka.Actor; +using Akka.Hosting; +using Akka.Persistence.Hosting; + +#nullable enable +namespace Akka.Persistence.MongoDb.Hosting; + +public static class AkkaPersistenceMongoDbHostingExtensions +{ + /// + /// Adds Akka.Persistence.SqlServer support to this . + /// + /// + /// The builder instance being configured. + /// + /// + /// Connection string used for database access. + /// + /// + /// + /// Determines which settings should be added by this method call. + /// + /// Default: + /// + /// + /// + /// Should the SQL store table be initialized automatically. + /// + /// Default: false + /// + /// + /// + /// An used to configure an instance. + /// + /// Default: null + /// + /// + /// + /// The configuration identifier for the plugins + /// + /// Default: "sql-server" + /// + /// + /// + /// 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 WithMongoDbPersistence( + this AkkaConfigurationBuilder builder, + string connectionString, + PersistenceMode mode = PersistenceMode.Both, + bool autoInitialize = true, + Action? journalBuilder = null, + string pluginIdentifier = "mongodb", + 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 MongoDbJournalOptions(isDefaultPlugin, pluginIdentifier) + { + ConnectionString = connectionString, + AutoInitialize = autoInitialize, + }; + + var adapters = new AkkaPersistenceJournalBuilder(journalOpt.Identifier, builder); + journalBuilder?.Invoke(adapters); + journalOpt.Adapters = adapters; + + var snapshotOpt = new MongoDbSnapshotOptions(isDefaultPlugin, pluginIdentifier) + { + ConnectionString = connectionString, + AutoInitialize = autoInitialize, + }; + + return mode switch + { + PersistenceMode.Journal => builder.WithMongoDbPersistence(journalOpt, null), + PersistenceMode.SnapshotStore => builder.WithMongoDbPersistence(null, snapshotOpt), + PersistenceMode.Both => builder.WithMongoDbPersistence(journalOpt, snapshotOpt), + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Invalid PersistenceMode defined.") + }; + } + + /// + /// Adds Akka.Persistence.MongoDb 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 WithMongoDbPersistence( + 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"); + + MongoDbJournalOptions? journalOptions = null; + if (journalOptionConfigurator is { }) + { + journalOptions = new MongoDbJournalOptions(isDefaultPlugin); + journalOptionConfigurator(journalOptions); + } + + MongoDbSnapshotOptions? snapshotOptions = null; + if (snapshotOptionConfigurator is { }) + { + snapshotOptions = new MongoDbSnapshotOptions(isDefaultPlugin); + snapshotOptionConfigurator(snapshotOptions); + } + + return builder.WithMongoDbPersistence(journalOptions, snapshotOptions); + } + + /// + /// Adds Akka.Persistence.MongoDb 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 WithMongoDbPersistence( + this AkkaConfigurationBuilder builder, + MongoDbJournalOptions? journalOptions = null, + MongoDbSnapshotOptions? 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(MongoDbPersistence.DefaultConfiguration(), 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(MongoDbPersistence.DefaultConfiguration(), HoconAddMode.Append), + }; + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.MongoDb.Hosting/MongoDbJournalOptions.cs b/src/Akka.Persistence.MongoDb.Hosting/MongoDbJournalOptions.cs new file mode 100644 index 0000000..1984dbf --- /dev/null +++ b/src/Akka.Persistence.MongoDb.Hosting/MongoDbJournalOptions.cs @@ -0,0 +1,82 @@ +using System; +using System.Text; +using Akka.Configuration; +using Akka.Hosting; +using Akka.Persistence.Hosting; + +#nullable enable +namespace Akka.Persistence.MongoDb.Hosting; + +public class MongoDbJournalOptions : JournalOptions +{ + private static readonly Config Default = MongoDbPersistence.DefaultConfiguration() + .GetConfig(MongoDbJournalSettings.JournalConfigPath); + + public MongoDbJournalOptions() : this(true) + { + } + + public MongoDbJournalOptions(bool isDefaultPlugin, string identifier = "mongodb") : base(isDefaultPlugin) + { + Identifier = identifier; + AutoInitialize = true; + } + + /// + /// Connection string used to access the MongoDb, also specifies the database. + /// + public string ConnectionString { get; set; } = ""; + + /// + /// Name of the collection for the event journal or snapshots + /// + public string? Collection { get; set; } + + /// + /// Name of the collection for the event journal metadata + /// + public string? MetadataCollection { get; set; } + + /// + /// Transaction + /// + public bool? UseWriteTransaction { get; set; } + + /// + /// When true, enables BSON serialization (which breaks features like Akka.Cluster.Sharding, AtLeastOnceDelivery, and so on.) + /// + public bool? LegacySerialization { get; set; } + + /// + /// Timeout for individual database operations. + /// + /// + /// Defaults to 10s. + /// + public TimeSpan? CallTimeout { get; set; } + + public override string Identifier { get; set; } + protected override Config InternalDefaultConfig { get; } = Default; + + protected override StringBuilder Build(StringBuilder sb) + { + sb.AppendLine($"connection-string = {ConnectionString.ToHocon()}"); + + if(Collection is not null) + sb.AppendLine($"collection = {Collection.ToHocon()}"); + + if(MetadataCollection is not null) + sb.AppendLine($"metadata-collection = {MetadataCollection.ToHocon()}"); + + if(CallTimeout is not null) + sb.AppendLine($"call-timeout = {CallTimeout.ToHocon()}"); + + if(UseWriteTransaction is not null) + sb.AppendLine($"use-write-transaction = {UseWriteTransaction.ToHocon()}"); + + if(LegacySerialization is not null) + sb.AppendLine($"legacy-serialization = {LegacySerialization.ToHocon()}"); + + return base.Build(sb); + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.MongoDb.Hosting/MongoDbSnapshotOptions.cs b/src/Akka.Persistence.MongoDb.Hosting/MongoDbSnapshotOptions.cs new file mode 100644 index 0000000..d5ca43d --- /dev/null +++ b/src/Akka.Persistence.MongoDb.Hosting/MongoDbSnapshotOptions.cs @@ -0,0 +1,74 @@ +using System; +using System.Text; +using Akka.Configuration; +using Akka.Hosting; +using Akka.Persistence.Hosting; + +#nullable enable +namespace Akka.Persistence.MongoDb.Hosting; + +public class MongoDbSnapshotOptions : SnapshotOptions +{ + private static readonly Config Default = MongoDbPersistence.DefaultConfiguration() + .GetConfig(MongoDbSnapshotSettings.SnapshotStoreConfigPath); + + public MongoDbSnapshotOptions() : this(true) + { + } + + public MongoDbSnapshotOptions(bool isDefault, string identifier = "mongodb") : base(isDefault) + { + Identifier = identifier; + AutoInitialize = true; + } + + /// + /// Connection string used to access the MongoDb, also specifies the database. + /// + public string ConnectionString { get; set; } = ""; + + /// + /// Name of the collection for the event journal or snapshots + /// + public string? Collection { get; set; } + + /// + /// Transaction + /// + public bool? UseWriteTransaction { get; set; } + + /// + /// When true, enables BSON serialization (which breaks features like Akka.Cluster.Sharding, AtLeastOnceDelivery, and so on.) + /// + public bool? LegacySerialization { get; set; } + + /// + /// Timeout for individual database operations. + /// + /// + /// Defaults to 10s. + /// + public TimeSpan? CallTimeout { get; set; } + + public override string Identifier { get; set; } + protected override Config InternalDefaultConfig { get; } = Default; + + protected override StringBuilder Build(StringBuilder sb) + { + sb.AppendLine($"connection-string = {ConnectionString.ToHocon()}"); + + if(UseWriteTransaction is not null) + sb.AppendLine($"use-write-transaction = {UseWriteTransaction.ToHocon()}"); + + if(Collection is not null) + sb.AppendLine($"collection = {Collection.ToHocon()}"); + + if(LegacySerialization is not null) + sb.AppendLine($"legacy-serialization = {LegacySerialization.ToHocon()}"); + + if(CallTimeout is not null) + sb.AppendLine($"call-timeout = {CallTimeout.ToHocon()}"); + + return base.Build(sb); + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.MongoDb.Hosting/README.md b/src/Akka.Persistence.MongoDb.Hosting/README.md new file mode 100644 index 0000000..8305f8d --- /dev/null +++ b/src/Akka.Persistence.MongoDb.Hosting/README.md @@ -0,0 +1,87 @@ +# Akka.Persistence.MongoDb.Hosting + +Akka.Hosting extension methods to add Akka.Persistence.MongoDb to an ActorSystem + +# Akka.Persistence.MongoDb Extension Methods + +## WithMongoDbPersistence() Method + +```csharp +public static AkkaConfigurationBuilder WithMongoDbPersistence( + this AkkaConfigurationBuilder builder, + string connectionString, + PersistenceMode mode = PersistenceMode.Both, + bool autoInitialize = true, + Action? journalBuilder = null, + string pluginIdentifier = "mongodb", + bool isDefaultPlugin = true); +``` + +```csharp +public static AkkaConfigurationBuilder WithMongoDbPersistence( + this AkkaConfigurationBuilder builder, + Action? journalOptionConfigurator = null, + Action? snapshotOptionConfigurator = null, + bool isDefaultPlugin = true) +``` + +```csharp +public static AkkaConfigurationBuilder WithMongoDbPersistence( + this AkkaConfigurationBuilder builder, + MongoDbJournalOptions? journalOptions = null, + MongoDbSnapshotOptions? snapshotOptions = null) +``` + +### Parameters + +* `connectionString` __string__ + + Connection string used for database access. + +* `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 Mongo Db 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 `MongoDbJournalOptions` instance. + +* `snapshotConfigurator` __Action\__ + + An Action delegate to configure a `MongoDbSnapshotOptions` instance. + +* `journalOptions` __MongoDbJournalOptions__ + + An `MongoDbJournalOptions` instance to configure the SqlServer journal. + +* `snapshotOptions` __MongoDbSnapshotOptions__ + + An `MongoDbSnapshotOptions` instance to configure the SqlServer snapshot store. + +## Example + +```csharp +using var host = new HostBuilder() + .ConfigureServices((context, services) => + { + services.AddAkka("mongoDbDemo", (builder, provider) => + { + builder + .WithMongoDbPersistence("your-mongodb-connection-string"); + }); + }).Build(); + +await host.RunAsync(); +``` \ No newline at end of file diff --git a/src/Akka.Persistence.MongoDb.Tests/Akka.Persistence.MongoDb.Tests.csproj b/src/Akka.Persistence.MongoDb.Tests/Akka.Persistence.MongoDb.Tests.csproj index b132dfd..7d7bdd5 100644 --- a/src/Akka.Persistence.MongoDb.Tests/Akka.Persistence.MongoDb.Tests.csproj +++ b/src/Akka.Persistence.MongoDb.Tests/Akka.Persistence.MongoDb.Tests.csproj @@ -1,11 +1,12 @@ - + - + $(NetFrameworkTestVersion);$(NetTestVersion);$(NetCoreTestVersion) + all @@ -18,6 +19,7 @@ + diff --git a/src/Akka.Persistence.MongoDb.Tests/Hosting/MongoDbJournalOptionsSpec.cs b/src/Akka.Persistence.MongoDb.Tests/Hosting/MongoDbJournalOptionsSpec.cs new file mode 100644 index 0000000..4defb24 --- /dev/null +++ b/src/Akka.Persistence.MongoDb.Tests/Hosting/MongoDbJournalOptionsSpec.cs @@ -0,0 +1,153 @@ +using System; +using System.IO; +using System.Text; +using Akka.Configuration; +using Akka.Persistence.MongoDb.Hosting; +using FluentAssertions; +using FluentAssertions.Extensions; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Akka.Persistence.MongoDb.Tests.Hosting +{ + public class MongoDbJournalOptionsSpec + { + [Fact(DisplayName = "MongoDbJournalOptions as default plugin should generate plugin setting")] + public void DefaultPluginJournalOptionsTest() + { + var options = new MongoDbJournalOptions(true); + var config = options.ToConfig(); + + config.GetString("akka.persistence.journal.plugin").Should().Be("akka.persistence.journal.mongodb"); + config.HasPath("akka.persistence.journal.mongodb").Should().BeTrue(); + } + + [Fact(DisplayName = "Empty MongoDbJournalOptions should equal empty config with default fallback")] + public void DefaultJournalOptionsTest() + { + var options = new MongoDbJournalOptions(false); + var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); + var baseRootConfig = Config.Empty + .WithFallback(MongoDbPersistence.DefaultConfiguration()); + + emptyRootConfig.GetString("akka.persistence.journal.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.journal.plugin")); + + var config = emptyRootConfig.GetConfig("akka.persistence.journal.mongodb"); + var baseConfig = baseRootConfig.GetConfig("akka.persistence.journal.mongodb"); + config.Should().NotBeNull(); + baseConfig.Should().NotBeNull(); + + config.GetString("class").Should().Be(baseConfig.GetString("class")); + config.GetString("connection-string").Should().Be(baseConfig.GetString("connection-string")); + config.GetBoolean("use-write-transaction").Should().Be(baseConfig.GetBoolean("use-write-transaction")); + config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); + config.GetString("plugin-dispatcher").Should().Be(baseConfig.GetString("plugin-dispatcher")); + config.GetString("collection").Should().Be(baseConfig.GetString("collection")); + config.GetString("metadata-collection").Should().Be(baseConfig.GetString("metadata-collection")); + config.GetBoolean("legacy-serialization").Should().Be(baseConfig.GetBoolean("legacy-serialization")); + config.GetTimeSpan("call-timeout").Should().Be(baseConfig.GetTimeSpan("call-timeout")); + } + + [Fact(DisplayName = "Empty MongoDbJournalOptions with custom identifier should equal empty config with default fallback")] + public void CustomIdJournalOptionsTest() + { + var options = new MongoDbJournalOptions(false, "custom"); + var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); + var baseRootConfig = Config.Empty + .WithFallback(MongoDbPersistence.DefaultConfiguration()); + + 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.mongodb"); + config.Should().NotBeNull(); + baseConfig.Should().NotBeNull(); + + config.GetString("class").Should().Be(baseConfig.GetString("class")); + config.GetString("connection-string").Should().Be(baseConfig.GetString("connection-string")); + config.GetBoolean("use-write-transaction").Should().Be(baseConfig.GetBoolean("use-write-transaction")); + config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); + config.GetString("plugin-dispatcher").Should().Be(baseConfig.GetString("plugin-dispatcher")); + config.GetString("collection").Should().Be(baseConfig.GetString("collection")); + config.GetString("metadata-collection").Should().Be(baseConfig.GetString("metadata-collection")); + config.GetBoolean("legacy-serialization").Should().Be(baseConfig.GetBoolean("legacy-serialization")); + config.GetTimeSpan("call-timeout").Should().Be(baseConfig.GetTimeSpan("call-timeout")); + } + + [Fact(DisplayName = "MongoDbJournalOptions should generate proper config")] + public void JournalOptionsTest() + { + var options = new MongoDbJournalOptions(true) + { + Identifier = "custom", + AutoInitialize = true, + ConnectionString = "testConnection", + Collection = "testCollection", + MetadataCollection = "metadataCollection", + UseWriteTransaction = true, + LegacySerialization = true, + CallTimeout = TimeSpan.FromHours(2) + }; + + 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.GetString("connection-string").Should().Be(options.ConnectionString); + config.GetBoolean("auto-initialize").Should().Be(options.AutoInitialize); + config.GetString("collection").Should().Be(options.Collection); + config.GetString("metadata-collection").Should().Be(options.MetadataCollection); + config.GetBoolean("use-write-transaction").Should().Be(options.UseWriteTransaction.Value); + config.GetBoolean("legacy-serialization").Should().Be(options.LegacySerialization.Value); + config.GetTimeSpan("call-timeout").Should().Be(options.CallTimeout.Value); + } + + const string Json = @" + { + ""Logging"": { + ""LogLevel"": { + ""Default"": ""Information"", + ""Microsoft.AspNetCore"": ""Warning"" + } + }, + ""Akka"": { + ""JournalOptions"": { + ""ConnectionString"": ""mongodb://localhost:27017"", + ""UseWriteTransaction"": ""true"", + ""Identifier"": ""custommongodb"", + ""AutoInitialize"": true, + ""IsDefaultPlugin"": false, + ""Collection"": ""CustomEnventJournalCollection"", + ""MetadataCollection"": ""CustomMetadataCollection"", + ""LegacySerialization"" : ""true"", + ""CallTimeout"": ""00:10:00"", + ""Serializer"": ""hyperion"", + } + } + }"; + + [Fact(DisplayName = "MongoDbJournalOptions should be bindable to IConfiguration")] + public void JournalOptionsIConfigurationBindingTest() + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(Json)); + var jsonConfig = new ConfigurationBuilder().AddJsonStream(stream).Build(); + + var options = jsonConfig.GetSection("Akka:JournalOptions").Get(); + options.ConnectionString.Should().Be("mongodb://localhost:27017"); + options.UseWriteTransaction.Should().BeTrue(); + options.Identifier.Should().Be("custommongodb"); + options.AutoInitialize.Should().BeTrue(); + options.IsDefaultPlugin.Should().BeFalse(); + options.Collection.Should().Be("CustomEnventJournalCollection"); + options.MetadataCollection.Should().Be("CustomMetadataCollection"); + options.LegacySerialization.Should().BeTrue(); + options.CallTimeout.Should().Be(10.Minutes()); + options.Serializer.Should().Be("hyperion"); + + // Dispose called here as project not using latest language features. + stream.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.MongoDb.Tests/Hosting/MongoDbSnapshotOptionsSpec.cs b/src/Akka.Persistence.MongoDb.Tests/Hosting/MongoDbSnapshotOptionsSpec.cs new file mode 100644 index 0000000..fd02cd7 --- /dev/null +++ b/src/Akka.Persistence.MongoDb.Tests/Hosting/MongoDbSnapshotOptionsSpec.cs @@ -0,0 +1,148 @@ +using System; +using System.IO; +using System.Text; +using Akka.Configuration; +using Akka.Persistence.MongoDb.Hosting; +using FluentAssertions; +using FluentAssertions.Extensions; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Akka.Persistence.MongoDb.Tests.Hosting +{ + public class MongoDbSnapshotOptionsSpec + { + [Fact(DisplayName = "MongoDbSnapshotOptions as default plugin should generate plugin setting")] + public void DefaultPluginSnapshotOptionsTest() + { + var options = new MongoDbSnapshotOptions(true); + var config = options.ToConfig(); + + config.GetString("akka.persistence.snapshot-store.plugin").Should().Be("akka.persistence.snapshot-store.mongodb"); + config.HasPath("akka.persistence.snapshot-store.mongodb").Should().BeTrue(); + } + + [Fact(DisplayName = "Empty MongoDbSnapshotOptions with default fallback should return default config")] + public void DefaultSnapshotOptionsTest() + { + var options = new MongoDbSnapshotOptions(false); + var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); + var baseRootConfig = Config.Empty + .WithFallback(MongoDbPersistence.DefaultConfiguration()); + + emptyRootConfig.GetString("akka.persistence.snapshot-store.plugin").Should().Be(baseRootConfig.GetString("akka.persistence.snapshot-store.plugin")); + + var config = emptyRootConfig.GetConfig("akka.persistence.snapshot-store.mongodb"); + var baseConfig = baseRootConfig.GetConfig("akka.persistence.snapshot-store.mongodb"); + config.Should().NotBeNull(); + baseConfig.Should().NotBeNull(); + + config.GetString("class").Should().Be(baseConfig.GetString("class")); + config.GetString("connection-string").Should().Be(baseConfig.GetString("connection-string")); + config.GetBoolean("use-write-transaction").Should().Be(baseConfig.GetBoolean("use-write-transaction")); + config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); + config.GetString("plugin-dispatcher").Should().Be(baseConfig.GetString("plugin-dispatcher")); + config.GetString("collection").Should().Be(baseConfig.GetString("collection")); + config.GetBoolean("legacy-serialization").Should().Be(baseConfig.GetBoolean("legacy-serialization")); + config.GetTimeSpan("call-timeout").Should().Be(baseConfig.GetTimeSpan("call-timeout")); + } + + [Fact(DisplayName = "Empty MongoDbSnapshotOptions with custom identifier should equal empty config with default fallback")] + public void CustomIdSnapshotOptionsTest() + { + var options = new MongoDbSnapshotOptions(false, "custom"); + var emptyRootConfig = options.ToConfig().WithFallback(options.DefaultConfig); + var baseRootConfig = Config.Empty + .WithFallback(MongoDbPersistence.DefaultConfiguration()); + + 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.mongodb"); + config.Should().NotBeNull(); + baseConfig.Should().NotBeNull(); + + config.GetString("class").Should().Be(baseConfig.GetString("class")); + config.GetString("connection-string").Should().Be(baseConfig.GetString("connection-string")); + config.GetBoolean("use-write-transaction").Should().Be(baseConfig.GetBoolean("use-write-transaction")); + config.GetBoolean("auto-initialize").Should().Be(baseConfig.GetBoolean("auto-initialize")); + config.GetString("plugin-dispatcher").Should().Be(baseConfig.GetString("plugin-dispatcher")); + config.GetString("collection").Should().Be(baseConfig.GetString("collection")); + config.GetBoolean("legacy-serialization").Should().Be(baseConfig.GetBoolean("legacy-serialization")); + config.GetTimeSpan("call-timeout").Should().Be(baseConfig.GetTimeSpan("call-timeout")); + } + + [Fact(DisplayName = "MongoDbSnapshotOptions should generate proper config")] + public void SnapshotOptionsTest() + { + var options = new MongoDbSnapshotOptions(true) + { + Identifier = "custom", + AutoInitialize = true, + ConnectionString = "testConnection", + Collection = "testCollection", + UseWriteTransaction = true, + LegacySerialization = true, + CallTimeout = TimeSpan.FromHours(2) + }; + + var baseConfig = options.ToConfig() + .WithFallback(MongoDbPersistence.DefaultConfiguration()); + + 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.GetString("connection-string").Should().Be(options.ConnectionString); + config.GetBoolean("auto-initialize").Should().Be(options.AutoInitialize); + config.GetString("collection").Should().Be(options.Collection); + config.GetBoolean("use-write-transaction").Should().Be(options.UseWriteTransaction.Value); + config.GetBoolean("legacy-serialization").Should().Be(options.LegacySerialization.Value); + config.GetTimeSpan("call-timeout").Should().Be(options.CallTimeout.Value); + } + + [Fact(DisplayName = "MongoDbSnapshotOptions should be bindable to IConfiguration")] + public void SnapshotOptionsIConfigurationBindingTest() + { + const string json = @" + { + ""Logging"": { + ""LogLevel"": { + ""Default"": ""Information"", + ""Microsoft.AspNetCore"": ""Warning"" + } + }, + ""Akka"": { + ""SnapshotOptions"": { + ""ConnectionString"": ""mongodb://localhost:27017"", + ""UseWriteTransaction"": ""true"", + ""Identifier"": ""custommongodb"", + ""AutoInitialize"": true, + ""IsDefaultPlugin"": false, + ""Collection"": ""CustomEnventJournalCollection"", + ""LegacySerialization"" : ""true"", + ""CallTimeout"": ""00:10:00"", + ""Serializer"": ""hyperion"", + } + } + }"; + + var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + var jsonConfig = new ConfigurationBuilder().AddJsonStream(stream).Build(); + + var options = jsonConfig.GetSection("Akka:SnapshotOptions").Get(); + options.ConnectionString.Should().Be("mongodb://localhost:27017"); + options.UseWriteTransaction.Should().BeTrue(); + options.Identifier.Should().Be("custommongodb"); + options.AutoInitialize.Should().BeTrue(); + options.IsDefaultPlugin.Should().BeFalse(); + options.Collection.Should().Be("CustomEnventJournalCollection"); + options.LegacySerialization.Should().BeTrue(); + options.CallTimeout.Should().Be(10.Minutes()); + options.Serializer.Should().Be("hyperion"); + + // Dispose called here as project not using latest language features. + stream.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Akka.Persistence.MongoDb/MongoDbSettings.cs b/src/Akka.Persistence.MongoDb/MongoDbSettings.cs index 82003c8..59d4490 100644 --- a/src/Akka.Persistence.MongoDb/MongoDbSettings.cs +++ b/src/Akka.Persistence.MongoDb/MongoDbSettings.cs @@ -64,6 +64,8 @@ protected MongoDbSettings(Config config) /// public class MongoDbJournalSettings : MongoDbSettings { + public const string JournalConfigPath = "akka.persistence.journal.mongodb"; + public string MetadataCollection { get; private set; } public MongoDbJournalSettings(Config config) : base(config) @@ -81,6 +83,8 @@ public MongoDbJournalSettings(Config config) : base(config) /// public class MongoDbSnapshotSettings : MongoDbSettings { + public const string SnapshotStoreConfigPath = "akka.persistence.snapshot-store.mongodb"; + public MongoDbSnapshotSettings(Config config) : base(config) { if (config == null) diff --git a/src/common.props b/src/common.props index cac50d1..8a10f41 100644 --- a/src/common.props +++ b/src/common.props @@ -22,6 +22,7 @@ 2.4.2 17.6.3 1.5.8 + 6.11.0