Browse Source

Merge branch 'master' into feature-asset-management

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
eb7a1494e4
  1. 4
      .dockerignore
  2. 4
      .gitignore
  3. 2
      Dockerfile.build
  4. 2
      README.md
  5. 32
      Squidex.sln
  6. 4
      src/Squidex.Core/Squidex.Core.csproj
  7. 2
      src/Squidex.Events/Squidex.Events.csproj
  8. 104
      src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs
  9. 100
      src/Squidex.Infrastructure.RabbitMq/RabbitMqEventConsumer.cs
  10. 15
      src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj
  11. 19
      src/Squidex.Infrastructure.Redis/RedisInfrastructureErrors.cs
  12. 27
      src/Squidex.Infrastructure.Redis/RedisPubSub.cs
  13. 19
      src/Squidex.Infrastructure.Redis/RedisSubscription.cs
  14. 1
      src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj
  15. 6
      src/Squidex.Infrastructure/CQRS/Commands/CommandContext.cs
  16. 22
      src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs
  17. 16
      src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs
  18. 7
      src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs
  19. 8
      src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs
  20. 15
      src/Squidex.Infrastructure/CQRS/Events/Envelope_1.cs
  21. 89
      src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs
  22. 2
      src/Squidex.Infrastructure/CQRS/Events/IEventConsumer.cs
  23. 5
      src/Squidex.Infrastructure/CQRS/Events/IEventStore.cs
  24. 13
      src/Squidex.Infrastructure/DisposableObjectBase.cs
  25. 25
      src/Squidex.Infrastructure/InfrastructureErrors.cs
  26. 102
      src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs
  27. 22
      src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs
  28. 34
      src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs
  29. 42
      src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs
  30. 28
      src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs
  31. 29
      src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs
  32. 20
      src/Squidex.Infrastructure/Log/DebugLogChannel.cs
  33. 34
      src/Squidex.Infrastructure/Log/FileChannel.cs
  34. 26
      src/Squidex.Infrastructure/Log/IArrayWriter.cs
  35. 14
      src/Squidex.Infrastructure/Log/ILogAppender.cs
  36. 14
      src/Squidex.Infrastructure/Log/ILogChannel.cs
  37. 27
      src/Squidex.Infrastructure/Log/IObjectWriter.cs
  38. 19
      src/Squidex.Infrastructure/Log/ISemanticLog.cs
  39. 39
      src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs
  40. 73
      src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs
  41. 95
      src/Squidex.Infrastructure/Log/Internal/FileLogChannel.cs
  42. 14
      src/Squidex.Infrastructure/Log/Internal/IConsole.cs
  43. 16
      src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs
  44. 29
      src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs
  45. 184
      src/Squidex.Infrastructure/Log/JsonLogWriter.cs
  46. 86
      src/Squidex.Infrastructure/Log/SemanticLog.cs
  47. 125
      src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs
  48. 19
      src/Squidex.Infrastructure/Log/SemanticLogLevel.cs
  49. 34
      src/Squidex.Infrastructure/Log/TimestampLogAppender.cs
  50. 23
      src/Squidex.Infrastructure/Singletons.cs
  51. 5
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  52. 5
      src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs
  53. 5
      src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  54. 5
      src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs
  55. 4
      src/Squidex.Read.MongoDb/Contents/Visitors/FindExtensions.cs
  56. 15
      src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs
  57. 5
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs
  58. 2
      src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj
  59. 5
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  60. 9
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  61. 2
      src/Squidex.Read/Squidex.Read.csproj
  62. 1
      src/Squidex.Write/Contents/ContentCommandHandler.cs
  63. 2
      src/Squidex.Write/Squidex.Write.csproj
  64. 85
      src/Squidex/Config/Domain/ClusterModule.cs
  65. 80
      src/Squidex/Config/Domain/EventPublishersModule.cs
  66. 53
      src/Squidex/Config/Domain/EventStoreModule.cs
  67. 55
      src/Squidex/Config/Domain/InfrastructureModule.cs
  68. 70
      src/Squidex/Config/Domain/PubSubModule.cs
  69. 6
      src/Squidex/Config/Domain/StoreModule.cs
  70. 51
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  71. 34
      src/Squidex/Config/Identity/IdentityServices.cs
  72. 6
      src/Squidex/Config/MyUrlsOptions.cs
  73. 6
      src/Squidex/Config/Web/WebDependencies.cs
  74. 7
      src/Squidex/Config/Web/WebUsages.cs
  75. 20
      src/Squidex/Controllers/UI/Account/AccountController.cs
  76. 59
      src/Squidex/Pipeline/ActionContextLogAppender.cs
  77. 38
      src/Squidex/Pipeline/LogPerformanceAttribute.cs
  78. 22
      src/Squidex/Pipeline/WebpackMiddleware.cs
  79. 13
      src/Squidex/Squidex.csproj
  80. 20
      src/Squidex/Startup.cs
  81. 10
      src/Squidex/app/app.module.ts
  82. 4
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  83. 4
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  84. 4
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  85. 4
      src/Squidex/app/features/settings/pages/clients/clients-page.component.ts
  86. 6
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html
  87. 4
      src/Squidex/app/features/settings/pages/languages/languages-page.component.html
  88. 6
      src/Squidex/app/framework/angular/date-time-editor.component.html
  89. 4
      src/Squidex/app/framework/angular/date-time-editor.component.scss
  90. 18
      src/Squidex/app/framework/angular/date-time-editor.component.ts
  91. 2
      src/Squidex/app/shared/services/auth.service.ts
  92. 25
      src/Squidex/app/shared/services/schemas.service.ts
  93. 19
      src/Squidex/app/theme/_panels.scss
  94. 75
      src/Squidex/appsettings.json
  95. 58
      src/Squidex/package.json
  96. 2
      tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj
  97. 6
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs
  98. 27
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs
  99. 21
      tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs
  100. 18
      tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs

4
.dockerignore

@ -19,4 +19,6 @@
**/node_modules/ **/node_modules/
# Scripts (should be copied from node_modules on build) # Scripts (should be copied from node_modules on build)
**/wwwroot/scripts/**/*.* **/wwwroot/scripts/**/*.*
**/src/Squidex/appsettings.Development.json

4
.gitignore

@ -17,4 +17,6 @@ _test-output/
node_modules/ node_modules/
# Scripts (should be copied from node_modules on build) # Scripts (should be copied from node_modules on build)
**/wwwroot/scripts/**/*.* **/wwwroot/scripts/**/*.*
/src/Squidex/appsettings.Development.json

2
Dockerfile.build

@ -37,7 +37,7 @@ WORKDIR /
# Build Frontend # Build Frontend
RUN cp -a /tmp/node_modules /src/Squidex/ \ RUN cp -a /tmp/node_modules /src/Squidex/ \
&& cd /src/Squidex \ && cd /src/Squidex \
%% npm run test:coverage \ && npm run test:coverage \
&& npm run build:copy \ && npm run build:copy \
&& npm run build && npm run build

2
README.md

@ -2,7 +2,7 @@
# What is Squidex? # What is Squidex?
Squidex is an open source headless CMS and content management hub. In contrast to antraditional CMS Squidex provides a rich API with OData filter and Swagger definitions. It is up to you to build your UI on top of it. It can be website, a native app or just another server. Squidex is an open source headless CMS and content management hub. In contrast to a traditional CMS Squidex provides a rich API with OData filter and Swagger definitions. It is up to you to build your UI on top of it. It can be website, a native app or just another server.
We built it on top of ASP.NET Core and CQRS and is tested for Windows and Linux on modern Browsers. We built it on top of ASP.NET Core and CQRS and is tested for Windows and Linux on modern Browsers.
[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/squidex-cms/Lobby) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/squidex-cms/Lobby)

32
Squidex.sln

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15 # Visual Studio 15
VisualStudioVersion = 15.0.26228.4 VisualStudioVersion = 15.0.26228.12
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex", "src\Squidex\Squidex.csproj", "{61F6BBCE-A080-4400-B194-70E2F5D2096E}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex", "src\Squidex\Squidex.csproj", "{61F6BBCE-A080-4400-B194-70E2F5D2096E}"
EndProject EndProject
@ -32,9 +32,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Read.Tests", "tests
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Infrastructure.Redis", "src\Squidex.Infrastructure.Redis\Squidex.Infrastructure.Redis.csproj", "{D7166C56-178A-4457-B56A-C615C7450DEE}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Infrastructure.Redis", "src\Squidex.Infrastructure.Redis\Squidex.Infrastructure.Redis.csproj", "{D7166C56-178A-4457-B56A-C615C7450DEE}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{1667F3B4-31E6-45B2-90FB-97B1ECFE9874}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squidex.Infrastructure.RabbitMq", "src\Squidex.Infrastructure.RabbitMq\Squidex.Infrastructure.RabbitMq.csproj", "{C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}"
EndProject
Project("{13B669BE-BB05-4DDF-9536-439F39A36129}") = "GenerateLanguages", "tools\GenerateLanguages\GenerateLanguages.csproj", "{927E1F1C-95F0-4991-B33F-603977204B02}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -150,18 +148,18 @@ Global
{D7166C56-178A-4457-B56A-C615C7450DEE}.Release|Any CPU.Build.0 = Release|Any CPU {D7166C56-178A-4457-B56A-C615C7450DEE}.Release|Any CPU.Build.0 = Release|Any CPU
{D7166C56-178A-4457-B56A-C615C7450DEE}.Release|x64.ActiveCfg = Release|Any CPU {D7166C56-178A-4457-B56A-C615C7450DEE}.Release|x64.ActiveCfg = Release|Any CPU
{D7166C56-178A-4457-B56A-C615C7450DEE}.Release|x86.ActiveCfg = Release|Any CPU {D7166C56-178A-4457-B56A-C615C7450DEE}.Release|x86.ActiveCfg = Release|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Debug|Any CPU.Build.0 = Debug|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Debug|Any CPU.Build.0 = Debug|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x64.ActiveCfg = Debug|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Debug|x64.ActiveCfg = Debug|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x64.Build.0 = Debug|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Debug|x64.Build.0 = Debug|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x86.ActiveCfg = Debug|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Debug|x86.ActiveCfg = Debug|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Debug|x86.Build.0 = Debug|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Debug|x86.Build.0 = Debug|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Release|Any CPU.ActiveCfg = Release|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Release|Any CPU.ActiveCfg = Release|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Release|Any CPU.Build.0 = Release|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Release|Any CPU.Build.0 = Release|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Release|x64.ActiveCfg = Release|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Release|x64.ActiveCfg = Release|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Release|x64.Build.0 = Release|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Release|x64.Build.0 = Release|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Release|x86.ActiveCfg = Release|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Release|x86.ActiveCfg = Release|Any CPU
{927E1F1C-95F0-4991-B33F-603977204B02}.Release|x86.Build.0 = Release|Any CPU {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -179,6 +177,6 @@ Global
{6A811927-3C37-430A-90F4-503E37123956} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {6A811927-3C37-430A-90F4-503E37123956} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{8B074219-F69A-4E41-83C6-12EE1E647779} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A} {8B074219-F69A-4E41-83C6-12EE1E647779} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{D7166C56-178A-4457-B56A-C615C7450DEE} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF} {D7166C56-178A-4457-B56A-C615C7450DEE} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{927E1F1C-95F0-4991-B33F-603977204B02} = {1667F3B4-31E6-45B2-90FB-97B1ECFE9874} {C1E5BBB6-6B6A-4DE5-B19D-0538304DE343} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

4
src/Squidex.Core/Squidex.Core.csproj

@ -13,8 +13,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="protobuf-net" Version="2.1.0" /> <PackageReference Include="protobuf-net" Version="2.1.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" /> <PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
<PackageReference Include="NodaTime" Version="2.0.0-beta20170123" /> <PackageReference Include="NodaTime" Version="2.0.0" />
<PackageReference Include="NJsonSchema" Version="8.10.6282.29572" /> <PackageReference Include="NJsonSchema" Version="8.27.6302.16041" />
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.6' "> <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.6' ">
<PackageReference Include="Microsoft.OData.Core" Version="6.15.0" /> <PackageReference Include="Microsoft.OData.Core" Version="6.15.0" />

2
src/Squidex.Events/Squidex.Events.csproj

@ -12,6 +12,6 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NodaTime" Version="2.0.0-beta20170123" /> <PackageReference Include="NodaTime" Version="2.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

