Browse Source

Refactoring and migration to rabbitmq

pull/1/head
Sebastian 9 years ago
parent
commit
30602ee08e
  1. 14
      Squidex.sln
  2. 4
      global.json
  3. 25
      src/Squidex.Infrastructure.MongoDb/EventStore/MongoEvent.cs
  4. 43
      src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventCommit.cs
  5. 137
      src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs
  6. 5
      src/Squidex.Infrastructure.MongoDb/MongoEntity.cs
  7. 2
      src/Squidex.Infrastructure.MongoDb/MongoExtensions.cs
  8. 3
      src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs
  9. 26
      src/Squidex.Infrastructure.MongoDb/Properties/AssemblyInfo.cs
  10. 3
      src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs
  11. 24
      src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.xproj
  12. 31
      src/Squidex.Infrastructure.MongoDb/project.json
  13. 19
      src/Squidex.Infrastructure.RabbitMq/InfrastructureErrors.cs
  14. 26
      src/Squidex.Infrastructure.RabbitMq/Properties/AssemblyInfo.cs
  15. 80
      src/Squidex.Infrastructure.RabbitMq/RabbitMqEventChannel.cs
  16. 21
      src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.xproj
  17. 31
      src/Squidex.Infrastructure.RabbitMq/project.json
  18. 9
      src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs
  19. 96
      src/Squidex.Infrastructure/CQRS/Commands/DefaultDomainObjectRepository.cs
  20. 250
      src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs
  21. 152
      src/Squidex.Infrastructure/CQRS/EventStore/EventStoreDomainObjectRepository.cs
  22. 66
      src/Squidex.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs
  23. 48
      src/Squidex.Infrastructure/CQRS/EventStore/EventWrapper.cs
  24. 16
      src/Squidex.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs
  25. 13
      src/Squidex.Infrastructure/CQRS/Events/DefaultNameResolver.cs
  26. 116
      src/Squidex.Infrastructure/CQRS/Events/EventBus.cs
  27. 23
      src/Squidex.Infrastructure/CQRS/Events/EventData.cs
  28. 59
      src/Squidex.Infrastructure/CQRS/Events/EventDataFormatter.cs
  29. 15
      src/Squidex.Infrastructure/CQRS/Events/IEventPublisher.cs
  30. 23
      src/Squidex.Infrastructure/CQRS/Events/IEventStore.cs
  31. 16
      src/Squidex.Infrastructure/CQRS/Events/IEventStream.cs
  32. 2
      src/Squidex.Infrastructure/CQRS/Events/IStreamNameResolver.cs
  33. 67
      src/Squidex.Infrastructure/DisposableObject.cs
  34. 2
      src/Squidex.Infrastructure/project.json
  35. 2
      src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs
  36. 1
      src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs
  37. 2
      src/Squidex.Store.MongoDb/History/MongoHistoryEventEntity.cs
  38. 1
      src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs
  39. 2
      src/Squidex.Store.MongoDb/Infrastructure/MongoPersistedGrantStore.cs
  40. 59
      src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionStorage.cs
  41. 7
      src/Squidex.Store.MongoDb/MongoDbModule.cs
  42. 1
      src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs
  43. 1
      src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs
  44. 25
      src/Squidex.Store.MongoDb/Utils/EntityMapper.cs
  45. 1
      src/Squidex.Store.MongoDb/project.json
  46. 1
      src/Squidex.Write/EnrichWithAppIdProcessor.cs
  47. 11
      src/Squidex/Config/Domain/InfrastructureModule.cs
  48. 4
      src/Squidex/Config/Domain/Serializers.cs
  49. 55
      src/Squidex/Config/EventStore/EventStoreModule.cs
  50. 7
      src/Squidex/Config/EventStore/EventStoreUsage.cs
  51. 28
      src/Squidex/Config/EventStore/MongoDbEventStoreModule.cs
  52. 16
      src/Squidex/Config/EventStore/MyRabbitMqOptions.cs
  53. 39
      src/Squidex/Config/EventStore/RabbitMqEventChannelModule.cs
  54. 3
      src/Squidex/Config/Identity/IdentityServices.cs
  55. 12
      src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs
  56. 7
      src/Squidex/Config/Swagger/XmlTagProcessor.cs
  57. 7
      src/Squidex/Startup.cs
  58. 8
      src/Squidex/appsettings.json
  59. 3
      src/Squidex/project.json
  60. 2
      tests/Squidex.Core.Tests/Schemas/FieldRegistryTests.cs
  61. 18
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs
  62. 11
      tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs
  63. 29
      tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs
  64. 62
      tests/Squidex.Infrastructure.Tests/DisposableObjectTests.cs
  65. 27
      tests/Squidex.Infrastructure.Tests/TaskExtensionsTests.cs
  66. 2
      tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs
  67. 9
      tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs
  68. 13
      tests/Squidex.Write.Tests/EnrichWithAppIdProcessorTests.cs

14
Squidex.sln

