From 30602ee08e1d9b00bb64d5e96bde4c3b61854933 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 6 Jan 2017 20:01:53 +0100 Subject: [PATCH] Refactoring and migration to rabbitmq --- Squidex.sln | 14 + global.json | 4 +- .../EventStore/MongoEvent.cs} | 25 +- .../EventStore/MongoEventCommit.cs | 43 +++ .../EventStore/MongoEventStore.cs | 137 ++++++++++ .../MongoEntity.cs | 5 +- .../MongoExtensions.cs | 2 +- .../MongoRepositoryBase.cs | 3 +- .../Properties/AssemblyInfo.cs | 26 ++ .../RefTokenSerializer.cs | 3 +- .../Squidex.Infrastructure.MongoDb.xproj | 24 ++ .../project.json | 31 +++ .../InfrastructureErrors.cs | 19 ++ .../Properties/AssemblyInfo.cs | 26 ++ .../RabbitMqEventChannel.cs | 80 ++++++ .../Squidex.Infrastructure.RabbitMq.xproj | 21 ++ .../project.json | 31 +++ .../CQRS/Commands/CommandContext.cs | 9 +- .../Commands/DefaultDomainObjectRepository.cs | 96 +++++++ .../CQRS/EventStore/EventStoreBus.cs | 250 ------------------ .../EventStoreDomainObjectRepository.cs | 152 ----------- .../CQRS/EventStore/EventStoreFormatter.cs | 66 ----- .../CQRS/EventStore/EventWrapper.cs | 48 ---- .../CQRS/EventStore/IStreamPositionStorage.cs | 16 -- .../DefaultNameResolver.cs | 13 +- .../CQRS/Events/EventBus.cs | 116 ++++++++ .../CQRS/Events/EventData.cs | 23 ++ .../CQRS/Events/EventDataFormatter.cs | 59 +++++ .../CQRS/Events/IEventPublisher.cs | 15 ++ .../CQRS/Events/IEventStore.cs | 23 ++ .../IEventStream.cs} | 16 +- .../IStreamNameResolver.cs | 2 +- .../DisposableObject.cs | 67 +++++ src/Squidex.Infrastructure/project.json | 2 +- .../Apps/MongoAppEntity.cs | 2 +- .../Apps/MongoAppRepository.cs | 1 + .../History/MongoHistoryEventEntity.cs | 2 +- .../History/MongoHistoryEventRepository.cs | 1 + .../MongoPersistedGrantStore.cs | 2 +- .../MongoStreamPositionStorage.cs | 59 ----- src/Squidex.Store.MongoDb/MongoDbModule.cs | 7 +- .../Schemas/MongoSchemaEntity.cs | 1 + .../Schemas/MongoSchemaRepository.cs | 1 + .../Utils/EntityMapper.cs | 25 +- src/Squidex.Store.MongoDb/project.json | 1 + src/Squidex.Write/EnrichWithAppIdProcessor.cs | 1 - .../Config/Domain/InfrastructureModule.cs | 11 +- src/Squidex/Config/Domain/Serializers.cs | 4 +- .../Config/EventStore/EventStoreModule.cs | 55 ---- .../Config/EventStore/EventStoreUsage.cs | 7 +- .../EventStore/MongoDbEventStoreModule.cs | 28 ++ ...ntStoreOptions.cs => MyRabbitMqOptions.cs} | 16 +- .../EventStore/RabbitMqEventChannelModule.cs | 39 +++ .../Config/Identity/IdentityServices.cs | 3 +- .../Swagger/XmlResponseTypesProcessor.cs | 12 +- src/Squidex/Config/Swagger/XmlTagProcessor.cs | 7 +- src/Squidex/Startup.cs | 7 +- src/Squidex/appsettings.json | 8 +- src/Squidex/project.json | 3 + .../Schemas/FieldRegistryTests.cs | 2 +- .../CQRS/Commands/CommandContextTests.cs | 18 +- .../DefaultNameResolverTests.cs | 11 +- .../EventDataFormatterTests.cs} | 29 +- .../DisposableObjectTests.cs | 62 +++++ .../TaskExtensionsTests.cs | 27 ++ ...ibuteTest.cs => TypeNameAttributeTests.cs} | 2 +- .../Apps/AppDomainObjectTests.cs | 9 + .../EnrichWithAppIdProcessorTests.cs | 13 + 68 files changed, 1141 insertions(+), 802 deletions(-) rename src/{Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionEntity.cs => Squidex.Infrastructure.MongoDb/EventStore/MongoEvent.cs} (55%) create mode 100644 src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventCommit.cs create mode 100644 src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs rename src/{Squidex.Store.MongoDb/Utils => Squidex.Infrastructure.MongoDb}/MongoEntity.cs (87%) rename src/{Squidex.Store.MongoDb/Utils => Squidex.Infrastructure.MongoDb}/MongoExtensions.cs (95%) rename src/{Squidex.Store.MongoDb/Utils => Squidex.Infrastructure.MongoDb}/MongoRepositoryBase.cs (97%) create mode 100644 src/Squidex.Infrastructure.MongoDb/Properties/AssemblyInfo.cs rename src/{Squidex.Store.MongoDb/Utils => Squidex.Infrastructure.MongoDb}/RefTokenSerializer.cs (96%) create mode 100644 src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.xproj create mode 100644 src/Squidex.Infrastructure.MongoDb/project.json create mode 100644 src/Squidex.Infrastructure.RabbitMq/InfrastructureErrors.cs create mode 100644 src/Squidex.Infrastructure.RabbitMq/Properties/AssemblyInfo.cs create mode 100644 src/Squidex.Infrastructure.RabbitMq/RabbitMqEventChannel.cs create mode 100644 src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.xproj create mode 100644 src/Squidex.Infrastructure.RabbitMq/project.json create mode 100644 src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs delete mode 100644 src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs delete mode 100644 src/Squidex.Infrastructure/CQRS/EventStore/EventStoreDomainObjectRepository.cs delete mode 100644 src/Squidex.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs delete mode 100644 src/Squidex.Infrastructure/CQRS/EventStore/EventWrapper.cs delete mode 100644 src/Squidex.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs rename src/Squidex.Infrastructure/CQRS/{EventStore => Events}/DefaultNameResolver.cs (67%) create mode 100644 src/Squidex.Infrastructure/CQRS/Events/EventBus.cs create mode 100644 src/Squidex.Infrastructure/CQRS/Events/EventData.cs create mode 100644 src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs create mode 100644 src/Squidex.Infrastructure/CQRS/Events/IEventPublisher.cs create mode 100644 src/Squidex.Infrastructure/CQRS/Events/IEventStore.cs rename src/Squidex.Infrastructure/CQRS/{EventStore/IReceivedEvent.cs => Events/IEventStream.cs} (55%) rename src/Squidex.Infrastructure/CQRS/{EventStore => Events}/IStreamNameResolver.cs (90%) create mode 100644 src/Squidex.Infrastructure/DisposableObject.cs delete mode 100644 src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionStorage.cs delete mode 100644 src/Squidex/Config/EventStore/EventStoreModule.cs create mode 100644 src/Squidex/Config/EventStore/MongoDbEventStoreModule.cs rename src/Squidex/Config/EventStore/{MyEventStoreOptions.cs => MyRabbitMqOptions.cs} (55%) create mode 100644 src/Squidex/Config/EventStore/RabbitMqEventChannelModule.cs rename tests/Squidex.Infrastructure.Tests/CQRS/{EventStore => Events}/DefaultNameResolverTests.cs (81%) rename tests/Squidex.Infrastructure.Tests/CQRS/{EventStore/EventStoreFormatterTests.cs => Events/EventDataFormatterTests.cs} (70%) create mode 100644 tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs create mode 100644 tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs rename tests/Squidex.Infrastructure.Tests/{TypeNameAttributeTest.cs => TypeNameAttributeTests.cs} (93%) diff --git a/Squidex.sln b/Squidex.sln index 82903287a..4399e5952 100644 --- a/Squidex.sln +++ b/Squidex.sln @@ -36,6 +36,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Squidex.Infrastructure.Test EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Squidex.Core.Tests", "tests\Squidex.Core.Tests\Squidex.Core.Tests.xproj", "{FD0AFD44-7A93-4F9E-B5ED-72582392E435}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Squidex.Infrastructure.MongoDb", "src\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.xproj", "{6A811927-3C37-430A-90F4-503E37123956}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Squidex.Infrastructure.RabbitMq", "src\Squidex.Infrastructure.RabbitMq\Squidex.Infrastructure.RabbitMq.xproj", "{3C9BA12D-F5F2-4355-8D30-8289E4D0752D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -82,6 +86,14 @@ Global {FD0AFD44-7A93-4F9E-B5ED-72582392E435}.Debug|Any CPU.Build.0 = Debug|Any CPU {FD0AFD44-7A93-4F9E-B5ED-72582392E435}.Release|Any CPU.ActiveCfg = Release|Any CPU {FD0AFD44-7A93-4F9E-B5ED-72582392E435}.Release|Any CPU.Build.0 = Release|Any CPU + {6A811927-3C37-430A-90F4-503E37123956}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6A811927-3C37-430A-90F4-503E37123956}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6A811927-3C37-430A-90F4-503E37123956}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6A811927-3C37-430A-90F4-503E37123956}.Release|Any CPU.Build.0 = Release|Any CPU + {3C9BA12D-F5F2-4355-8D30-8289E4D0752D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C9BA12D-F5F2-4355-8D30-8289E4D0752D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C9BA12D-F5F2-4355-8D30-8289E4D0752D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C9BA12D-F5F2-4355-8D30-8289E4D0752D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -97,5 +109,7 @@ Global {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} {7FD0A92B-7862-4BB1-932B-B52A9CACB56B} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {FD0AFD44-7A93-4F9E-B5ED-72582392E435} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} + {6A811927-3C37-430A-90F4-503E37123956} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} + {3C9BA12D-F5F2-4355-8D30-8289E4D0752D} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} EndGlobalSection EndGlobal diff --git a/global.json b/global.json index 1aea5e477..62fed04ac 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ -{ - "projects": [ "src", "tests" ], +{ + "projects": [ "src", "tests", "." ], "sdk": { "version": "1.0.0-preview2-1-003177" } diff --git a/src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionEntity.cs b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEvent.cs similarity index 55% rename from src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionEntity.cs rename to src/Squidex.Infrastructure.MongoDb/EventStore/MongoEvent.cs index 2157f405a..8d5425b5c 100644 --- a/src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionEntity.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEvent.cs @@ -1,29 +1,32 @@ // ========================================================================== -// MongoStreamPositionEntity.cs +// MongoEvent.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -using System.Runtime.Serialization; -using MongoDB.Bson; +using System; using MongoDB.Bson.Serialization.Attributes; -namespace Squidex.Store.MongoDb.Infrastructure +namespace Squidex.Infrastructure.MongoDb.EventStore { - [DataContract] - public class MongoStreamPositionEntity + public class MongoEvent { - [BsonId] - public ObjectId Id { get; set; } - + [BsonElement] [BsonRequired] + public Guid EventId { get; set; } + [BsonElement] - public string SubscriptionName { get; set; } + [BsonRequired] + public string Payload { get; set; } + [BsonElement] [BsonRequired] + public string Metadata { get; set; } + [BsonElement] - public int? Position { get; set; } + [BsonRequired] + public string Type { get; set; } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventCommit.cs b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventCommit.cs new file mode 100644 index 000000000..d499ae7ed --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventCommit.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// MongoEventCommit.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Squidex.Infrastructure.MongoDb.EventStore +{ + public sealed class MongoEventCommit + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + [BsonRequired] + [BsonElement] + public DateTime Timestamp { get; set; } + + [BsonElement] + [BsonRequired] + public List Events { get; set; } + + [BsonElement] + [BsonRequired] + public string EventStream { get; set; } + + [BsonElement] + [BsonRequired] + public int EventsVersion { get; set; } + + [BsonElement] + [BsonRequired] + public int EventCount { get; set; } + } +} diff --git a/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs new file mode 100644 index 000000000..d2b7f4bd4 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs @@ -0,0 +1,137 @@ +// ========================================================================== +// MongoEventStore.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Reflection; +// ReSharper disable ClassNeverInstantiated.Local +// ReSharper disable UnusedMember.Local + +namespace Squidex.Infrastructure.MongoDb.EventStore +{ + public class MongoEventStore : MongoRepositoryBase, IEventStore + { + private sealed class EventCountEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public Guid Id { get; set; } + + [BsonElement] + [BsonRequired] + public int EventCount { get; set; } + } + + public MongoEventStore(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Events"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.EventStream).Ascending(x => x.EventsVersion), new CreateIndexOptions { Unique = true }); + } + + public IObservable GetEventsAsync(string streamName) + { + Guard.NotNullOrEmpty(streamName, nameof(streamName)); + + return Observable.Create(async (observer, ct) => + { + try + { + await Collection.Find(x => x.EventStream == streamName).ForEachAsync(commit => + { + foreach (var @event in commit.Events) + { + var eventData = SimpleMapper.Map(@event, new EventData()); + + observer.OnNext(eventData); + } + }, ct); + + observer.OnCompleted(); + } + catch (Exception e) + { + observer.OnError(e); + } + }); + } + + public IObservable GetEventsAsync() + { + return Observable.Create(async (observer, ct) => + { + try + { + await Collection.Find(new BsonDocument()).ForEachAsync(commit => + { + foreach (var @event in commit.Events) + { + var eventData = SimpleMapper.Map(@event, new EventData()); + + observer.OnNext(eventData); + } + }, ct); + + observer.OnCompleted(); + } + catch (Exception e) + { + observer.OnError(e); + } + }); + } + + public async Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, IEnumerable events) + { + var allCommits = + await Collection.Find(c => c.EventStream == streamName) + .Project(Projection.Include(x => x.EventCount)) + .ToListAsync(); + + var currentVersion = allCommits.Sum(x => x["EventCount"].ToInt32()) - 1; + if (currentVersion != expectedVersion) + { + throw new InvalidOperationException($"Current version: {currentVersion}, expected version: {expectedVersion}"); + } + + var now = DateTime.UtcNow; + + var commit = new MongoEventCommit + { + Id = commitId, + Events = events.Select(x => SimpleMapper.Map(x, new MongoEvent())).ToList(), + EventStream = streamName, + EventsVersion = expectedVersion, + Timestamp = now + }; + + if (commit.Events.Any()) + { + commit.EventCount = commit.Events.Count; + + await Collection.InsertOneAsync(commit); + } + } + } +} diff --git a/src/Squidex.Store.MongoDb/Utils/MongoEntity.cs b/src/Squidex.Infrastructure.MongoDb/MongoEntity.cs similarity index 87% rename from src/Squidex.Store.MongoDb/Utils/MongoEntity.cs rename to src/Squidex.Infrastructure.MongoDb/MongoEntity.cs index 5be22ac25..52274d458 100644 --- a/src/Squidex.Store.MongoDb/Utils/MongoEntity.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoEntity.cs @@ -9,11 +9,10 @@ using System; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; -using Squidex.Read; -namespace Squidex.Store.MongoDb.Utils +namespace Squidex.Infrastructure.MongoDb { - public abstract class MongoEntity : IEntity + public abstract class MongoEntity { [BsonId] [BsonElement] diff --git a/src/Squidex.Store.MongoDb/Utils/MongoExtensions.cs b/src/Squidex.Infrastructure.MongoDb/MongoExtensions.cs similarity index 95% rename from src/Squidex.Store.MongoDb/Utils/MongoExtensions.cs rename to src/Squidex.Infrastructure.MongoDb/MongoExtensions.cs index f5d25e52a..603f41d85 100644 --- a/src/Squidex.Store.MongoDb/Utils/MongoExtensions.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoExtensions.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using MongoDB.Driver; -namespace Squidex.Store.MongoDb.Utils +namespace Squidex.Infrastructure.MongoDb { public static class MongoExtensions { diff --git a/src/Squidex.Store.MongoDb/Utils/MongoRepositoryBase.cs b/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs similarity index 97% rename from src/Squidex.Store.MongoDb/Utils/MongoRepositoryBase.cs rename to src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs index 56c413a80..93cf9568c 100644 --- a/src/Squidex.Store.MongoDb/Utils/MongoRepositoryBase.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs @@ -9,9 +9,8 @@ using System.Globalization; using System.Threading.Tasks; using MongoDB.Driver; -using Squidex.Infrastructure; -namespace Squidex.Store.MongoDb.Utils +namespace Squidex.Infrastructure.MongoDb { public abstract class MongoRepositoryBase { diff --git a/src/Squidex.Infrastructure.MongoDb/Properties/AssemblyInfo.cs b/src/Squidex.Infrastructure.MongoDb/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..9cd60f027 --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// AssemblyInfo.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Squidex.Infrastructure.MongoDb")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("6a811927-3c37-430a-90f4-503e37123956")] diff --git a/src/Squidex.Store.MongoDb/Utils/RefTokenSerializer.cs b/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs similarity index 96% rename from src/Squidex.Store.MongoDb/Utils/RefTokenSerializer.cs rename to src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs index 7168b23a8..553a6a728 100644 --- a/src/Squidex.Store.MongoDb/Utils/RefTokenSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs @@ -8,11 +8,10 @@ using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization.Serializers; -using Squidex.Infrastructure; // ReSharper disable InvertIf -namespace Squidex.Store.MongoDb.Utils +namespace Squidex.Infrastructure.MongoDb { public class RefTokenSerializer : SerializerBase { diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.xproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.xproj new file mode 100644 index 000000000..5fed50e5f --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.xproj @@ -0,0 +1,24 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 6a811927-3c37-430a-90f4-503e37123956 + Squidex.Infrastructure.MongoDb + .\obj + .\bin\ + v4.6.1 + + + 2.0 + + + + + + + + \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/project.json b/src/Squidex.Infrastructure.MongoDb/project.json new file mode 100644 index 000000000..c9f998b8d --- /dev/null +++ b/src/Squidex.Infrastructure.MongoDb/project.json @@ -0,0 +1,31 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Autofac": "4.2.1", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "MongoDB.Driver": "2.4.1", + "NETStandard.Library": "1.6.1", + "Newtonsoft.Json": "9.0.2-beta1", + "NodaTime": "2.0.0-alpha20160729", + "Squidex.Infrastructure": "1.0.0-*", + "System.Linq": "4.3.0", + "System.Reactive": "3.1.1", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Security.Claims": "4.3.0" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + } + } + }, + "buildOptions": { + "embed": [ + "*.csv" + ] + }, + "tooling": { + "defaultNamespace": "Squidex.Infrastructure.MongoDb" + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.RabbitMq/InfrastructureErrors.cs b/src/Squidex.Infrastructure.RabbitMq/InfrastructureErrors.cs new file mode 100644 index 000000000..fc1832891 --- /dev/null +++ b/src/Squidex.Infrastructure.RabbitMq/InfrastructureErrors.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// InfrastructureErrors.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Microsoft.Extensions.Logging; + +namespace Squidex.Infrastructure.RabbitMq +{ + public class InfrastructureErrors + { + public static readonly EventId EventHandlingFailed = new EventId(10001, "EventHandlingFailed"); + + public static readonly EventId EventDeserializationFailed = new EventId(10002, "EventDeserializationFailed"); + } +} diff --git a/src/Squidex.Infrastructure.RabbitMq/Properties/AssemblyInfo.cs b/src/Squidex.Infrastructure.RabbitMq/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..e861a9aa1 --- /dev/null +++ b/src/Squidex.Infrastructure.RabbitMq/Properties/AssemblyInfo.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// AssemblyInfo.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Squidex.Infrastructure.RabbitMq")] +[assembly: AssemblyTrademark("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("3c9ba12d-f5f2-4355-8d30-8289e4d0752d")] diff --git a/src/Squidex.Infrastructure.RabbitMq/RabbitMqEventChannel.cs b/src/Squidex.Infrastructure.RabbitMq/RabbitMqEventChannel.cs new file mode 100644 index 000000000..5ee0b39e0 --- /dev/null +++ b/src/Squidex.Infrastructure.RabbitMq/RabbitMqEventChannel.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// EventChannel.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Text; +using Newtonsoft.Json; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Infrastructure.RabbitMq +{ + public sealed class RabbitMqEventChannel : DisposableObject, IEventPublisher, IEventStream + { + private const string Exchange = "Squidex"; + private readonly Lazy currentChannel; + + public RabbitMqEventChannel(IConnectionFactory connectionFactory) + { + Guard.NotNull(connectionFactory, nameof(connectionFactory)); + + currentChannel = new Lazy(() => Connect(connectionFactory)); + } + + protected override void DisposeObject(bool disposing) + { + if (currentChannel.IsValueCreated) + { + currentChannel.Value.Dispose(); + } + } + + public void Publish(EventData events) + { + ThrowIfDisposed(); + + var channel = currentChannel.Value; + + channel.BasicPublish(Exchange, string.Empty, null, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(events))); + } + + public void Connect(string queueName, Action received) + { + ThrowIfDisposed(); + + var channel = currentChannel.Value; + + queueName = $"{queueName}_{Environment.MachineName}"; + + channel.QueueDeclare(queueName, true, false, false); + channel.QueueBind(queueName, Exchange, string.Empty); + + var consumer = new EventingBasicConsumer(channel); + + consumer.Received += (model, e) => + { + var eventData = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(e.Body)); + + received(eventData); + }; + + channel.BasicConsume(queueName, false, consumer); + } + + private static IModel Connect(IConnectionFactory connectionFactory) + { + var connection = connectionFactory.CreateConnection(); + var channel = connection.CreateModel(); + + channel.ExchangeDeclare(Exchange, ExchangeType.Fanout, true); + + return channel; + } + } +} diff --git a/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.xproj b/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.xproj new file mode 100644 index 000000000..b5975e41c --- /dev/null +++ b/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 3c9ba12d-f5f2-4355-8d30-8289e4d0752d + Squidex.Infrastructure.RabbitMq + .\obj + .\bin\ + v4.6.1 + + + + 2.0 + + + diff --git a/src/Squidex.Infrastructure.RabbitMq/project.json b/src/Squidex.Infrastructure.RabbitMq/project.json new file mode 100644 index 000000000..4590de87d --- /dev/null +++ b/src/Squidex.Infrastructure.RabbitMq/project.json @@ -0,0 +1,31 @@ +{ + "version": "1.0.0-*", + "dependencies": { + "Autofac": "4.2.1", + "Microsoft.Extensions.Logging": "1.1.0", + "Microsoft.NETCore.App": "1.1.0", + "NETStandard.Library": "1.6.1", + "Newtonsoft.Json": "9.0.2-beta1", + "NodaTime": "2.0.0-alpha20160729", + "RabbitMQ.Client": "5.0.0-pre2", + "Squidex.Infrastructure": "1.0.0-*", + "System.Linq": "4.3.0", + "System.Reactive": "3.1.1", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Security.Claims": "4.3.0" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + } + } + }, + "buildOptions": { + "embed": [ + "*.csv" + ] + }, + "tooling": { + "defaultNamespace": "Squidex.Infrastructure.RabbitMq" + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs b/src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs index 58abe5bb4..31f76e399 100644 --- a/src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs +++ b/src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs @@ -23,7 +23,12 @@ namespace Squidex.Infrastructure.CQRS.Commands public bool IsHandled { - get { return result != null || exception != null; } + get { return IsSucceeded || IsFailed; } + } + + public bool IsFailed + { + get { return exception != null; } } public bool IsSucceeded @@ -55,7 +60,7 @@ namespace Squidex.Infrastructure.CQRS.Commands public void Fail(Exception handlerException) { - if (IsHandled) + if (IsFailed) { return; } diff --git a/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs new file mode 100644 index 000000000..afa7ed2ce --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs @@ -0,0 +1,96 @@ +// ========================================================================== +// DefaultDomainObjectRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Squidex.Infrastructure.CQRS.Events; + +namespace Squidex.Infrastructure.CQRS.Commands +{ + public sealed class DefaultDomainObjectRepository : IDomainObjectRepository + { + private readonly IStreamNameResolver nameResolver; + private readonly IDomainObjectFactory factory; + private readonly IEventStore eventStore; + private readonly IEventPublisher eventPublisher; + private readonly EventDataFormatter formatter; + + public DefaultDomainObjectRepository( + IDomainObjectFactory factory, + IEventStore eventStore, + IEventPublisher eventPublisher, + IStreamNameResolver nameResolver, + EventDataFormatter formatter) + { + Guard.NotNull(factory, nameof(factory)); + Guard.NotNull(formatter, nameof(formatter)); + Guard.NotNull(eventStore, nameof(eventStore)); + Guard.NotNull(eventPublisher, nameof(eventPublisher)); + Guard.NotNull(nameResolver, nameof(nameResolver)); + + this.factory = factory; + this.eventStore = eventStore; + this.formatter = formatter; + this.eventPublisher = eventPublisher; + this.nameResolver = nameResolver; + } + + public async Task GetByIdAsync(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate + { + Guard.GreaterThan(version, 0, nameof(version)); + + var streamName = nameResolver.GetStreamName(typeof(TDomainObject), id); + + var domainObject = (TDomainObject)factory.CreateNew(typeof(TDomainObject), id); + + var events = await eventStore.GetEventsAsync(streamName).ToList(); + + if (events.Count == 0) + { + throw new DomainObjectNotFoundException(id.ToString(), typeof(TDomainObject)); + } + + foreach (var eventData in events) + { + var envelope = formatter.Parse(eventData); + + domainObject.ApplyEvent(envelope); + } + + if (domainObject.Version != version && version < int.MaxValue) + { + throw new DomainObjectVersionException(id.ToString(), typeof(TDomainObject), domainObject.Version, version); + } + + return domainObject; + } + + public async Task SaveAsync(IAggregate domainObject, ICollection> events, Guid commitId) + { + Guard.NotNull(domainObject, nameof(domainObject)); + + var streamName = nameResolver.GetStreamName(domainObject.GetType(), domainObject.Id); + + var versionCurrent = domainObject.Version; + var versionBefore = versionCurrent - events.Count; + var versionExpected = versionBefore == 0 ? -1 : versionBefore - 1; + + var eventsToSave = events.Select(x => formatter.ToEventData(x, commitId)).ToList(); + + await eventStore.AppendEventsAsync(commitId, streamName, versionExpected, eventsToSave); + + foreach (var eventData in eventsToSave) + { + eventPublisher.Publish(eventData); + } + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs b/src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs deleted file mode 100644 index a1693d4c7..000000000 --- a/src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs +++ /dev/null @@ -1,250 +0,0 @@ -// ========================================================================== -// EventStoreBus.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; -using EventStore.ClientAPI; -using EventStore.ClientAPI.SystemData; -using Microsoft.Extensions.Logging; -using Squidex.Infrastructure.CQRS.Events; -// ReSharper disable InvertIf - -namespace Squidex.Infrastructure.CQRS.EventStore -{ - public sealed class EventStoreBus : IDisposable - { - private readonly IEventStoreConnection connection; - private readonly UserCredentials credentials; - private readonly EventStoreFormatter formatter; - private readonly IEnumerable liveConsumers; - private readonly IEnumerable catchConsumers; - private readonly ILogger logger; - private readonly IStreamPositionStorage positions; - private readonly List catchSubscriptions = new List(); - private EventStoreSubscription liveSubscription; - private string streamName; - private bool isSubscribed; - - public EventStoreBus( - ILogger logger, - IEnumerable liveConsumers, - IEnumerable catchConsumers, - IStreamPositionStorage positions, - IEventStoreConnection connection, - UserCredentials credentials, - EventStoreFormatter formatter) - { - Guard.NotNull(logger, nameof(logger)); - Guard.NotNull(formatter, nameof(formatter)); - Guard.NotNull(positions, nameof(positions)); - Guard.NotNull(connection, nameof(connection)); - Guard.NotNull(credentials, nameof(credentials)); - Guard.NotNull(liveConsumers, nameof(liveConsumers)); - Guard.NotNull(catchConsumers, nameof(catchConsumers)); - - this.logger = logger; - this.formatter = formatter; - this.positions = positions; - this.connection = connection; - this.credentials = credentials; - this.liveConsumers = liveConsumers; - this.catchConsumers = catchConsumers; - } - - public void Dispose() - { - lock (catchSubscriptions) - { - foreach (var catchSubscription in catchSubscriptions) - { - catchSubscription.Stop(TimeSpan.FromMinutes(1)); - } - - liveSubscription.Unsubscribe(); - } - } - - public void Subscribe(string streamToConnect = "$all") - { - Guard.NotNullOrEmpty(streamToConnect, nameof(streamToConnect)); - - if (isSubscribed) - { - return; - } - - streamName = streamToConnect; - - SubscribeLive(); - SubscribeCatch(); - - isSubscribed = true; - } - - private void SubscribeLive() - { - Task.Run(async () => - { - liveSubscription = - await connection.SubscribeToStreamAsync(streamName, true, - (subscription, resolvedEvent) => - { - OnLiveEvent(resolvedEvent); - }, (subscription, dropped, ex) => - { - OnConnectionDropped(); - }, credentials); - }).Wait(); - } - - private void OnConnectionDropped() - { - try - { - liveSubscription.Close(); - - logger.LogError("Subscription closed"); - } - finally - { - SubscribeLive(); - } - } - - private void SubscribeCatch() - { - foreach (var catchConsumer in catchConsumers) - { - SubscribeCatchFor(catchConsumer); - } - } - - private void SubscribeCatchFor(IEventConsumer consumer) - { - var subscriptionName = consumer.GetType().GetTypeInfo().Name; - - var position = positions.ReadPosition(subscriptionName); - - logger.LogInformation("[{0}]: Subscribing from {0}", consumer, position ?? 0); - - var settings = - new CatchUpSubscriptionSettings( - int.MaxValue, 4096, - true, - true); - - var catchSubscription = - connection.SubscribeToStreamFrom(streamName, position, settings, - (subscription, resolvedEvent) => - { - OnCatchEvent(consumer, resolvedEvent, subscriptionName, subscription); - }, userCredentials: credentials); - - lock (catchSubscriptions) - { - catchSubscriptions.Add(catchSubscription); - } - } - - private void OnLiveEvent(ResolvedEvent resolvedEvent) - { - Envelope @event = null; - - try - { - @event = formatter.Parse(new EventWrapper(resolvedEvent)); - } - catch (Exception ex) - { - logger.LogError(InfrastructureErrors.EventDeserializationFailed, ex, - "[LiveConsumers]: Failed to deserialize event {0}#{1}", streamName, - resolvedEvent.OriginalEventNumber); - } - - if (@event != null) - { - DispatchConsumers(liveConsumers, @event).Wait(); - } - } - - private void OnCatchEvent(IEventConsumer consumer, ResolvedEvent resolvedEvent, string subscriptionName, EventStoreCatchUpSubscription subscription) - { - if (resolvedEvent.OriginalEvent.EventStreamId.StartsWith("$", StringComparison.OrdinalIgnoreCase)) - { - return; - } - - var isFailed = false; - - Envelope @event = null; - - try - { - @event = formatter.Parse(new EventWrapper(resolvedEvent)); - } - catch (Exception ex) - { - logger.LogError(InfrastructureErrors.EventDeserializationFailed, ex, - "[{consumer}]: Failed to deserialize event {1}#{2}", consumer, streamName, - resolvedEvent.OriginalEventNumber); - - isFailed = true; - } - - if (@event != null) - { - try - { - logger.LogInformation("Received event {0} ({1})", @event.Payload.GetType().Name, @event.Headers.AggregateId()); - - consumer.On(@event).Wait(); - - positions.WritePosition(subscriptionName, resolvedEvent.OriginalEventNumber); - } - catch (Exception ex) - { - logger.LogError(InfrastructureErrors.EventHandlingFailed, ex, - "[{0}]: Failed to handle event {1} ({2})", consumer, - @event.Payload, - @event.Headers.EventId()); - } - } - - if (isFailed) - { - lock (catchSubscriptions) - { - subscription.Stop(); - - catchSubscriptions.Remove(subscription); - } - } - } - - private Task DispatchConsumers(IEnumerable consumers, Envelope @event) - { - return Task.WhenAll(consumers.Select(c => DispatchConsumer(@event, c)).ToList()); - } - - private async Task DispatchConsumer(Envelope @event, IEventConsumer consumer) - { - try - { - await consumer.On(@event); - } - catch (Exception ex) - { - logger.LogError(InfrastructureErrors.EventHandlingFailed, ex, - "[{0}]: Failed to handle event {1} ({2})", consumer, @event.Payload, @event.Headers.EventId()); - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/EventStore/EventStoreDomainObjectRepository.cs b/src/Squidex.Infrastructure/CQRS/EventStore/EventStoreDomainObjectRepository.cs deleted file mode 100644 index 175b23e44..000000000 --- a/src/Squidex.Infrastructure/CQRS/EventStore/EventStoreDomainObjectRepository.cs +++ /dev/null @@ -1,152 +0,0 @@ -// ========================================================================== -// EventStoreDomainObjectRepository.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using EventStore.ClientAPI; -using EventStore.ClientAPI.SystemData; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.CQRS.Events; - -// ReSharper disable RedundantAssignment -// ReSharper disable ConvertIfStatementToSwitchStatement -// ReSharper disable TooWideLocalVariableScope - -namespace Squidex.Infrastructure.CQRS.EventStore -{ - public sealed class EventStoreDomainObjectRepository : IDomainObjectRepository - { - private const int WritePageSize = 500; - private const int ReadPageSize = 500; - private readonly IEventStoreConnection connection; - private readonly IStreamNameResolver nameResolver; - private readonly IDomainObjectFactory factory; - private readonly UserCredentials credentials; - private readonly EventStoreFormatter formatter; - - public EventStoreDomainObjectRepository( - IDomainObjectFactory factory, - IStreamNameResolver nameResolver, - IEventStoreConnection connection, - UserCredentials credentials, - EventStoreFormatter formatter) - { - Guard.NotNull(factory, nameof(factory)); - Guard.NotNull(formatter, nameof(formatter)); - Guard.NotNull(connection, nameof(connection)); - Guard.NotNull(credentials, nameof(credentials)); - Guard.NotNull(nameResolver, nameof(nameResolver)); - - this.factory = factory; - this.formatter = formatter; - this.connection = connection; - this.credentials = credentials; - this.nameResolver = nameResolver; - } - - public async Task GetByIdAsync(Guid id, int version = int.MaxValue) where TDomainObject : class, IAggregate - { - Guard.GreaterThan(version, 0, nameof(version)); - - var streamName = nameResolver.GetStreamName(typeof(TDomainObject), id); - - var domainObject = (TDomainObject)factory.CreateNew(typeof(TDomainObject), id); - - var sliceStart = 0; - var sliceCount = 0; - - StreamEventsSlice currentSlice; - do - { - sliceCount = sliceStart + ReadPageSize <= version ? ReadPageSize : version - sliceStart + 1; - - currentSlice = await connection.ReadStreamEventsForwardAsync(streamName, sliceStart, sliceCount, false, credentials); - - if (currentSlice.Status == SliceReadStatus.StreamNotFound) - { - throw new DomainObjectNotFoundException(id.ToString(), typeof(TDomainObject)); - } - - if (currentSlice.Status == SliceReadStatus.StreamDeleted) - { - throw new DomainObjectDeletedException(id.ToString(), typeof(TDomainObject)); - } - - sliceStart = currentSlice.NextEventNumber; - - foreach (var resolved in currentSlice.Events) - { - var envelope = formatter.Parse(new EventWrapper(resolved)); - - domainObject.ApplyEvent(envelope); - } - } - while (version >= currentSlice.NextEventNumber && !currentSlice.IsEndOfStream); - - if (domainObject.Version != version && version < int.MaxValue) - { - throw new DomainObjectVersionException(id.ToString(), typeof(TDomainObject), domainObject.Version, version); - } - - return domainObject; - } - - public async Task SaveAsync(IAggregate domainObject, ICollection> events, Guid commitId) - { - Guard.NotNull(domainObject, nameof(domainObject)); - - var streamName = nameResolver.GetStreamName(domainObject.GetType(), domainObject.Id); - - var versionCurrent = domainObject.Version; - var versionBefore = versionCurrent - events.Count; - var versionExpected = versionBefore == 0 ? ExpectedVersion.NoStream : versionBefore - 1; - - var eventsToSave = events.Select(x => formatter.ToEventData(x, commitId)).ToList(); - - await InsertEventsAsync(streamName, versionExpected, eventsToSave); - - domainObject.ClearUncommittedEvents(); - } - - private async Task InsertEventsAsync(string streamName, int expectedVersion, IReadOnlyCollection eventsToSave) - { - if (eventsToSave.Count > 0) - { - if (eventsToSave.Count < WritePageSize) - { - await connection.AppendToStreamAsync(streamName, expectedVersion, eventsToSave, credentials); - } - else - { - var transaction = await connection.StartTransactionAsync(streamName, expectedVersion, credentials); - - try - { - for (var p = 0; p < eventsToSave.Count; p += WritePageSize) - { - await transaction.WriteAsync(eventsToSave.Skip(p).Take(WritePageSize)); - } - - await transaction.CommitAsync(); - } - finally - { - transaction.Dispose(); - } - } - } - else - { - Debug.WriteLine($"No events to insert for: {streamName}", "GetEventStoreRepository"); - } - } - } -} diff --git a/src/Squidex.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs b/src/Squidex.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs deleted file mode 100644 index e94e1911b..000000000 --- a/src/Squidex.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// EventStoreFormatter.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Text; -using EventStore.ClientAPI; -using Newtonsoft.Json; -using NodaTime; -using Squidex.Infrastructure.CQRS.Events; - -// ReSharper disable InconsistentNaming - -namespace Squidex.Infrastructure.CQRS.EventStore -{ - public class EventStoreFormatter - { - private readonly JsonSerializerSettings serializerSettings; - - public EventStoreFormatter(JsonSerializerSettings serializerSettings = null) - { - this.serializerSettings = serializerSettings ?? new JsonSerializerSettings(); - } - - public Envelope Parse(IReceivedEvent @event) - { - var headers = ReadJson(@event.Metadata); - - var eventType = TypeNameRegistry.GetType(@event.EventType); - var eventData = ReadJson(@event.Payload, eventType); - - var envelope = new Envelope(eventData, headers); - - envelope.Headers.Set(CommonHeaders.Timestamp, Instant.FromDateTimeUtc(DateTime.SpecifyKind(@event.Created, DateTimeKind.Utc))); - envelope.Headers.Set(CommonHeaders.EventNumber, @event.EventNumber); - - return envelope; - } - - public EventData ToEventData(Envelope envelope, Guid commitId) - { - var eventType = TypeNameRegistry.GetName(envelope.Payload.GetType()); - - envelope.Headers.Set(CommonHeaders.CommitId, commitId); - - var headers = WriteJson(envelope.Headers); - var content = WriteJson(envelope.Payload); - - return new EventData(envelope.Headers.EventId(), eventType, true, content, headers); - } - - private T ReadJson(byte[] data, Type type = null) - { - return (T)JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data), type ?? typeof(T), serializerSettings); - } - - private byte[] WriteJson(object value) - { - return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(value, serializerSettings)); - } - } -} diff --git a/src/Squidex.Infrastructure/CQRS/EventStore/EventWrapper.cs b/src/Squidex.Infrastructure/CQRS/EventStore/EventWrapper.cs deleted file mode 100644 index 561e63819..000000000 --- a/src/Squidex.Infrastructure/CQRS/EventStore/EventWrapper.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// EventWrapper.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using EventStore.ClientAPI; - -namespace Squidex.Infrastructure.CQRS.EventStore -{ - internal sealed class EventWrapper : IReceivedEvent - { - private readonly ResolvedEvent @event; - - public int EventNumber - { - get { return @event.OriginalEventNumber; } - } - - public string EventType - { - get { return @event.Event.EventType; } - } - - public byte[] Metadata - { - get { return @event.Event.Metadata; } - } - - public byte[] Payload - { - get { return @event.Event.Data; } - } - - public DateTime Created - { - get { return @event.Event.Created; } - } - - public EventWrapper(ResolvedEvent @event) - { - this.@event = @event; - } - } -} diff --git a/src/Squidex.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs b/src/Squidex.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs deleted file mode 100644 index 53285d3fc..000000000 --- a/src/Squidex.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// IStreamPositionStorage.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== -namespace Squidex.Infrastructure.CQRS.EventStore -{ - public interface IStreamPositionStorage - { - int? ReadPosition(string subscriptionName); - - void WritePosition(string subscriptionName, int position); - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/EventStore/DefaultNameResolver.cs b/src/Squidex.Infrastructure/CQRS/Events/DefaultNameResolver.cs similarity index 67% rename from src/Squidex.Infrastructure/CQRS/EventStore/DefaultNameResolver.cs rename to src/Squidex.Infrastructure/CQRS/Events/DefaultNameResolver.cs index 90338f188..b2294384a 100644 --- a/src/Squidex.Infrastructure/CQRS/EventStore/DefaultNameResolver.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/DefaultNameResolver.cs @@ -7,21 +7,12 @@ // ========================================================================== using System; -using System.Globalization; -namespace Squidex.Infrastructure.CQRS.EventStore +namespace Squidex.Infrastructure.CQRS.Events { public sealed class DefaultNameResolver : IStreamNameResolver { private const string Suffix = "DomainObject"; - private readonly string prefix; - - public DefaultNameResolver(string prefix) - { - Guard.NotNullOrEmpty(prefix, nameof(prefix)); - - this.prefix = prefix.ToLowerInvariant(); - } public string GetStreamName(Type aggregateType, Guid id) { @@ -32,7 +23,7 @@ namespace Squidex.Infrastructure.CQRS.EventStore typeName = typeName.Substring(0, typeName.Length - Suffix.Length); } - return string.Format(CultureInfo.InvariantCulture, "{0}-{1}-{2}", prefix, typeName, id); + return $"{typeName}-{id}"; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventBus.cs b/src/Squidex.Infrastructure/CQRS/Events/EventBus.cs new file mode 100644 index 000000000..b6d409066 --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Events/EventBus.cs @@ -0,0 +1,116 @@ +// ========================================================================== +// EventBus.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NodaTime; + +// ReSharper disable ConvertIfStatementToConditionalTernaryExpression +// ReSharper disable InvertIf + +namespace Squidex.Infrastructure.CQRS.Events +{ + public sealed class EventBus + { + private readonly EventDataFormatter formatter; + private readonly IEnumerable liveConsumers; + private readonly IEnumerable catchConsumers; + private readonly IEventStream eventStream; + private readonly ILogger logger; + private bool isSubscribed; + + public EventBus( + ILogger logger, + IEventStream eventStream, + IEnumerable liveConsumers, + IEnumerable catchConsumers, + EventDataFormatter formatter) + { + Guard.NotNull(logger, nameof(logger)); + Guard.NotNull(formatter, nameof(formatter)); + Guard.NotNull(eventStream, nameof(eventStream)); + Guard.NotNull(liveConsumers, nameof(liveConsumers)); + Guard.NotNull(catchConsumers, nameof(catchConsumers)); + + this.logger = logger; + this.formatter = formatter; + this.eventStream = eventStream; + this.liveConsumers = liveConsumers; + this.catchConsumers = catchConsumers; + } + + public void Subscribe() + { + if (isSubscribed) + { + return; + } + + var startTime = SystemClock.Instance.GetCurrentInstant(); + + eventStream.Connect("squidex", eventData => + { + var @event = ParseEvent(eventData); + + if (@event == null) + { + return; + } + + var isLive = @event.Headers.Timestamp() >= startTime; + + if (isLive) + { + DispatchConsumers(liveConsumers.OfType().Union(catchConsumers), @event); + } + else + { + DispatchConsumers(liveConsumers, @event); + } + }); + + isSubscribed = true; + } + + private void DispatchConsumers(IEnumerable consumers, Envelope @event) + { + Task.WaitAll(consumers.Select(c => DispatchConsumer(@event, c)).ToArray()); + } + + private async Task DispatchConsumer(Envelope @event, IEventConsumer consumer) + { + try + { + await consumer.On(@event); + } + catch (Exception ex) + { + logger.LogError(InfrastructureErrors.EventHandlingFailed, ex, "[{0}]: Failed to handle event {1} ({2})", consumer, @event.Payload, @event.Headers.EventId()); + } + } + + private Envelope ParseEvent(EventData eventData) + { + try + { + var @event = formatter.Parse(eventData); + + return @event; + } + catch (Exception ex) + { + logger.LogError(InfrastructureErrors.EventDeserializationFailed, ex, "Failed to parse event {0}", eventData.EventId); + + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventData.cs b/src/Squidex.Infrastructure/CQRS/Events/EventData.cs new file mode 100644 index 000000000..6ad9a4239 --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Events/EventData.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// EventData.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure.CQRS.Events +{ + public class EventData + { + public Guid EventId { get; set; } + + public string Payload { get; set; } + + public string Metadata { get; set; } + + public string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs b/src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs new file mode 100644 index 000000000..d38729d30 --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs @@ -0,0 +1,59 @@ +// ========================================================================== +// EventDataFormatter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Newtonsoft.Json; + +// ReSharper disable InconsistentNaming + +namespace Squidex.Infrastructure.CQRS.Events +{ + public class EventDataFormatter + { + private readonly JsonSerializerSettings serializerSettings; + + public EventDataFormatter(JsonSerializerSettings serializerSettings = null) + { + this.serializerSettings = serializerSettings ?? new JsonSerializerSettings(); + } + + public Envelope Parse(EventData eventData) + { + var headers = ReadJson(eventData.Metadata); + + var eventType = TypeNameRegistry.GetType(eventData.Type); + var eventContent = ReadJson(eventData.Payload, eventType); + + var envelope = new Envelope(eventContent, headers); + + return envelope; + } + + public EventData ToEventData(Envelope envelope, Guid commitId) + { + var eventType = TypeNameRegistry.GetName(envelope.Payload.GetType()); + + envelope.SetCommitId(commitId); + + var headers = WriteJson(envelope.Headers); + var content = WriteJson(envelope.Payload); + + return new EventData { EventId = envelope.Headers.EventId(), Type = eventType, Payload = content, Metadata = headers }; + } + + private T ReadJson(string data, Type type = null) + { + return (T)JsonConvert.DeserializeObject(data, type ?? typeof(T), serializerSettings); + } + + private string WriteJson(object value) + { + return JsonConvert.SerializeObject(value, serializerSettings); + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventPublisher.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventPublisher.cs new file mode 100644 index 000000000..e4fe2c4a8 --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Events/IEventPublisher.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// IEventPublisher.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Infrastructure.CQRS.Events +{ + public interface IEventPublisher + { + void Publish(EventData events); + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventStore.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventStore.cs new file mode 100644 index 000000000..38b0f5fad --- /dev/null +++ b/src/Squidex.Infrastructure/CQRS/Events/IEventStore.cs @@ -0,0 +1,23 @@ +// ========================================================================== +// IEventStore.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Infrastructure.CQRS.Events +{ + public interface IEventStore + { + IObservable GetEventsAsync(); + + IObservable GetEventsAsync(string streamName); + + Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, IEnumerable events); + } +} diff --git a/src/Squidex.Infrastructure/CQRS/EventStore/IReceivedEvent.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventStream.cs similarity index 55% rename from src/Squidex.Infrastructure/CQRS/EventStore/IReceivedEvent.cs rename to src/Squidex.Infrastructure/CQRS/Events/IEventStream.cs index d7ea30e76..e4bd07049 100644 --- a/src/Squidex.Infrastructure/CQRS/EventStore/IReceivedEvent.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/IEventStream.cs @@ -1,5 +1,5 @@ // ========================================================================== -// IReceivedEvent.cs +// IEventStream.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,18 +8,10 @@ using System; -namespace Squidex.Infrastructure.CQRS.EventStore +namespace Squidex.Infrastructure.CQRS.Events { - public interface IReceivedEvent + public interface IEventStream { - int EventNumber { get; } - - string EventType { get; } - - byte[] Metadata { get; } - - byte[] Payload { get; } - - DateTime Created { get; } + void Connect(string queueName, Action received); } } diff --git a/src/Squidex.Infrastructure/CQRS/EventStore/IStreamNameResolver.cs b/src/Squidex.Infrastructure/CQRS/Events/IStreamNameResolver.cs similarity index 90% rename from src/Squidex.Infrastructure/CQRS/EventStore/IStreamNameResolver.cs rename to src/Squidex.Infrastructure/CQRS/Events/IStreamNameResolver.cs index 27141db91..f5c971e68 100644 --- a/src/Squidex.Infrastructure/CQRS/EventStore/IStreamNameResolver.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/IStreamNameResolver.cs @@ -8,7 +8,7 @@ using System; -namespace Squidex.Infrastructure.CQRS.EventStore +namespace Squidex.Infrastructure.CQRS.Events { public interface IStreamNameResolver { diff --git a/src/Squidex.Infrastructure/DisposableObject.cs b/src/Squidex.Infrastructure/DisposableObject.cs new file mode 100644 index 000000000..98da1ace5 --- /dev/null +++ b/src/Squidex.Infrastructure/DisposableObject.cs @@ -0,0 +1,67 @@ +// ========================================================================== +// EnumExtensions.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Infrastructure +{ + public abstract class DisposableObject : IDisposable + { + private readonly object disposeLock = new object(); + private bool isDisposed; + public bool IsDisposed + { + get + { + return isDisposed; + } + } + + public void Dispose() + { + Dispose(true); + + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (isDisposed) + { + return; + } + + if (disposing) + { + lock (disposeLock) + { + if (!isDisposed) + { + DisposeObject(true); + } + } + } + else + { + DisposeObject(false); + } + + isDisposed = true; + } + + protected abstract void DisposeObject(bool disposing); + + protected void ThrowIfDisposed() + { + if (isDisposed) + { + throw new ObjectDisposedException(GetType().Name); + } + } + } +} diff --git a/src/Squidex.Infrastructure/project.json b/src/Squidex.Infrastructure/project.json index c6150e385..beb4df819 100644 --- a/src/Squidex.Infrastructure/project.json +++ b/src/Squidex.Infrastructure/project.json @@ -2,7 +2,6 @@ "version": "1.0.0-*", "dependencies": { "Autofac": "4.2.1", - "EventStore.ClientAPI.NetCore": "0.0.1-alpha", "Microsoft.Extensions.Logging": "1.1.0", "Microsoft.NETCore.App": "1.1.0", "NETStandard.Library": "1.6.1", @@ -10,6 +9,7 @@ "NodaTime": "2.0.0-alpha20160729", "protobuf-net": "2.1.0", "System.Linq": "4.3.0", + "System.Reactive": "3.1.1", "System.Reflection.TypeExtensions": "4.3.0", "System.Security.Claims": "4.3.0" }, diff --git a/src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs b/src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs index d3c501e8b..b9964386e 100644 --- a/src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs +++ b/src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs @@ -10,8 +10,8 @@ using System.Collections.Generic; using System.Linq; using MongoDB.Bson.Serialization.Attributes; using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; using Squidex.Read.Apps; -using Squidex.Store.MongoDb.Utils; namespace Squidex.Store.MongoDb.Apps { diff --git a/src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs index df67f657f..fca8200e8 100644 --- a/src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs +++ b/src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs @@ -18,6 +18,7 @@ using Squidex.Read.Apps; using Squidex.Read.Apps.Repositories; using Squidex.Store.MongoDb.Utils; using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Store.MongoDb.Apps { diff --git a/src/Squidex.Store.MongoDb/History/MongoHistoryEventEntity.cs b/src/Squidex.Store.MongoDb/History/MongoHistoryEventEntity.cs index d81232c91..394be544d 100644 --- a/src/Squidex.Store.MongoDb/History/MongoHistoryEventEntity.cs +++ b/src/Squidex.Store.MongoDb/History/MongoHistoryEventEntity.cs @@ -11,8 +11,8 @@ using System.Collections.Generic; using MongoDB.Bson.Serialization.Attributes; using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.MongoDb; using Squidex.Read; -using Squidex.Store.MongoDb.Utils; namespace Squidex.Store.MongoDb.History { diff --git a/src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs b/src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs index 1226d512c..39b4cf991 100644 --- a/src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs +++ b/src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs @@ -18,6 +18,7 @@ using Squidex.Read.History; using Squidex.Read.History.Repositories; using Squidex.Store.MongoDb.Utils; using System.Linq; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Store.MongoDb.History { diff --git a/src/Squidex.Store.MongoDb/Infrastructure/MongoPersistedGrantStore.cs b/src/Squidex.Store.MongoDb/Infrastructure/MongoPersistedGrantStore.cs index 74255fede..0f39fd6da 100644 --- a/src/Squidex.Store.MongoDb/Infrastructure/MongoPersistedGrantStore.cs +++ b/src/Squidex.Store.MongoDb/Infrastructure/MongoPersistedGrantStore.cs @@ -12,7 +12,7 @@ using IdentityServer4.Models; using IdentityServer4.Stores; using MongoDB.Bson.Serialization; using MongoDB.Driver; -using Squidex.Store.MongoDb.Utils; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Store.MongoDb.Infrastructure { diff --git a/src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionStorage.cs b/src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionStorage.cs deleted file mode 100644 index 905514dea..000000000 --- a/src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionStorage.cs +++ /dev/null @@ -1,59 +0,0 @@ -// ========================================================================== -// MongoStreamPositionStorage.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Threading.Tasks; -using MongoDB.Driver; -using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.EventStore; -using Squidex.Store.MongoDb.Utils; - -// ReSharper disable InvertIf - -namespace Squidex.Store.MongoDb.Infrastructure -{ - public sealed class MongoStreamPositionStorage : MongoRepositoryBase, IStreamPositionStorage - { - public MongoStreamPositionStorage(IMongoDatabase database) - : base(database) - { - } - - protected override Task SetupCollectionAsync(IMongoCollection collection) - { - return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.SubscriptionName), new CreateIndexOptions { Unique = true }); - } - - protected override string CollectionName() - { - return "StreamPositions"; - } - - public int? ReadPosition(string subscriptionName) - { - Guard.NotNullOrEmpty(subscriptionName, nameof(subscriptionName)); - - var document = Collection.Find(t => t.SubscriptionName == subscriptionName).FirstOrDefault(); - - if (document == null) - { - document = new MongoStreamPositionEntity { SubscriptionName = subscriptionName }; - - Collection.InsertOne(document); - } - - return document.Position; - } - - public void WritePosition(string subscriptionName, int position) - { - Guard.NotNullOrEmpty(subscriptionName, nameof(subscriptionName)); - - Collection.UpdateOne(t => t.SubscriptionName == subscriptionName, Update.Set(t => t.Position, position)); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Store.MongoDb/MongoDbModule.cs b/src/Squidex.Store.MongoDb/MongoDbModule.cs index 795ea6b9a..84e53291e 100644 --- a/src/Squidex.Store.MongoDb/MongoDbModule.cs +++ b/src/Squidex.Store.MongoDb/MongoDbModule.cs @@ -13,7 +13,7 @@ using Microsoft.AspNetCore.Identity.MongoDB; using Microsoft.Extensions.Options; using MongoDB.Driver; using Squidex.Infrastructure.CQRS.Events; -using Squidex.Infrastructure.CQRS.EventStore; +using Squidex.Infrastructure.MongoDb; using Squidex.Read.Apps.Repositories; using Squidex.Read.History.Repositories; using Squidex.Read.Schemas.Repositories; @@ -23,7 +23,6 @@ using Squidex.Store.MongoDb.History; using Squidex.Store.MongoDb.Infrastructure; using Squidex.Store.MongoDb.Schemas; using Squidex.Store.MongoDb.Users; -using Squidex.Store.MongoDb.Utils; namespace Squidex.Store.MongoDb { @@ -66,10 +65,6 @@ namespace Squidex.Store.MongoDb .As() .SingleInstance(); - builder.RegisterType() - .As() - .SingleInstance(); - builder.RegisterType() .As() .InstancePerLifetimeScope(); diff --git a/src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs b/src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs index 9f9c4ede0..30886a40c 100644 --- a/src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs +++ b/src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs @@ -12,6 +12,7 @@ using MongoDB.Bson.Serialization.Attributes; using Squidex.Core.Schemas; using Squidex.Core.Schemas.Json; using Squidex.Infrastructure; +using Squidex.Infrastructure.MongoDb; using Squidex.Read.Schemas.Repositories; using Squidex.Store.MongoDb.Utils; diff --git a/src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs index 95434412d..0a1b5b80b 100644 --- a/src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs @@ -18,6 +18,7 @@ using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Dispatching; +using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.Reflection; using Squidex.Read.Schemas.Repositories; using Squidex.Store.MongoDb.Utils; diff --git a/src/Squidex.Store.MongoDb/Utils/EntityMapper.cs b/src/Squidex.Store.MongoDb/Utils/EntityMapper.cs index 5be7b195a..5cb1bc027 100644 --- a/src/Squidex.Store.MongoDb/Utils/EntityMapper.cs +++ b/src/Squidex.Store.MongoDb/Utils/EntityMapper.cs @@ -13,6 +13,7 @@ using MongoDB.Driver; using Newtonsoft.Json.Linq; using Squidex.Events; using Squidex.Infrastructure.CQRS; +using Squidex.Infrastructure.MongoDb; using Squidex.Read; // ReSharper disable ConvertIfStatementToConditionalTernaryExpression // ReSharper disable SuspiciousTypeConversion.Global @@ -25,33 +26,33 @@ namespace Squidex.Store.MongoDb.Utils { var entity = new T(); - AssignId(headers, entity, useAggregateId); - AssignAppId(headers, entity); - AssignCreated(headers, entity); - AssignCreatedBy(headers, entity); + UpdateWithId(headers, entity, useAggregateId); + UpdateWithAppId(headers, entity); + UpdateWithCreated(headers, entity); + UpdateWithCreatedBy(headers, entity); return Update(entity, headers); } public static T Update(T entity, EnvelopeHeaders headers) where T : MongoEntity { - AssignLastModified(headers, entity); - AssignLastModifiedBy(headers, entity); + UpdateWithLastModified(headers, entity); + UpdateWithLastModifiedBy(headers, entity); return entity; } - private static void AssignCreated(EnvelopeHeaders headers, MongoEntity entity) + private static void UpdateWithCreated(EnvelopeHeaders headers, MongoEntity entity) { entity.Created = headers.Timestamp().ToDateTimeUtc(); } - private static void AssignLastModified(EnvelopeHeaders headers, MongoEntity entity) + private static void UpdateWithLastModified(EnvelopeHeaders headers, MongoEntity entity) { entity.LastModified = headers.Timestamp().ToDateTimeUtc(); } - private static void AssignCreatedBy(EnvelopeHeaders headers, MongoEntity entity) + private static void UpdateWithCreatedBy(EnvelopeHeaders headers, MongoEntity entity) { var createdBy = entity as ITrackCreatedByEntity; @@ -61,7 +62,7 @@ namespace Squidex.Store.MongoDb.Utils } } - private static void AssignLastModifiedBy(EnvelopeHeaders headers, MongoEntity entity) + private static void UpdateWithLastModifiedBy(EnvelopeHeaders headers, MongoEntity entity) { var modifiedBy = entity as ITrackLastModifiedByEntity; @@ -71,7 +72,7 @@ namespace Squidex.Store.MongoDb.Utils } } - private static void AssignAppId(EnvelopeHeaders headers, MongoEntity entity) + private static void UpdateWithAppId(EnvelopeHeaders headers, MongoEntity entity) { var appEntity = entity as IAppRefEntity; @@ -81,7 +82,7 @@ namespace Squidex.Store.MongoDb.Utils } } - private static void AssignId(EnvelopeHeaders headers, MongoEntity entity, bool useAggregateId) + private static void UpdateWithId(EnvelopeHeaders headers, MongoEntity entity, bool useAggregateId) { if (useAggregateId) { diff --git a/src/Squidex.Store.MongoDb/project.json b/src/Squidex.Store.MongoDb/project.json index c99a29345..88ce1d837 100644 --- a/src/Squidex.Store.MongoDb/project.json +++ b/src/Squidex.Store.MongoDb/project.json @@ -10,6 +10,7 @@ "Squidex.Core": "1.0.0-*", "Squidex.Events": "1.0.0-*", "Squidex.Infrastructure": "1.0.0-*", + "Squidex.Infrastructure.MongoDb": "1.0.0-*", "Squidex.Read": "1.0.0-*" }, "frameworks": { diff --git a/src/Squidex.Write/EnrichWithAppIdProcessor.cs b/src/Squidex.Write/EnrichWithAppIdProcessor.cs index 8e5680d0d..2bb3b9a53 100644 --- a/src/Squidex.Write/EnrichWithAppIdProcessor.cs +++ b/src/Squidex.Write/EnrichWithAppIdProcessor.cs @@ -25,7 +25,6 @@ namespace Squidex.Write if (appDomainObject != null) { @event.SetAppId(aggregate.Id); - } else { diff --git a/src/Squidex/Config/Domain/InfrastructureModule.cs b/src/Squidex/Config/Domain/InfrastructureModule.cs index 77d1de265..e0b47fe76 100644 --- a/src/Squidex/Config/Domain/InfrastructureModule.cs +++ b/src/Squidex/Config/Domain/InfrastructureModule.cs @@ -13,8 +13,7 @@ using Squidex.Core.Schemas; using Squidex.Core.Schemas.Json; using Squidex.Infrastructure.CQRS.Autofac; using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.CQRS.EventStore; -using Squidex.Store.MongoDb.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Config.Domain { @@ -30,15 +29,11 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() - .As() - .SingleInstance(); - builder.RegisterType() .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .As() .SingleInstance(); @@ -50,7 +45,7 @@ namespace Squidex.Config.Domain .As() .SingleInstance(); - builder.RegisterType() + builder.RegisterType() .AsSelf() .SingleInstance(); diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/Serializers.cs index 574d84a53..9ea2ca4c6 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/src/Squidex/Config/Domain/Serializers.cs @@ -13,7 +13,7 @@ using Newtonsoft.Json.Serialization; using Squidex.Core.Schemas; using Squidex.Events.Schemas; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.EventStore; +using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Json; namespace Squidex.Config.Domain @@ -52,7 +52,7 @@ namespace Squidex.Config.Domain services.AddSingleton(t => CreateSettings()); services.AddSingleton(t => CreateSerializer(t.GetRequiredService())); - services.AddSingleton(); + services.AddSingleton(); return services; } diff --git a/src/Squidex/Config/EventStore/EventStoreModule.cs b/src/Squidex/Config/EventStore/EventStoreModule.cs deleted file mode 100644 index 8da9846b7..000000000 --- a/src/Squidex/Config/EventStore/EventStoreModule.cs +++ /dev/null @@ -1,55 +0,0 @@ -// ========================================================================== -// EventStoreModule.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System.Net; -using Autofac; -using EventStore.ClientAPI; -using EventStore.ClientAPI.SystemData; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure.CQRS.EventStore; - -namespace Squidex.Config.EventStore -{ - public class EventStoreModule : Module - { - protected override void Load(ContainerBuilder builder) - { - builder.Register(context => - { - var options = context.Resolve>().Value; - - var eventStore = - EventStoreConnection.Create( - ConnectionSettings.Create() - .UseConsoleLogger() - .UseDebugLogger() - .KeepReconnecting() - .KeepRetrying(), - new IPEndPoint(IPAddress.Parse(options.IPAddress), options.Port)); - - eventStore.ConnectAsync().Wait(); - - return eventStore; - }).SingleInstance(); - - builder.Register(context => - { - var options = context.Resolve>().Value; - - return new UserCredentials(options.Username, options.Password); - }).SingleInstance(); - - builder.Register(context => - { - var options = context.Resolve>().Value; - - return new DefaultNameResolver(options.Prefix); - }).As().SingleInstance(); - } - } -} diff --git a/src/Squidex/Config/EventStore/EventStoreUsage.cs b/src/Squidex/Config/EventStore/EventStoreUsage.cs index 1e97dde59..94a8f6aa2 100644 --- a/src/Squidex/Config/EventStore/EventStoreUsage.cs +++ b/src/Squidex/Config/EventStore/EventStoreUsage.cs @@ -8,8 +8,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure.CQRS.EventStore; +using Squidex.Infrastructure.CQRS.Events; namespace Squidex.Config.EventStore { @@ -17,9 +16,7 @@ namespace Squidex.Config.EventStore { public static IApplicationBuilder UseMyEventStore(this IApplicationBuilder app) { - var options = app.ApplicationServices.GetRequiredService>().Value; - - app.ApplicationServices.GetService().Subscribe(options.Prefix); + app.ApplicationServices.GetService().Subscribe(); return app; } diff --git a/src/Squidex/Config/EventStore/MongoDbEventStoreModule.cs b/src/Squidex/Config/EventStore/MongoDbEventStoreModule.cs new file mode 100644 index 000000000..cf3ad0945 --- /dev/null +++ b/src/Squidex/Config/EventStore/MongoDbEventStoreModule.cs @@ -0,0 +1,28 @@ +// ========================================================================== +// MongoDbEventStoreModule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using Autofac; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.MongoDb.EventStore; + +namespace Squidex.Config.EventStore +{ + public class MongoDbEventStoreModule : Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + } + } +} diff --git a/src/Squidex/Config/EventStore/MyEventStoreOptions.cs b/src/Squidex/Config/EventStore/MyRabbitMqOptions.cs similarity index 55% rename from src/Squidex/Config/EventStore/MyEventStoreOptions.cs rename to src/Squidex/Config/EventStore/MyRabbitMqOptions.cs index b002fbd81..aa8b50534 100644 --- a/src/Squidex/Config/EventStore/MyEventStoreOptions.cs +++ b/src/Squidex/Config/EventStore/MyRabbitMqOptions.cs @@ -1,5 +1,5 @@ // ========================================================================== -// MyEventStoreOptions.cs +// MyRabbitMqOptions.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,16 +8,8 @@ namespace Squidex.Config.EventStore { - public sealed class MyEventStoreOptions + public sealed class MyRabbitMqOptions { - public string IPAddress { get; set; } - - public string Username { get; set; } - - public string Password { get; set; } - - public string Prefix { get; set; } - - public int Port { get; set; } + public string ConnectionString { get; set; } } -} +} \ No newline at end of file diff --git a/src/Squidex/Config/EventStore/RabbitMqEventChannelModule.cs b/src/Squidex/Config/EventStore/RabbitMqEventChannelModule.cs new file mode 100644 index 000000000..ca2c38c36 --- /dev/null +++ b/src/Squidex/Config/EventStore/RabbitMqEventChannelModule.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// RabbitMqEventChannelModule.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Autofac; +using Microsoft.Extensions.Options; +using RabbitMQ.Client; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.RabbitMq; + +namespace Squidex.Config.EventStore +{ + public class RabbitMqEventChannelModule : Module + { + protected override void Load(ContainerBuilder builder) + { + builder.Register(context => + { + var options = context.Resolve>().Value; + + var factory = new ConnectionFactory(); + + factory.SetUri(new Uri(options.ConnectionString)); + + return factory; + }).As().SingleInstance(); + + builder.RegisterType() + .As() + .As() + .SingleInstance(); + } + } +} diff --git a/src/Squidex/Config/Identity/IdentityServices.cs b/src/Squidex/Config/Identity/IdentityServices.cs index 07030c5f9..5ac4a6365 100644 --- a/src/Squidex/Config/Identity/IdentityServices.cs +++ b/src/Squidex/Config/Identity/IdentityServices.cs @@ -11,7 +11,6 @@ using System.Reflection; using System.Security.Cryptography.X509Certificates; using IdentityServer4.Models; using IdentityServer4.Stores; -using IdentityServer4.Stores.InMemory; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Identity.MongoDB; using Microsoft.Extensions.DependencyInjection; @@ -50,7 +49,7 @@ namespace Squidex.Config.Identity services.AddIdentityServer(options => { - options.UserInteractionOptions.ErrorUrl = "/account/error/"; + options.UserInteraction.ErrorUrl = "/account/error/"; }) .AddAspNetIdentity() .AddInMemoryApiResources(GetApiResources()) diff --git a/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs b/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs index c72839679..6b70a52f9 100644 --- a/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs +++ b/src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs @@ -8,7 +8,9 @@ using System; using System.Text.RegularExpressions; +using System.Threading.Tasks; using NJsonSchema; +using NJsonSchema.Generation; using NJsonSchema.Infrastructure; using NSwag; using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors; @@ -22,13 +24,13 @@ namespace Squidex.Config.Swagger { private static readonly Regex ResponseRegex = new Regex("(?[0-9]{3}) => (?.*)", RegexOptions.Compiled); - public bool Process(OperationProcessorContext context) + public async Task ProcessAsync(OperationProcessorContext context) { var hasOkResponse = false; var operation = context.OperationDescription.Operation; - var returnsDescription = context.MethodInfo.GetXmlDocumentation("returns") ?? string.Empty; + var returnsDescription = await context.MethodInfo.GetXmlDocumentationTagAsync("returns") ?? string.Empty; foreach (Match match in ResponseRegex.Matches(returnsDescription)) { @@ -51,7 +53,7 @@ namespace Squidex.Config.Swagger } } - AddInternalErrorResponse(context, operation); + await AddInternalErrorResponseAsync(context, operation); if (!hasOkResponse) { @@ -61,7 +63,7 @@ namespace Squidex.Config.Swagger return true; } - private static void AddInternalErrorResponse(OperationProcessorContext context, SwaggerOperation operation) + private static async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation) { if (operation.Responses.ContainsKey("500")) { @@ -73,7 +75,7 @@ namespace Squidex.Config.Swagger var response = new SwaggerResponse { Description = "Operation failed." }; - response.Schema = context.SwaggerGenerator.GenerateAndAppendSchemaFromType(errorType, errorSchema.IsNullable, null); + response.Schema = await context.SwaggerGenerator.GenerateAndAppendSchemaFromTypeAsync(errorType, errorSchema.IsNullable, null); operation.Responses.Add("500", response); } diff --git a/src/Squidex/Config/Swagger/XmlTagProcessor.cs b/src/Squidex/Config/Swagger/XmlTagProcessor.cs index ecaf8831a..ebabe5353 100644 --- a/src/Squidex/Config/Swagger/XmlTagProcessor.cs +++ b/src/Squidex/Config/Swagger/XmlTagProcessor.cs @@ -7,6 +7,7 @@ // ========================================================================== using System.Reflection; +using System.Threading.Tasks; using NJsonSchema.Infrastructure; using NSwag.Annotations; using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors; @@ -31,7 +32,7 @@ namespace Squidex.Config.Swagger if (tag != null) { - var description = controllerType.GetXmlSummary(); + var description = controllerType.GetXmlSummaryAsync().Result; if (description != null) { @@ -47,7 +48,7 @@ namespace Squidex.Config.Swagger } } - public bool Process(OperationProcessorContext context) + public Task ProcessAsync(OperationProcessorContext context) { var tagAttribute = context.MethodInfo.DeclaringType.GetTypeInfo().GetCustomAttribute(); @@ -58,7 +59,7 @@ namespace Squidex.Config.Swagger context.OperationDescription.Operation.Tags.Add(tagAttribute.Name); } - return true; + return Task.FromResult(true); } } } diff --git a/src/Squidex/Startup.cs b/src/Squidex/Startup.cs index 0f34095de..932beab32 100644 --- a/src/Squidex/Startup.cs +++ b/src/Squidex/Startup.cs @@ -71,17 +71,18 @@ namespace Squidex services.Configure( Configuration.GetSection("stores:mongoDb")); - services.Configure( - Configuration.GetSection("stores:eventStore")); + services.Configure( + Configuration.GetSection("stores:rabbitMq")); services.Configure( Configuration.GetSection("urls")); services.Configure( Configuration.GetSection("identity")); var builder = new ContainerBuilder(); - builder.RegisterModule(); builder.RegisterModule(); + builder.RegisterModule(); builder.RegisterModule(); + builder.RegisterModule(); builder.RegisterModule(); builder.RegisterModule(); builder.RegisterModule(); diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index f9545d4fb..2a0812d6d 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -7,12 +7,8 @@ "connectionString": "mongodb://localhost", "databaseName": "Squidex" }, - "eventStore": { - "ipAddress": "127.0.0.1", - "port": 1113, - "prefix": "squidex_v5", - "username": "admin", - "password": "changeit" + "rabbitMq": { + "connectionString": "amqp://guest:guest@localhost/" } }, "identity": { diff --git a/src/Squidex/project.json b/src/Squidex/project.json index 267413560..c334570ee 100644 --- a/src/Squidex/project.json +++ b/src/Squidex/project.json @@ -24,11 +24,14 @@ "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "MongoDB.Driver": "2.4.1", "NJsonSchema": "7.3.6214.20986", + "NSwag.AspNetCore": "8.5.0", "OpenCover": "4.6.519", "ReportGenerator": "2.5.2", "Squidex.Core": "1.0.0-*", "Squidex.Events": "1.0.0-*", "Squidex.Infrastructure": "1.0.0-*", + "Squidex.Infrastructure.MongoDb": "1.0.0-*", + "Squidex.Infrastructure.RabbitMq": "1.0.0-*", "Squidex.Read": "1.0.0-*", "Squidex.Store.MongoDb": "1.0.0-*", "Squidex.Write": "1.0.0-*", diff --git a/tests/Squidex.Core.Tests/Schemas/FieldRegistryTests.cs b/tests/Squidex.Core.Tests/Schemas/FieldRegistryTests.cs index 41696f23a..f5e40c12f 100644 --- a/tests/Squidex.Core.Tests/Schemas/FieldRegistryTests.cs +++ b/tests/Squidex.Core.Tests/Schemas/FieldRegistryTests.cs @@ -17,7 +17,7 @@ namespace Squidex.Core.Schemas { private readonly FieldRegistry sut = new FieldRegistry(); - public sealed class InvalidProperties : FieldProperties + private sealed class InvalidProperties : FieldProperties { protected override IEnumerable ValidateCore() { diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs index 1317e064e..c9eb029b4 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs @@ -26,8 +26,9 @@ namespace Squidex.Infrastructure.CQRS.Commands Assert.Equal(command, sut.Command); Assert.Null(sut.Exception); - Assert.False(sut.IsSucceeded); Assert.False(sut.IsHandled); + Assert.False(sut.IsSucceeded); + Assert.False(sut.IsFailed); } [Fact] @@ -39,8 +40,9 @@ namespace Squidex.Infrastructure.CQRS.Commands sut.Fail(exc); Assert.Equal(exc, sut.Exception); - Assert.False(sut.IsSucceeded); Assert.True(sut.IsHandled); + Assert.True(sut.IsFailed); + Assert.False(sut.IsSucceeded); } [Fact] @@ -53,18 +55,20 @@ namespace Squidex.Infrastructure.CQRS.Commands Assert.Null(sut.Exception); Assert.True(sut.IsSucceeded); Assert.True(sut.IsHandled); + Assert.False(sut.IsFailed); } [Fact] - public void Shoud_not_change_status_when_already_succeeded() + public void Should_replace_status_when_already_succeeded() { var sut = new CommandContext(command); sut.Succeed(Guid.NewGuid()); sut.Fail(new Exception()); - Assert.Null(sut.Exception); + Assert.NotNull(sut.Exception); Assert.True(sut.IsHandled); + Assert.True(sut.IsFailed); Assert.True(sut.IsSucceeded); } @@ -77,12 +81,13 @@ namespace Squidex.Infrastructure.CQRS.Commands sut.Succeed(guid); Assert.Equal(guid, sut.Result()); - Assert.True(sut.IsSucceeded); Assert.True(sut.IsHandled); + Assert.True(sut.IsSucceeded); + Assert.False(sut.IsFailed); } [Fact] - public void Shoud_not_change_status_when_already_failed() + public void Should_not_change_status_when_already_failed() { var sut = new CommandContext(command); @@ -91,6 +96,7 @@ namespace Squidex.Infrastructure.CQRS.Commands Assert.NotNull(sut.Exception); Assert.True(sut.IsHandled); + Assert.True(sut.IsFailed); Assert.False(sut.IsSucceeded); } } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/EventStore/DefaultNameResolverTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs similarity index 81% rename from tests/Squidex.Infrastructure.Tests/CQRS/EventStore/DefaultNameResolverTests.cs rename to tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs index 273799358..575039533 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/EventStore/DefaultNameResolverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs @@ -7,13 +7,14 @@ // ========================================================================== using System; -using Squidex.Infrastructure.CQRS.Events; using Xunit; -namespace Squidex.Infrastructure.CQRS.EventStore +namespace Squidex.Infrastructure.CQRS.Events { public class DefaultNameResolverTests { + private readonly DefaultNameResolver sut = new DefaultNameResolver(); + private sealed class MyUser : DomainObject { public MyUser(Guid id, int version) @@ -41,23 +42,21 @@ namespace Squidex.Infrastructure.CQRS.EventStore [Fact] public void Should_calculate_name() { - var sut = new DefaultNameResolver("Squidex"); var user = new MyUser(Guid.NewGuid(), 1); var name = sut.GetStreamName(typeof(MyUser), user.Id); - Assert.Equal($"squidex-myUser-{user.Id}", name); + Assert.Equal($"myUser-{user.Id}", name); } [Fact] public void Should_calculate_name_and_remove_suffix() { - var sut = new DefaultNameResolver("Squidex"); var user = new MyUserDomainObject(Guid.NewGuid(), 1); var name = sut.GetStreamName(typeof(MyUserDomainObject), user.Id); - Assert.Equal($"squidex-myUser-{user.Id}", name); + Assert.Equal($"myUser-{user.Id}", name); } } } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/EventStore/EventStoreFormatterTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs similarity index 70% rename from tests/Squidex.Infrastructure.Tests/CQRS/EventStore/EventStoreFormatterTests.cs rename to tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs index 829758a11..7525d9f54 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/EventStore/EventStoreFormatterTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs @@ -10,11 +10,10 @@ using System; using System.Linq; using Newtonsoft.Json; using NodaTime; -using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.Json; using Xunit; -namespace Squidex.Infrastructure.CQRS.EventStore +namespace Squidex.Infrastructure.CQRS.Events { public class EventStoreFormatterTests { @@ -23,19 +22,6 @@ namespace Squidex.Infrastructure.CQRS.EventStore public string MyProperty { get; set; } } - public sealed class MyReceivedEvent : IReceivedEvent - { - public int EventNumber { get; set; } - - public string EventType { get; set; } - - public byte[] Metadata { get; set; } - - public byte[] Payload { get; set; } - - public DateTime Created { get; set; } - } - private static readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings(); static EventStoreFormatterTests() @@ -60,20 +46,11 @@ namespace Squidex.Infrastructure.CQRS.EventStore inputEvent.SetEventNumber(1); inputEvent.SetTimestamp(SystemClock.Instance.GetCurrentInstant()); - var sut = new EventStoreFormatter(serializerSettings); + var sut = new EventDataFormatter(serializerSettings); var eventData = sut.ToEventData(inputEvent.To(), commitId); - var receivedEvent = new MyReceivedEvent - { - Payload = eventData.Data, - Created = inputEvent.Headers.Timestamp().ToDateTimeUtc(), - EventNumber = 1, - EventType = "event", - Metadata = eventData.Metadata - }; - - var outputEvent = sut.Parse(receivedEvent).To(); + var outputEvent = sut.Parse(eventData).To(); CompareHeaders(outputEvent.Headers, inputEvent.Headers); diff --git a/tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs b/tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs new file mode 100644 index 000000000..2801b87fd --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// DisposableObjectTest.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class DisposableObjectTests + { + public sealed class MyDisposableObject : DisposableObject + { + public int DisposeCallCount { get; set; } + + protected override void DisposeObject(bool disposing) + { + DisposeCallCount++; + } + + public void Ensure() + { + ThrowIfDisposed(); + } + } + + [Fact] + public void Should_not_throw_exception_when_not_disposed() + { + var sut = new MyDisposableObject(); + + sut.Ensure(); + } + + [Fact] + public void Should_dispose_once() + { + var sut = new MyDisposableObject(); + + sut.Dispose(); + sut.Dispose(); + + Assert.True(sut.IsDisposed); + + Assert.Equal(1, sut.DisposeCallCount); + } + + [Fact] + public void Should_throw_exception_when_disposed() + { + var sut = new MyDisposableObject(); + + sut.Dispose(); + + Assert.Throws(() => sut.Ensure()); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs b/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs new file mode 100644 index 000000000..2d4a1628c --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// TaskExtensionsTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Tasks; +using Xunit; + +namespace Squidex.Infrastructure +{ + public class TaskExtensionsTests + { + [Fact] + public void Should_do_nothing_on_forget() + { + var task = Task.FromResult(123); + + task.Forget(); + + Assert.Equal(123, task.Result); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTest.cs b/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs similarity index 93% rename from tests/Squidex.Infrastructure.Tests/TypeNameAttributeTest.cs rename to tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs index 819050ded..e1f6f2925 100644 --- a/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTest.cs +++ b/tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs @@ -10,7 +10,7 @@ using Xunit; namespace Squidex.Infrastructure { - public class TypeNameAttributeTest + public class TypeNameAttributeTests { [Fact] public void Should_instantiate() diff --git a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs index 3ab392639..0884a256b 100644 --- a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs @@ -89,6 +89,15 @@ namespace Squidex.Write.Apps Assert.Throws(() => sut.AssignContributor(new AssignContributor { ContributorId = user.Identifier, Permission = PermissionLevel.Editor })); } + [Fact] + public void AssignContributor_should_throw_if_user_already_contributor() + { + CreateApp(); + sut.AssignContributor(new AssignContributor { ContributorId = contributorId, Permission = PermissionLevel.Editor }); + + Assert.Throws(() => sut.AssignContributor(new AssignContributor { ContributorId = contributorId, Permission = PermissionLevel.Editor })); + } + [Fact] public void AssignContributor_should_create_events() { diff --git a/tests/Squidex.Write.Tests/EnrichWithAppIdProcessorTests.cs b/tests/Squidex.Write.Tests/EnrichWithAppIdProcessorTests.cs index 3ad9f630a..ee609dc30 100644 --- a/tests/Squidex.Write.Tests/EnrichWithAppIdProcessorTests.cs +++ b/tests/Squidex.Write.Tests/EnrichWithAppIdProcessorTests.cs @@ -12,6 +12,7 @@ using Squidex.Events; using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Events; +using Squidex.Write.Apps; using Xunit; namespace Squidex.Write @@ -42,6 +43,18 @@ namespace Squidex.Write Assert.False(envelope.Headers.Contains("AppId")); } + [Fact] + public async Task Should_attach_app_id_from_domain_object() + { + var appId = Guid.NewGuid(); + + var envelope = new Envelope(new MyEvent()); + + await sut.ProcessEventAsync(envelope, new AppDomainObject(appId, 1), new MyNormalCommand()); + + Assert.Equal(appId, envelope.Headers.AppId()); + } + [Fact] public async Task Should_attach_app_id_to_event_envelope() {