104
src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
@ -17,6 +18,7 @@ using NodaTime;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable ClassNeverInstantiated.Local // ReSharper disable ClassNeverInstantiated.Local
// ReSharper disable UnusedMember.Local // ReSharper disable UnusedMember.Local
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -55,61 +57,82 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
{ {
var indexNames = var indexNames =
await Task.WhenAll( await Task.WhenAll(
collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.EventsOffset), new CreateIndexOptions { Unique = true }),
collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.EventStreamOffset).Ascending(x => x.EventStream), new CreateIndexOptions { Unique = true }),
collection.Indexes.CreateOneAsync(IndexKeys.Descending(x => x.EventsOffset), new CreateIndexOptions { Unique = true }), collection.Indexes.CreateOneAsync(IndexKeys.Descending(x => x.EventsOffset), new CreateIndexOptions { Unique = true }),
collection.Indexes.CreateOneAsync(IndexKeys.Descending(x => x.EventStreamOffset).Ascending(x => x.EventStream), new CreateIndexOptions { Unique = true })); collection.Indexes.CreateOneAsync(IndexKeys.Descending(x => x.EventStreamOffset).Ascending(x => x.EventStream), new CreateIndexOptions { Unique = true }));
eventsOffsetIndex = indexNames[0]; eventsOffsetIndex = indexNames[0];
} }
public IObservable<StoredEvent> GetEventsAsync(string streamName) public IObservable<StoredEvent> GetEventsAsync(string streamFilter, long lastReceivedEventNumber = -1)
{ {
Guard.NotNullOrEmpty(streamName, nameof(streamName)); return Observable.Create<StoredEvent>((observer, ct) =>
return Observable.Create<StoredEvent>(async (observer, ct) =>
{ {
await Collection.Find(x => x.EventStream == streamName).ForEachAsync(commit => return GetEventsAsync(storedEvent =>
{ {
var eventNumber = commit.EventsOffset; observer.OnNext(storedEvent);
var eventStreamNumber = commit.EventStreamOffset;
foreach (var @event in commit.Events)
{
eventNumber++;
eventStreamNumber++;
var eventData = SimpleMapper.Map(@event, new EventData()); return Tasks.TaskHelper.Done;
}, ct, streamFilter, lastReceivedEventNumber);
observer.OnNext(new StoredEvent(eventNumber, eventStreamNumber, eventData));
}
}, ct);
}); });
} }
public IObservable<StoredEvent> GetEventsAsync(long lastReceivedEventNumber = -1) public async Task GetEventsAsync(Func<StoredEvent, Task> callback, CancellationToken cancellationToken, string streamFilter = null, long lastReceivedEventNumber = -1)
{ {
return Observable.Create<StoredEvent>(async (observer, ct) => Guard.NotNull(callback, nameof(callback));
var filters = new List<FilterDefinition<MongoEventCommit>>();
if (lastReceivedEventNumber >= 0)
{ {
var commitOffset = await GetPreviousOffset(lastReceivedEventNumber); var commitOffset = await GetPreviousOffsetAsync(lastReceivedEventNumber);
filters.Add(Filter.Gte(x => x.EventsOffset, commitOffset));
}
await Collection.Find(x => x.EventsOffset >= commitOffset).SortBy(x => x.EventsOffset).ForEachAsync(commit => if (!string.IsNullOrWhiteSpace(streamFilter) && !string.Equals(streamFilter, "*", StringComparison.OrdinalIgnoreCase))
{
if (streamFilter.StartsWith("^"))
{
filters.Add(Filter.Regex(x => x.EventStream, streamFilter));
}
else
{ {
var eventNumber = commit.EventsOffset; filters.Add(Filter.Eq(x => x.EventStream, streamFilter));
var eventStreamNumber = commit.EventStreamOffset; }
}
foreach (var @event in commit.Events) FilterDefinition<MongoEventCommit> filter = new BsonDocument();
{
eventNumber++;
eventStreamNumber++;
if (eventNumber > lastReceivedEventNumber) if (filters.Count > 1)
{ {
var eventData = SimpleMapper.Map(@event, new EventData()); filter = Filter.And(filters);
}
else if (filters.Count == 1)
{
filter = filters[0];
}
observer.OnNext(new StoredEvent(eventNumber, eventStreamNumber, eventData)); await Collection.Find(filter).SortBy(x => x.EventsOffset).ForEachAsync(async commit =>
} {
var eventNumber = commit.EventsOffset;
var eventStreamNumber = commit.EventStreamOffset;
foreach (var mongoEvent in commit.Events)
{
eventNumber++;
eventStreamNumber++;
if (eventNumber > lastReceivedEventNumber)
{
var eventData = SimpleMapper.Map(mongoEvent, new EventData());
await callback(new StoredEvent(eventNumber, eventStreamNumber, eventData));
} }
}, ct);
}); }
}, cancellationToken);
} }
public async Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, IEnumerable<EventData> events) public async Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, IEnumerable<EventData> events)
@ -130,7 +153,7 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
if (commitEvents.Any()) if (commitEvents.Any())
{ {
var offset = await GetEventOffset(); var offset = await GetEventOffsetAsync();
var commit = new MongoEventCommit var commit = new MongoEventCommit
{ {
@ -157,7 +180,7 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
{ {
if (ex.Message.IndexOf(eventsOffsetIndex, StringComparison.OrdinalIgnoreCase) >= 0) if (ex.Message.IndexOf(eventsOffsetIndex, StringComparison.OrdinalIgnoreCase) >= 0)
{ {
commit.EventsOffset = await GetEventOffset(); commit.EventsOffset = await GetEventOffsetAsync();
} }
else if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey) else if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{ {
@ -174,25 +197,24 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
} }
} }
private async Task<long> GetPreviousOffset(long startEventNumber) private async Task<long> GetPreviousOffsetAsync(long startEventNumber)
{ {
var document = var document =
await Collection.Find(x => x.EventsOffset <= startEventNumber) await Collection.Find(x => x.EventsOffset <= startEventNumber)
.Project<BsonDocument>(Projection .Project<BsonDocument>(Projection
.Include(x => x.EventStreamOffset) .Include(x => x.EventsOffset))
.Include(x => x.EventsCount))
.SortByDescending(x => x.EventsOffset).Limit(1) .SortByDescending(x => x.EventsOffset).Limit(1)
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
if (document != null) if (document != null)
{ {
return document["EventStreamOffset"].ToInt64(); return document["EventsOffset"].ToInt64();
} }
return -1; return -1;
} }
private async Task<long> GetEventOffset() private async Task<long> GetEventOffsetAsync()
{ {
var document = var document =
await Collection.Find(new BsonDocument()) await Collection.Find(new BsonDocument())

100
src/Squidex.Infrastructure.RabbitMq/RabbitMqEventConsumer.cs

@ -0,0 +1,100 @@
// ==========================================================================
// RabbitMqEventConsumer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json;
using RabbitMQ.Client;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf
namespace Squidex.Infrastructure.RabbitMq
{
public sealed class RabbitMqEventConsumer : DisposableObjectBase, IExternalSystem, IEventConsumer
{
private readonly JsonSerializerSettings serializerSettings;
private readonly string eventPublisherName;
private readonly string exchange;
private readonly string eventsFilter;
private readonly ConnectionFactory connectionFactory;
private readonly Lazy<IConnection> connection;
private readonly Lazy<IModel> channel;
public string Name
{
get { return eventPublisherName; }
}
public string EventsFilter
{
get { return eventsFilter; }
}
public RabbitMqEventConsumer(JsonSerializerSettings serializerSettings, string eventPublisherName, string uri, string exchange, string eventsFilter)
{
Guard.NotNullOrEmpty(uri, nameof(uri));
Guard.NotNullOrEmpty(eventPublisherName, nameof(eventPublisherName));
Guard.NotNullOrEmpty(exchange, nameof(exchange));
Guard.NotNull(serializerSettings, nameof(serializerSettings));
connectionFactory = new ConnectionFactory { Uri = uri };
connection = new Lazy<IConnection>(connectionFactory.CreateConnection);
channel = new Lazy<IModel>(() => connection.Value.CreateModel());
this.exchange = exchange;
this.eventsFilter = eventsFilter;
this.eventPublisherName = eventPublisherName;
this.serializerSettings = serializerSettings;
}
protected override void DisposeObject(bool disposing)
{
if (connection.IsValueCreated)
{
connection.Value.Close();
connection.Value.Dispose();
}
}
public void Connect()
{
try
{
var currentConnection = connection.Value;
if (!currentConnection.IsOpen)
{
throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}");
}
}
catch (Exception e)
{
throw new ConfigurationException($"RabbitMq event bus failed to connect to {connectionFactory.Endpoint}", e);
}
}
public Task ClearAsync()
{
return TaskHelper.Done;
}
public Task On(Envelope<IEvent> @event)
{
var jsonString = JsonConvert.SerializeObject(@event, serializerSettings);
var jsonBytes = Encoding.UTF8.GetBytes(jsonString);
channel.Value.BasicPublish(exchange, string.Empty, null, jsonBytes);
return TaskHelper.Done;
}
}
}

15
src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard1.6</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RabbitMQ.Client" Version="4.1.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup>
</Project>

19
src/Squidex.Infrastructure.Redis/RedisInfrastructureErrors.cs

@ -1,19 +0,0 @@
// ==========================================================================
// RedisInfrastructureErrors.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Logging;
namespace Squidex.Infrastructure.Redis
{
public static class RedisInfrastructureErrors
{
public static readonly EventId InvalidatingReceivedFailed = new EventId(50001, "InvalidingReceivedFailed");
public static readonly EventId InvalidatingPublishedFailed = new EventId(50002, "InvalidatingPublishedFailed");
}
}

27
src/Squidex.Infrastructure.Redis/RedisPubSub.cs

@ -8,7 +8,7 @@
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Microsoft.Extensions.Logging; using Squidex.Infrastructure.Log;
using StackExchange.Redis; using StackExchange.Redis;
namespace Squidex.Infrastructure.Redis namespace Squidex.Infrastructure.Redis
@ -16,31 +16,30 @@ namespace Squidex.Infrastructure.Redis
public class RedisPubSub : IPubSub, IExternalSystem public class RedisPubSub : IPubSub, IExternalSystem
{ {
private readonly ConcurrentDictionary<string, RedisSubscription> subscriptions = new ConcurrentDictionary<string, RedisSubscription>(); private readonly ConcurrentDictionary<string, RedisSubscription> subscriptions = new ConcurrentDictionary<string, RedisSubscription>();
private readonly IConnectionMultiplexer redis; private readonly IConnectionMultiplexer redisClient;
private readonly ILogger<RedisPubSub> logger; private readonly ISemanticLog log;
private readonly ISubscriber subscriber; private readonly ISubscriber redisSubscriber;
public RedisPubSub(IConnectionMultiplexer redis, ILogger<RedisPubSub> logger) public RedisPubSub(IConnectionMultiplexer redis, ISemanticLog log)
{ {
Guard.NotNull(redis, nameof(redis)); Guard.NotNull(redis, nameof(redis));
Guard.NotNull(logger, nameof(logger)); Guard.NotNull(log, nameof(log));
this.redis = redis; this.log = log;
this.logger = logger; redisClient = redis;
redisSubscriber = redis.GetSubscriber();
subscriber = redis.GetSubscriber();
} }
public void Connect() public void Connect()
{ {
try try
{ {
redis.GetStatus(); redisClient.GetStatus();
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new ConfigurationException($"Redis connection failed to connect to database {redis.Configuration}", ex); throw new ConfigurationException($"Redis connection failed to connect to database {redisClient.Configuration}", ex);
} }
} }
@ -48,14 +47,14 @@ namespace Squidex.Infrastructure.Redis
{ {
Guard.NotNullOrEmpty(channelName, nameof(channelName)); Guard.NotNullOrEmpty(channelName, nameof(channelName));
subscriptions.GetOrAdd(channelName, c => new RedisSubscription(subscriber, c, logger)).Publish(token, notifySelf); subscriptions.GetOrAdd(channelName, c => new RedisSubscription(redisSubscriber, c, log)).Publish(token, notifySelf);
} }
public IDisposable Subscribe(string channelName, Action<string> handler) public IDisposable Subscribe(string channelName, Action<string> handler)
{ {
Guard.NotNullOrEmpty(channelName, nameof(channelName)); Guard.NotNullOrEmpty(channelName, nameof(channelName));
return subscriptions.GetOrAdd(channelName, c => new RedisSubscription(subscriber, c, logger)).Subscribe(handler); return subscriptions.GetOrAdd(channelName, c => new RedisSubscription(redisSubscriber, c, log)).Subscribe(handler);
} }
} }
} }

19
src/Squidex.Infrastructure.Redis/RedisSubscription.cs

@ -9,7 +9,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Microsoft.Extensions.Logging; using Squidex.Infrastructure.Log;
using StackExchange.Redis; using StackExchange.Redis;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -22,11 +22,11 @@ namespace Squidex.Infrastructure.Redis
private readonly Subject<string> subject = new Subject<string>(); private readonly Subject<string> subject = new Subject<string>();
private readonly ISubscriber subscriber; private readonly ISubscriber subscriber;
private readonly string channelName; private readonly string channelName;
private readonly ILogger<RedisPubSub> logger; private readonly ISemanticLog log;
public RedisSubscription(ISubscriber subscriber, string channelName, ILogger<RedisPubSub> logger) public RedisSubscription(ISubscriber subscriber, string channelName, ISemanticLog log)
{ {
this.logger = logger; this.log = log;
this.subscriber = subscriber; this.subscriber = subscriber;
this.subscriber.Subscribe(channelName, (channel, value) => HandleInvalidation(value)); this.subscriber.Subscribe(channelName, (channel, value) => HandleInvalidation(value));
@ -38,13 +38,16 @@ namespace Squidex.Infrastructure.Redis
{ {
try try
{ {
var message = string.Join("#", (notifySelf ? Guid.Empty : InstanceId).ToString()); var message = string.Join("#", (notifySelf ? Guid.Empty : InstanceId).ToString(), token);
subscriber.Publish(channelName, message); subscriber.Publish(channelName, message);
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(RedisInfrastructureErrors.InvalidatingReceivedFailed, ex, "Failed to send invalidation message {0}", token); log.LogError(ex, w => w
.WriteProperty("action", "PublishRedisMessage")
.WriteProperty("state", "Failed")
.WriteProperty("token", token));
} }
} }
@ -78,7 +81,9 @@ namespace Squidex.Infrastructure.Redis
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(RedisInfrastructureErrors.InvalidatingReceivedFailed, ex, "Failed to receive invalidation message."); log.LogError(ex, w => w
.WriteProperty("action", "ReceiveRedisMessage")
.WriteProperty("state", "Failed"));
} }
} }

1
src/Squidex.Infrastructure.Redis/Squidex.Infrastructure.Redis.csproj

@ -10,7 +10,6 @@
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" /> <PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

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