@ -36,6 +36,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Squidex.Infrastructure.Test
EndProject EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Squidex.Core.Tests", "tests\Squidex.Core.Tests\Squidex.Core.Tests.xproj", "{FD0AFD44-7A93-4F9E-B5ED-72582392E435}" Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Squidex.Core.Tests", "tests\Squidex.Core.Tests\Squidex.Core.Tests.xproj", "{FD0AFD44-7A93-4F9E-B5ED-72582392E435}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{FD0AFD44-7A93-4F9E-B5ED-72582392E435}.Release|Any CPU.Build.0 = 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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -97,5 +109,7 @@ Global
{9A3DEA7E-1681-4D48-AC5C-1F0DE421A203} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} {9A3DEA7E-1681-4D48-AC5C-1F0DE421A203} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{7FD0A92B-7862-4BB1-932B-B52A9CACB56B} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {7FD0A92B-7862-4BB1-932B-B52A9CACB56B} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{FD0AFD44-7A93-4F9E-B5ED-72582392E435} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} {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 EndGlobalSection
EndGlobal EndGlobal

4
global.json

@ -1,5 +1,5 @@
{ {
"projects": [ "src", "tests" ], "projects": [ "src", "tests", "." ],
"sdk": { "sdk": {
"version": "1.0.0-preview2-1-003177" "version": "1.0.0-preview2-1-003177"
} }

25
src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionEntity.cs → src/Squidex.Infrastructure.MongoDb/EventStore/MongoEvent.cs

@ -1,29 +1,32 @@
// ========================================================================== // ==========================================================================
// MongoStreamPositionEntity.cs // MongoEvent.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System.Runtime.Serialization; using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
namespace Squidex.Store.MongoDb.Infrastructure namespace Squidex.Infrastructure.MongoDb.EventStore
{ {
[DataContract] public class MongoEvent
public class MongoStreamPositionEntity
{ {
[BsonId] [BsonElement]
public ObjectId Id { get; set; }
[BsonRequired] [BsonRequired]
public Guid EventId { get; set; }
[BsonElement] [BsonElement]
public string SubscriptionName { get; set; } [BsonRequired]
public string Payload { get; set; }
[BsonElement]
[BsonRequired] [BsonRequired]
public string Metadata { get; set; }
[BsonElement] [BsonElement]
public int? Position { get; set; } [BsonRequired]
public string Type { get; set; }
} }
} }

43
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<MongoEvent> Events { get; set; }
[BsonElement]
[BsonRequired]
public string EventStream { get; set; }
[BsonElement]
[BsonRequired]
public int EventsVersion { get; set; }
[BsonElement]
[BsonRequired]
public int EventCount { get; set; }
}
}

137
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<MongoEventCommit>, 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<MongoEventCommit> collection)
{
return collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.EventStream).Ascending(x => x.EventsVersion), new CreateIndexOptions { Unique = true });
}
public IObservable<EventData> GetEventsAsync(string streamName)
{
Guard.NotNullOrEmpty(streamName, nameof(streamName));
return Observable.Create<EventData>(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<EventData> GetEventsAsync()
{
return Observable.Create<EventData>(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<EventData> events)
{
var allCommits =
await Collection.Find(c => c.EventStream == streamName)
.Project<BsonDocument>(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);
}
}
}
}

5
src/Squidex.Store.MongoDb/Utils/MongoEntity.cs → src/Squidex.Infrastructure.MongoDb/MongoEntity.cs

@ -9,11 +9,10 @@
using System; using System;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes; 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] [BsonId]
[BsonElement] [BsonElement]

2
src/Squidex.Store.MongoDb/Utils/MongoExtensions.cs → src/Squidex.Infrastructure.MongoDb/MongoExtensions.cs

@ -9,7 +9,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
namespace Squidex.Store.MongoDb.Utils namespace Squidex.Infrastructure.MongoDb
{ {
public static class MongoExtensions public static class MongoExtensions
{ {

3
src/Squidex.Store.MongoDb/Utils/MongoRepositoryBase.cs → src/Squidex.Infrastructure.MongoDb/MongoRepositoryBase.cs

@ -9,9 +9,8 @@
using System.Globalization; using System.Globalization;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure;
namespace Squidex.Store.MongoDb.Utils namespace Squidex.Infrastructure.MongoDb
{ {
public abstract class MongoRepositoryBase<TEntity> public abstract class MongoRepositoryBase<TEntity>
{ {

26
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")]

3
src/Squidex.Store.MongoDb/Utils/RefTokenSerializer.cs → src/Squidex.Infrastructure.MongoDb/RefTokenSerializer.cs

@ -8,11 +8,10 @@
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Serializers; using MongoDB.Bson.Serialization.Serializers;
using Squidex.Infrastructure;
// ReSharper disable InvertIf // ReSharper disable InvertIf
namespace Squidex.Store.MongoDb.Utils namespace Squidex.Infrastructure.MongoDb
{ {
public class RefTokenSerializer : SerializerBase<RefToken> public class RefTokenSerializer : SerializerBase<RefToken>
{ {

24
src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.xproj

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>6a811927-3c37-430a-90f4-503e37123956</ProjectGuid>
<RootNamespace>Squidex.Infrastructure.MongoDb</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
<ProjectExtensions>
<VisualStudio>
<UserProperties project_1json__JSONSchema="http://json.schemastore.org/project-1.0.0-beta6" />
</VisualStudio>
</ProjectExtensions>
</Project>

31
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"
}
}

19
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");
}
}

26
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")]

80
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<IModel> currentChannel;
public RabbitMqEventChannel(IConnectionFactory connectionFactory)
{
Guard.NotNull(connectionFactory, nameof(connectionFactory));
currentChannel = new Lazy<IModel>(() => 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<EventData> 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<EventData>(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;
}
}
}

21
src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.xproj

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>3c9ba12d-f5f2-4355-8d30-8289e4d0752d</ProjectGuid>
<RootNamespace>Squidex.Infrastructure.RabbitMq</RootNamespace>
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">.\obj</BaseIntermediateOutputPath>
<OutputPath Condition="'$(OutputPath)'=='' ">.\bin\</OutputPath>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

31
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"
}
}

9
src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs

@ -23,7 +23,12 @@ namespace Squidex.Infrastructure.CQRS.Commands
public bool IsHandled public bool IsHandled
{ {
get { return result != null || exception != null; } get { return IsSucceeded || IsFailed; }
}
public bool IsFailed
{
get { return exception != null; }
} }
public bool IsSucceeded public bool IsSucceeded
@ -55,7 +60,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
public void Fail(Exception handlerException) public void Fail(Exception handlerException)
{ {
if (IsHandled) if (IsFailed)
{ {
return; return;
} }

96
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<TDomainObject> GetByIdAsync<TDomainObject>(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<Envelope<IEvent>> 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);
}
}
}
}

250
src/Squidex.Infrastructure/CQRS/EventStore/EventStoreBus.cs

@ -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<ILiveEventConsumer> liveConsumers;
private readonly IEnumerable<ICatchEventConsumer> catchConsumers;
private readonly ILogger<EventStoreBus> logger;
private readonly IStreamPositionStorage positions;
private readonly List<EventStoreCatchUpSubscription> catchSubscriptions = new List<EventStoreCatchUpSubscription>();
private EventStoreSubscription liveSubscription;
private string streamName;
private bool isSubscribed;
public EventStoreBus(
ILogger<EventStoreBus> logger,
IEnumerable<ILiveEventConsumer> liveConsumers,
IEnumerable<ICatchEventConsumer> 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<IEvent> @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<IEvent> @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<IEventConsumer> consumers, Envelope<IEvent> @event)
{
return Task.WhenAll(consumers.Select(c => DispatchConsumer(@event, c)).ToList());
}
private async Task DispatchConsumer(Envelope<IEvent> @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());
}
}
}
}

