diff --git a/CQRSlite.sln b/CQRSlite.sln index 93821b9..ef0081f 100644 --- a/CQRSlite.sln +++ b/CQRSlite.sln @@ -1,29 +1,33 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26228.4 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29709.97 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Framework", "Framework", "{693A83BF-7B14-459E-B245-82B7B9AF810E}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample", "Sample", "{DC63AD03-D195-4E43-8120-0BD27AC86D46}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CQRSlite", "Framework\CQRSlite\CQRSlite.csproj", "{71DFCD1D-AC1E-4CAE-9998-C747662DBF0E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CQRSlite", "Framework\CQRSlite\CQRSlite.csproj", "{71DFCD1D-AC1E-4CAE-9998-C747662DBF0E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CQRSlite.Tests", "Framework\CQRSlite.Tests\CQRSlite.Tests.csproj", "{2694E1AD-335F-467B-9C0C-4979871830D3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CQRSlite.Tests", "Framework\CQRSlite.Tests\CQRSlite.Tests.csproj", "{2694E1AD-335F-467B-9C0C-4979871830D3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CQRSlite.Tests.Extensions", "Framework\CQRSlite.Tests.Extensions\CQRSlite.Tests.Extensions.csproj", "{0A3F1993-C603-4CD9-BCB3-5791255B54AA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CQRSlite.Tests.Extensions", "Framework\CQRSlite.Tests.Extensions\CQRSlite.Tests.Extensions.csproj", "{0A3F1993-C603-4CD9-BCB3-5791255B54AA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CQRSCode", "Sample\CQRSCode\CQRSCode.csproj", "{94A8AD91-1300-471D-AAC0-F6F310A54927}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CQRSCode", "Sample\CQRSCode\CQRSCode.csproj", "{94A8AD91-1300-471D-AAC0-F6F310A54927}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CQRSWeb", "Sample\CQRSWeb\CQRSWeb.csproj", "{C1214159-C75F-4CAC-82BD-A5E4E3F7F0E2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CQRSWeb", "Sample\CQRSWeb\CQRSWeb.csproj", "{C1214159-C75F-4CAC-82BD-A5E4E3F7F0E2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CQRSTest", "Sample\CQRSTest\CQRSTest.csproj", "{DE6E654C-00A3-4829-AABF-42F534078D00}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CQRSTest", "Sample\CQRSTest\CQRSTest.csproj", "{DE6E654C-00A3-4829-AABF-42F534078D00}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D88E36CC-4CB2-436D-881B-5B6BB4174B42}" ProjectSection(SolutionItems) = preProject CQRSlite.nuspec = CQRSlite.nuspec EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CQRSlite.Storage", "Framework\CQRSlite.Storage\CQRSlite.Storage.csproj", "{0B20F4C1-94BE-41C6-BAA6-DC128A5D8ED3}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CQRSlite.Storage.Tests", "Framework\CQRSlite.Storage.Tests\CQRSlite.Storage.Tests.csproj", "{329529E4-0834-417F-B527-F02E1B36EAF2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -54,6 +58,14 @@ Global {DE6E654C-00A3-4829-AABF-42F534078D00}.Debug|Any CPU.Build.0 = Debug|Any CPU {DE6E654C-00A3-4829-AABF-42F534078D00}.Release|Any CPU.ActiveCfg = Release|Any CPU {DE6E654C-00A3-4829-AABF-42F534078D00}.Release|Any CPU.Build.0 = Release|Any CPU + {0B20F4C1-94BE-41C6-BAA6-DC128A5D8ED3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B20F4C1-94BE-41C6-BAA6-DC128A5D8ED3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B20F4C1-94BE-41C6-BAA6-DC128A5D8ED3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B20F4C1-94BE-41C6-BAA6-DC128A5D8ED3}.Release|Any CPU.Build.0 = Release|Any CPU + {329529E4-0834-417F-B527-F02E1B36EAF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {329529E4-0834-417F-B527-F02E1B36EAF2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {329529E4-0834-417F-B527-F02E1B36EAF2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {329529E4-0834-417F-B527-F02E1B36EAF2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -65,5 +77,10 @@ Global {94A8AD91-1300-471D-AAC0-F6F310A54927} = {DC63AD03-D195-4E43-8120-0BD27AC86D46} {C1214159-C75F-4CAC-82BD-A5E4E3F7F0E2} = {DC63AD03-D195-4E43-8120-0BD27AC86D46} {DE6E654C-00A3-4829-AABF-42F534078D00} = {DC63AD03-D195-4E43-8120-0BD27AC86D46} + {0B20F4C1-94BE-41C6-BAA6-DC128A5D8ED3} = {693A83BF-7B14-459E-B245-82B7B9AF810E} + {329529E4-0834-417F-B527-F02E1B36EAF2} = {693A83BF-7B14-459E-B245-82B7B9AF810E} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9F74664F-E6ED-4414-8D79-A70086E2A37D} EndGlobalSection EndGlobal diff --git a/Framework/CQRSlite.Storage.Tests/CQRSlite.Storage.Tests.csproj b/Framework/CQRSlite.Storage.Tests/CQRSlite.Storage.Tests.csproj new file mode 100644 index 0000000..eb17e3c --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/CQRSlite.Storage.Tests.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + + + + + + + + + diff --git a/Framework/CQRSlite.Storage.Tests/Events/ForumCommandHandlers.cs b/Framework/CQRSlite.Storage.Tests/Events/ForumCommandHandlers.cs new file mode 100644 index 0000000..fd0c91a --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/Events/ForumCommandHandlers.cs @@ -0,0 +1,43 @@ +using CQRSlite.Commands; +using CQRSlite.Domain; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CQRSlite.Storage.Tests.Events { + public class ForumCommandHandlers : ICancellableCommandHandler, + ICancellableCommandHandler { + + private ISession _session; + public ForumCommandHandlers(ISession session) { + _session = session; + + } + + + public async Task Handle(TestCreateForum message, CancellationToken token = default) { + var forum = new TestForum(message.Id, + message.Name, + message.Description, + message.ForumGroupId, + message.CanView, + message.CanPost, + message.CanModerate + ); + + await _session.Add(forum); + await _session.Commit(); + } + + public async Task Handle(TestAddPostToForum post, CancellationToken token = default) { + var forum = await _session.Get(post.ForumId, post.ExpectedVersion, token); + + + forum.AddPost(post.Id, post.Poster, post.PosterRoles, post.Markup, post.Posted, post.Tags); + + await _session.Commit(token); + } + } +} diff --git a/Framework/CQRSlite.Storage.Tests/Events/ForumEventHandlers.cs b/Framework/CQRSlite.Storage.Tests/Events/ForumEventHandlers.cs new file mode 100644 index 0000000..5bdbbe8 --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/Events/ForumEventHandlers.cs @@ -0,0 +1,13 @@ +using CQRSlite.Events; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace CQRSlite.Storage.Tests.Events { + public class ForumEventHandlers : IEventHandler { + public Task Handle(TestForumCreated message) { + return Task.CompletedTask; + } + } +} diff --git a/Framework/CQRSlite.Storage.Tests/Events/TestAddPostToForum.cs b/Framework/CQRSlite.Storage.Tests/Events/TestAddPostToForum.cs new file mode 100644 index 0000000..0a5e379 --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/Events/TestAddPostToForum.cs @@ -0,0 +1,35 @@ +using CQRSlite.Commands; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage.Tests.Events { + public class TestAddPostToForum : ICommand { + public TestAddPostToForum(Guid id, + Guid forumId, + string poster, + string[] PosterRoles, + string Markup, + DateTimeOffset posted, + string[] tags, + int originalVersion) { + Id = id; + ForumId = forumId; + Poster = poster; + this.PosterRoles = PosterRoles; + this.Markup = Markup; + Posted = posted; + Tags = tags; + ExpectedVersion = originalVersion; + } + + public Guid Id { get; set; } + public Guid ForumId { get; set; } + public string Poster { get; set; } + public string[] PosterRoles { get; set; } + public string Markup { get; set; } + public DateTimeOffset Posted { get; set; } + public string[] Tags { get; set; } + public int ExpectedVersion { get; set; } + } +} diff --git a/Framework/CQRSlite.Storage.Tests/Events/TestCreateForum.cs b/Framework/CQRSlite.Storage.Tests/Events/TestCreateForum.cs new file mode 100644 index 0000000..128bac6 --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/Events/TestCreateForum.cs @@ -0,0 +1,29 @@ +using CQRSlite.Commands; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage.Tests.Events { + public class TestCreateForum : ICommand { + + + public TestCreateForum(Guid id, string name, string description, Guid forumGroupId, string[] CanView, string[] CanPost, string[] CanModerate) { + + this.Id = id; + this.Name = name; + this.Description = description; + ForumGroupId = forumGroupId; + this.CanView = CanView; + this.CanPost = CanPost; + this.CanModerate = CanModerate; + } + + public string Name { get; set; } + public string Description { get; set; } + public Guid ForumGroupId { get; set; } + public string[] CanView { get; set; } + public string[] CanPost { get; set; } + public string[] CanModerate { get; set; } + public Guid Id { get; set; } + } +} \ No newline at end of file diff --git a/Framework/CQRSlite.Storage.Tests/Events/TestForum.cs b/Framework/CQRSlite.Storage.Tests/Events/TestForum.cs new file mode 100644 index 0000000..b77cccf --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/Events/TestForum.cs @@ -0,0 +1,127 @@ +using CQRSlite.Domain; +using CQRSlite.Storage.Tests.Events; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage.Tests { + [SnapshotStrategy(1)] + public class TestForum : Snapshotting.SnapshotAggregateRoot { + public string Name { get; private set; } + public string Description { get; private set; } + public Guid ForumGroupId { get; private set; } + public string[] CanView { get; private set; } + public string[] CanPost { get; private set; } + public string[] CanModerate { get; private set; } + + private TestForum() { } + public TestForum(Guid id, + string name, + string description, + Guid forumGroupId, + string[] canView, + string[] canPost, + string[] canModerate) { + + if (string.IsNullOrEmpty(name)) { + throw new ArgumentException("message", nameof(name)); + } + + if (string.IsNullOrEmpty(description)) { + throw new ArgumentException("message", nameof(description)); + } + + if (canView is null) { + throw new ArgumentNullException(nameof(canView)); + } + + if (canPost is null) { + throw new ArgumentNullException(nameof(canPost)); + } + + if (canModerate is null) { + throw new ArgumentNullException(nameof(canModerate)); + } + + Id = id; + + ApplyChange(new TestForumCreated(Id, name, description, forumGroupId, canView, canPost, canModerate)); + } + + private void Apply(TestForumCreated e) { + this.Name = e.Name; + this.Description = e.Description; + this.ForumGroupId = e.ForumGroupId; + this.CanView = e.CanView; + this.CanPost = e.CanPost; + this.CanModerate = e.CanModerate; + } + + + public void AddPost(Guid id, + string poster, + string[] PosterRoles, + string Markup, + DateTimeOffset posted, + string[] tags) { + + // TODO: CanPost test + + TestPostAdded post = new TestPostAdded(this.Id, + this.Id, + poster, + PosterRoles, + Markup, + posted, + tags); + + ApplyChange(post); + + } + public IList Posts { get; set; } + private void Apply(TestPostAdded e) { + if(this.Posts == null) { + this.Posts = new List(); + } + Posts.Add(new TestPost() { + ForumId = this.Id, + Id = e.Id, + Markup = e.Markup, + Posted = e.Posted, + Poster = e.Poster, + PosterRoles = e.PosterRoles, + Tags = e.Tags, + Number = (this.Posts.Count + 1) + }) ; + + } + + protected override TestForumSnapshot CreateSnapshot() { + return new TestForumSnapshot() { + CanModerate = this.CanModerate, + CanPost = this.CanPost, + CanView = this.CanView, + Description = this.Description, + ForumGroupId = this.ForumGroupId, + Id = this.Id, + Name = this.Name, + Posts = this.Posts, + Version = this.Version + + + }; + } + + protected override void RestoreFromSnapshot(TestForumSnapshot snapshot) { + this.CanModerate = snapshot.CanModerate; + this.CanPost = snapshot.CanPost; + this.CanView = snapshot.CanView; + this.Description = snapshot.Description; + this.ForumGroupId = snapshot.ForumGroupId; + this.Id = snapshot.Id; + this.Name = snapshot.Name; + this.Posts = snapshot.Posts; + this.Version = snapshot.Version; + } + } +} diff --git a/Framework/CQRSlite.Storage.Tests/Events/TestForumCreated.cs b/Framework/CQRSlite.Storage.Tests/Events/TestForumCreated.cs new file mode 100644 index 0000000..e5bca29 --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/Events/TestForumCreated.cs @@ -0,0 +1,30 @@ +using CQRSlite.Events; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage.Tests { + public class TestForumCreated : IEvent { + + public TestForumCreated(Guid id, string name, string description, Guid forumGroupId, string[] CanView, string[] CanPost, string[] CanModerate) { + + this.Id = id; + this.Name = name; + this.Description = description; + ForumGroupId = forumGroupId; + this.CanView = CanView; + this.CanPost = CanPost; + this.CanModerate = CanModerate; + } + + public string Name { get; set; } + public string Description { get; set; } + public Guid ForumGroupId { get; set; } + public string[] CanView { get; set; } + public string[] CanPost { get; set; } + public string[] CanModerate { get; set; } + public Guid Id { get; set; } + public int Version { get; set; } + public DateTimeOffset TimeStamp { get; set ; } + } +} diff --git a/Framework/CQRSlite.Storage.Tests/Events/TestForumSnapshot.cs b/Framework/CQRSlite.Storage.Tests/Events/TestForumSnapshot.cs new file mode 100644 index 0000000..18d9cb9 --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/Events/TestForumSnapshot.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage.Tests.Events { + public class TestForumSnapshot : Snapshotting.Snapshot { + public string Name { get; set; } + public string Description { get; set; } + public Guid ForumGroupId { get; set; } + public string[] CanView { get; set; } + public string[] CanPost { get; set; } + public string[] CanModerate { get; set; } + public IList Posts { get; set; } + } +} diff --git a/Framework/CQRSlite.Storage.Tests/Events/TestPost.cs b/Framework/CQRSlite.Storage.Tests/Events/TestPost.cs new file mode 100644 index 0000000..05c96b4 --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/Events/TestPost.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage.Tests.Events { + public class TestPost { + + public Guid Id { get; set; } + public Guid ForumId { get; set; } + public string Poster { get; set; } + public string[] PosterRoles { get; set; } + public string Markup { get; set; } + public DateTimeOffset Posted { get; set; } + public string[] Tags { get; set; } + public int Number { get; set; } + } +} diff --git a/Framework/CQRSlite.Storage.Tests/Events/TestPostAdded.cs b/Framework/CQRSlite.Storage.Tests/Events/TestPostAdded.cs new file mode 100644 index 0000000..fcdc0d3 --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/Events/TestPostAdded.cs @@ -0,0 +1,37 @@ +using CQRSlite.Events; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage.Tests.Events { + public class TestPostAdded : IEvent { + + public TestPostAdded(Guid id, + Guid forumId, + string poster, + string[] PosterRoles, + string Markup, + DateTimeOffset posted, + string[] tags) { + Id = id; + ForumId = forumId; + Poster = poster; + this.PosterRoles = PosterRoles; + this.Markup = Markup; + Posted = posted; + Tags = tags; + + } + + public Guid Id { get; set; } + public Guid ForumId { get; set; } + public string Poster { get; set; } + public string[] PosterRoles { get; set; } + public string Markup { get; set; } + public DateTimeOffset Posted { get; set; } + public string[] Tags { get; set; } + public int Version { get; set; } + public DateTimeOffset TimeStamp { get; set; } + public int Number { get; set; } + } +} diff --git a/Framework/CQRSlite.Storage.Tests/TestEvents.cs b/Framework/CQRSlite.Storage.Tests/TestEvents.cs new file mode 100644 index 0000000..54ec241 --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/TestEvents.cs @@ -0,0 +1,89 @@ +using CQRSlite.Commands; +using CQRSlite.Domain; +using CQRSlite.Storage.Tests.Events; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Linq; + +namespace CQRSlite.Storage.Tests { + + + [TestClass] + public class TestEvents { + + const string LoremIpsum = @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; + [DataTestMethod] + [DataRow("Announcements", + "View important announcements from the game operators", + "87097683-97e3-449d-b82c-210331884858", + new string[] { "All" }, + new string[] { "Owners" }, + new string[] { "Moderators" } + )] + [DataRow("General", + "View general announcements from the game operators", + "bc0fdc6c-db79-4930-9813-c8df338a5f1a", + new string[] { "All" }, + new string[] { "Owners" }, + new string[] { "Moderators" } + )] + [DataRow("Bugs", + "View general announcements from the game operators", + "55704416-cd80-49d3-8fe9-7ed4abbe1088", + new string[] { "All" }, + new string[] { "Owners" }, + new string[] { "Moderators" } + )] + + public void TestForums(string name, string description, string forumGroupStringId, string[] CanView, string[] CanPost, string[] CanModerate) { + + ISession session = (ISession)TestStartup.GetServiceProvider().GetService(typeof(ISession)); + + ICommandSender cmd = (ICommandSender)TestStartup.GetServiceProvider().GetService(typeof(ICommandSender)); + + Guid forumGroupId = Guid.Parse(forumGroupStringId); + Guid id = Guid.NewGuid(); + cmd.Send(new TestCreateForum(id, + name, + description, + forumGroupId, + CanView, + CanPost, + CanModerate) + ).Wait(); + + + + TestForum f = session.Get(id).Result; +//Assert.AreEqual(f.CanModerate, CanModerate); + //Assert.AreEqual(f.CanPost, CanPost); + //Assert.AreEqual(f.CanView, CanView); + Assert.AreEqual(f.Description, description); + Assert.AreEqual(f.ForumGroupId, forumGroupId); + Assert.AreEqual(f.Id, id); + Assert.AreEqual(f.Name, name); + int posts = 100; + + for (int postnum = 0; postnum < posts; postnum++) { + f = session.Get(id).Result; + cmd.Send(new TestAddPostToForum( + Guid.NewGuid(), + f.Id, + "pm@focopmile.com", + CanPost, + String.Join(Environment.NewLine, Enumerable.Repeat(LoremIpsum, 40)), + DateTime.Now, + new string[] { "hi", "there" }, + f.Version + + )).Wait(); + + } + + + f = session.Get(id).Result; + Assert.AreEqual(f.Posts.Count, posts); + + } + } +} diff --git a/Framework/CQRSlite.Storage.Tests/TestStartup.cs b/Framework/CQRSlite.Storage.Tests/TestStartup.cs new file mode 100644 index 0000000..8acbafb --- /dev/null +++ b/Framework/CQRSlite.Storage.Tests/TestStartup.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Text; +using CQRSlite.Caching; +using CQRSlite.Commands; +using CQRSlite.Domain; +using CQRSlite.Events; +using CQRSlite.Queries; +using CQRSlite.Routing; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Reflection; +using CQRSlite.Messages; +using CQRSlite.Storage.Tests.Events; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; +using CQRSlite.Snapshotting; + +namespace CQRSlite.Storage.Tests { + + [TestClass] + public class TestStartup { + private static object _servicesLock = new object(); + private static IServiceProvider Services { get; set; } + + public static IServiceProvider GetServiceProvider() { + if (Services == null) { + lock (_servicesLock) { + if (Services == null) { + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(); + var config = builder.Build(); + config[BlobEventStore.CONNECTIONSTRING_KEY] = LOCALCONNECTIONSTRING; + config[BlobSnapshotStore.CONNECTIONSTRING_KEY] = LOCALCONNECTIONSTRING; + IConfigurationRoot configuration = builder.Build(); + + IServiceCollection services = new ServiceCollection(); + //Add Cqrs services + + // ISnapshotStore snapshotStore, ISnapshotStrategy snapshotStrategy + services.AddSingleton(new Router()); + services.AddSingleton(y => y.GetService()); + services.AddSingleton(y => y.GetService()); + services.AddSingleton(y => y.GetService()); + services.AddSingleton(y => y.GetService()); + services.AddSingleton(config); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddScoped(y => new BlobPathProvider("events", "snapshots")); + services.AddScoped(y => new BlobPathProvider("events", "snapshots")); + + services.AddScoped(y => new SnapshotRepository(y.GetService(), y.GetService(), new Repository(y.GetService()), y.GetService())); + services.AddScoped(); + //Scan for commandhandlers and eventhandlers + services.Scan(scan => scan + .FromAssemblies(typeof(ForumCommandHandlers).GetTypeInfo().Assembly) + .AddClasses(classes => classes.Where(x => { + var allInterfaces = x.GetInterfaces(); + return + allInterfaces.Any(y => y.GetTypeInfo().IsGenericType && y.GetTypeInfo().GetGenericTypeDefinition() == typeof(IHandler<>)) || + allInterfaces.Any(y => y.GetTypeInfo().IsGenericType && y.GetTypeInfo().GetGenericTypeDefinition() == typeof(ICancellableHandler<>)) || + allInterfaces.Any(y => y.GetTypeInfo().IsGenericType && y.GetTypeInfo().GetGenericTypeDefinition() == typeof(IQueryHandler<,>)) || + allInterfaces.Any(y => y.GetTypeInfo().IsGenericType && y.GetTypeInfo().GetGenericTypeDefinition() == typeof(ICancellableQueryHandler<,>)); + })) + .AsSelf() + .WithTransientLifetime() + ); + + Services = services.BuildServiceProvider(); + + RouteRegistrar r = new RouteRegistrar(Services); + r.RegisterInAssemblyOf(typeof(ForumCommandHandlers)); + } + } + } + return Services; + } + + [TestInitialize] + public static void Startup() { + + Console.WriteLine("TestInitialize"); + } + + + private static string _LOCALCONNECTIONSTRING = null; + public static string LOCALCONNECTIONSTRING { + + get + { + //return "azure.blob://development=true"; + if (true) { + if (string.IsNullOrEmpty(_LOCALCONNECTIONSTRING)) { + string path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "CQRSlist.Storage.Tests"); + if (System.IO.Directory.Exists(path)) { + try { + System.IO.Directory.Delete(path, true); + } + finally { } + } + System.IO.Directory.CreateDirectory(path); + Console.WriteLine(path); + _LOCALCONNECTIONSTRING = "disk://path=" + path; + } + } + // use if you have the Azure emulator + //return "azure.blob://development=true"; + return _LOCALCONNECTIONSTRING; + } + } + + + + } +} diff --git a/Framework/CQRSlite.Storage/AttributeSnapshotStrategy.cs b/Framework/CQRSlite.Storage/AttributeSnapshotStrategy.cs new file mode 100644 index 0000000..49bbc59 --- /dev/null +++ b/Framework/CQRSlite.Storage/AttributeSnapshotStrategy.cs @@ -0,0 +1,50 @@ +using CQRSlite.Domain; +using CQRSlite.Snapshotting; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace CQRSlite.Storage { + public class AttributeSnapshotStrategy : ISnapshotStrategy { + + public const string DEFAULT_SNAPSHOT_INTERVAL_KEY = "CQRSlite.Storage.DefaultSnapshotInterval"; + private int defaultSnapshotInterval = 100; + public AttributeSnapshotStrategy(IConfiguration config) { + if(config != null && !string.IsNullOrEmpty(config[DEFAULT_SNAPSHOT_INTERVAL_KEY])) { + int.TryParse(config[DEFAULT_SNAPSHOT_INTERVAL_KEY], out defaultSnapshotInterval); + } + } + + public bool IsSnapshotable(Type aggregateType) { + if (aggregateType.GetTypeInfo().BaseType == null) + return false; + if (aggregateType.GetTypeInfo().BaseType.GetTypeInfo().IsGenericType && + aggregateType.GetTypeInfo().BaseType.GetGenericTypeDefinition() == typeof(SnapshotAggregateRoot<>)) + return true; + return IsSnapshotable(aggregateType.GetTypeInfo().BaseType); + } + + public bool ShouldMakeSnapShot(AggregateRoot aggregate) { + int snapshotInterval = defaultSnapshotInterval; + + // Get instance of the attribute. + SnapshotStrategyAttribute snapshotStrategy = + (SnapshotStrategyAttribute)Attribute.GetCustomAttribute(aggregate.GetType(), typeof(SnapshotStrategyAttribute)); + + if(snapshotStrategy != null) { + snapshotInterval = snapshotStrategy.Interval; + } + + if (!IsSnapshotable(aggregate.GetType())) + return false; + + var i = aggregate.Version; + for (var j = 0; j < aggregate.GetUncommittedChanges().Length; j++) + if (++i % snapshotInterval == 0 && i != 0) + return true; + return false; + } + } +} diff --git a/Framework/CQRSlite.Storage/BlobEventStore.cs b/Framework/CQRSlite.Storage/BlobEventStore.cs new file mode 100644 index 0000000..7e5c4cd --- /dev/null +++ b/Framework/CQRSlite.Storage/BlobEventStore.cs @@ -0,0 +1,82 @@ +extern alias AzureStorageNet; +using CQRSlite.Events; +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Storage.Net; +using Storage.Net.Blobs; +using Newtonsoft.Json; +using AzureStorageNet; + +namespace CQRSlite.Storage { + public class BlobEventStore : IEventStore { + + public static string CONNECTIONSTRING_KEY = "CQRSlite.Storage.BlobEventStore.ConnectionString"; + + IBlobStorage _storage; + IBlobEventStorePathProvider _path; + IEventPublisher _publisher; + public BlobEventStore(IConfiguration config, IBlobEventStorePathProvider path, IEventPublisher publisher) { + + if(config[CONNECTIONSTRING_KEY].StartsWith("azure")) { + AzureStorageNet.Storage.Net.Factory.UseAzureStorage(StorageFactory.Modules); + } + _storage = StorageFactory.Blobs.FromConnectionString(config[CONNECTIONSTRING_KEY]); + _path = path; + _publisher = publisher; + + } + public async Task> Get(Guid aggregateId, int fromVersion, CancellationToken cancellationToken = default) { + + IEvent lastEvent = null; + if (fromVersion <= -1) { + fromVersion = 0; + } + fromVersion++; + IList events = new List(); + int lastVersion = fromVersion; + do { + var json = await _storage.ReadTextAsync(_path.GetEventPath(aggregateId, lastVersion), null, cancellationToken); + if (!string.IsNullOrEmpty(json)) { + lastEvent = (IEvent)JsonConvert.DeserializeObject(json, JsonSettings); + events.Add(lastEvent); + } else { + lastEvent = null; + } + lastVersion++; + } while (lastEvent != null); + + + return events; + } + + private static JsonSerializerSettings JsonSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All, + Formatting = Formatting.Indented + }; + + public async Task Save(IEnumerable events, CancellationToken cancellationToken = default) { + + List tasksStore = new List(); + List tasksAttributes = new List(); + List tasksPublishers = new List(); + foreach (IEvent e in events) { + var json = JsonConvert.SerializeObject(e, JsonSettings); + await _storage.WriteTextAsync(_path.GetEventPath(e.Id, e.Version), json, null, cancellationToken); + + Blob blob = await _storage.GetBlobAsync(_path.GetEventPath(e.Id, e.Version), cancellationToken); + blob.Properties["Version"] = e.Version.ToString(); + blob.Properties["Id"] = e.Id.ToString(); + blob.Properties["Type"] = e.GetType().ToString(); + await _storage.SetBlobAsync(blob, cancellationToken); + + await _publisher.Publish(e, cancellationToken); + } + + } + } +} diff --git a/Framework/CQRSlite.Storage/BlobPathProvider.cs b/Framework/CQRSlite.Storage/BlobPathProvider.cs new file mode 100644 index 0000000..7f35798 --- /dev/null +++ b/Framework/CQRSlite.Storage/BlobPathProvider.cs @@ -0,0 +1,33 @@ +using CQRSlite.Domain; +using CQRSlite.Events; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage { + public class BlobPathProvider : IBlobEventStorePathProvider, IBlobSnapshotStorePathProvider { + + private string _eventPrefix = null; + private string _snapshotPrefix = null; + public BlobPathProvider(string eventPrefix, string snapshotPrefix) { + _eventPrefix = eventPrefix; + _snapshotPrefix = snapshotPrefix; + } + + string IBlobEventStorePathProvider.GetAggregatePath(Guid aggregateId) { + return global::Storage.Net.StoragePath.Combine(_eventPrefix, + aggregateId.ToString()); + } + + string IBlobEventStorePathProvider.GetEventPath(Guid id, int version) { + return global::Storage.Net.StoragePath.Combine(_eventPrefix, + id.ToString(), + version.ToString().PadLeft(int.MaxValue.ToString().Length, '0') + ".json"); + + } + + string IBlobSnapshotStorePathProvider.GetSnapshotPath(Guid aggregateId) { + return global::Storage.Net.StoragePath.Combine(_snapshotPrefix, aggregateId.ToString() + ".json"); + } + } +} diff --git a/Framework/CQRSlite.Storage/BlobSnapshotStore.cs b/Framework/CQRSlite.Storage/BlobSnapshotStore.cs new file mode 100644 index 0000000..694f9fd --- /dev/null +++ b/Framework/CQRSlite.Storage/BlobSnapshotStore.cs @@ -0,0 +1,60 @@ +extern alias AzureStorageNet; +using CQRSlite.Snapshotting; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using Storage.Net; +using Storage.Net.Blobs; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AzureStorageNet; + + +namespace CQRSlite.Storage { + public class BlobSnapshotStore : ISnapshotStore { + + public static string CONNECTIONSTRING_KEY = "CQRSlite.Storage.BlobSnapshotStore.ConnectionString"; + + IBlobStorage _storage; + IBlobSnapshotStorePathProvider _path; + + public BlobSnapshotStore(IConfiguration config, IBlobSnapshotStorePathProvider path) { + + if (config[CONNECTIONSTRING_KEY].StartsWith("azure")) { + AzureStorageNet.Storage.Net.Factory.UseAzureStorage(StorageFactory.Modules); + } + _storage = StorageFactory.Blobs.FromConnectionString(config[CONNECTIONSTRING_KEY]); + + _path = path; + + } + + private static JsonSerializerSettings JsonSettings = new JsonSerializerSettings() { + TypeNameHandling = TypeNameHandling.All, + Formatting = Formatting.Indented + }; + + public async Task Get(Guid id, CancellationToken cancellationToken = default) { + var json = await _storage.ReadTextAsync(_path.GetSnapshotPath(id)); + + if(!string.IsNullOrEmpty(json)) { + return (Snapshot)JsonConvert.DeserializeObject(json, JsonSettings); + } + return null; + + } + + public async Task Save(Snapshot snapshot, CancellationToken cancellationToken = default) { + var json = JsonConvert.SerializeObject(snapshot, JsonSettings); + await _storage.WriteTextAsync(_path.GetSnapshotPath(snapshot.Id), json, null, cancellationToken); + Blob b = await _storage.GetBlobAsync(_path.GetSnapshotPath(snapshot.Id), cancellationToken); + b.Properties["Version"] = snapshot.Version.ToString() ; + b.Properties["Id"] = snapshot.Id.ToString(); + await _storage.SetBlobAsync(b, cancellationToken); + + + } + } +} diff --git a/Framework/CQRSlite.Storage/CQRSlite.Storage.csproj b/Framework/CQRSlite.Storage/CQRSlite.Storage.csproj new file mode 100644 index 0000000..74a81ad --- /dev/null +++ b/Framework/CQRSlite.Storage/CQRSlite.Storage.csproj @@ -0,0 +1,27 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + + + AzureStorageNet + + + + + diff --git a/Framework/CQRSlite.Storage/IBlobEventStorePathProvider.cs b/Framework/CQRSlite.Storage/IBlobEventStorePathProvider.cs new file mode 100644 index 0000000..86f672e --- /dev/null +++ b/Framework/CQRSlite.Storage/IBlobEventStorePathProvider.cs @@ -0,0 +1,11 @@ +using CQRSlite.Events; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage { + public interface IBlobEventStorePathProvider { + string GetEventPath(Guid id, int version); + string GetAggregatePath(Guid aggregateId); + } +} diff --git a/Framework/CQRSlite.Storage/IBlobSnapshotStorePathProvider.cs b/Framework/CQRSlite.Storage/IBlobSnapshotStorePathProvider.cs new file mode 100644 index 0000000..e29cb4d --- /dev/null +++ b/Framework/CQRSlite.Storage/IBlobSnapshotStorePathProvider.cs @@ -0,0 +1,10 @@ +using CQRSlite.Domain; +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage { + public interface IBlobSnapshotStorePathProvider { + string GetSnapshotPath(Guid aggregateId); + } +} diff --git a/Framework/CQRSlite.Storage/SnapshotStrategyAttribute.cs b/Framework/CQRSlite.Storage/SnapshotStrategyAttribute.cs new file mode 100644 index 0000000..4570005 --- /dev/null +++ b/Framework/CQRSlite.Storage/SnapshotStrategyAttribute.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CQRSlite.Storage { + [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public class SnapshotStrategyAttribute : Attribute { + public int Interval {get;private set;} + public SnapshotStrategyAttribute(int interval) { + Interval = interval; + } + } +}