@ -13,6 +13,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
public sealed class CommandContext public sealed class CommandContext
{ {
private readonly ICommand command; private readonly ICommand command;
private readonly Guid contextId = Guid.NewGuid();
private Exception exception; private Exception exception;
private Tuple<object> result; private Tuple<object> result;
@ -41,6 +42,11 @@ namespace Squidex.Infrastructure.CQRS.Commands
get { return exception; } get { return exception; }
} }
public Guid ContextId
{
get { return contextId; }
}
public CommandContext(ICommand command) public CommandContext(ICommand command)
{ {
Guard.NotNull(command, nameof(command)); Guard.NotNull(command, nameof(command));

22
src/Squidex.Infrastructure/CQRS/Commands/LogExceptionHandler.cs

@ -7,7 +7,7 @@
// ========================================================================== // ==========================================================================
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -16,11 +16,13 @@ namespace Squidex.Infrastructure.CQRS.Commands
{ {
public sealed class LogExceptionHandler : ICommandHandler public sealed class LogExceptionHandler : ICommandHandler
{ {
private readonly ILogger<LogExceptionHandler> logger; private readonly ISemanticLog log;
public LogExceptionHandler(ILogger<LogExceptionHandler> logger) public LogExceptionHandler(ISemanticLog log)
{ {
this.logger = logger; Guard.NotNull(log, nameof(log));
this.log = log;
} }
public Task<bool> HandleAsync(CommandContext context) public Task<bool> HandleAsync(CommandContext context)
@ -29,12 +31,20 @@ namespace Squidex.Infrastructure.CQRS.Commands
if (exception != null) if (exception != null)
{ {
logger.LogError(InfrastructureErrors.CommandFailed, exception, "Handling {0} command failed", context.Command); log.LogError(exception, w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Failed")
.WriteProperty("commandType", context.Command.GetType().Name));
} }
if (!context.IsHandled) if (!context.IsHandled)
{ {
logger.LogCritical(InfrastructureErrors.CommandUnknown, exception, "Unknown command {0}", context.Command); log.LogFatal(exception, w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Unhandled")
.WriteProperty("commandType", context.Command.GetType().Name));
} }
return TaskHelper.False; return TaskHelper.False;

16
src/Squidex.Infrastructure/CQRS/Commands/LogExecutingHandler.cs

@ -7,23 +7,29 @@
// ========================================================================== // ==========================================================================
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
public sealed class LogExecutingHandler : ICommandHandler public sealed class LogExecutingHandler : ICommandHandler
{ {
private readonly ILogger<LogExecutingHandler> logger; private readonly ISemanticLog log;
public LogExecutingHandler(ILogger<LogExecutingHandler> logger) public LogExecutingHandler(ISemanticLog log)
{ {
this.logger = logger; Guard.NotNull(log, nameof(log));
this.log = log;
} }
public Task<bool> HandleAsync(CommandContext context) public Task<bool> HandleAsync(CommandContext context)
{ {
logger.LogInformation("Handling {0} command", context.Command); log.LogInformation(w => w
.WriteProperty("action", "HandleCommand.")
.WriteProperty("actionId", context.ContextId.ToString())
.WriteProperty("state", "Started")
.WriteProperty("commandType", context.Command.GetType().Name));
return TaskHelper.False; return TaskHelper.False;
} }

7
src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs

@ -17,6 +17,11 @@ namespace Squidex.Infrastructure.CQRS.Events
public string Name { get; } public string Name { get; }
public string EventsFilter
{
get { return inners.FirstOrDefault()?.EventsFilter; }
}
public CompoundEventConsumer(IEventConsumer first, params IEventConsumer[] inners) public CompoundEventConsumer(IEventConsumer first, params IEventConsumer[] inners)
{ {
Guard.NotNull(first, nameof(first)); Guard.NotNull(first, nameof(first));
@ -24,7 +29,7 @@ namespace Squidex.Infrastructure.CQRS.Events
this.inners = new[] { first }.Union(inners).ToArray(); this.inners = new[] { first }.Union(inners).ToArray();
Name = first.GetType().Name; Name = first.Name;
} }
public CompoundEventConsumer(string name, params IEventConsumer[] inners) public CompoundEventConsumer(string name, params IEventConsumer[] inners)

8
src/Squidex.Infrastructure/CQRS/Events/DefaultMemoryEventNotifier.cs → src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs

@ -1,5 +1,5 @@
// ========================================================================== // ==========================================================================
// InMemoryEventNotifier.cs // DefaultEventNotifier.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex Group // Copyright (c) Squidex Group
@ -10,13 +10,13 @@ using System;
namespace Squidex.Infrastructure.CQRS.Events namespace Squidex.Infrastructure.CQRS.Events
{ {
public sealed class DefaultMemoryEventNotifier : IEventNotifier public sealed class DefaultEventNotifier : IEventNotifier
{ {
private static readonly string ChannelName = typeof(DefaultMemoryEventNotifier).Name; private static readonly string ChannelName = typeof(DefaultEventNotifier).Name;
private readonly IPubSub invalidator; private readonly IPubSub invalidator;
public DefaultMemoryEventNotifier(IPubSub invalidator) public DefaultEventNotifier(IPubSub invalidator)
{ {
Guard.NotNull(invalidator, nameof(invalidator)); Guard.NotNull(invalidator, nameof(invalidator));

15
src/Squidex.Infrastructure/CQRS/Events/Envelope_1.cs

@ -6,9 +6,6 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using NodaTime;
namespace Squidex.Infrastructure.CQRS.Events namespace Squidex.Infrastructure.CQRS.Events
{ {
public class Envelope<TPayload> where TPayload : class public class Envelope<TPayload> where TPayload : class
@ -45,18 +42,6 @@ namespace Squidex.Infrastructure.CQRS.Events
this.headers = headers; this.headers = headers;
} }
public static Envelope<TPayload> Create(TPayload payload)
{
var eventId = Guid.NewGuid();
var envelope =
new Envelope<TPayload>(payload)
.SetEventId(eventId)
.SetTimestamp(SystemClock.Instance.GetCurrentInstant());
return envelope;
}
public Envelope<TOther> To<TOther>() where TOther : class public Envelope<TOther> To<TOther>() where TOther : class
{ {
return new Envelope<TOther>(payload as TOther, headers.Clone()); return new Envelope<TOther>(payload as TOther, headers.Clone());

89
src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs

@ -7,9 +7,8 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Reactive.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Timers; using Squidex.Infrastructure.Timers;
// ReSharper disable MethodSupportsCancellation // ReSharper disable MethodSupportsCancellation
@ -24,23 +23,23 @@ namespace Squidex.Infrastructure.CQRS.Events
private readonly IEventStore eventStore; private readonly IEventStore eventStore;
private readonly IEventNotifier eventNotifier; private readonly IEventNotifier eventNotifier;
private readonly IEventConsumerInfoRepository eventConsumerInfoRepository; private readonly IEventConsumerInfoRepository eventConsumerInfoRepository;
private readonly ILogger<EventReceiver> logger; private readonly ISemanticLog log;
private CompletionTimer timer; private CompletionTimer timer;
public EventReceiver( public EventReceiver(
EventDataFormatter formatter, EventDataFormatter formatter,
IEventStore eventStore, IEventStore eventStore,
IEventNotifier eventNotifier, IEventNotifier eventNotifier,
IEventConsumerInfoRepository eventConsumerInfoRepository, IEventConsumerInfoRepository eventConsumerInfoRepository,
ILogger<EventReceiver> logger) ISemanticLog log)
{ {
Guard.NotNull(logger, nameof(logger)); Guard.NotNull(log, nameof(log));
Guard.NotNull(formatter, nameof(formatter)); Guard.NotNull(formatter, nameof(formatter));
Guard.NotNull(eventStore, nameof(eventStore)); Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventNotifier, nameof(eventNotifier)); Guard.NotNull(eventNotifier, nameof(eventNotifier));
Guard.NotNull(eventConsumerInfoRepository, nameof(eventConsumerInfoRepository)); Guard.NotNull(eventConsumerInfoRepository, nameof(eventConsumerInfoRepository));
this.logger = logger; this.log = log;
this.formatter = formatter; this.formatter = formatter;
this.eventStore = eventStore; this.eventStore = eventStore;
this.eventNotifier = eventNotifier; this.eventNotifier = eventNotifier;
@ -57,13 +56,17 @@ namespace Squidex.Infrastructure.CQRS.Events
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogCritical(InfrastructureErrors.EventHandlingFailed, ex, "Event stream {0} has been aborted"); log.LogWarning(ex, w => w
.WriteProperty("action", "DisposeEventReceiver")
.WriteProperty("state", "Failed"));
} }
} }
} }
public void Trigger() public void Next()
{ {
ThrowIfDisposed();
timer?.Trigger(); timer?.Trigger();
} }
@ -71,6 +74,8 @@ namespace Squidex.Infrastructure.CQRS.Events
{ {
Guard.NotNull(eventConsumer, nameof(eventConsumer)); Guard.NotNull(eventConsumer, nameof(eventConsumer));
ThrowIfDisposed();
if (timer != null) if (timer != null)
{ {
return; return;
@ -78,7 +83,7 @@ namespace Squidex.Infrastructure.CQRS.Events
var consumerName = eventConsumer.Name; var consumerName = eventConsumer.Name;
var consumerStarted = false; var consumerStarted = false;
timer = new CompletionTimer(delay, async ct => timer = new CompletionTimer(delay, async ct =>
{ {
if (!consumerStarted) if (!consumerStarted)
@ -104,18 +109,13 @@ namespace Squidex.Infrastructure.CQRS.Events
{ {
return; return;
} }
await eventStore.GetEventsAsync(lastHandledEventNumber) await eventStore.GetEventsAsync(se => HandleEventAsync(eventConsumer, se, consumerName), ct,
.Select(storedEvent => eventConsumer.EventsFilter, lastHandledEventNumber);
{
HandleEventAsync(eventConsumer, storedEvent, consumerName).Wait();
return storedEvent;
}).DefaultIfEmpty();
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(InfrastructureErrors.EventHandlingFailed, ex, "Failed to handle events"); log.LogFatal(ex, w => w.WriteProperty("action", "EventHandlingFailed"));
await eventConsumerInfoRepository.StopAsync(consumerName, ex.ToString()); await eventConsumerInfoRepository.StopAsync(consumerName, ex.ToString());
} }
@ -134,18 +134,31 @@ namespace Squidex.Infrastructure.CQRS.Events
private async Task ResetAsync(IEventConsumer eventConsumer, string consumerName) private async Task ResetAsync(IEventConsumer eventConsumer, string consumerName)
{ {
var actionId = Guid.NewGuid().ToString();
try try
{ {
logger.LogDebug("[{0}]: Reset started", eventConsumer); log.LogInformation(w => w
.WriteProperty("action", "EventConsumerReset")
.WriteProperty("actionId", actionId)
.WriteProperty("state", "Started")
.WriteProperty("eventConsumer", eventConsumer.GetType().Name));
await eventConsumer.ClearAsync(); await eventConsumer.ClearAsync();
await eventConsumerInfoRepository.SetLastHandledEventNumberAsync(consumerName, -1); await eventConsumerInfoRepository.SetLastHandledEventNumberAsync(consumerName, -1);
logger.LogDebug("[{0}]: Reset completed", eventConsumer); log.LogInformation(w => w
.WriteProperty("action", "EventConsumerReset")
.WriteProperty("actionId", actionId)
.WriteProperty("state", "Completed")
.WriteProperty("eventConsumer", eventConsumer.GetType().Name));
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(InfrastructureErrors.EventResetFailed, ex, "[{0}]: Reset failed", eventConsumer); log.LogFatal(ex, w => w
.WriteProperty("action", "EventConsumerReset")
.WriteProperty("actionId", actionId)
.WriteProperty("state", "Completed")
.WriteProperty("eventConsumer", eventConsumer.GetType().Name));
throw; throw;
} }
@ -153,17 +166,37 @@ namespace Squidex.Infrastructure.CQRS.Events
private async Task DispatchConsumer(Envelope<IEvent> @event, IEventConsumer eventConsumer) private async Task DispatchConsumer(Envelope<IEvent> @event, IEventConsumer eventConsumer)
{ {
var eventId = @event.Headers.EventId().ToString();
var eventType = @event.Payload.GetType().Name;
try try
{ {
logger.LogDebug("[{0}]: Handling event {1} ({2})", eventConsumer, @event.Payload, @event.Headers.EventId()); log.LogInformation(w => w
.WriteProperty("action", "HandleEvent")
.WriteProperty("actionId", eventId)
.WriteProperty("state", "Started")
.WriteProperty("eventId", eventId)
.WriteProperty("eventType", eventType)
.WriteProperty("eventConsumer", eventConsumer.GetType().Name));
await eventConsumer.On(@event); await eventConsumer.On(@event);
logger.LogDebug("[{0}]: Handled event {1} ({2})", eventConsumer, @event.Payload, @event.Headers.EventId()); log.LogInformation(w => w
.WriteProperty("action", "HandleEvent")
.WriteProperty("actionId", eventId)
.WriteProperty("state", "Completed")
.WriteProperty("eventId", eventId)
.WriteProperty("eventType", eventType)
.WriteProperty("eventConsumer", eventConsumer.GetType().Name));
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(InfrastructureErrors.EventHandlingFailed, ex, "[{0}]: Failed to handle event {1} ({2})", eventConsumer, @event.Payload, @event.Headers.EventId()); log.LogError(ex, w => w
.WriteProperty("action", "HandleEvent")
.WriteProperty("actionId", eventId)
.WriteProperty("state", "Started")
.WriteProperty("eventId", eventId)
.WriteProperty("eventType", eventType)
.WriteProperty("eventConsumer", eventConsumer.GetType().Name));
throw; throw;
} }
@ -182,7 +215,11 @@ namespace Squidex.Infrastructure.CQRS.Events
} }
catch (Exception ex) catch (Exception ex)
{ {
logger.LogError(InfrastructureErrors.EventDeserializationFailed, ex, "Failed to parse event {0}", storedEvent.Data.EventId); log.LogFatal(ex, w => w
.WriteProperty("action", "ParseEvent")
.WriteProperty("state", "Failed")
.WriteProperty("eventId", storedEvent.Data.EventId.ToString())
.WriteProperty("eventNumber", storedEvent.EventNumber));
throw; throw;
} }

2
src/Squidex.Infrastructure/CQRS/Events/IEventConsumer.cs

@ -14,6 +14,8 @@ namespace Squidex.Infrastructure.CQRS.Events
{ {
string Name { get; } string Name { get; }
string EventsFilter { get; }
Task ClearAsync(); Task ClearAsync();
Task On(Envelope<IEvent> @event); Task On(Envelope<IEvent> @event);

5
src/Squidex.Infrastructure/CQRS/Events/IEventStore.cs

@ -8,15 +8,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Squidex.Infrastructure.CQRS.Events namespace Squidex.Infrastructure.CQRS.Events
{ {
public interface IEventStore public interface IEventStore
{ {
IObservable<StoredEvent> GetEventsAsync(long lastReceivedEventNumber = -1); IObservable<StoredEvent> GetEventsAsync(string streamFilter = null, long lastReceivedEventNumber = -1);
IObservable<StoredEvent> GetEventsAsync(string streamName); Task GetEventsAsync(Func<StoredEvent, Task> callback, CancellationToken cancellationToken, string streamFilter = null, long lastReceivedEventNumber = -1);
Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, IEnumerable<EventData> events); Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, IEnumerable<EventData> events);
} }

13
src/Squidex.Infrastructure/DisposableObjectBase.cs

@ -36,20 +36,13 @@ namespace Squidex.Infrastructure
return; return;
} }
if (disposing) lock (disposeLock)
{ {
lock (disposeLock) if (!isDisposed)
{ {
if (!isDisposed) DisposeObject(disposing);
{
DisposeObject(true);
}
} }
} }
else
{
DisposeObject(false);
}
isDisposed = true; isDisposed = true;
} }

25
src/Squidex.Infrastructure/InfrastructureErrors.cs

@ -1,25 +0,0 @@
// ==========================================================================
// InfrastructureErrors.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Logging;
namespace Squidex.Infrastructure
{
public class InfrastructureErrors
{
public static readonly EventId CommandUnknown = new EventId(20000, "CommandUnknown");
public static readonly EventId CommandFailed = new EventId(20001, "CommandFailed");
public static readonly EventId EventResetFailed = new EventId(10000, "EventResetFailed");
public static readonly EventId EventHandlingFailed = new EventId(10001, "EventHandlingFailed");
public static readonly EventId EventDeserializationFailed = new EventId(10002, "EventDeserializationFailed");
}
}

102
src/Squidex.Infrastructure/Log/Adapter/SemanticLogLogger.cs

@ -0,0 +1,102 @@
// ==========================================================================
// SemanticLogLogger.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Microsoft.Extensions.Logging;
// ReSharper disable SwitchStatementMissingSomeCases
namespace Squidex.Infrastructure.Log.Adapter
{
internal sealed class SemanticLogLogger : ILogger
{
private readonly ISemanticLog semanticLog;
public SemanticLogLogger(ISemanticLog semanticLog)
{
this.semanticLog = semanticLog;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
SemanticLogLevel semanticLogLevel;
switch (logLevel)
{
case LogLevel.Trace:
semanticLogLevel = SemanticLogLevel.Trace;
break;
case LogLevel.Debug:
semanticLogLevel = SemanticLogLevel.Debug;
break;
case LogLevel.Information:
semanticLogLevel = SemanticLogLevel.Information;
break;
case LogLevel.Warning:
semanticLogLevel = SemanticLogLevel.Warning;
break;
case LogLevel.Error:
semanticLogLevel = SemanticLogLevel.Error;
break;
case LogLevel.Critical:
semanticLogLevel = SemanticLogLevel.Fatal;
break;
default:
semanticLogLevel = SemanticLogLevel.Debug;
break;
}
semanticLog.Log(semanticLogLevel, writer =>
{
var message = formatter(state, exception);
if (!string.IsNullOrWhiteSpace(message))
{
writer.WriteProperty(nameof(message), message);
}
if (eventId.Id > 0)
{
writer.WriteObject(nameof(eventId), eventIdWriter =>
{
eventIdWriter.WriteProperty("id", eventId.Id);
if (!string.IsNullOrWhiteSpace(eventId.Name))
{
eventIdWriter.WriteProperty("name", eventId.Name);
}
});
}
if (exception != null)
{
writer.WriteException(exception);
}
});
}
public bool IsEnabled(LogLevel logLevel)
{
return true;
}
public IDisposable BeginScope<TState>(TState state)
{
return NoopDisposable.Instance;
}
private class NoopDisposable : IDisposable
{
public static readonly NoopDisposable Instance = new NoopDisposable();
public void Dispose()
{
}
}
}
}

22
src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs

@ -0,0 +1,22 @@
// ==========================================================================
// SemanticLogLoggerFactoryExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Logging;
namespace Squidex.Infrastructure.Log.Adapter
{
public static class SemanticLogLoggerFactoryExtensions
{
public static ILoggerFactory AddSemanticLog(this ILoggerFactory factory, ISemanticLog semanticLog)
{
factory.AddProvider(new SemanticLogLoggerProvider(semanticLog));
return factory;
}
}
}

34
src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs

@ -0,0 +1,34 @@
// ==========================================================================
// SemanticLogLoggerProvider.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Logging;
namespace Squidex.Infrastructure.Log.Adapter
{
public class SemanticLogLoggerProvider : ILoggerProvider
{
private readonly ISemanticLog semanticLog;
public SemanticLogLoggerProvider(ISemanticLog semanticLog)
{
this.semanticLog = semanticLog;
}
public ILogger CreateLogger(string categoryName)
{
return new SemanticLogLogger(semanticLog.CreateScope(writer =>
{
writer.WriteProperty("category", categoryName);
}));
}
public void Dispose()
{
}
}
}

42
src/Squidex.Infrastructure/Log/ApplicationInfoLogAppender.cs

@ -0,0 +1,42 @@
// ==========================================================================
// ApplicationInfoLogAppender.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Reflection;
namespace Squidex.Infrastructure.Log
{
public sealed class ApplicationInfoLogAppender : ILogAppender
{
private readonly string applicationName;
private readonly string applicationVersion;
private readonly string applicationSessionId;
public ApplicationInfoLogAppender(Type type, Guid applicationSession)
: this(type?.GetTypeInfo().Assembly, applicationSession)
{
}
public ApplicationInfoLogAppender(Assembly assembly, Guid applicationSession)
{
Guard.NotNull(assembly, nameof(assembly));
applicationName = assembly.GetName().Name;
applicationVersion = assembly.GetName().Version.ToString();
applicationSessionId = applicationSession.ToString();
}
public void Append(IObjectWriter writer)
{
writer.WriteObject("app", w => w
.WriteProperty("name", applicationName)
.WriteProperty("version", applicationVersion)
.WriteProperty("sessionId", applicationSessionId));
}
}
}

28
src/Squidex.Infrastructure/Log/ConsoleLogChannel.cs

@ -0,0 +1,28 @@
// ==========================================================================
// ConsoleLogChannel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.Log.Internal;
namespace Squidex.Infrastructure.Log
{
public sealed class ConsoleLogChannel : ILogChannel, IDisposable
{
private readonly ConsoleLogProcessor processor = new ConsoleLogProcessor();
public void Dispose()
{
processor.Dispose();
}
public void Log(SemanticLogLevel logLevel, string message)
{
processor.EnqueueMessage(new LogMessageEntry { Message = message, Level = logLevel });
}
}
}

29
src/Squidex.Infrastructure/Log/ConstantsLogWriter.cs

@ -0,0 +1,29 @@
// ==========================================================================
// ConstantsLogWriter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Log
{
public sealed class ConstantsLogWriter : ILogAppender
{
private readonly Action<IObjectWriter> objectWriter;
public ConstantsLogWriter(Action<IObjectWriter> objectWriter)
{
Guard.NotNull(objectWriter, nameof(objectWriter));
this.objectWriter = objectWriter;
}
public void Append(IObjectWriter writer)
{
objectWriter(writer);
}
}
}

20
src/Squidex.Infrastructure/Log/DebugLogChannel.cs

@ -0,0 +1,20 @@
// ==========================================================================
// DebugLogChannel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Diagnostics;
namespace Squidex.Infrastructure.Log
{
public sealed class DebugLogChannel : ILogChannel
{
public void Log(SemanticLogLevel logLevel, string message)
{
Debug.WriteLine(message);
}
}
}

34
src/Squidex.Infrastructure/Log/FileChannel.cs

@ -0,0 +1,34 @@
// ==========================================================================
// FileChannel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Squidex.Infrastructure.Log.Internal;
namespace Squidex.Infrastructure.Log
{
public sealed class FileChannel : ILogChannel, IExternalSystem
{
private readonly FileLogProcessor processor;
public FileChannel(string path)
{
Guard.NotNullOrEmpty(path, nameof(path));
processor = new FileLogProcessor(path);
}
public void Log(SemanticLogLevel logLevel, string message)
{
processor.EnqueueMessage(new LogMessageEntry { Message = message, Level = logLevel });
}
public void Connect()
{
processor.Connect();
}
}
}

26
src/Squidex.Infrastructure/Log/IArrayWriter.cs

@ -0,0 +1,26 @@
// ==========================================================================
// IArrayWriter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Log
{
public interface IArrayWriter
{
IArrayWriter WriteValue(string value);
IArrayWriter WriteValue(double value);
IArrayWriter WriteValue(long value);
IArrayWriter WriteValue(bool value);
IArrayWriter WriteValue(TimeSpan value);
IArrayWriter WriteValue(DateTime value);
IArrayWriter WriteValue(DateTimeOffset value);
IArrayWriter WriteObject(Action<IObjectWriter> objectWriter);
}
}

14
src/Squidex.Infrastructure/Log/ILogAppender.cs

@ -0,0 +1,14 @@
// ==========================================================================
// ILogAppender.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.Log
{
public interface ILogAppender
{
void Append(IObjectWriter writer);
}
}

14
src/Squidex.Infrastructure/Log/ILogChannel.cs

@ -0,0 +1,14 @@
// ==========================================================================
// ILogChannel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.Log
{
public interface ILogChannel
{
void Log(SemanticLogLevel logLevel, string message);
}
}

27
src/Squidex.Infrastructure/Log/IObjectWriter.cs

@ -0,0 +1,27 @@
// ==========================================================================
// IObjectWriter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Log
{
public interface IObjectWriter
{
IObjectWriter WriteProperty(string property, string value);
IObjectWriter WriteProperty(string property, double value);
IObjectWriter WriteProperty(string property, long value);
IObjectWriter WriteProperty(string property, bool value);
IObjectWriter WriteProperty(string property, TimeSpan value);
IObjectWriter WriteProperty(string property, DateTime value);
IObjectWriter WriteProperty(string property, DateTimeOffset value);
IObjectWriter WriteObject(string property, Action<IObjectWriter> objectWriter);
IObjectWriter WriteArray(string property, Action<IArrayWriter> arrayWriter);
}
}

19
src/Squidex.Infrastructure/Log/ISemanticLog.cs

@ -0,0 +1,19 @@
// ==========================================================================
// ISemanticLog.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Log
{
public interface ISemanticLog
{
void Log(SemanticLogLevel logLevel, Action<IObjectWriter> action);
ISemanticLog CreateScope(Action<IObjectWriter> objectWriter);
}
}

39
src/Squidex.Infrastructure/Log/Internal/AnsiLogConsole.cs

@ -0,0 +1,39 @@
// ==========================================================================
// AnsiLogConsole.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Text;
// ReSharper disable SwitchStatementMissingSomeCases
namespace Squidex.Infrastructure.Log.Internal
{
public class AnsiLogConsole : IConsole
{
private readonly StringBuilder outputBuilder = new StringBuilder();
public void WriteLine(SemanticLogLevel level, string message)
{
if (level >= SemanticLogLevel.Error)
{
outputBuilder.Append("\x1B[1m\x1B[31m");
outputBuilder.Append(message);
outputBuilder.Append("\x1B[39m\x1B[22m");
outputBuilder.AppendLine();
Console.Error.Write(outputBuilder.ToString());
}
else
{
Console.Out.Write(outputBuilder.ToString());
}
outputBuilder.Clear();
}
}
}

73
src/Squidex.Infrastructure/Log/Internal/ConsoleLogProcessor.cs

@ -0,0 +1,73 @@
// ==========================================================================
// ConsoleLogProcessor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Log.Internal
{
public class ConsoleLogProcessor : IDisposable
{
private readonly IConsole console;
private const int MaxQueuedMessages = 1024;
private readonly BlockingCollection<LogMessageEntry> messageQueue = new BlockingCollection<LogMessageEntry>(MaxQueuedMessages);
private readonly Task outputTask;
public ConsoleLogProcessor()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
console = new WindowsLogConsole();
}
else
{
console = new AnsiLogConsole();
}
outputTask = Task.Factory.StartNew(ProcessLogQueue, this, TaskCreationOptions.LongRunning);
}
public void EnqueueMessage(LogMessageEntry message)
{
messageQueue.Add(message);
}
private void ProcessLogQueue()
{
foreach (var entry in messageQueue.GetConsumingEnumerable())
{
console.WriteLine(entry.Level, entry.Message);
}
}
private static void ProcessLogQueue(object state)
{
var processor = (ConsoleLogProcessor)state;
processor.ProcessLogQueue();
}
public void Dispose()
{
messageQueue.CompleteAdding();
try
{
outputTask.Wait(1500);
}
catch (TaskCanceledException)
{
}
catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException)
{
}
}
}
}

95
src/Squidex.Infrastructure/Log/Internal/FileLogChannel.cs

@ -0,0 +1,95 @@
// ==========================================================================
// FileLogChannel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Log.Internal
{
public class FileLogProcessor : IDisposable
{
private const int MaxQueuedMessages = 1024;
private const int Retries = 10;
private readonly BlockingCollection<LogMessageEntry> messageQueue = new BlockingCollection<LogMessageEntry>(MaxQueuedMessages);
private readonly Task outputTask;
private readonly string path;
public FileLogProcessor(string path)
{
this.path = path;
outputTask = Task.Factory.StartNew(ProcessLogQueue, this, TaskCreationOptions.LongRunning);
}
public void Connect()
{
var fileInfo = new FileInfo(path);
if (!fileInfo.Directory.Exists)
{
throw new ConfigurationException($"Log directory '{fileInfo.Directory.FullName}' does not exist.");
}
}
public void EnqueueMessage(LogMessageEntry message)
{
messageQueue.Add(message);
}
private async Task ProcessLogQueue()
{
foreach (var entry in messageQueue.GetConsumingEnumerable())
{
for (var i = 1; i <= Retries; i++)
{
try
{
File.AppendAllText(path, entry.Message + Environment.NewLine, Encoding.UTF8);
break;
}
catch (Exception ex)
{
await Task.Delay(i * 10);
if (i == Retries)
{
Console.WriteLine("Failed to write to log file '{0}': {1}", path, ex);
}
}
}
}
}
private static Task ProcessLogQueue(object state)
{
var processor = (FileLogProcessor)state;
return processor.ProcessLogQueue();
}
public void Dispose()
{
messageQueue.CompleteAdding();
try
{
outputTask.Wait(1500);
}
catch (TaskCanceledException)
{
}
catch (AggregateException ex) when (ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException)
{
}
}
}
}

14
src/Squidex.Infrastructure/Log/Internal/IConsole.cs

@ -0,0 +1,14 @@
// ==========================================================================
// IConsole.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.Log.Internal
{
public interface IConsole
{
void WriteLine(SemanticLogLevel level, string message);
}
}

16
src/Squidex.Infrastructure/Log/Internal/LogMessageEntry.cs

@ -0,0 +1,16 @@
// ==========================================================================
// LogMessageEntry.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.Log.Internal
{
public struct LogMessageEntry
{
public SemanticLogLevel Level;
public string Message;
}
}

29
src/Squidex.Infrastructure/Log/Internal/WindowsLogConsole.cs

@ -0,0 +1,29 @@
// ==========================================================================
// WindowsLogConsole.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Log.Internal
{
public class WindowsLogConsole : IConsole
{
public void WriteLine(SemanticLogLevel level, string message)
{
if (level >= SemanticLogLevel.Error)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(message);
Console.ResetColor();
}
else
{
Console.Out.WriteLine(message);
}
}
}
}

184
src/Squidex.Infrastructure/Log/JsonLogWriter.cs

@ -0,0 +1,184 @@
// ==========================================================================
// JsonLogWriter.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Globalization;
using System.IO;
using Newtonsoft.Json;
namespace Squidex.Infrastructure.Log
{
public sealed class JsonLogWriter : IObjectWriter, IArrayWriter
{
private readonly bool extraLine;
private readonly StringWriter textWriter = new StringWriter();
private readonly JsonWriter jsonWriter;
public JsonLogWriter(Formatting formatting = Formatting.None, bool extraLine = false)
{
this.extraLine = extraLine;
jsonWriter = new JsonTextWriter(textWriter) { Formatting = formatting };
jsonWriter.WriteStartObject();
}
IArrayWriter IArrayWriter.WriteValue(string value)
{
jsonWriter.WriteValue(value);
return this;
}
IArrayWriter IArrayWriter.WriteValue(double value)
{
jsonWriter.WriteValue(value);
return this;
}
IArrayWriter IArrayWriter.WriteValue(long value)
{
jsonWriter.WriteValue(value);
return this;
}
IArrayWriter IArrayWriter.WriteValue(bool value)
{
jsonWriter.WriteValue(value);
return this;
}
IArrayWriter IArrayWriter.WriteValue(DateTime value)
{
jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture));
return this;
}
IArrayWriter IArrayWriter.WriteValue(DateTimeOffset value)
{
jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture));
return this;
}
IArrayWriter IArrayWriter.WriteValue(TimeSpan value)
{
jsonWriter.WriteValue(value);
return this;
}
IObjectWriter IObjectWriter.WriteProperty(string property, string value)
{
jsonWriter.WritePropertyName(property);
jsonWriter.WriteValue(value);
return this;
}
IObjectWriter IObjectWriter.WriteProperty(string property, double value)
{
jsonWriter.WritePropertyName(property);
jsonWriter.WriteValue(value);
return this;
}
IObjectWriter IObjectWriter.WriteProperty(string property, long value)
{
jsonWriter.WritePropertyName(property);
jsonWriter.WriteValue(value);
return this;
}
IObjectWriter IObjectWriter.WriteProperty(string property, bool value)
{
jsonWriter.WritePropertyName(property);
jsonWriter.WriteValue(value);
return this;
}
IObjectWriter IObjectWriter.WriteProperty(string property, DateTime value)
{
jsonWriter.WritePropertyName(property);
jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture));
return this;
}
IObjectWriter IObjectWriter.WriteProperty(string property, DateTimeOffset value)
{
jsonWriter.WritePropertyName(property);
jsonWriter.WriteValue(value.ToString("o", CultureInfo.InvariantCulture));
return this;
}
IObjectWriter IObjectWriter.WriteProperty(string property, TimeSpan value)
{
jsonWriter.WritePropertyName(property);
jsonWriter.WriteValue(value);
return this;
}
IObjectWriter IObjectWriter.WriteObject(string property, Action<IObjectWriter> objectWriter)
{
jsonWriter.WritePropertyName(property);
jsonWriter.WriteStartObject();
objectWriter?.Invoke(this);
jsonWriter.WriteEndObject();
return this;
}
IObjectWriter IObjectWriter.WriteArray(string property, Action<IArrayWriter> arrayWriter)
{
jsonWriter.WritePropertyName(property);
jsonWriter.WriteStartArray();
arrayWriter?.Invoke(this);
jsonWriter.WriteEndArray();
return this;
}
IArrayWriter IArrayWriter.WriteObject(Action<IObjectWriter> objectWriter)
{
jsonWriter.WriteStartObject();
objectWriter?.Invoke(this);
jsonWriter.WriteEndObject();
return this;
}
public override string ToString()
{
jsonWriter.WriteEndObject();
var result = textWriter.ToString();
if (extraLine)
{
result += Environment.NewLine;
}
return result;
}
}
}

86
src/Squidex.Infrastructure/Log/SemanticLog.cs

@ -0,0 +1,86 @@
// ==========================================================================
// SemanticLog.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
namespace Squidex.Infrastructure.Log
{
public sealed class SemanticLog : ISemanticLog
{
private readonly IEnumerable<ILogChannel> channels;
private readonly IEnumerable<ILogAppender> appenders;
private readonly Func<IObjectWriter> writerFactory;
public SemanticLog(
IEnumerable<ILogChannel> channels,
IEnumerable<ILogAppender> appenders,
Func<IObjectWriter> writerFactory)
{
Guard.NotNull(channels, nameof(channels));
Guard.NotNull(appenders, nameof(appenders));
this.channels = channels;
this.appenders = appenders;
this.writerFactory = writerFactory;
}
public void Log(SemanticLogLevel logLevel, Action<IObjectWriter> action)
{
Guard.NotNull(action, nameof(action));
var formattedText = FormatText(logLevel, action);
List<Exception> exceptions = null;
foreach (var channel in channels)
{
try
{
channel.Log(logLevel, formattedText);
}
catch (Exception ex)
{
if (exceptions == null)
{
exceptions = new List<Exception>();
}
exceptions.Add(ex);
}
}
if (exceptions != null && exceptions.Count > 0)
{
throw new AggregateException("An error occurred while writing to logger(s).", exceptions);
}
}
private string FormatText(SemanticLogLevel logLevel, Action<IObjectWriter> objectWriter)
{
var writer = writerFactory();
writer.WriteProperty(nameof(logLevel), logLevel.ToString());
objectWriter(writer);
foreach (var appender in appenders)
{
appender.Append(writer);
}
return writer.ToString();
}
public ISemanticLog CreateScope(Action<IObjectWriter> objectWriter)
{
return new SemanticLog(channels, appenders.Union(new ILogAppender[] { new ConstantsLogWriter(objectWriter) }).ToArray(), writerFactory);
}
}
}

125
src/Squidex.Infrastructure/Log/SemanticLogExtensions.cs

@ -0,0 +1,125 @@
// ==========================================================================
// SemanticLogExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Diagnostics;
// ReSharper disable InvertIf
namespace Squidex.Infrastructure.Log
{
public static class SemanticLogExtensions
{
public static void LogTrace(this ISemanticLog log, Action<IObjectWriter> objectWriter)
{
log.Log(SemanticLogLevel.Trace, objectWriter);
}
public static IDisposable MeasureTrace(this ISemanticLog log, Action<IObjectWriter> objectWriter)
{
return new TimeMeasurer(log, SemanticLogLevel.Trace, objectWriter);
}
public static void LogDebug(this ISemanticLog log, Action<IObjectWriter> objectWriter)
{
log.Log(SemanticLogLevel.Debug, objectWriter);
}
public static IDisposable MeasureDebug(this ISemanticLog log, Action<IObjectWriter> objectWriter)
{
return new TimeMeasurer(log, SemanticLogLevel.Debug, objectWriter);
}
public static void LogInformation(this ISemanticLog log, Action<IObjectWriter> objectWriter)
{
log.Log(SemanticLogLevel.Information, objectWriter);
}
public static IDisposable MeasureInformation(this ISemanticLog log, Action<IObjectWriter> objectWriter)
{
return new TimeMeasurer(log, SemanticLogLevel.Information, objectWriter);
}
public static void LogWarning(this ISemanticLog log, Action<IObjectWriter> objectWriter)
{
log.Log(SemanticLogLevel.Warning, objectWriter);
}
public static void LogWarning(this ISemanticLog log, Exception exception, Action<IObjectWriter> objectWriter = null)
{
log.Log(SemanticLogLevel.Warning, writer => writer.WriteException(exception, objectWriter));
}
public static void LogError(this ISemanticLog log, Action<IObjectWriter> objectWriter)
{
log.Log(SemanticLogLevel.Error, objectWriter);
}
public static void LogError(this ISemanticLog log, Exception exception, Action<IObjectWriter> objectWriter = null)
{
log.Log(SemanticLogLevel.Error, writer => writer.WriteException(exception, objectWriter));
}
public static void LogFatal(this ISemanticLog log, Action<IObjectWriter> objectWriter)
{
log.Log(SemanticLogLevel.Fatal, objectWriter);
}
public static void LogFatal(this ISemanticLog log, Exception exception, Action<IObjectWriter> objectWriter = null)
{
log.Log(SemanticLogLevel.Fatal, writer => writer.WriteException(exception, objectWriter));
}
private static void WriteException(this IObjectWriter writer, Exception exception, Action<IObjectWriter> objectWriter)
{
objectWriter?.Invoke(writer);
if (exception != null)
{
writer.WriteException(exception);
}
}
public static IObjectWriter WriteException(this IObjectWriter writer, Exception exception)
{
return writer.WriteObject(nameof(exception), inner =>
{
inner.WriteProperty("message", exception.Message);
inner.WriteProperty("stackTrace", exception.StackTrace);
});
}
private sealed class TimeMeasurer : IDisposable
{
private readonly Stopwatch watch = Stopwatch.StartNew();
private readonly SemanticLogLevel logLevel;
private readonly Action<IObjectWriter> objectWriter;
private readonly ISemanticLog log;
public TimeMeasurer(ISemanticLog log, SemanticLogLevel logLevel, Action<IObjectWriter> objectWriter)
{
this.logLevel = logLevel;
this.log = log;
this.objectWriter = objectWriter;
}
public void Dispose()
{
watch.Stop();
log.Log(logLevel, writer =>
{
objectWriter?.Invoke(writer);
writer.WriteProperty("elapsedMs", watch.ElapsedMilliseconds);
});
}
}
}
}

19
src/Squidex.Infrastructure/Log/SemanticLogLevel.cs

@ -0,0 +1,19 @@
// ==========================================================================
// SemanticLogLevel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.Log
{
public enum SemanticLogLevel
{
Trace,
Debug,
Information,
Warning,
Error,
Fatal
}
}

34
src/Squidex.Infrastructure/Log/TimestampLogAppender.cs

@ -0,0 +1,34 @@
// ==========================================================================
// TimestampLogAppender.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Log
{
public sealed class TimestampLogAppender : ILogAppender
{
private readonly Func<DateTime> timestamp;
public TimestampLogAppender()
: this(() => DateTime.UtcNow)
{
}
public TimestampLogAppender(Func<DateTime> timestamp)
{
Guard.NotNull(timestamp, nameof(timestamp));
this.timestamp = timestamp;
}
public void Append(IObjectWriter writer)
{
writer.WriteProperty("timestamp", timestamp());
}
}
}

23
src/Squidex.Infrastructure/Singletons.cs

@ -0,0 +1,23 @@
// ==========================================================================
// Singletons.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Concurrent;
namespace Squidex.Infrastructure
{
public static class Singletons<T>
{
private static readonly ConcurrentDictionary<string, T> instances = new ConcurrentDictionary<string, T>(StringComparer.OrdinalIgnoreCase);
public static T GetOrAdd(string key, Func<string, T> factory)
{
return instances.GetOrAdd(key, factory);
}
}
}

5
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>netstandard1.6</TargetFramework> <TargetFramework>netstandard1.6</TargetFramework>
<NoWarn>$(NoWarn);IDE0017</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType> <DebugType>full</DebugType>
@ -10,8 +11,8 @@
<PackageReference Include="ImageSharp" Version="1.0.0-alpha4-00031" /> <PackageReference Include="ImageSharp" Version="1.0.0-alpha4-00031" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.2-beta2" /> <PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
<PackageReference Include="NodaTime" Version="2.0.0-beta20170123" /> <PackageReference Include="NodaTime" Version="2.0.0" />
<PackageReference Include="System.Linq" Version="4.3.0" /> <PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Reactive" Version="3.1.1" /> <PackageReference Include="System.Reactive" Version="3.1.1" />
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.3.0" /> <PackageReference Include="System.Reflection.TypeExtensions" Version="4.3.0" />

5
src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs

@ -23,6 +23,11 @@ namespace Squidex.Read.MongoDb.Apps
get { return GetType().Name; } get { return GetType().Name; }
} }
public string EventsFilter
{
get { return "^app-"; }
}
public Task On(Envelope<IEvent> @event) public Task On(Envelope<IEvent> @event)
{ {
return this.DispatchActionAsync(@event.Payload, @event.Headers); return this.DispatchActionAsync(@event.Payload, @event.Headers);

5
src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -37,6 +37,11 @@ namespace Squidex.Read.MongoDb.Contents
get { return GetType().Name; } get { return GetType().Name; }
} }
public string EventsFilter
{
get { return "^content-"; }
}
public async Task ClearAsync() public async Task ClearAsync()
{ {
using (var collections = await database.ListCollectionsAsync()) using (var collections = await database.ListCollectionsAsync())

5
src/Squidex.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs

@ -19,6 +19,11 @@ namespace Squidex.Read.MongoDb.Contents.Visitors
{ {
var path = model.EntityContainer.EntitySets().First().Path.Path.Last().Split('.').Last(); var path = model.EntityContainer.EntitySets().First().Path.Path.Last().Split('.').Last();
if (query.StartsWith("?"))
{
query = query.Substring(1);
}
var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative)); var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative));
return parser; return parser;

4
src/Squidex.Read.MongoDb/Contents/Visitors/FindExtensions.cs

@ -51,6 +51,10 @@ namespace Squidex.Read.MongoDb.Contents.Visitors
{ {
cursor = cursor.Skip((int)skip.Value); cursor = cursor.Skip((int)skip.Value);
} }
else
{
cursor = cursor.Skip(null);
}
return cursor; return cursor;
} }

15
src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs

@ -27,6 +27,16 @@ namespace Squidex.Read.MongoDb.History
private readonly Dictionary<string, string> texts = new Dictionary<string, string>(); private readonly Dictionary<string, string> texts = new Dictionary<string, string>();
private int sessionEventCount; private int sessionEventCount;
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "*"; }
}
public MongoHistoryEventRepository(IMongoDatabase database, IEnumerable<IHistoryEventsCreator> creators) public MongoHistoryEventRepository(IMongoDatabase database, IEnumerable<IHistoryEventsCreator> creators)
: base(database) : base(database)
{ {
@ -67,11 +77,6 @@ namespace Squidex.Read.MongoDb.History
return entities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList(); return entities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList();
} }
public string Name
{
get { return GetType().Name; }
}
public async Task On(Envelope<IEvent> @event) public async Task On(Envelope<IEvent> @event)
{ {
foreach (var creator in creators) foreach (var creator in creators)

5
src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs

@ -26,6 +26,11 @@ namespace Squidex.Read.MongoDb.Schemas
get { return GetType().Name; } get { return GetType().Name; }
} }
public string EventsFilter
{
get { return "^schema-"; }
}
public Task On(Envelope<IEvent> @event) public Task On(Envelope<IEvent> @event)
{ {
return this.DispatchActionAsync(@event.Payload, @event.Headers); return this.DispatchActionAsync(@event.Payload, @event.Headers);

2
src/Squidex.Read.MongoDb/Squidex.Read.MongoDb.csproj

@ -15,7 +15,7 @@
<ProjectReference Include="..\Squidex.Read\Squidex.Read.csproj" /> <ProjectReference Include="..\Squidex.Read\Squidex.Read.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="IdentityServer4" Version="1.3.1" /> <PackageReference Include="IdentityServer4" Version="1.4.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="1.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Identity" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.MongoDB" Version="1.0.2" /> <PackageReference Include="Microsoft.AspNetCore.Identity.MongoDB" Version="1.0.2" />
<PackageReference Include="MongoDB.Driver" Version="2.4.3" /> <PackageReference Include="MongoDB.Driver" Version="2.4.3" />

5
src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs

@ -32,6 +32,11 @@ namespace Squidex.Read.Apps.Services.Implementations
get { return GetType().Name; } get { return GetType().Name; }
} }
public string EventsFilter
{
get { return "*"; }
}
public CachingAppProvider(IMemoryCache cache, IAppRepository repository) public CachingAppProvider(IMemoryCache cache, IAppRepository repository)
: base(cache) : base(cache)
{ {

9
src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -32,6 +32,11 @@ namespace Squidex.Read.Schemas.Services.Implementations
get { return GetType().Name; } get { return GetType().Name; }
} }
public string EventsFilter
{
get { return "*"; }
}
public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository) public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository)
: base(cache) : base(cache)
{ {
@ -108,6 +113,10 @@ namespace Squidex.Read.Schemas.Services.Implementations
{ {
Remove(fieldEvent.AppId, fieldEvent.SchemaId); Remove(fieldEvent.AppId, fieldEvent.SchemaId);
} }
else if (@event.Payload is SchemaCreated schemaCreatedEvent)
{
Remove(schemaCreatedEvent.AppId, schemaCreatedEvent.SchemaId);
}
else if (@event.Payload is SchemaDeleted schemaDeletedEvent) else if (@event.Payload is SchemaDeleted schemaDeletedEvent)
{ {
Remove(schemaDeletedEvent.AppId, schemaDeletedEvent.SchemaId); Remove(schemaDeletedEvent.AppId, schemaDeletedEvent.SchemaId);

2
src/Squidex.Read/Squidex.Read.csproj

@ -14,7 +14,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="1.1.1" />
<PackageReference Include="NodaTime" Version="2.0.0-beta20170123" /> <PackageReference Include="NodaTime" Version="2.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="IdentityServer4"> <Reference Include="IdentityServer4">

1
src/Squidex.Write/Contents/ContentCommandHandler.cs

@ -92,7 +92,6 @@ namespace Squidex.Write.Contents
Guard.Valid(command, nameof(command), message); Guard.Valid(command, nameof(command), message);
var taskForApp = appProvider.FindAppByIdAsync(command.AppId.Id); var taskForApp = appProvider.FindAppByIdAsync(command.AppId.Id);
var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id); var taskForSchema = schemas.FindSchemaByIdAsync(command.SchemaId.Id);
await Task.WhenAll(taskForApp, taskForSchema); await Task.WhenAll(taskForApp, taskForSchema);

2
src/Squidex.Write/Squidex.Write.csproj

@ -14,6 +14,6 @@
<ProjectReference Include="..\Squidex.Read\Squidex.Read.csproj" /> <ProjectReference Include="..\Squidex.Read\Squidex.Read.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="NodaTime" Version="2.0.0-beta20170123" /> <PackageReference Include="NodaTime" Version="2.0.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

85
src/Squidex/Config/Domain/ClusterModule.cs

@ -1,85 +0,0 @@
// ==========================================================================
// ClusterModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Autofac;
using Microsoft.Extensions.Configuration;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Redis;
using StackExchange.Redis;
namespace Squidex.Config.Domain
{
public class ClusterModule : Module
{
private IConfiguration Configuration { get; }
public ClusterModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
var handleEvents = Configuration.GetValue<bool>("squidex:handleEvents");
if (handleEvents)
{
builder.RegisterType<EventReceiver>()
.AsSelf()
.InstancePerDependency();
}
var clustererType = Configuration.GetValue<string>("squidex:clusterer:type");
if (string.IsNullOrWhiteSpace(clustererType))
{
throw new ConfigurationException("You must specify the clusterer type in the 'squidex:clusterer:type' configuration section.");
}
if (string.Equals(clustererType, "Redis", StringComparison.OrdinalIgnoreCase))
{
var connectionString = Configuration.GetValue<string>("squidex:clusterer:redis:connectionString");
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ConfigurationException("You must specify the Redis connection string in the 'squidex:clusterer:redis:connectionString' configuration section.");
}
try
{
var connectionMultiplexer = ConnectionMultiplexer.Connect(connectionString);
builder.RegisterInstance(connectionMultiplexer)
.As<IConnectionMultiplexer>()
.SingleInstance();
}
catch (Exception ex)
{
throw new ConfigurationException($"Redis connection failed to connect to database {connectionString}", ex);
}
builder.RegisterType<RedisPubSub>()
.As<IPubSub>()
.As<IExternalSystem>()
.SingleInstance();
}
else if (string.Equals(clustererType, "None", StringComparison.OrdinalIgnoreCase))
{
builder.RegisterType<InMemoryPubSub>()
.As<IPubSub>()
.SingleInstance();
}
else
{
throw new ConfigurationException($"Unsupported clusterer type '{clustererType}' for key 'squidex:clusterer:type', supported: Redis, None.");
}
}
}
}

80
src/Squidex/Config/Domain/EventPublishersModule.cs

@ -0,0 +1,80 @@
// ==========================================================================
// RabbitMqModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Autofac;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.RabbitMq;
// ReSharper disable InvertIf
namespace Squidex.Config.Domain
{
public sealed class EventPublishersModule : Module
{
private IConfiguration Configuration { get; }
public EventPublishersModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
var eventPublishers = Configuration.GetSection("eventPublishers");
foreach (var child in eventPublishers.GetChildren())
{
var eventPublisherType = child.GetValue<string>("type");
if (string.IsNullOrWhiteSpace(eventPublisherType))
{
throw new ConfigurationException($"Configure EventPublisher type with 'eventPublishers:{child.Key}:type'.");
}
var eventsFilter = Configuration.GetValue<string>("eventsFilter");
var enabled = child.GetValue<bool>("enabled");
if (string.Equals(eventPublisherType, "RabbitMq", StringComparison.OrdinalIgnoreCase))
{
var configuration = child.GetValue<string>("configuration");
if (string.IsNullOrWhiteSpace(configuration))
{
throw new ConfigurationException($"Configure EventPublisher RabbitMq configuration with 'eventPublishers:{child.Key}:configuration'.");
}
var exchange = child.GetValue<string>("exchange");
if (string.IsNullOrWhiteSpace(exchange))
{
throw new ConfigurationException($"Configure EventPublisher RabbitMq exchange with 'eventPublishers:{child.Key}:configuration'.");
}
var name = $"EventPublishers_{child.Key}";
if (enabled)
{
builder.Register(c => new RabbitMqEventConsumer(c.Resolve<JsonSerializerSettings>(), name, configuration, exchange, eventsFilter))
.As<IEventConsumer>()
.As<IExternalSystem>()
.SingleInstance();
}
}
else
{
throw new ConfigurationException($"Unsupported value '{child.Key}' for 'eventPublishers:{child.Key}:type', supported: RabbitMq.");
}
}
}
}
}