152
src/Squidex.Infrastructure/CQRS/EventStore/EventStoreDomainObjectRepository.cs

@ -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<TDomainObject> GetByIdAsync<TDomainObject>(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<Envelope<IEvent>> 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<EventData> 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");
}
}
}
}

66
src/Squidex.Infrastructure/CQRS/EventStore/EventStoreFormatter.cs

@ -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<IEvent> Parse(IReceivedEvent @event)
{
var headers = ReadJson<PropertiesBag>(@event.Metadata);
var eventType = TypeNameRegistry.GetType(@event.EventType);
var eventData = ReadJson<IEvent>(@event.Payload, eventType);
var envelope = new Envelope<IEvent>(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<IEvent> 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<T>(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));
}
}
}

48
src/Squidex.Infrastructure/CQRS/EventStore/EventWrapper.cs

@ -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;
}
}
}

16
src/Squidex.Infrastructure/CQRS/EventStore/IStreamPositionStorage.cs

@ -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);
}
}

13
src/Squidex.Infrastructure/CQRS/EventStore/DefaultNameResolver.cs → src/Squidex.Infrastructure/CQRS/Events/DefaultNameResolver.cs

@ -7,21 +7,12 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Globalization;
namespace Squidex.Infrastructure.CQRS.EventStore namespace Squidex.Infrastructure.CQRS.Events
{ {
public sealed class DefaultNameResolver : IStreamNameResolver public sealed class DefaultNameResolver : IStreamNameResolver
{ {
private const string Suffix = "DomainObject"; 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) public string GetStreamName(Type aggregateType, Guid id)
{ {
@ -32,7 +23,7 @@ namespace Squidex.Infrastructure.CQRS.EventStore
typeName = typeName.Substring(0, typeName.Length - Suffix.Length); typeName = typeName.Substring(0, typeName.Length - Suffix.Length);
} }
return string.Format(CultureInfo.InvariantCulture, "{0}-{1}-{2}", prefix, typeName, id); return $"{typeName}-{id}";
} }
} }
} }

116
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<ILiveEventConsumer> liveConsumers;
private readonly IEnumerable<ICatchEventConsumer> catchConsumers;
private readonly IEventStream eventStream;
private readonly ILogger<EventBus> logger;
private bool isSubscribed;
public EventBus(
ILogger<EventBus> logger,
IEventStream eventStream,
IEnumerable<ILiveEventConsumer> liveConsumers,
IEnumerable<ICatchEventConsumer> 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<IEventConsumer>().Union(catchConsumers), @event);
}
else
{
DispatchConsumers(liveConsumers, @event);
}
});
isSubscribed = true;
}
private void DispatchConsumers(IEnumerable<IEventConsumer> consumers, Envelope<IEvent> @event)
{
Task.WaitAll(consumers.Select(c => DispatchConsumer(@event, c)).ToArray());
}
private async Task DispatchConsumer(Envelope<IEvent> @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<IEvent> 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;
}
}
}
}

23
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; }
}
}

59
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<IEvent> Parse(EventData eventData)
{
var headers = ReadJson<PropertiesBag>(eventData.Metadata);
var eventType = TypeNameRegistry.GetType(eventData.Type);
var eventContent = ReadJson<IEvent>(eventData.Payload, eventType);
var envelope = new Envelope<IEvent>(eventContent, headers);
return envelope;
}
public EventData ToEventData(Envelope<IEvent> 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<T>(string data, Type type = null)
{
return (T)JsonConvert.DeserializeObject(data, type ?? typeof(T), serializerSettings);
}
private string WriteJson(object value)
{
return JsonConvert.SerializeObject(value, serializerSettings);
}
}
}

15
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);
}
}

23
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<EventData> GetEventsAsync();
IObservable<EventData> GetEventsAsync(string streamName);
Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, IEnumerable<EventData> events);
}
}

16
src/Squidex.Infrastructure/CQRS/EventStore/IReceivedEvent.cs → src/Squidex.Infrastructure/CQRS/Events/IEventStream.cs

@ -1,5 +1,5 @@
// ========================================================================== // ==========================================================================
// IReceivedEvent.cs // IEventStream.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
@ -8,18 +8,10 @@
using System; using System;
namespace Squidex.Infrastructure.CQRS.EventStore namespace Squidex.Infrastructure.CQRS.Events
{ {
public interface IReceivedEvent public interface IEventStream
{ {
int EventNumber { get; } void Connect(string queueName, Action<EventData> received);
string EventType { get; }
byte[] Metadata { get; }
byte[] Payload { get; }
DateTime Created { get; }
} }
} }

2
src/Squidex.Infrastructure/CQRS/EventStore/IStreamNameResolver.cs → src/Squidex.Infrastructure/CQRS/Events/IStreamNameResolver.cs

@ -8,7 +8,7 @@
using System; using System;
namespace Squidex.Infrastructure.CQRS.EventStore namespace Squidex.Infrastructure.CQRS.Events
{ {
public interface IStreamNameResolver public interface IStreamNameResolver
{ {

67
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);
}
}
}
}