53
src/Squidex/Config/Domain/EventStoreModule.cs

@ -8,17 +8,20 @@
using System; using System;
using Autofac; using Autofac;
using Autofac.Core;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using MongoDB.Driver; using MongoDB.Driver;
using NodaTime;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb.EventStore; using Squidex.Infrastructure.MongoDb.EventStore;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
{ {
public class EventStoreModule : Module public sealed class EventStoreModule : Module
{ {
private const string MongoClientRegistration = "EventStoreMongoClient";
private const string MongoDatabaseRegistration = "EventStoreMongoDatabase";
private IConfiguration Configuration { get; } private IConfiguration Configuration { get; }
public EventStoreModule(IConfiguration configuration) public EventStoreModule(IConfiguration configuration)
@ -28,45 +31,55 @@ namespace Squidex.Config.Domain
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
var storeType = Configuration.GetValue<string>("squidex:eventStore:type"); var consumeEvents = Configuration.GetValue<bool>("eventStore:consume");
if (string.IsNullOrWhiteSpace(storeType)) if (consumeEvents)
{ {
throw new ConfigurationException("You must specify the store type in the 'squidex:eventStore:type' configuration section."); builder.RegisterType<EventReceiver>()
.AsSelf()
.InstancePerDependency();
} }
if (string.Equals(storeType, "MongoDb", StringComparison.OrdinalIgnoreCase)) var eventStoreType = Configuration.GetValue<string>("eventStore:type");
if (string.IsNullOrWhiteSpace(eventStoreType))
{ {
var databaseName = Configuration.GetValue<string>("squidex:eventStore:mongoDb:databaseName"); throw new ConfigurationException("Configure EventStore type with 'eventStore:type'.");
}
if (string.IsNullOrWhiteSpace(databaseName)) if (string.Equals(eventStoreType, "MongoDb", StringComparison.OrdinalIgnoreCase))
{
var configuration = Configuration.GetValue<string>("eventStore:mongoDb:configuration");
if (string.IsNullOrWhiteSpace(configuration))
{ {
throw new ConfigurationException("You must specify the MongoDB database name in the 'squidex:eventStore:mongoDb:databaseName' configuration section."); throw new ConfigurationException("Configure EventStore MongoDb configuration with 'eventStore:mongoDb:configuration'.");
} }
var connectionString = Configuration.GetValue<string>("squidex:eventStore:mongoDb:connectionString"); var database = Configuration.GetValue<string>("eventStore:mongoDb:database");
if (string.IsNullOrWhiteSpace(connectionString)) if (string.IsNullOrWhiteSpace(database))
{ {
throw new ConfigurationException("You must specify the MongoDB connection string in the 'squidex:eventStore:mongoDb:connectionString' configuration section."); throw new ConfigurationException("Configure EventStore MongoDb Database name with 'eventStore:mongoDb:database'.");
} }
builder.Register(c => builder.Register(c => Singletons<IMongoClient>.GetOrAdd(configuration, s => new MongoClient(s)))
{ .Named<IMongoClient>(MongoClientRegistration)
var mongoDbClient = new MongoClient(connectionString); .SingleInstance();
var mongoDatabase = mongoDbClient.GetDatabase(databaseName);
var eventStore = new MongoEventStore(mongoDatabase, c.Resolve<IEventNotifier>(), c.Resolve<IClock>()); builder.Register(c => c.ResolveNamed<IMongoClient>(MongoClientRegistration).GetDatabase(database))
.Named<IMongoDatabase>(MongoDatabaseRegistration)
.SingleInstance();
return eventStore; builder.RegisterType<MongoEventStore>()
}) .WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IExternalSystem>() .As<IExternalSystem>()
.As<IEventStore>() .As<IEventStore>()
.SingleInstance(); .SingleInstance();
} }
else else
{ {
throw new ConfigurationException($"Unsupported store type '{storeType}' for key 'squidex:eventStore:type', supported: MongoDb."); throw new ConfigurationException($"Unsupported value '{eventStoreType}' for 'eventStore:type', supported: MongoDb.");
} }
} }
} }

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

@ -6,12 +6,14 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using Autofac; using Autofac;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using NodaTime; using NodaTime;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
using Squidex.Core.Schemas.Json; using Squidex.Core.Schemas.Json;
@ -19,12 +21,14 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Log;
using Squidex.Pipeline;
// ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable UnusedAutoPropertyAccessor.Local
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
{ {
public class InfrastructureModule : Module public sealed class InfrastructureModule : Module
{ {
private IConfiguration Configuration { get; } private IConfiguration Configuration { get; }
@ -35,6 +39,53 @@ namespace Squidex.Config.Domain
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
if (Configuration.GetValue<bool>("logging:human"))
{
builder.Register(c => new Func<IObjectWriter>(() => new JsonLogWriter(Formatting.Indented, true)))
.AsSelf()
.SingleInstance();
}
else
{
builder.Register(c => new Func<IObjectWriter>(() => new JsonLogWriter()))
.AsSelf()
.SingleInstance();
}
var loggingFile = Configuration.GetValue<string>("logging:file");
if (!string.IsNullOrWhiteSpace(loggingFile))
{
builder.RegisterInstance(new FileChannel(loggingFile))
.As<ILogChannel>()
.As<IExternalSystem>()
.SingleInstance();
}
builder.Register(c => new ApplicationInfoLogAppender(GetType(), Guid.NewGuid()))
.As<ILogAppender>()
.SingleInstance();
builder.RegisterType<ActionContextLogAppender>()
.As<ILogAppender>()
.SingleInstance();
builder.RegisterType<TimestampLogAppender>()
.As<ILogAppender>()
.SingleInstance();
builder.RegisterType<DebugLogChannel>()
.As<ILogChannel>()
.SingleInstance();
builder.RegisterType<ConsoleLogChannel>()
.As<ILogChannel>()
.SingleInstance();
builder.RegisterType<SemanticLog>()
.As<ISemanticLog>()
.SingleInstance();
builder.Register(c => SystemClock.Instance) builder.Register(c => SystemClock.Instance)
.As<IClock>() .As<IClock>()
.SingleInstance(); .SingleInstance();
@ -63,7 +114,7 @@ namespace Squidex.Config.Domain
.As<ICommandBus>() .As<ICommandBus>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<DefaultMemoryEventNotifier>() builder.RegisterType<DefaultEventNotifier>()
.As<IEventNotifier>() .As<IEventNotifier>()
.SingleInstance(); .SingleInstance();

70
src/Squidex/Config/Domain/PubSubModule.cs

@ -0,0 +1,70 @@
// ==========================================================================
// ClusterModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Autofac;
using Autofac.Core;
using Microsoft.Extensions.Configuration;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Redis;
using StackExchange.Redis;
namespace Squidex.Config.Domain
{
public sealed class PubSubModule : Module
{
private const string RedisRegistration = "PubSubRedis";
private IConfiguration Configuration { get; }
public PubSubModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
var pubSubType = Configuration.GetValue<string>("pubSub:type");
if (string.IsNullOrWhiteSpace(pubSubType))
{
throw new ConfigurationException("Configure the PubSub type with 'pubSub:type'.");
}
if (string.Equals(pubSubType, "Redis", StringComparison.OrdinalIgnoreCase))
{
var configuration = Configuration.GetValue<string>("pubsub:redis:configuration");
if (string.IsNullOrWhiteSpace(configuration))
{
throw new ConfigurationException("Configure PubSub Redis configuration with pubSub:redis:configuration'.");
}
builder.Register(c => Singletons<IConnectionMultiplexer>.GetOrAdd(configuration, s => ConnectionMultiplexer.Connect(s)))
.Named<IConnectionMultiplexer>(RedisRegistration)
.SingleInstance();
builder.RegisterType<RedisPubSub>()
.WithParameter(ResolvedParameter.ForNamed<IConnectionMultiplexer>(RedisRegistration))
.As<IPubSub>()
.As<IExternalSystem>()
.SingleInstance();
}
else if (string.Equals(pubSubType, "InMemory", StringComparison.OrdinalIgnoreCase))
{
builder.RegisterType<InMemoryPubSub>()
.As<IPubSub>()
.SingleInstance();
}
else
{
throw new ConfigurationException($"Unsupported value '{pubSubType}' for 'pubSub:type', supported: Redis, InMemory.");
}
}
}
}

6
src/Squidex/Config/Domain/StoreModule.cs

@ -24,11 +24,11 @@ namespace Squidex.Config.Domain
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
var storeType = Configuration.GetValue<string>("squidex:stores:type"); var storeType = Configuration.GetValue<string>("store:type");
if (string.IsNullOrWhiteSpace(storeType)) if (string.IsNullOrWhiteSpace(storeType))
{ {
throw new ConfigurationException("You must specify the store type in the 'squidex:stores:type' configuration section."); throw new ConfigurationException("Configure the Store type with 'store:type'.");
} }
if (string.Equals(storeType, "MongoDB", StringComparison.OrdinalIgnoreCase)) if (string.Equals(storeType, "MongoDB", StringComparison.OrdinalIgnoreCase))
@ -37,7 +37,7 @@ namespace Squidex.Config.Domain
} }
else else
{ {
throw new ConfigurationException($"Unsupported store type '{storeType}' for key 'squidex:stores:type', supported: MongoDb."); throw new ConfigurationException($"Unsupported value '{storeType}' for 'stores:type', supported: MongoDb.");
} }
} }
} }

51
src/Squidex/Config/Domain/StoreMongoDbModule.cs