2
src/Squidex.Infrastructure/project.json

@ -2,7 +2,6 @@
"version": "1.0.0-*", "version": "1.0.0-*",
"dependencies": { "dependencies": {
"Autofac": "4.2.1", "Autofac": "4.2.1",
"EventStore.ClientAPI.NetCore": "0.0.1-alpha",
"Microsoft.Extensions.Logging": "1.1.0", "Microsoft.Extensions.Logging": "1.1.0",
"Microsoft.NETCore.App": "1.1.0", "Microsoft.NETCore.App": "1.1.0",
"NETStandard.Library": "1.6.1", "NETStandard.Library": "1.6.1",
@ -10,6 +9,7 @@
"NodaTime": "2.0.0-alpha20160729", "NodaTime": "2.0.0-alpha20160729",
"protobuf-net": "2.1.0", "protobuf-net": "2.1.0",
"System.Linq": "4.3.0", "System.Linq": "4.3.0",
"System.Reactive": "3.1.1",
"System.Reflection.TypeExtensions": "4.3.0", "System.Reflection.TypeExtensions": "4.3.0",
"System.Security.Claims": "4.3.0" "System.Security.Claims": "4.3.0"
}, },

2
src/Squidex.Store.MongoDb/Apps/MongoAppEntity.cs

@ -10,8 +10,8 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Read.Apps; using Squidex.Read.Apps;
using Squidex.Store.MongoDb.Utils;
namespace Squidex.Store.MongoDb.Apps namespace Squidex.Store.MongoDb.Apps
{ {

1
src/Squidex.Store.MongoDb/Apps/MongoAppRepository.cs

@ -18,6 +18,7 @@ using Squidex.Read.Apps;
using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Repositories;
using Squidex.Store.MongoDb.Utils; using Squidex.Store.MongoDb.Utils;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Store.MongoDb.Apps namespace Squidex.Store.MongoDb.Apps
{ {

2
src/Squidex.Store.MongoDb/History/MongoHistoryEventEntity.cs

@ -11,8 +11,8 @@ using System.Collections.Generic;
using MongoDB.Bson.Serialization.Attributes; using MongoDB.Bson.Serialization.Attributes;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.MongoDb;
using Squidex.Read; using Squidex.Read;
using Squidex.Store.MongoDb.Utils;
namespace Squidex.Store.MongoDb.History namespace Squidex.Store.MongoDb.History
{ {

1
src/Squidex.Store.MongoDb/History/MongoHistoryEventRepository.cs

@ -18,6 +18,7 @@ using Squidex.Read.History;
using Squidex.Read.History.Repositories; using Squidex.Read.History.Repositories;
using Squidex.Store.MongoDb.Utils; using Squidex.Store.MongoDb.Utils;
using System.Linq; using System.Linq;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Store.MongoDb.History namespace Squidex.Store.MongoDb.History
{ {

2
src/Squidex.Store.MongoDb/Infrastructure/MongoPersistedGrantStore.cs

@ -12,7 +12,7 @@ using IdentityServer4.Models;
using IdentityServer4.Stores; using IdentityServer4.Stores;
using MongoDB.Bson.Serialization; using MongoDB.Bson.Serialization;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Store.MongoDb.Utils; using Squidex.Infrastructure.MongoDb;
namespace Squidex.Store.MongoDb.Infrastructure namespace Squidex.Store.MongoDb.Infrastructure
{ {

59
src/Squidex.Store.MongoDb/Infrastructure/MongoStreamPositionStorage.cs

@ -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<MongoStreamPositionEntity>, IStreamPositionStorage
{
public MongoStreamPositionStorage(IMongoDatabase database)
: base(database)
{
}
protected override Task SetupCollectionAsync(IMongoCollection<MongoStreamPositionEntity> 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));
}
}
}

7
src/Squidex.Store.MongoDb/MongoDbModule.cs

@ -13,7 +13,7 @@ using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using MongoDB.Driver; using MongoDB.Driver;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.EventStore; using Squidex.Infrastructure.MongoDb;
using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Repositories;
using Squidex.Read.History.Repositories; using Squidex.Read.History.Repositories;
using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Repositories;
@ -23,7 +23,6 @@ using Squidex.Store.MongoDb.History;
using Squidex.Store.MongoDb.Infrastructure; using Squidex.Store.MongoDb.Infrastructure;
using Squidex.Store.MongoDb.Schemas; using Squidex.Store.MongoDb.Schemas;
using Squidex.Store.MongoDb.Users; using Squidex.Store.MongoDb.Users;
using Squidex.Store.MongoDb.Utils;
namespace Squidex.Store.MongoDb namespace Squidex.Store.MongoDb
{ {
@ -66,10 +65,6 @@ namespace Squidex.Store.MongoDb
.As<IPersistedGrantStore>() .As<IPersistedGrantStore>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<MongoStreamPositionStorage>()
.As<IStreamPositionStorage>()
.SingleInstance();
builder.RegisterType<MongoUserRepository>() builder.RegisterType<MongoUserRepository>()
.As<IUserRepository>() .As<IUserRepository>()
.InstancePerLifetimeScope(); .InstancePerLifetimeScope();

1
src/Squidex.Store.MongoDb/Schemas/MongoSchemaEntity.cs

@ -12,6 +12,7 @@ using MongoDB.Bson.Serialization.Attributes;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
using Squidex.Core.Schemas.Json; using Squidex.Core.Schemas.Json;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.MongoDb;
using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Repositories;
using Squidex.Store.MongoDb.Utils; using Squidex.Store.MongoDb.Utils;

1
src/Squidex.Store.MongoDb/Schemas/MongoSchemaRepository.cs

@ -18,6 +18,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Repositories;
using Squidex.Store.MongoDb.Utils; using Squidex.Store.MongoDb.Utils;

25
src/Squidex.Store.MongoDb/Utils/EntityMapper.cs

@ -13,6 +13,7 @@ using MongoDB.Driver;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Squidex.Events; using Squidex.Events;
using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.MongoDb;
using Squidex.Read; using Squidex.Read;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression // ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable SuspiciousTypeConversion.Global // ReSharper disable SuspiciousTypeConversion.Global
@ -25,33 +26,33 @@ namespace Squidex.Store.MongoDb.Utils
{ {
var entity = new T(); var entity = new T();
AssignId(headers, entity, useAggregateId); UpdateWithId(headers, entity, useAggregateId);
AssignAppId(headers, entity); UpdateWithAppId(headers, entity);
AssignCreated(headers, entity); UpdateWithCreated(headers, entity);
AssignCreatedBy(headers, entity); UpdateWithCreatedBy(headers, entity);
return Update(entity, headers); return Update(entity, headers);
} }
public static T Update<T>(T entity, EnvelopeHeaders headers) where T : MongoEntity public static T Update<T>(T entity, EnvelopeHeaders headers) where T : MongoEntity
{ {
AssignLastModified(headers, entity); UpdateWithLastModified(headers, entity);
AssignLastModifiedBy(headers, entity); UpdateWithLastModifiedBy(headers, entity);
return entity; return entity;
} }
private static void AssignCreated(EnvelopeHeaders headers, MongoEntity entity) private static void UpdateWithCreated(EnvelopeHeaders headers, MongoEntity entity)
{ {
entity.Created = headers.Timestamp().ToDateTimeUtc(); 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(); 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; 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; 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; 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) if (useAggregateId)
{ {

1
src/Squidex.Store.MongoDb/project.json

@ -10,6 +10,7 @@
"Squidex.Core": "1.0.0-*", "Squidex.Core": "1.0.0-*",
"Squidex.Events": "1.0.0-*", "Squidex.Events": "1.0.0-*",
"Squidex.Infrastructure": "1.0.0-*", "Squidex.Infrastructure": "1.0.0-*",
"Squidex.Infrastructure.MongoDb": "1.0.0-*",
"Squidex.Read": "1.0.0-*" "Squidex.Read": "1.0.0-*"
}, },
"frameworks": { "frameworks": {

1
src/Squidex.Write/EnrichWithAppIdProcessor.cs

@ -25,7 +25,6 @@ namespace Squidex.Write
if (appDomainObject != null) if (appDomainObject != null)
{ {
@event.SetAppId(aggregate.Id); @event.SetAppId(aggregate.Id);
} }
else else
{ {

11
src/Squidex/Config/Domain/InfrastructureModule.cs

@ -13,8 +13,7 @@ using Squidex.Core.Schemas;
using Squidex.Core.Schemas.Json; using Squidex.Core.Schemas.Json;
using Squidex.Infrastructure.CQRS.Autofac; using Squidex.Infrastructure.CQRS.Autofac;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.CQRS.EventStore; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Store.MongoDb.Infrastructure;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
{ {
@ -30,15 +29,11 @@ namespace Squidex.Config.Domain
.As<IActionContextAccessor>() .As<IActionContextAccessor>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<MongoStreamPositionStorage>()
.As<IStreamPositionStorage>()
.SingleInstance();
builder.RegisterType<AutofacDomainObjectFactory>() builder.RegisterType<AutofacDomainObjectFactory>()
.As<IDomainObjectFactory>() .As<IDomainObjectFactory>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<EventStoreDomainObjectRepository>() builder.RegisterType<DefaultDomainObjectRepository>()
.As<IDomainObjectRepository>() .As<IDomainObjectRepository>()
.SingleInstance(); .SingleInstance();
@ -50,7 +45,7 @@ namespace Squidex.Config.Domain
.As<ICommandBus>() .As<ICommandBus>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<EventStoreBus>() builder.RegisterType<EventBus>()
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();

4
src/Squidex/Config/Domain/Serializers.cs

@ -13,7 +13,7 @@ using Newtonsoft.Json.Serialization;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
using Squidex.Events.Schemas; using Squidex.Events.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.EventStore; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
@ -52,7 +52,7 @@ namespace Squidex.Config.Domain
services.AddSingleton(t => CreateSettings()); services.AddSingleton(t => CreateSettings());
services.AddSingleton(t => CreateSerializer(t.GetRequiredService<JsonSerializerSettings>())); services.AddSingleton(t => CreateSerializer(t.GetRequiredService<JsonSerializerSettings>()));
services.AddSingleton<EventStoreFormatter>(); services.AddSingleton<EventDataFormatter>();
return services; return services;
} }

55
src/Squidex/Config/EventStore/EventStoreModule.cs

@ -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<IOptions<MyEventStoreOptions>>().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<IOptions<MyEventStoreOptions>>().Value;
return new UserCredentials(options.Username, options.Password);
}).SingleInstance();
builder.Register(context =>
{
var options = context.Resolve<IOptions<MyEventStoreOptions>>().Value;
return new DefaultNameResolver(options.Prefix);
}).As<IStreamNameResolver>().SingleInstance();
}
}
}

7
src/Squidex/Config/EventStore/EventStoreUsage.cs

@ -8,8 +8,7 @@
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.EventStore;
namespace Squidex.Config.EventStore namespace Squidex.Config.EventStore
{ {
@ -17,9 +16,7 @@ namespace Squidex.Config.EventStore
{ {
public static IApplicationBuilder UseMyEventStore(this IApplicationBuilder app) public static IApplicationBuilder UseMyEventStore(this IApplicationBuilder app)
{ {
var options = app.ApplicationServices.GetRequiredService<IOptions<MyEventStoreOptions>>().Value; app.ApplicationServices.GetService<EventBus>().Subscribe();
app.ApplicationServices.GetService<EventStoreBus>().Subscribe(options.Prefix);
return app; return app;
} }

28
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<MongoEventStore>()
.As<IEventStore>()
.SingleInstance();
builder.RegisterType<DefaultNameResolver>()
.As<IStreamNameResolver>()
.SingleInstance();
}
}
}

16
src/Squidex/Config/EventStore/MyEventStoreOptions.cs → src/Squidex/Config/EventStore/MyRabbitMqOptions.cs

@ -1,5 +1,5 @@
// ========================================================================== // ==========================================================================
// MyEventStoreOptions.cs // MyRabbitMqOptions.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
@ -8,16 +8,8 @@
namespace Squidex.Config.EventStore namespace Squidex.Config.EventStore
{ {
public sealed class MyEventStoreOptions public sealed class MyRabbitMqOptions
{ {
public string IPAddress { get; set; } public string ConnectionString { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Prefix { get; set; }
public int Port { get; set; }
} }
} }

39
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<IOptions<MyRabbitMqOptions>>().Value;
var factory = new ConnectionFactory();
factory.SetUri(new Uri(options.ConnectionString));
return factory;
}).As<IConnectionFactory>().SingleInstance();
builder.RegisterType<RabbitMqEventChannel>()
.As<IEventPublisher>()
.As<IEventStream>()
.SingleInstance();
}
}
}

3
src/Squidex/Config/Identity/IdentityServices.cs

@ -11,7 +11,6 @@ using System.Reflection;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using IdentityServer4.Models; using IdentityServer4.Models;
using IdentityServer4.Stores; using IdentityServer4.Stores;
using IdentityServer4.Stores.InMemory;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity.MongoDB; using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -50,7 +49,7 @@ namespace Squidex.Config.Identity
services.AddIdentityServer(options => services.AddIdentityServer(options =>
{ {
options.UserInteractionOptions.ErrorUrl = "/account/error/"; options.UserInteraction.ErrorUrl = "/account/error/";
}) })
.AddAspNetIdentity<IdentityUser>() .AddAspNetIdentity<IdentityUser>()
.AddInMemoryApiResources(GetApiResources()) .AddInMemoryApiResources(GetApiResources())

12
src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs

@ -8,7 +8,9 @@
using System; using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NJsonSchema; using NJsonSchema;
using NJsonSchema.Generation;
using NJsonSchema.Infrastructure; using NJsonSchema.Infrastructure;
using NSwag; using NSwag;
using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors; using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors;
@ -22,13 +24,13 @@ namespace Squidex.Config.Swagger
{ {
private static readonly Regex ResponseRegex = new Regex("(?<Code>[0-9]{3}) => (?<Description>.*)", RegexOptions.Compiled); private static readonly Regex ResponseRegex = new Regex("(?<Code>[0-9]{3}) => (?<Description>.*)", RegexOptions.Compiled);
public bool Process(OperationProcessorContext context) public async Task<bool> ProcessAsync(OperationProcessorContext context)
{ {
var hasOkResponse = false; var hasOkResponse = false;
var operation = context.OperationDescription.Operation; 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)) foreach (Match match in ResponseRegex.Matches(returnsDescription))
{ {
@ -51,7 +53,7 @@ namespace Squidex.Config.Swagger
} }
} }
AddInternalErrorResponse(context, operation); await AddInternalErrorResponseAsync(context, operation);
if (!hasOkResponse) if (!hasOkResponse)
{ {
@ -61,7 +63,7 @@ namespace Squidex.Config.Swagger
return true; return true;
} }
private static void AddInternalErrorResponse(OperationProcessorContext context, SwaggerOperation operation) private static async Task AddInternalErrorResponseAsync(OperationProcessorContext context, SwaggerOperation operation)
{ {
if (operation.Responses.ContainsKey("500")) if (operation.Responses.ContainsKey("500"))
{ {
@ -73,7 +75,7 @@ namespace Squidex.Config.Swagger
var response = new SwaggerResponse { Description = "Operation failed." }; 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); operation.Responses.Add("500", response);
} }

7
src/Squidex/Config/Swagger/XmlTagProcessor.cs

@ -7,6 +7,7 @@
// ========================================================================== // ==========================================================================
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using NJsonSchema.Infrastructure; using NJsonSchema.Infrastructure;
using NSwag.Annotations; using NSwag.Annotations;
using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors; using NSwag.CodeGeneration.SwaggerGenerators.WebApi.Processors;
@ -31,7 +32,7 @@ namespace Squidex.Config.Swagger
if (tag != null) if (tag != null)
{ {
var description = controllerType.GetXmlSummary(); var description = controllerType.GetXmlSummaryAsync().Result;
if (description != null) if (description != null)
{ {
@ -47,7 +48,7 @@ namespace Squidex.Config.Swagger
} }
} }
public bool Process(OperationProcessorContext context) public Task<bool> ProcessAsync(OperationProcessorContext context)
{ {
var tagAttribute = var tagAttribute =
context.MethodInfo.DeclaringType.GetTypeInfo().GetCustomAttribute<SwaggerTagAttribute>(); context.MethodInfo.DeclaringType.GetTypeInfo().GetCustomAttribute<SwaggerTagAttribute>();
@ -58,7 +59,7 @@ namespace Squidex.Config.Swagger
context.OperationDescription.Operation.Tags.Add(tagAttribute.Name); context.OperationDescription.Operation.Tags.Add(tagAttribute.Name);
} }
return true; return Task.FromResult(true);
} }
} }
} }

7
src/Squidex/Startup.cs

@ -71,17 +71,18 @@ namespace Squidex
services.Configure<MyMongoDbOptions>( services.Configure<MyMongoDbOptions>(
Configuration.GetSection("stores:mongoDb")); Configuration.GetSection("stores:mongoDb"));
services.Configure<MyEventStoreOptions>( services.Configure<MyRabbitMqOptions>(
Configuration.GetSection("stores:eventStore")); Configuration.GetSection("stores:rabbitMq"));
services.Configure<MyUrlsOptions>( services.Configure<MyUrlsOptions>(
Configuration.GetSection("urls")); Configuration.GetSection("urls"));
services.Configure<MyIdentityOptions>( services.Configure<MyIdentityOptions>(
Configuration.GetSection("identity")); Configuration.GetSection("identity"));
var builder = new ContainerBuilder(); var builder = new ContainerBuilder();
builder.RegisterModule<EventStoreModule>();
builder.RegisterModule<InfrastructureModule>(); builder.RegisterModule<InfrastructureModule>();
builder.RegisterModule<MongoDbEventStoreModule>();
builder.RegisterModule<MongoDbModule>(); builder.RegisterModule<MongoDbModule>();
builder.RegisterModule<RabbitMqEventChannelModule>();
builder.RegisterModule<ReadModule>(); builder.RegisterModule<ReadModule>();
builder.RegisterModule<WebModule>(); builder.RegisterModule<WebModule>();
builder.RegisterModule<WriteModule>(); builder.RegisterModule<WriteModule>();

8
src/Squidex/appsettings.json

@ -7,12 +7,8 @@
"connectionString": "mongodb://localhost", "connectionString": "mongodb://localhost",
"databaseName": "Squidex" "databaseName": "Squidex"
}, },
"eventStore": { "rabbitMq": {
"ipAddress": "127.0.0.1", "connectionString": "amqp://guest:guest@localhost/"
"port": 1113,
"prefix": "squidex_v5",
"username": "admin",
"password": "changeit"
} }
}, },
"identity": { "identity": {

3
src/Squidex/project.json

@ -24,11 +24,14 @@
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "1.1.0",
"MongoDB.Driver": "2.4.1", "MongoDB.Driver": "2.4.1",
"NJsonSchema": "7.3.6214.20986", "NJsonSchema": "7.3.6214.20986",
"NSwag.AspNetCore": "8.5.0",
"OpenCover": "4.6.519", "OpenCover": "4.6.519",
"ReportGenerator": "2.5.2", "ReportGenerator": "2.5.2",
"Squidex.Core": "1.0.0-*", "Squidex.Core": "1.0.0-*",
"Squidex.Events": "1.0.0-*", "Squidex.Events": "1.0.0-*",
"Squidex.Infrastructure": "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.Read": "1.0.0-*",
"Squidex.Store.MongoDb": "1.0.0-*", "Squidex.Store.MongoDb": "1.0.0-*",
"Squidex.Write": "1.0.0-*", "Squidex.Write": "1.0.0-*",

2
tests/Squidex.Core.Tests/Schemas/FieldRegistryTests.cs

@ -17,7 +17,7 @@ namespace Squidex.Core.Schemas
{ {
private readonly FieldRegistry sut = new FieldRegistry(); private readonly FieldRegistry sut = new FieldRegistry();
public sealed class InvalidProperties : FieldProperties private sealed class InvalidProperties : FieldProperties
{ {
protected override IEnumerable<ValidationError> ValidateCore() protected override IEnumerable<ValidationError> ValidateCore()
{ {

18
tests/Squidex.Infrastructure.Tests/CQRS/Commands/CommandContextTests.cs

@ -26,8 +26,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
Assert.Equal(command, sut.Command); Assert.Equal(command, sut.Command);
Assert.Null(sut.Exception); Assert.Null(sut.Exception);
Assert.False(sut.IsSucceeded);
Assert.False(sut.IsHandled); Assert.False(sut.IsHandled);
Assert.False(sut.IsSucceeded);
Assert.False(sut.IsFailed);
} }
[Fact] [Fact]
@ -39,8 +40,9 @@ namespace Squidex.Infrastructure.CQRS.Commands
sut.Fail(exc); sut.Fail(exc);
Assert.Equal(exc, sut.Exception); Assert.Equal(exc, sut.Exception);
Assert.False(sut.IsSucceeded);
Assert.True(sut.IsHandled); Assert.True(sut.IsHandled);
Assert.True(sut.IsFailed);
Assert.False(sut.IsSucceeded);
} }
[Fact] [Fact]
@ -53,18 +55,20 @@ namespace Squidex.Infrastructure.CQRS.Commands
Assert.Null(sut.Exception); Assert.Null(sut.Exception);
Assert.True(sut.IsSucceeded); Assert.True(sut.IsSucceeded);
Assert.True(sut.IsHandled); Assert.True(sut.IsHandled);
Assert.False(sut.IsFailed);
} }
[Fact] [Fact]
public void Shoud_not_change_status_when_already_succeeded() public void Should_replace_status_when_already_succeeded()
{ {
var sut = new CommandContext(command); var sut = new CommandContext(command);
sut.Succeed(Guid.NewGuid()); sut.Succeed(Guid.NewGuid());
sut.Fail(new Exception()); sut.Fail(new Exception());
Assert.Null(sut.Exception); Assert.NotNull(sut.Exception);
Assert.True(sut.IsHandled); Assert.True(sut.IsHandled);
Assert.True(sut.IsFailed);
Assert.True(sut.IsSucceeded); Assert.True(sut.IsSucceeded);
} }
@ -77,12 +81,13 @@ namespace Squidex.Infrastructure.CQRS.Commands
sut.Succeed(guid); sut.Succeed(guid);
Assert.Equal(guid, sut.Result<Guid>()); Assert.Equal(guid, sut.Result<Guid>());
Assert.True(sut.IsSucceeded);
Assert.True(sut.IsHandled); Assert.True(sut.IsHandled);
Assert.True(sut.IsSucceeded);
Assert.False(sut.IsFailed);
} }
[Fact] [Fact]
public void Shoud_not_change_status_when_already_failed() public void Should_not_change_status_when_already_failed()
{ {
var sut = new CommandContext(command); var sut = new CommandContext(command);
@ -91,6 +96,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
Assert.NotNull(sut.Exception); Assert.NotNull(sut.Exception);
Assert.True(sut.IsHandled); Assert.True(sut.IsHandled);
Assert.True(sut.IsFailed);
Assert.False(sut.IsSucceeded); Assert.False(sut.IsSucceeded);
} }
} }