@ -34,8 +34,9 @@ namespace Squidex.Config.Domain
{ {
public class StoreMongoDbModule : Module public class StoreMongoDbModule : Module
{ {
private const string MongoDatabaseName = "MongoDatabaseName"; private const string MongoClientRegistration = "StoreMongoClient";
private const string MongoDatabaseNameContent = "MongoDatabaseNameContent"; private const string MongoDatabaseRegistration = "StoreMongoDatabaseName";
private const string MongoContentDatabaseRegistration = "StoreMongoDatabaseNameContent";
private IConfiguration Configuration { get; } private IConfiguration Configuration { get; }
@ -46,42 +47,42 @@ namespace Squidex.Config.Domain
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
var databaseName = Configuration.GetValue<string>("squidex:stores:mongoDb:databaseName"); var configuration = Configuration.GetValue<string>("store:mongoDb:configuration");
if (string.IsNullOrWhiteSpace(databaseName)) if (string.IsNullOrWhiteSpace(configuration))
{ {
throw new ConfigurationException("You must specify the MongoDB database name in the 'squidex:stores:mongoDb:databaseName' configuration section."); throw new ConfigurationException("Configure the Store MongoDb configuration with 'store:mongoDb:configuration'.");
} }
var connectionString = Configuration.GetValue<string>("squidex:stores:mongoDb:connectionString"); var database = Configuration.GetValue<string>("store:mongoDb:database");
if (string.IsNullOrWhiteSpace(connectionString)) if (string.IsNullOrWhiteSpace(database))
{ {
throw new ConfigurationException("You must specify the MongoDB connection string in the 'squidex:stores:mongoDb:connectionString' configuration section."); throw new ConfigurationException("Configure the Store MongoDb database with 'store:mongoDb:database'.");
} }
var databaseNameContent = Configuration.GetValue<string>("squidex:stores:mongoDb:databaseNameContent"); var contentDatabase = Configuration.GetValue<string>("store:mongoDb:databaseNameContent");
if (string.IsNullOrWhiteSpace(databaseNameContent)) if (string.IsNullOrWhiteSpace(contentDatabase))
{ {
databaseNameContent = databaseName; contentDatabase = database;
} }
builder.Register(c => new MongoClient(connectionString)) builder.Register(c => Singletons<IMongoClient>.GetOrAdd(configuration, s => new MongoClient(s)))
.As<IMongoClient>() .Named<IMongoClient>(MongoClientRegistration)
.SingleInstance(); .SingleInstance();
builder.Register(c => c.Resolve<IMongoClient>().GetDatabase(databaseName)) builder.Register(c => c.ResolveNamed<IMongoClient>(MongoClientRegistration).GetDatabase(database))
.Named<IMongoDatabase>(MongoDatabaseName) .Named<IMongoDatabase>(MongoDatabaseRegistration)
.SingleInstance(); .SingleInstance();
builder.Register(c => c.Resolve<IMongoClient>().GetDatabase(databaseNameContent)) builder.Register(c => c.ResolveNamed<IMongoClient>(MongoClientRegistration).GetDatabase(contentDatabase))
.Named<IMongoDatabase>(MongoDatabaseNameContent) .Named<IMongoDatabase>(MongoContentDatabaseRegistration)
.SingleInstance(); .SingleInstance();
builder.Register<IUserStore<IdentityUser>>(c => builder.Register<IUserStore<IdentityUser>>(c =>
{ {
var usersCollection = c.ResolveNamed<IMongoDatabase>(MongoDatabaseName).GetCollection<IdentityUser>("Identity_Users"); var usersCollection = c.ResolveNamed<IMongoDatabase>(MongoDatabaseRegistration).GetCollection<IdentityUser>("Identity_Users");
IndexChecks.EnsureUniqueIndexOnNormalizedEmail(usersCollection); IndexChecks.EnsureUniqueIndexOnNormalizedEmail(usersCollection);
IndexChecks.EnsureUniqueIndexOnNormalizedUserName(usersCollection); IndexChecks.EnsureUniqueIndexOnNormalizedUserName(usersCollection);
@ -92,7 +93,7 @@ namespace Squidex.Config.Domain
builder.Register<IRoleStore<IdentityRole>>(c => builder.Register<IRoleStore<IdentityRole>>(c =>
{ {
var rolesCollection = c.ResolveNamed<IMongoDatabase>(MongoDatabaseName).GetCollection<IdentityRole>("Identity_Roles"); var rolesCollection = c.ResolveNamed<IMongoDatabase>(MongoDatabaseRegistration).GetCollection<IdentityRole>("Identity_Roles");
IndexChecks.EnsureUniqueIndexOnNormalizedRoleName(rolesCollection); IndexChecks.EnsureUniqueIndexOnNormalizedRoleName(rolesCollection);
@ -105,25 +106,25 @@ namespace Squidex.Config.Domain
.InstancePerLifetimeScope(); .InstancePerLifetimeScope();
builder.RegisterType<MongoPersistedGrantStore>() builder.RegisterType<MongoPersistedGrantStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName)) .WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IPersistedGrantStore>() .As<IPersistedGrantStore>()
.SingleInstance(); .SingleInstance();
builder.RegisterType<MongoEventConsumerInfoRepository>() builder.RegisterType<MongoEventConsumerInfoRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName)) .WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IEventConsumerInfoRepository>() .As<IEventConsumerInfoRepository>()
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
builder.RegisterType<MongoContentRepository>() builder.RegisterType<MongoContentRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseNameContent)) .WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoContentDatabaseRegistration))
.As<IContentRepository>() .As<IContentRepository>()
.As<IEventConsumer>() .As<IEventConsumer>()
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
builder.RegisterType<MongoHistoryEventRepository>() builder.RegisterType<MongoHistoryEventRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName)) .WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IHistoryEventRepository>() .As<IHistoryEventRepository>()
.As<IEventConsumer>() .As<IEventConsumer>()
.As<IExternalSystem>() .As<IExternalSystem>()
@ -131,14 +132,14 @@ namespace Squidex.Config.Domain
.SingleInstance(); .SingleInstance();
builder.RegisterType<MongoSchemaRepository>() builder.RegisterType<MongoSchemaRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName)) .WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<ISchemaRepository>() .As<ISchemaRepository>()
.As<IExternalSystem>() .As<IExternalSystem>()
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
builder.RegisterType<MongoAppRepository>() builder.RegisterType<MongoAppRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName)) .WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IAppRepository>() .As<IAppRepository>()
.As<IEventConsumer>() .As<IEventConsumer>()
.As<IExternalSystem>() .As<IExternalSystem>()

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