11
tests/Squidex.Infrastructure.Tests/CQRS/EventStore/DefaultNameResolverTests.cs → tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultNameResolverTests.cs

@ -7,13 +7,14 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Infrastructure.CQRS.Events;
using Xunit; using Xunit;
namespace Squidex.Infrastructure.CQRS.EventStore namespace Squidex.Infrastructure.CQRS.Events
{ {
public class DefaultNameResolverTests public class DefaultNameResolverTests
{ {
private readonly DefaultNameResolver sut = new DefaultNameResolver();
private sealed class MyUser : DomainObject private sealed class MyUser : DomainObject
{ {
public MyUser(Guid id, int version) public MyUser(Guid id, int version)
@ -41,23 +42,21 @@ namespace Squidex.Infrastructure.CQRS.EventStore
[Fact] [Fact]
public void Should_calculate_name() public void Should_calculate_name()
{ {
var sut = new DefaultNameResolver("Squidex");
var user = new MyUser(Guid.NewGuid(), 1); var user = new MyUser(Guid.NewGuid(), 1);
var name = sut.GetStreamName(typeof(MyUser), user.Id); var name = sut.GetStreamName(typeof(MyUser), user.Id);
Assert.Equal($"squidex-myUser-{user.Id}", name); Assert.Equal($"myUser-{user.Id}", name);
} }
[Fact] [Fact]
public void Should_calculate_name_and_remove_suffix() public void Should_calculate_name_and_remove_suffix()
{ {
var sut = new DefaultNameResolver("Squidex");
var user = new MyUserDomainObject(Guid.NewGuid(), 1); var user = new MyUserDomainObject(Guid.NewGuid(), 1);
var name = sut.GetStreamName(typeof(MyUserDomainObject), user.Id); var name = sut.GetStreamName(typeof(MyUserDomainObject), user.Id);
Assert.Equal($"squidex-myUser-{user.Id}", name); Assert.Equal($"myUser-{user.Id}", name);
} }
} }
} }

29
tests/Squidex.Infrastructure.Tests/CQRS/EventStore/EventStoreFormatterTests.cs → tests/Squidex.Infrastructure.Tests/CQRS/Events/EventDataFormatterTests.cs

@ -10,11 +10,10 @@ using System;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using NodaTime; using NodaTime;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Xunit; using Xunit;
namespace Squidex.Infrastructure.CQRS.EventStore namespace Squidex.Infrastructure.CQRS.Events
{ {
public class EventStoreFormatterTests public class EventStoreFormatterTests
{ {
@ -23,19 +22,6 @@ namespace Squidex.Infrastructure.CQRS.EventStore
public string MyProperty { get; set; } 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(); private static readonly JsonSerializerSettings serializerSettings = new JsonSerializerSettings();
static EventStoreFormatterTests() static EventStoreFormatterTests()
@ -60,20 +46,11 @@ namespace Squidex.Infrastructure.CQRS.EventStore
inputEvent.SetEventNumber(1); inputEvent.SetEventNumber(1);
inputEvent.SetTimestamp(SystemClock.Instance.GetCurrentInstant()); inputEvent.SetTimestamp(SystemClock.Instance.GetCurrentInstant());
var sut = new EventStoreFormatter(serializerSettings); var sut = new EventDataFormatter(serializerSettings);
var eventData = sut.ToEventData(inputEvent.To<IEvent>(), commitId); var eventData = sut.ToEventData(inputEvent.To<IEvent>(), commitId);
var receivedEvent = new MyReceivedEvent var outputEvent = sut.Parse(eventData).To<MyEvent>();
{
Payload = eventData.Data,
Created = inputEvent.Headers.Timestamp().ToDateTimeUtc(),
EventNumber = 1,
EventType = "event",
Metadata = eventData.Metadata
};
var outputEvent = sut.Parse(receivedEvent).To<MyEvent>();
CompareHeaders(outputEvent.Headers, inputEvent.Headers); CompareHeaders(outputEvent.Headers, inputEvent.Headers);

62
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<ObjectDisposedException>(() => sut.Ensure());
}
}
}

27
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);
}
}
}