@ -1,4 +1,5 @@
// ========================================================================== 
// ==========================================================================
// IdentityServices.cs // IdentityServices.cs
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
@ -30,29 +31,40 @@ namespace Squidex.Config.Identity
{ {
var dataProtection = services.AddDataProtection().SetApplicationName("Squidex"); var dataProtection = services.AddDataProtection().SetApplicationName("Squidex");
var clustererType = configuration.GetValue<string>("squidex:clusterer:type"); var keyStoreType = configuration.GetValue<string>("identity:keysStore:type");
if (string.IsNullOrWhiteSpace(keyStoreType))
{
throw new ConfigurationException("Configure KeyStore type with 'identity:keysStore:type'.");
}
if (clustererType.Equals("redis", StringComparison.OrdinalIgnoreCase)) if (string.Equals(keyStoreType, "Redis", StringComparison.OrdinalIgnoreCase))
{ {
var connectionString = configuration.GetValue<string>("squidex:clusterer:redis:connectionString"); var redisConfiguration = configuration.GetValue<string>("identity:keysStore:redis:configuration");
if (string.IsNullOrWhiteSpace(connectionString)) if (string.IsNullOrWhiteSpace(redisConfiguration))
{ {
throw new ConfigurationException("You must specify the Redis connection string in the 'squidex:clusterer:redis:connectionString' configuration section."); throw new ConfigurationException("Configure KeyStore Redis configuration with 'identity:keysStore:redis:configuration'.");
} }
var connectionMultiplexer = ConnectionMultiplexer.Connect(connectionString); var connectionMultiplexer = Singletons<ConnectionMultiplexer>.GetOrAdd(redisConfiguration, s => ConnectionMultiplexer.Connect(s));
dataProtection.PersistKeysToRedis(connectionMultiplexer); dataProtection.PersistKeysToRedis(connectionMultiplexer);
} }
else else if (string.Equals(keyStoreType, "Folder", StringComparison.OrdinalIgnoreCase))
{ {
var keysFolder = configuration.GetValue<string>("squidex:identity:keysFolder"); var folderPath = configuration.GetValue<string>("identity:keysStore:folder:path");
if (!string.IsNullOrWhiteSpace(keysFolder)) if (string.IsNullOrWhiteSpace(folderPath))
{ {
dataProtection.PersistKeysToFileSystem(new DirectoryInfo(keysFolder)); throw new ConfigurationException("Configure KeyStore Folder path with 'identity:keysStore:folder:path'.");
} }
dataProtection.PersistKeysToFileSystem(new DirectoryInfo(folderPath));
}
else if (!string.Equals(keyStoreType, "InMemory", StringComparison.OrdinalIgnoreCase))
{
throw new ConfigurationException($"Unsupported value '{keyStoreType}' for 'identity:keysStore:type', supported: Redis, Folder, InMemory.");
} }
return services; return services;

6
src/Squidex/Config/MyUrlsOptions.cs

@ -7,6 +7,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using Squidex.Infrastructure;
namespace Squidex.Config namespace Squidex.Config
{ {
@ -18,6 +19,11 @@ namespace Squidex.Config
public string BuildUrl(string path, bool trailingSlash = true) public string BuildUrl(string path, bool trailingSlash = true)
{ {
if (string.IsNullOrWhiteSpace(BaseUrl))
{
throw new ConfigurationException("Configure BaseUrl with 'urls:baseUrl'.");
}
var url = $"{BaseUrl.TrimEnd('/')}/{path.Trim('/')}"; var url = $"{BaseUrl.TrimEnd('/')}/{path.Trim('/')}";
if (trailingSlash && if (trailingSlash &&

6
src/Squidex/Config/Web/WebDependencies.cs

@ -8,6 +8,7 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Config.Domain; using Squidex.Config.Domain;
using Squidex.Pipeline;
namespace Squidex.Config.Web namespace Squidex.Config.Web
{ {
@ -15,7 +16,10 @@ namespace Squidex.Config.Web
{ {
public static void AddMyMvc(this IServiceCollection services) public static void AddMyMvc(this IServiceCollection services)
{ {
services.AddMvc().AddMySerializers(); services.AddMvc(options =>
{
options.Filters.Add(typeof(LogPerformanceAttribute));
}).AddMySerializers();
} }
} }
} }

7
src/Squidex/Config/Web/WebUsages.cs

@ -21,7 +21,12 @@ namespace Squidex.Config.Web
{ {
public static void UseMyForwardingRules(this IApplicationBuilder app) public static void UseMyForwardingRules(this IApplicationBuilder app)
{ {
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto, RequireHeaderSymmetry = false }); app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto,
ForwardLimit = null,
RequireHeaderSymmetry = false
});
app.UseMiddleware<EnforceHttpsMiddleware>(); app.UseMiddleware<EnforceHttpsMiddleware>();
} }

20
src/Squidex/Controllers/UI/Account/AccountController.cs

@ -16,12 +16,12 @@ using IdentityServer4.Services;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.MongoDB; using Microsoft.AspNetCore.Identity.MongoDB;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NSwag.Annotations; using NSwag.Annotations;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Config; using Squidex.Config;
using Squidex.Config.Identity; using Squidex.Config.Identity;
using Squidex.Core.Identity; using Squidex.Core.Identity;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf // ReSharper disable InvertIf
@ -33,12 +33,11 @@ namespace Squidex.Controllers.UI.Account
[SwaggerIgnore] [SwaggerIgnore]
public sealed class AccountController : Controller public sealed class AccountController : Controller
{ {
private static readonly EventId IdentityEventId = new EventId(8000, "IdentityEventId");
private readonly SignInManager<IdentityUser> signInManager; private readonly SignInManager<IdentityUser> signInManager;
private readonly UserManager<IdentityUser> userManager; private readonly UserManager<IdentityUser> userManager;
private readonly IOptions<MyIdentityOptions> identityOptions; private readonly IOptions<MyIdentityOptions> identityOptions;
private readonly IOptions<MyUrlsOptions> urlOptions; private readonly IOptions<MyUrlsOptions> urlOptions;
private readonly ILogger<AccountController> logger; private readonly ISemanticLog log;
private readonly IIdentityServerInteractionService interactions; private readonly IIdentityServerInteractionService interactions;
public AccountController( public AccountController(
@ -46,10 +45,10 @@ namespace Squidex.Controllers.UI.Account
UserManager<IdentityUser> userManager, UserManager<IdentityUser> userManager,
IOptions<MyIdentityOptions> identityOptions, IOptions<MyIdentityOptions> identityOptions,
IOptions<MyUrlsOptions> urlOptions, IOptions<MyUrlsOptions> urlOptions,
ILogger<AccountController> logger, ISemanticLog log,
IIdentityServerInteractionService interactions) IIdentityServerInteractionService interactions)
{ {
this.logger = logger; this.log = log;
this.urlOptions = urlOptions; this.urlOptions = urlOptions;
this.userManager = userManager; this.userManager = userManager;
this.interactions = interactions; this.interactions = interactions;
@ -264,14 +263,19 @@ namespace Squidex.Controllers.UI.Account
errorMessageBuilder.AppendLine(error.Description); errorMessageBuilder.AppendLine(error.Description);
} }
logger.LogError(IdentityEventId, "Operation '{0}' failed with errors: {1}", operationName, errorMessageBuilder.ToString()); log.LogError(w => w
.WriteProperty("action", operationName)
.WriteProperty("status", "Failed")
.WriteProperty("message", errorMessageBuilder.ToString()));
} }
return result.Succeeded; return result.Succeeded;
} }
catch (Exception e) catch (Exception ex)
{ {
logger.LogError(IdentityEventId, e, "Operation '{0}' failed with exception", operationName); log.LogError(ex, w => w
.WriteProperty("action", operationName)
.WriteProperty("status", "Failed"));
return false; return false;
} }

59
src/Squidex/Pipeline/ActionContextLogAppender.cs

@ -0,0 +1,59 @@
// ==========================================================================
// HttpLogAppender.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Squidex.Infrastructure.Log;
namespace Squidex.Pipeline
{
public class ActionContextLogAppender : ILogAppender
{
private readonly IActionContextAccessor actionContextAccessor;
public ActionContextLogAppender(IActionContextAccessor actionContextAccessor)
{
this.actionContextAccessor = actionContextAccessor;
}
public void Append(IObjectWriter writer)
{
var actionContext = actionContextAccessor.ActionContext;
if (actionContext == null)
{
return;
}
var httpContext = actionContext.HttpContext;
Guid requestId;
if (httpContext.Items.TryGetValue(nameof(requestId), out var value) && value is Guid requestIdValue)
{
requestId = requestIdValue;
}
else
{
httpContext.Items[nameof(requestId)] = requestId = Guid.NewGuid();
}
writer.WriteObject("web", w => w
.WriteProperty("requestId", requestId.ToString())
.WriteProperty("requestPath", httpContext.Request.Path)
.WriteProperty("requestMethod", httpContext.Request.Method)
.WriteObject("routeValues", r =>
{
foreach (var kvp in actionContext.ActionDescriptor.RouteValues)
{
r.WriteProperty(kvp.Key, kvp.Value);
}
}));
}
}
}

38
src/Squidex/Pipeline/LogPerformanceAttribute.cs

@ -0,0 +1,38 @@
// ==========================================================================
// LogPerformanceAttribute.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Infrastructure.Log;
namespace Squidex.Pipeline
{
public sealed class LogPerformanceAttribute : ActionFilterAttribute
{
private readonly ISemanticLog log;
public LogPerformanceAttribute(ISemanticLog log)
{
this.log = log;
}
public override void OnActionExecuting(ActionExecutingContext context)
{
context.HttpContext.Items["Watch"] = Stopwatch.StartNew();
}
public override void OnActionExecuted(ActionExecutedContext context)
{
var stopWatch = (Stopwatch)context.HttpContext.Items["Watch"];
stopWatch.Stop();
log.LogInformation(w => w.WriteProperty("elapsedRequestMs", stopWatch.ElapsedMilliseconds));
}
}
}

22
src/Squidex/Pipeline/WebpackMiddleware.cs

@ -10,7 +10,6 @@ using System;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
// ReSharper disable LoopCanBeConvertedToQuery // ReSharper disable LoopCanBeConvertedToQuery
@ -23,12 +22,9 @@ namespace Squidex.Pipeline
private static readonly string[] Scripts = { "shims.js", "app.js" }; private static readonly string[] Scripts = { "shims.js", "app.js" };
private static readonly string[] Styles = new string[0]; private static readonly string[] Styles = new string[0];
private readonly RequestDelegate next; private readonly RequestDelegate next;
private readonly ILogger logger;
public WebpackMiddleware(RequestDelegate next, ILoggerFactory loggerFactory) public WebpackMiddleware(RequestDelegate next)
{ {
logger = loggerFactory.CreateLogger<WebpackMiddleware>();
this.next = next; this.next = next;
} }
@ -76,15 +72,13 @@ namespace Squidex.Pipeline
context.Response.Body = body; context.Response.Body = body;
} }
private string InjectStyles(string response) private static string InjectStyles(string response)
{ {
if (!response.Contains("</head>")) if (!response.Contains("</head>"))
{ {
return response; return response;
} }
logger.LogInformation("A full html page is returned so the necessary styles for webpack will be injected");
var stylesTag = string.Empty; var stylesTag = string.Empty;
foreach (var file in Styles) foreach (var file in Styles)
@ -94,20 +88,16 @@ namespace Squidex.Pipeline
response = response.Replace("</head>", $"{stylesTag}</head>"); response = response.Replace("</head>", $"{stylesTag}</head>");
logger.LogInformation($"Inject style {stylesTag} as a last element in the head ");
return response; return response;
} }
private string InjectScripts(string response) private static string InjectScripts(string response)
{ {
if (!response.Contains("</body>")) if (!response.Contains("</body>"))
{ {
return response; return response;
} }
logger.LogInformation("A full html page is returned so the necessary script for webpack will be injected");
var scriptsTag = string.Empty; var scriptsTag = string.Empty;
foreach (var file in Scripts) foreach (var file in Scripts)
@ -117,8 +107,6 @@ namespace Squidex.Pipeline
response = response.Replace("</body>", $"{scriptsTag}</body>"); response = response.Replace("</body>", $"{scriptsTag}</body>");
logger.LogInformation($"Inject script {scriptsTag} as a last element in the body ");
return response; return response;
} }

13
src/Squidex/Squidex.csproj

@ -15,7 +15,7 @@
<ItemGroup> <ItemGroup>
<EmbeddedResource Include="Config\Identity\Cert\*.*;Docs\*.md" /> <EmbeddedResource Include="Config\Identity\Cert\*.*;Docs\*.md" />
<None Update="dockerfile"> <None Update="dockerfile">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</None> </None>
</ItemGroup> </ItemGroup>
@ -23,6 +23,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Squidex.Core\Squidex.Core.csproj" /> <ProjectReference Include="..\Squidex.Core\Squidex.Core.csproj" />
<ProjectReference Include="..\Squidex.Events\Squidex.Events.csproj" /> <ProjectReference Include="..\Squidex.Events\Squidex.Events.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.RabbitMq\Squidex.Infrastructure.RabbitMq.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure\Squidex.Infrastructure.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure.MongoDb\Squidex.Infrastructure.MongoDb.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.Redis\Squidex.Infrastructure.Redis.csproj" /> <ProjectReference Include="..\Squidex.Infrastructure.Redis\Squidex.Infrastructure.Redis.csproj" />
@ -34,9 +35,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Autofac" Version="4.4.0" /> <PackageReference Include="Autofac" Version="4.4.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.0.0" /> <PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.0.0" />
<PackageReference Include="IdentityServer4" Version="1.3.1" /> <PackageReference Include="IdentityServer4" Version="1.4.2" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.1.0" /> <PackageReference Include="IdentityServer4.AccessTokenValidation" Version="1.1.0" />
<PackageReference Include="IdentityServer4.AspNetIdentity" Version="1.0.0" /> <PackageReference Include="IdentityServer4.AspNetIdentity" Version="1.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="1.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="1.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="1.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="1.1.1" />
@ -56,9 +57,9 @@
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.1" />
<PackageReference Include="MongoDB.Driver" Version="2.4.3" /> <PackageReference Include="MongoDB.Driver" Version="2.4.3" />
<PackageReference Include="NJsonSchema" Version="8.10.6282.29572" /> <PackageReference Include="NJsonSchema" Version="8.27.6302.16041" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0-beta20170123" /> <PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="9.11.0" /> <PackageReference Include="NSwag.AspNetCore" Version="9.12.0" />
<PackageReference Include="OpenCover" Version="4.6.519" /> <PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="ReportGenerator" Version="2.5.6" /> <PackageReference Include="ReportGenerator" Version="2.5.6" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" /> <PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" />

20
src/Squidex/Startup.cs

@ -22,6 +22,8 @@ using Squidex.Config.Domain;
using Squidex.Config.Identity; using Squidex.Config.Identity;
using Squidex.Config.Swagger; using Squidex.Config.Swagger;
using Squidex.Config.Web; using Squidex.Config.Web;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Log.Adapter;
// ReSharper disable ConvertClosureToMethodGroup // ReSharper disable ConvertClosureToMethodGroup
// ReSharper disable AccessToModifiedClosure // ReSharper disable AccessToModifiedClosure
@ -49,7 +51,7 @@ namespace Squidex
.SetBasePath(env.ContentRootPath) .SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", true, true) .AddJsonFile("appsettings.json", true, true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true)
.AddEnvironmentVariables(); .AddEnvironmentVariables("SQUIDEX__");
Configuration = builder.Build(); Configuration = builder.Build();
} }
@ -69,15 +71,16 @@ namespace Squidex
services.AddRouting(); services.AddRouting();
services.Configure<MyUrlsOptions>( services.Configure<MyUrlsOptions>(
Configuration.GetSection("squidex:urls")); Configuration.GetSection("urls"));
services.Configure<MyIdentityOptions>( services.Configure<MyIdentityOptions>(
Configuration.GetSection("squidex:identity")); Configuration.GetSection("identity"));
var builder = new ContainerBuilder(); var builder = new ContainerBuilder();
builder.Populate(services); builder.Populate(services);
builder.RegisterModule(new ClusterModule(Configuration)); builder.RegisterModule(new EventPublishersModule(Configuration));
builder.RegisterModule(new EventStoreModule(Configuration)); builder.RegisterModule(new EventStoreModule(Configuration));
builder.RegisterModule(new InfrastructureModule(Configuration)); builder.RegisterModule(new InfrastructureModule(Configuration));
builder.RegisterModule(new PubSubModule(Configuration));
builder.RegisterModule(new ReadModule(Configuration)); builder.RegisterModule(new ReadModule(Configuration));
builder.RegisterModule(new StoreModule(Configuration)); builder.RegisterModule(new StoreModule(Configuration));
builder.RegisterModule(new WebModule(Configuration)); builder.RegisterModule(new WebModule(Configuration));
@ -95,8 +98,7 @@ namespace Squidex
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{ {
loggerFactory.AddConsole(LogLevel.Debug); loggerFactory.AddSemanticLog(app.ApplicationServices.GetRequiredService<ISemanticLog>());
loggerFactory.AddDebug();
app.TestExternalSystems(); app.TestExternalSystems();
@ -158,7 +160,8 @@ namespace Squidex
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
app.UseWebpackProxy(); app.UseWebpackProxy();
app.Use((context, next) => { app.Use((context, next) =>
{
if (!Path.HasExtension(context.Request.Path.Value)) if (!Path.HasExtension(context.Request.Path.Value))
{ {
context.Request.Path = new PathString("/index.html"); context.Request.Path = new PathString("/index.html");
@ -168,7 +171,8 @@ namespace Squidex
} }
else else
{ {
app.Use((context, next) => { app.Use((context, next) =>
{
if (!Path.HasExtension(context.Request.Path.Value)) if (!Path.HasExtension(context.Request.Path.Value))
{ {
context.Request.Path = new PathString("/build/index.html"); context.Request.Path = new PathString("/build/index.html");

10
src/Squidex/app/app.module.ts

@ -5,8 +5,9 @@
* Copyright (c) Sebastian Stehle. All rights reserved * Copyright (c) Sebastian Stehle. All rights reserved
*/ */
import { NgModule } from '@angular/core'; import { ApplicationRef, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
@ -47,6 +48,7 @@ export function configUserReport() {
@NgModule({ @NgModule({
imports: [ imports: [
BrowserModule, BrowserModule,
BrowserAnimationsModule,
SqxFrameworkModule.forRoot(), SqxFrameworkModule.forRoot(),
SqxSharedModule.forRoot(), SqxSharedModule.forRoot(),
SqxShellModule, SqxShellModule,
@ -62,14 +64,14 @@ export function configUserReport() {
{ provide: TitlesConfig, useFactory: configTitles }, { provide: TitlesConfig, useFactory: configTitles },
{ provide: UserReportConfig, useFactory: configUserReport } { provide: UserReportConfig, useFactory: configUserReport }
], ],
bootstrap: [AppComponent] entryComponents: [AppComponent]
}) })
export class AppModule { export class AppModule {
/*public ngDoBootstrap(appRef: ApplicationRef) { public ngDoBootstrap(appRef: ApplicationRef) {
try { try {
appRef.bootstrap(AppComponent); appRef.bootstrap(AppComponent);
} catch (e) { } catch (e) {
console.log('Application element not found'); console.log('Application element not found');
} }
}*/ }
} }

4
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -35,7 +35,7 @@
</thead> </thead>
<tbody> <tbody>
<template ngFor let-eventConsumer [ngForOf]="eventConsumers"> <ng-template ngFor let-eventConsumer [ngForOf]="eventConsumers">
<tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0"> <tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0">
<td> <td>
<span class="truncate"> <span class="truncate">
@ -60,7 +60,7 @@
</td> </td>
</tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</template> </ng-template>
</tbody> </tbody>
</table> </table>
</div> </div>

4
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -45,7 +45,7 @@
</thead> </thead>
<tbody> <tbody>
<template ngFor let-user [ngForOf]="usersItems"> <ng-template ngFor let-user [ngForOf]="usersItems">
<tr> <tr>
<td> <td>
<img class="user-picture" [attr.title]="user.name" [attr.src]="user.pictureUrl" /> <img class="user-picture" [attr.title]="user.name" [attr.src]="user.pictureUrl" />
@ -68,7 +68,7 @@
</td> </td>
</tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</template> </ng-template>
</tbody> </tbody>
</table> </table>

4
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -53,7 +53,7 @@
</thead> </thead>
<tbody> <tbody>
<template ngFor let-content [ngForOf]="contentItems"> <ng-template ngFor let-content [ngForOf]="contentItems">
<tr [routerLink]="[content.id]" routerLinkActive="active" class="content" <tr [routerLink]="[content.id]" routerLinkActive="active" class="content"
[sqxContent]="content" [sqxContent]="content"
[language]="languageSelected" [language]="languageSelected"
@ -63,7 +63,7 @@
(publishing)="publishContent(content)" (publishing)="publishContent(content)"
(deleting)="deleteContent(content)"></tr> (deleting)="deleteContent(content)"></tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</template> </ng-template>
</tbody> </tbody>
</table> </table>

4
src/Squidex/app/features/settings/pages/clients/clients-page.component.ts

@ -34,6 +34,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
public appClients: ImmutableArray<AppClientDto>; public appClients: ImmutableArray<AppClientDto>;
public addClientFormSubmitted = false;
public addClientForm: FormGroup = public addClientForm: FormGroup =
this.formBuilder.group({ this.formBuilder.group({
name: ['', name: ['',
@ -92,10 +93,12 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
} }
public resetClientForm() { public resetClientForm() {
this.addClientFormSubmitted = false;
this.addClientForm.reset(); this.addClientForm.reset();
} }
public attachClient() { public attachClient() {
this.addClientFormSubmitted = true;
this.addClientForm.markAsDirty(); this.addClientForm.markAsDirty();
if (this.addClientForm.valid) { if (this.addClientForm.valid) {
@ -104,6 +107,7 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
const requestDto = new CreateAppClientDto(this.addClientForm.get('name').value); const requestDto = new CreateAppClientDto(this.addClientForm.get('name').value);
const reset = () => { const reset = () => {
this.addClientFormSubmitted = false;
this.addClientForm.reset(); this.addClientForm.reset();
this.addClientForm.enable(); this.addClientForm.enable();
}; };

6
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.html

@ -40,7 +40,7 @@
</thead> </thead>
<tbody> <tbody>
<template ngFor let-contributor [ngForOf]="appContributors"> <ng-template ngFor let-contributor [ngForOf]="appContributors">
<tr> <tr>
<td> <td>
<img class="user-picture" [attr.title]="userName(contributor.contributorId) | async" [attr.src]="userPicture(contributor.contributorId) | async" /> <img class="user-picture" [attr.title]="userName(contributor.contributorId) | async" [attr.src]="userPicture(contributor.contributorId) | async" />
@ -63,14 +63,14 @@
</td> </td>
</tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</template> </ng-template>
</tbody> </tbody>
</table> </table>
<div class="table-items-footer"> <div class="table-items-footer">
<form class="form-inline" [formGroup]="addContributorForm" (ngSubmit)="assignContributor()"> <form class="form-inline" [formGroup]="addContributorForm" (ngSubmit)="assignContributor()">
<div class="form-group mr-2"> <div class="form-group mr-2">
<sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="contributor"></sqx-autocomplete> <sqx-autocomplete [source]="usersDataSource" formControlName="user" [inputName]="'contributor'"></sqx-autocomplete>
</div> </div>
<button type="submit" class="btn btn-success" [disabled]="!addContributorForm.valid">Add Contributor</button> <button type="submit" class="btn btn-success" [disabled]="!addContributorForm.valid">Add Contributor</button>

4
src/Squidex/app/features/settings/pages/languages/languages-page.component.html

@ -36,7 +36,7 @@
</thead> </thead>
<tbody> <tbody>
<template ngFor let-language [ngForOf]="appLanguages"> <ng-template ngFor let-language [ngForOf]="appLanguages">
<tr> <tr>
<td> <td>
<span class="language-code"> <span class="language-code">
@ -60,7 +60,7 @@
</td> </td>
</tr> </tr>
<tr class="spacer"></tr> <tr class="spacer"></tr>
</template> </ng-template>
</tbody> </tbody>
</table> </table>

6
src/Squidex/app/framework/angular/date-time-editor.component.html

@ -6,6 +6,12 @@
<div class="form-group time-group" *ngIf="showTime"> <div class="form-group time-group" *ngIf="showTime">
<input type="text" class="form-control" [formControl]="timeControl" (blur)="touched()" /> <input type="text" class="form-control" [formControl]="timeControl" (blur)="touched()" />
</div> </div>
<div class="form-group" *ngIf="showTime">
<button class="btn btn-secondary" (click)="writeNow()">Now</button>
</div>
<div class="form-group" *ngIf="!showTime">
<button class="btn btn-secondary" (click)="writeNow()">Today</button>
</div>
<div class="form-group" [class.hidden]="!hasValue"> <div class="form-group" [class.hidden]="!hasValue">
<button class="btn btn-link clear" [disabled]="isDisabled" (click)="reset()">Clear</button> <button class="btn btn-link clear" [disabled]="isDisabled" (click)="reset()">Clear</button>
</div> </div>

4
src/Squidex/app/framework/angular/date-time-editor.component.scss

@ -35,6 +35,10 @@ $form-color: #fff;
} }
.time-group { .time-group {
& {
padding-right: .25rem;
}
.form-control { .form-control {
width: 7.5rem; width: 7.5rem;
} }

18
src/Squidex/app/framework/angular/date-time-editor.component.ts

@ -31,10 +31,6 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, Af
private changeCallback: (value: any) => void = NOOP; private changeCallback: (value: any) => void = NOOP;
private touchedCallback: () => void = NOOP; private touchedCallback: () => void = NOOP;
public get showTime() {
return this.mode === 'DateTime';
}
public timeControl = new FormControl(); public timeControl = new FormControl();
public dateControl = new FormControl(); public dateControl = new FormControl();
@ -45,6 +41,10 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, Af
@Input() @Input()
public enforceTime: boolean; public enforceTime: boolean;
public get showTime() {
return this.mode === 'DateTime';
}
public get hasValue() { public get hasValue() {
return this.dateValue !== null; return this.dateValue !== null;
} }
@ -133,6 +133,16 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, Af
this.touchedCallback(); this.touchedCallback();
} }
public writeNow() {
this.writeValue(new Date().toUTCString());
this.updateControls();
this.updateValue();
this.touched();
return false;
}
public reset() { public reset() {
this.timeControl.setValue(null, { emitEvent: false }); this.timeControl.setValue(null, { emitEvent: false });
this.dateControl.setValue(null, { emitEvent: false }); this.dateControl.setValue(null, { emitEvent: false });

2
src/Squidex/app/shared/services/auth.service.ts

@ -205,7 +205,7 @@ export class AuthService {
} }
}) })
.catch((error: Response) => { .catch((error: Response) => {
if (error.status === 401 || error.status === 404) { if (error.status === 401) {
this.logoutRedirect(); this.logoutRedirect();
return Observable.empty<Response>(); return Observable.empty<Response>();

25
src/Squidex/app/shared/services/schemas.service.ts

@ -109,6 +109,7 @@ export class FieldDto {
export abstract class FieldPropertiesDto { export abstract class FieldPropertiesDto {
constructor( constructor(
public readonly fieldType: string,
public readonly label: string, public readonly label: string,
public readonly hints: string, public readonly hints: string,
public readonly placeholder: string, public readonly placeholder: string,
@ -132,9 +133,7 @@ export class StringFieldPropertiesDto extends FieldPropertiesDto {
public readonly maxLength?: number | null, public readonly maxLength?: number | null,
public readonly allowedValues?: string[] public readonly allowedValues?: string[]
) { ) {
super(label, hints, placeholder, isRequired, isListField, isLocalizable); super('String', label, hints, placeholder, isRequired, isListField, isLocalizable);
this['fieldType'] = 'String';
} }
} }
@ -149,9 +148,7 @@ export class NumberFieldPropertiesDto extends FieldPropertiesDto {
public readonly minValue?: number, public readonly minValue?: number,
public readonly allowedValues?: number[] public readonly allowedValues?: number[]
) { ) {
super(label, hints, placeholder, isRequired, isListField, isLocalizable); super('Number', label, hints, placeholder, isRequired, isListField, isLocalizable);
this['fieldType'] = 'Number';
} }
} }
@ -165,9 +162,7 @@ export class DateTimeFieldPropertiesDto extends FieldPropertiesDto {
public readonly maxValue?: string, public readonly maxValue?: string,
public readonly minValue?: string public readonly minValue?: string
) { ) {
super(label, hints, placeholder, isRequired, isListField, isLocalizable); super('DateTime', label, hints, placeholder, isRequired, isListField, isLocalizable);
this['fieldType'] = 'DateTime';
} }
} }
@ -179,9 +174,7 @@ export class BooleanFieldPropertiesDto extends FieldPropertiesDto {
public readonly editor: string, public readonly editor: string,
public readonly defaultValue?: boolean public readonly defaultValue?: boolean
) { ) {
super(label, hints, placeholder, isRequired, isListField, isLocalizable); super('Boolean', label, hints, placeholder, isRequired, isListField, isLocalizable);
this['fieldType'] = 'Boolean';
} }
} }
@ -192,9 +185,7 @@ export class GeolocationFieldPropertiesDto extends FieldPropertiesDto {
isLocalizable: boolean, isLocalizable: boolean,
public readonly editor: string public readonly editor: string
) { ) {
super(label, hints, placeholder, isRequired, isListField, isLocalizable); super('Geolocation', label, hints, placeholder, isRequired, isListField, isLocalizable);
this['fieldType'] = 'Geolocation';
} }
} }
@ -204,9 +195,7 @@ export class JsonFieldPropertiesDto extends FieldPropertiesDto {
isListField: boolean, isListField: boolean,
isLocalizable: boolean isLocalizable: boolean
) { ) {
super(label, hints, placeholder, isRequired, isListField, isLocalizable); super('Json', label, hints, placeholder, isRequired, isListField, isLocalizable);
this['fieldType'] = 'Json';
} }
} }

19
src/Squidex/app/theme/_panels.scss

@ -4,6 +4,7 @@
.panel-container { .panel-container {
@include fixed($size-navbar-height, 0, 0, $size-sidebar-width); @include fixed($size-navbar-height, 0, 0, $size-sidebar-width);
overflow-x: auto; overflow-x: auto;
overflow-y: hidden;
} }
@mixin panel-icon { @mixin panel-icon {
@ -56,6 +57,7 @@
@include flex-box; @include flex-box;
@include flex-flow(row); @include flex-flow(row);
@include flex-grow(1); @include flex-grow(1);
overflow: hidden;
} }
&-content { &-content {
@ -90,7 +92,6 @@
&-sidebar { &-sidebar {
& { & {
background: $panel-light-background; background: $panel-light-background;
border-top: 1px solid $color-border;
border-left: 1px solid $color-border; border-left: 1px solid $color-border;
min-width: $panel-sidebar; min-width: $panel-sidebar;
max-width: $panel-sidebar; max-width: $panel-sidebar;
@ -98,21 +99,21 @@
& .panel-link { & .panel-link {
& { & {
@include transition(background-color .3s ease);
@include panel-icon; @include panel-icon;
display: block; display: block;
padding-top: .6rem; padding-top: .6rem;
padding-bottom: .6rem; padding-bottom: .6rem;
margin-top: -1px; }
margin-right: -1px;
border-top: 1px solid transparent; &:hover {
border-bottom: 1px solid transparent; color: $color-theme-blue-dark;
border-right: 1px solid $color-border;
} }
&.active { &.active {
border-top-color: $color-border; color: $color-accent-dark;
border-bottom-color: $color-border; border: 0;
border-right-color: $panel-light-background; background: $color-theme-blue;
} }
} }
} }

75
src/Squidex/appsettings.json

@ -1,34 +1,53 @@
{ {
"squidex": { "urls": {
"urls": { "baseUrl": "http://localhost:5000"
"baseUrl": "http://localhost:5000" },
"logging": {
"human": false
},
"pubSub": {
"type": "InMemory",
"redis": {
"configuration": "localhost:6379,resolveDns=1"
}
},
"eventStore": {
"type": "MongoDb",
"mongoDb": {
"configuration": "mongodb://localhost",
"database": "Squidex"
}, },
"clusterer": { "consume": true
"type": "none", },
"eventPublishers": {
"allToRabbitMq": {
"type": "RabbitMq",
"configuration": "amqp://guest:guest@localhost/",
"exchange": "squidex",
"enabled": false,
"eventsFilter": "*"
}
},
"store": {
"type": "MongoDb",
"mongoDb": {
"configuration": "mongodb://localhost",
"contentDatabase": "SquidexContent",
"database": "Squidex"
}
},
"identity": {
"googleClient": "1006817248705-t3lb3ge808m9am4t7upqth79hulk456l.apps.googleusercontent.com",
"googleSecret": "QsEi-fHqkGw2_PjJmtNHf2wg",
"lockAutomatically": true,
"keysStore": {
"type": "InMemory",
"redis": { "redis": {
"connectionString": "localhost:6379,resolveDns=1" "configuration": "localhost:6379,resolveDns=1"
},
"folder": {
"path": "keys"
} }
}, }
"eventStore": {
"type": "mongoDb",
"mongoDb": {
"connectionString": "mongodb://localhost",
"databaseName": "Squidex"
}
},
"stores": {
"type": "mongoDb",
"mongoDb": {
"connectionString": "mongodb://localhost",
"databaseName": "Squidex",
"databaseNameContent": "SquidexContent"
}
},
"identity": {
"googleClient": "1006817248705-t3lb3ge808m9am4t7upqth79hulk456l.apps.googleusercontent.com",
"googleSecret": "QsEi-fHqkGw2_PjJmtNHf2wg",
"lockAutomatically": true
},
"handleEvents": true
} }
} }

58
src/Squidex/package.json

@ -10,33 +10,35 @@
"test:clean": "rimraf _test-output", "test:clean": "rimraf _test-output",
"dev": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/ & cpx node_modules/redoc/dist/redoc.min.js wwwroot/scripts/ && webpack-dev-server --config app-config/webpack.run.dev.js --inline --port 3000", "dev": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/ & cpx node_modules/redoc/dist/redoc.min.js wwwroot/scripts/ && webpack-dev-server --config app-config/webpack.run.dev.js --inline --port 3000",
"build": "webpack --config app-config/webpack.run.prod.js --display-error-details --bail", "build": "webpack --config app-config/webpack.run.prod.js --display-error-details --bail",
"build:nobail": "webpack --config app-config/webpack.run.prod.js --display-error-details",
"build:copy": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/ & cpx node_modules/redoc/dist/redoc.min.js wwwroot/scripts/", "build:copy": "cpx node_modules/oidc-client/dist/oidc-client.min.js wwwroot/scripts/ & cpx node_modules/redoc/dist/redoc.min.js wwwroot/scripts/",
"build:clean": "rimraf wwwroot/build" "build:clean": "rimraf wwwroot/build"
}, },
"dependencies": { "dependencies": {
"@angular/common": "2.4.9", "@angular/animations": "4.0.1",
"@angular/compiler": "2.4.9", "@angular/common": "4.0.1",
"@angular/core": "2.4.9", "@angular/compiler": "4.0.1",
"@angular/forms": "2.4.9", "@angular/core": "4.0.1",
"@angular/http": "2.4.9", "@angular/forms": "4.0.1",
"@angular/platform-browser": "2.4.9", "@angular/http": "4.0.1",
"@angular/platform-browser-dynamic": "2.4.9", "@angular/platform-browser": "4.0.1",
"@angular/router": "3.4.9", "@angular/platform-browser-dynamic": "4.0.1",
"@angular/router": "4.0.1",
"babel-polyfill": "6.23.0", "babel-polyfill": "6.23.0",
"bootstrap": "4.0.0-alpha.6", "bootstrap": "4.0.0-alpha.6",
"core-js": "2.4.1", "core-js": "2.4.1",
"moment": "2.17.1", "moment": "2.18.1",
"mousetrap": "1.6.0", "mousetrap": "1.6.1",
"oidc-client": "1.3.0-beta.3", "oidc-client": "1.3.0",
"pikaday": "1.5.1", "pikaday": "1.5.1",
"redoc": "1.10.0", "redoc": "1.12.1",
"rxjs": "5.2.0", "rxjs": "5.3.0",
"zone.js": "0.7.7" "zone.js": "0.8.5"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler-cli": "2.4.9", "@angular/compiler-cli": "4.0.1",
"@angular/tsc-wrapped": "0.5.2", "@angular/tsc-wrapped": "4.0.1",
"@ngtools/webpack": "1.2.12", "@ngtools/webpack": "1.3.0",
"@types/core-js": "0.9.35", "@types/core-js": "0.9.35",
"@types/jasmine": "2.5.43", "@types/jasmine": "2.5.43",
"@types/mousetrap": "1.5.33", "@types/mousetrap": "1.5.33",
@ -45,10 +47,10 @@
"angular2-template-loader": "0.6.2", "angular2-template-loader": "0.6.2",
"awesome-typescript-loader": "3.1.2", "awesome-typescript-loader": "3.1.2",
"cpx": "1.5.0", "cpx": "1.5.0",
"css-loader": "0.27.2", "css-loader": "0.28.0",
"exports-loader": "0.6.4", "exports-loader": "0.6.4",
"extract-text-webpack-plugin": "2.1.0", "extract-text-webpack-plugin": "2.1.0",
"file-loader": "0.10.1", "file-loader": "0.11.1",
"html-loader": "0.4.5", "html-loader": "0.4.5",
"html-webpack-plugin": "2.28.0", "html-webpack-plugin": "2.28.0",
"istanbul-instrumenter-loader": "0.2.0", "istanbul-instrumenter-loader": "0.2.0",
@ -59,25 +61,25 @@
"karma-coverage": "1.1.1", "karma-coverage": "1.1.1",
"karma-htmlfile-reporter": "0.3.5", "karma-htmlfile-reporter": "0.3.5",
"karma-jasmine": "1.1.0", "karma-jasmine": "1.1.0",
"karma-mocha-reporter": "2.2.2", "karma-mocha-reporter": "2.2.3",
"karma-phantomjs-launcher": "1.0.4", "karma-phantomjs-launcher": "1.0.4",
"karma-sourcemap-loader": "0.3.7", "karma-sourcemap-loader": "0.3.7",
"karma-webpack": "2.0.2", "karma-webpack": "2.0.3",
"node-sass": "4.5.0", "node-sass": "4.5.2",
"null-loader": "0.1.1", "null-loader": "0.1.1",
"phantomjs-prebuilt": "2.1.14", "phantomjs-prebuilt": "2.1.14",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"rimraf": "2.6.1", "rimraf": "2.6.1",
"sass-lint": "1.10.2", "sass-lint": "1.10.2",
"sass-loader": "6.0.3", "sass-loader": "6.0.3",
"style-loader": "0.13.2", "style-loader": "0.16.1",
"tslint": "4.5.1", "tslint": "4.5.1",
"tslint-loader": "3.4.3", "tslint-loader": "3.4.3",
"typemoq": "1.3.1", "typemoq": "1.4.1",
"typescript": "2.2.1", "typescript": "2.2.2",
"underscore": "1.8.3", "underscore": "1.8.3",
"webpack": "2.2.1", "webpack": "2.3.3",
"webpack-dev-server": "2.4.1", "webpack-dev-server": "2.4.2",
"webpack-merge": "4.0.0" "webpack-merge": "4.1.0"
} }
} }

2
tests/Squidex.Core.Tests/Squidex.Core.Tests.csproj

@ -12,7 +12,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="4.19.2" /> <PackageReference Include="FluentAssertions" Version="4.19.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
<PackageReference Include="Moq" Version="4.7.1" /> <PackageReference Include="Moq" Version="4.7.8" />
<PackageReference Include="xunit" Version="2.2.0" /> <PackageReference Include="xunit" Version="2.2.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
</ItemGroup> </ItemGroup>

6
tests/Squidex.Infrastructure.Tests/CQRS/Commands/DefaultDomainObjectRepositoryTests.cs

@ -74,7 +74,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
[Fact] [Fact]
public async Task Should_throw_exception_when_event_store_returns_no_events() public async Task Should_throw_exception_when_event_store_returns_no_events()
{ {
eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(Observable.Empty<StoredEvent>()); eventStore.Setup(x => x.GetEventsAsync(streamName, -1)).Returns(Observable.Empty<StoredEvent>());
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetByIdAsync<MyDomainObject>(aggregateId)); await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetByIdAsync<MyDomainObject>(aggregateId));
} }
@ -94,7 +94,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
new StoredEvent(1, 1, eventData2) new StoredEvent(1, 1, eventData2)
}; };
eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(events.ToObservable()); eventStore.Setup(x => x.GetEventsAsync(streamName, -1)).Returns(events.ToObservable());
eventDataFormatter.Setup(x => x.Parse(eventData1)).Returns(new Envelope<IEvent>(event1)); eventDataFormatter.Setup(x => x.Parse(eventData1)).Returns(new Envelope<IEvent>(event1));
eventDataFormatter.Setup(x => x.Parse(eventData2)).Returns(new Envelope<IEvent>(event2)); eventDataFormatter.Setup(x => x.Parse(eventData2)).Returns(new Envelope<IEvent>(event2));
@ -119,7 +119,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
new StoredEvent(1, 1, eventData2) new StoredEvent(1, 1, eventData2)
}; };
eventStore.Setup(x => x.GetEventsAsync(streamName)).Returns(events.ToObservable()); eventStore.Setup(x => x.GetEventsAsync(streamName, -1)).Returns(events.ToObservable());
eventDataFormatter.Setup(x => x.Parse(eventData1)).Returns(new Envelope<IEvent>(event1)); eventDataFormatter.Setup(x => x.Parse(eventData1)).Returns(new Envelope<IEvent>(event1));
eventDataFormatter.Setup(x => x.Parse(eventData2)).Returns(new Envelope<IEvent>(event2)); eventDataFormatter.Setup(x => x.Parse(eventData2)).Returns(new Envelope<IEvent>(event2));

27
tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExceptionHandlerTests.cs

@ -8,43 +8,38 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Xunit; using Xunit;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Moq; using Moq;
using Squidex.Infrastructure.Log;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
public class LogExceptionHandlerTests public class LogExceptionHandlerTests
{ {
private readonly MyLogger logger = new MyLogger(); private readonly MyLog log = new MyLog();
private readonly LogExceptionHandler sut; private readonly LogExceptionHandler sut;
private readonly ICommand command = new Mock<ICommand>().Object; private readonly ICommand command = new Mock<ICommand>().Object;
private sealed class MyLogger : ILogger<LogExceptionHandler> private sealed class MyLog : ISemanticLog
{ {
public HashSet<LogLevel> LogLevels { get; } = new HashSet<LogLevel>(); public HashSet<SemanticLogLevel> LogLevels { get; } = new HashSet<SemanticLogLevel>();
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatterr) public void Log(SemanticLogLevel logLevel, Action<IObjectWriter> action)
{ {
LogLevels.Add(logLevel); LogLevels.Add(logLevel);
} }
public bool IsEnabled(LogLevel logLevel) public ISemanticLog CreateScope(Action<IObjectWriter> objectWriter)
{ {
return false; throw new NotSupportedException();
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
} }
} }
public LogExceptionHandlerTests() public LogExceptionHandlerTests()
{ {
sut = new LogExceptionHandler(logger); sut = new LogExceptionHandler(log);
} }
[Fact] [Fact]
@ -57,7 +52,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
var isHandled = await sut.HandleAsync(context); var isHandled = await sut.HandleAsync(context);
Assert.False(isHandled); Assert.False(isHandled);
Assert.Equal(0, logger.LogLevels.Count); Assert.Equal(0, log.LogLevels.Count);
} }
[Fact] [Fact]
@ -70,7 +65,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
var isHandled = await sut.HandleAsync(context); var isHandled = await sut.HandleAsync(context);
Assert.False(isHandled); Assert.False(isHandled);
Assert.Equal(new[] { LogLevel.Error }, logger.LogLevels.ToArray()); Assert.Equal(new[] { SemanticLogLevel.Error }, log.LogLevels.ToArray());
} }
[Fact] [Fact]
@ -81,7 +76,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
var isHandled = await sut.HandleAsync(context); var isHandled = await sut.HandleAsync(context);
Assert.False(isHandled); Assert.False(isHandled);
Assert.Equal(new[] { LogLevel.Critical }, logger.LogLevels.ToArray()); Assert.Equal(new[] { SemanticLogLevel.Fatal }, log.LogLevels.ToArray());
} }
} }
} }

21
tests/Squidex.Infrastructure.Tests/CQRS/Commands/LogExecutingHandlerTests.cs

@ -8,41 +8,36 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq; using Moq;
using Squidex.Infrastructure.Log;
using Xunit; using Xunit;
namespace Squidex.Infrastructure.CQRS.Commands namespace Squidex.Infrastructure.CQRS.Commands
{ {
public class LogExecutingHandlerTests public class LogExecutingHandlerTests
{ {
private readonly MyLogger logger = new MyLogger(); private readonly MyLog log = new MyLog();
private readonly LogExecutingHandler sut; private readonly LogExecutingHandler sut;
private readonly ICommand command = new Mock<ICommand>().Object; private readonly ICommand command = new Mock<ICommand>().Object;
private sealed class MyLogger : ILogger<LogExecutingHandler> private sealed class MyLog : ISemanticLog
{ {
public int LogCount { get; private set; } public int LogCount { get; private set; }
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatterr) public void Log(SemanticLogLevel logLevel, Action<IObjectWriter> action)
{ {
LogCount++; LogCount++;
} }
public bool IsEnabled(LogLevel logLevel) public ISemanticLog CreateScope(Action<IObjectWriter> objectWriter)
{ {
return false; throw new NotSupportedException();
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
} }
} }
public LogExecutingHandlerTests() public LogExecutingHandlerTests()
{ {
sut = new LogExecutingHandler(logger); sut = new LogExecutingHandler(log);
} }
[Fact] [Fact]
@ -53,7 +48,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
var isHandled = await sut.HandleAsync(context); var isHandled = await sut.HandleAsync(context);
Assert.False(isHandled); Assert.False(isHandled);
Assert.Equal(1, logger.LogCount); Assert.Equal(1, log.LogCount);
} }
} }
} }

18
tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs

@ -33,9 +33,25 @@ namespace Squidex.Infrastructure.CQRS.Events
[Fact] [Fact]
public void Should_return_first_inner_name() public void Should_return_first_inner_name()
{ {
const string name = "my-inner-consumer";
consumer1.Setup(x => x.Name).Returns(name);
var sut = new CompoundEventConsumer(consumer1.Object, consumer2.Object);
Assert.Equal(name, sut.Name);
}
[Fact]
public void Should_return_first_inner_filter()
{
const string filter = "my-inner-filter";
consumer1.Setup(x => x.EventsFilter).Returns(filter);
var sut = new CompoundEventConsumer(consumer1.Object, consumer2.Object); var sut = new CompoundEventConsumer(consumer1.Object, consumer2.Object);
Assert.Equal(consumer1.Object.GetType().Name, sut.Name); Assert.Equal(filter, sut.EventsFilter);
} }
[Fact] [Fact]

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save