2
tests/Squidex.Infrastructure.Tests/TypeNameAttributeTest.cs → tests/Squidex.Infrastructure.Tests/TypeNameAttributeTests.cs

@ -10,7 +10,7 @@ using Xunit;
namespace Squidex.Infrastructure namespace Squidex.Infrastructure
{ {
public class TypeNameAttributeTest public class TypeNameAttributeTests
{ {
[Fact] [Fact]
public void Should_instantiate() public void Should_instantiate()

9
tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs

@ -89,6 +89,15 @@ namespace Squidex.Write.Apps
Assert.Throws<ValidationException>(() => sut.AssignContributor(new AssignContributor { ContributorId = user.Identifier, Permission = PermissionLevel.Editor })); Assert.Throws<ValidationException>(() => 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<ValidationException>(() => sut.AssignContributor(new AssignContributor { ContributorId = contributorId, Permission = PermissionLevel.Editor }));
}
[Fact] [Fact]
public void AssignContributor_should_create_events() public void AssignContributor_should_create_events()
{ {

13
tests/Squidex.Write.Tests/EnrichWithAppIdProcessorTests.cs

@ -12,6 +12,7 @@ using Squidex.Events;
using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Write.Apps;
using Xunit; using Xunit;
namespace Squidex.Write namespace Squidex.Write
@ -42,6 +43,18 @@ namespace Squidex.Write
Assert.False(envelope.Headers.Contains("AppId")); 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<IEvent>(new MyEvent());
await sut.ProcessEventAsync(envelope, new AppDomainObject(appId, 1), new MyNormalCommand());
Assert.Equal(appId, envelope.Headers.AppId());
}
[Fact] [Fact]
public async Task Should_attach_app_id_to_event_envelope() public async Task Should_attach_app_id_to_event_envelope()
{ {

Loading…
Cancel
Save