Browse Source

Merge branch 'master' into feature-asset-management

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
eb7a1494e4
  1. 2
      .dockerignore
  2. 2
      .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. 83
      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. 18
      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

2
.dockerignore

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

2
.gitignore

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

2
Dockerfile.build

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

2
README.md

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

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

@ -13,8 +13,8 @@
<ItemGroup>
<PackageReference Include="protobuf-net" Version="2.1.0" />
<PackageReference Include="System.Collections.Immutable" Version="1.3.1" />
<PackageReference Include="NodaTime" Version="2.0.0-beta20170123" />
<PackageReference Include="NJsonSchema" Version="8.10.6282.29572" />
<PackageReference Include="NodaTime" Version="2.0.0" />
<PackageReference Include="NJsonSchema" Version="8.27.6302.16041" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.6' ">
<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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NodaTime" Version="2.0.0-beta20170123" />
<PackageReference Include="NodaTime" Version="2.0.0" />
</ItemGroup>
</Project>

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

@ -10,6 +10,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
@ -17,6 +18,7 @@ using NodaTime;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Reflection;
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
// ReSharper disable ClassNeverInstantiated.Local
// ReSharper disable UnusedMember.Local
// ReSharper disable InvertIf
@ -55,61 +57,82 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
{
var indexNames =
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.EventStreamOffset).Ascending(x => x.EventStream), new CreateIndexOptions { Unique = true }));
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>(async (observer, ct) =>
return Observable.Create<StoredEvent>((observer, ct) =>
{
await Collection.Find(x => x.EventStream == streamName).ForEachAsync(commit =>
return GetEventsAsync(storedEvent =>
{
var eventNumber = commit.EventsOffset;
var eventStreamNumber = commit.EventStreamOffset;
foreach (var @event in commit.Events)
{
eventNumber++;
eventStreamNumber++;
var eventData = SimpleMapper.Map(@event, new EventData());
observer.OnNext(storedEvent);
observer.OnNext(new StoredEvent(eventNumber, eventStreamNumber, eventData));
}
}, ct);
return Tasks.TaskHelper.Done;
}, ct, streamFilter, lastReceivedEventNumber);
});
}
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);
await Collection.Find(x => x.EventsOffset >= commitOffset).SortBy(x => x.EventsOffset).ForEachAsync(commit =>
filters.Add(Filter.Gte(x => x.EventsOffset, commitOffset));
}
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;
var eventStreamNumber = commit.EventStreamOffset;
filters.Add(Filter.Eq(x => x.EventStream, streamFilter));
}
}
foreach (var @event in commit.Events)
{
eventNumber++;
eventStreamNumber++;
FilterDefinition<MongoEventCommit> filter = new BsonDocument();
if (eventNumber > lastReceivedEventNumber)
{
var eventData = SimpleMapper.Map(@event, new EventData());
if (filters.Count > 1)
{
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)
@ -130,7 +153,7 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
if (commitEvents.Any())
{
var offset = await GetEventOffset();
var offset = await GetEventOffsetAsync();
var commit = new MongoEventCommit
{
@ -157,7 +180,7 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
{
if (ex.Message.IndexOf(eventsOffsetIndex, StringComparison.OrdinalIgnoreCase) >= 0)
{
commit.EventsOffset = await GetEventOffset();
commit.EventsOffset = await GetEventOffsetAsync();
}
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 =
await Collection.Find(x => x.EventsOffset <= startEventNumber)
.Project<BsonDocument>(Projection
.Include(x => x.EventStreamOffset)
.Include(x => x.EventsCount))
.Include(x => x.EventsOffset))
.SortByDescending(x => x.EventsOffset).Limit(1)
.FirstOrDefaultAsync();
if (document != null)
{
return document["EventStreamOffset"].ToInt64();
return document["EventsOffset"].ToInt64();
}
return -1;
}
private async Task<long> GetEventOffset()
private async Task<long> GetEventOffsetAsync()
{
var document =
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.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.Log;
using StackExchange.Redis;
namespace Squidex.Infrastructure.Redis
@ -16,31 +16,30 @@ namespace Squidex.Infrastructure.Redis
public class RedisPubSub : IPubSub, IExternalSystem
{
private readonly ConcurrentDictionary<string, RedisSubscription> subscriptions = new ConcurrentDictionary<string, RedisSubscription>();
private readonly IConnectionMultiplexer redis;
private readonly ILogger<RedisPubSub> logger;
private readonly ISubscriber subscriber;
private readonly IConnectionMultiplexer redisClient;
private readonly ISemanticLog log;
private readonly ISubscriber redisSubscriber;
public RedisPubSub(IConnectionMultiplexer redis, ILogger<RedisPubSub> logger)
public RedisPubSub(IConnectionMultiplexer redis, ISemanticLog log)
{
Guard.NotNull(redis, nameof(redis));
Guard.NotNull(logger, nameof(logger));
Guard.NotNull(log, nameof(log));
this.redis = redis;
this.log = log;
this.logger = logger;
subscriber = redis.GetSubscriber();
redisClient = redis;
redisSubscriber = redis.GetSubscriber();
}
public void Connect()
{
try
{
redis.GetStatus();
redisClient.GetStatus();
}
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));
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)
{
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.Linq;
using System.Reactive.Subjects;
using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.Log;
using StackExchange.Redis;
// ReSharper disable InvertIf
@ -22,11 +22,11 @@ namespace Squidex.Infrastructure.Redis
private readonly Subject<string> subject = new Subject<string>();
private readonly ISubscriber subscriber;
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.Subscribe(channelName, (channel, value) => HandleInvalidation(value));
@ -38,13 +38,16 @@ namespace Squidex.Infrastructure.Redis
{
try
{
var message = string.Join("#", (notifySelf ? Guid.Empty : InstanceId).ToString());
var message = string.Join("#", (notifySelf ? Guid.Empty : InstanceId).ToString(), token);
subscriber.Publish(channelName, message);
}
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)
{
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" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="StackExchange.Redis.StrongName" Version="1.2.1" />
</ItemGroup>
</Project>

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

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

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

@ -7,7 +7,7 @@
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf
@ -16,11 +16,13 @@ namespace Squidex.Infrastructure.CQRS.Commands
{
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)
@ -29,12 +31,20 @@ namespace Squidex.Infrastructure.CQRS.Commands
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)
{
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;

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

@ -7,23 +7,29 @@
// ==========================================================================
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Commands
{
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)
{
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;
}

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

@ -17,6 +17,11 @@ namespace Squidex.Infrastructure.CQRS.Events
public string Name { get; }
public string EventsFilter
{
get { return inners.FirstOrDefault()?.EventsFilter; }
}
public CompoundEventConsumer(IEventConsumer first, params IEventConsumer[] inners)
{
Guard.NotNull(first, nameof(first));
@ -24,7 +29,7 @@ namespace Squidex.Infrastructure.CQRS.Events
this.inners = new[] { first }.Union(inners).ToArray();
Name = first.GetType().Name;
Name = first.Name;
}
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
// ==========================================================================
// Copyright (c) Squidex Group
@ -10,13 +10,13 @@ using System;
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;
public DefaultMemoryEventNotifier(IPubSub invalidator)
public DefaultEventNotifier(IPubSub invalidator)
{
Guard.NotNull(invalidator, nameof(invalidator));

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

@ -6,9 +6,6 @@
// All rights reserved.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Infrastructure.CQRS.Events
{
public class Envelope<TPayload> where TPayload : class
@ -45,18 +42,6 @@ namespace Squidex.Infrastructure.CQRS.Events
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
{
return new Envelope<TOther>(payload as TOther, headers.Clone());

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

@ -7,9 +7,8 @@
// ==========================================================================
using System;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Timers;
// ReSharper disable MethodSupportsCancellation
@ -24,7 +23,7 @@ namespace Squidex.Infrastructure.CQRS.Events
private readonly IEventStore eventStore;
private readonly IEventNotifier eventNotifier;
private readonly IEventConsumerInfoRepository eventConsumerInfoRepository;
private readonly ILogger<EventReceiver> logger;
private readonly ISemanticLog log;
private CompletionTimer timer;
public EventReceiver(
@ -32,15 +31,15 @@ namespace Squidex.Infrastructure.CQRS.Events
IEventStore eventStore,
IEventNotifier eventNotifier,
IEventConsumerInfoRepository eventConsumerInfoRepository,
ILogger<EventReceiver> logger)
ISemanticLog log)
{
Guard.NotNull(logger, nameof(logger));
Guard.NotNull(log, nameof(log));
Guard.NotNull(formatter, nameof(formatter));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventNotifier, nameof(eventNotifier));
Guard.NotNull(eventConsumerInfoRepository, nameof(eventConsumerInfoRepository));
this.logger = logger;
this.log = log;
this.formatter = formatter;
this.eventStore = eventStore;
this.eventNotifier = eventNotifier;
@ -57,13 +56,17 @@ namespace Squidex.Infrastructure.CQRS.Events
}
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();
}
@ -71,6 +74,8 @@ namespace Squidex.Infrastructure.CQRS.Events
{
Guard.NotNull(eventConsumer, nameof(eventConsumer));
ThrowIfDisposed();
if (timer != null)
{
return;
@ -105,17 +110,12 @@ namespace Squidex.Infrastructure.CQRS.Events
return;
}
await eventStore.GetEventsAsync(lastHandledEventNumber)
.Select(storedEvent =>
{
HandleEventAsync(eventConsumer, storedEvent, consumerName).Wait();
return storedEvent;
}).DefaultIfEmpty();
await eventStore.GetEventsAsync(se => HandleEventAsync(eventConsumer, se, consumerName), ct,
eventConsumer.EventsFilter, lastHandledEventNumber);
}
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());
}
@ -134,18 +134,31 @@ namespace Squidex.Infrastructure.CQRS.Events
private async Task ResetAsync(IEventConsumer eventConsumer, string consumerName)
{
var actionId = Guid.NewGuid().ToString();
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 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)
{
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;
}
@ -153,17 +166,37 @@ namespace Squidex.Infrastructure.CQRS.Events
private async Task DispatchConsumer(Envelope<IEvent> @event, IEventConsumer eventConsumer)
{
var eventId = @event.Headers.EventId().ToString();
var eventType = @event.Payload.GetType().Name;
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);
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)
{
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;
}
@ -182,7 +215,11 @@ namespace Squidex.Infrastructure.CQRS.Events
}
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;
}

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

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

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

@ -8,15 +8,16 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.CQRS.Events
{
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);
}

13
src/Squidex.Infrastructure/DisposableObjectBase.cs

@ -36,20 +36,13 @@ namespace Squidex.Infrastructure
return;
}
if (disposing)
lock (disposeLock)
{
lock (disposeLock)
if (!isDisposed)
{
if (!isDisposed)
{
DisposeObject(true);
}
DisposeObject(disposing);
}
}
else
{
DisposeObject(false);
}
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">
<PropertyGroup>
<TargetFramework>netstandard1.6</TargetFramework>
<NoWarn>$(NoWarn);IDE0017</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebugType>full</DebugType>
@ -10,8 +11,8 @@
<PackageReference Include="ImageSharp" Version="1.0.0-alpha4-00031" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="1.1.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="1.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="9.0.2-beta2" />
<PackageReference Include="NodaTime" Version="2.0.0-beta20170123" />
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
<PackageReference Include="NodaTime" Version="2.0.0" />
<PackageReference Include="System.Linq" Version="4.3.0" />
<PackageReference Include="System.Reactive" Version="3.1.1" />
<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; }
}
public string EventsFilter
{
get { return "^app-"; }
}
public Task On(Envelope<IEvent> @event)
{
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; }
}
public string EventsFilter
{
get { return "^content-"; }
}
public async Task ClearAsync()
{
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();
if (query.StartsWith("?"))
{
query = query.Substring(1);
}
var parser = new ODataUriParser(model, new Uri($"{path}?{query}", UriKind.Relative));
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);
}
else
{
cursor = cursor.Skip(null);
}
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 int sessionEventCount;
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "*"; }
}
public MongoHistoryEventRepository(IMongoDatabase database, IEnumerable<IHistoryEventsCreator> creators)
: base(database)
{
@ -67,11 +77,6 @@ namespace Squidex.Read.MongoDb.History
return entities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList();
}
public string Name
{
get { return GetType().Name; }
}
public async Task On(Envelope<IEvent> @event)
{
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; }
}
public string EventsFilter
{
get { return "^schema-"; }
}
public Task On(Envelope<IEvent> @event)
{
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" />
</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.MongoDB" Version="1.0.2" />
<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; }
}
public string EventsFilter
{
get { return "*"; }
}
public CachingAppProvider(IMemoryCache cache, IAppRepository repository)
: 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; }
}
public string EventsFilter
{
get { return "*"; }
}
public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository)
: base(cache)
{
@ -108,6 +113,10 @@ namespace Squidex.Read.Schemas.Services.Implementations
{
Remove(fieldEvent.AppId, fieldEvent.SchemaId);
}
else if (@event.Payload is SchemaCreated schemaCreatedEvent)
{
Remove(schemaCreatedEvent.AppId, schemaCreatedEvent.SchemaId);
}
else if (@event.Payload is SchemaDeleted schemaDeletedEvent)
{
Remove(schemaDeletedEvent.AppId, schemaDeletedEvent.SchemaId);

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

@ -14,7 +14,7 @@
</ItemGroup>
<ItemGroup>
<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>
<Reference Include="IdentityServer4">

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

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

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

@ -14,6 +14,6 @@
<ProjectReference Include="..\Squidex.Read\Squidex.Read.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NodaTime" Version="2.0.0-beta20170123" />
<PackageReference Include="NodaTime" Version="2.0.0" />
</ItemGroup>
</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 Autofac;
using Autofac.Core;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
using NodaTime;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb.EventStore;
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; }
public EventStoreModule(IConfiguration configuration)
@ -28,45 +31,55 @@ namespace Squidex.Config.Domain
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 =>
{
var mongoDbClient = new MongoClient(connectionString);
var mongoDatabase = mongoDbClient.GetDatabase(databaseName);
builder.Register(c => Singletons<IMongoClient>.GetOrAdd(configuration, s => new MongoClient(s)))
.Named<IMongoClient>(MongoClientRegistration)
.SingleInstance();
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<IEventStore>()
.SingleInstance();
}
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.
// ==========================================================================
using System;
using Autofac;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using NodaTime;
using Squidex.Core.Schemas;
using Squidex.Core.Schemas.Json;
@ -19,12 +21,14 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Log;
using Squidex.Pipeline;
// ReSharper disable UnusedAutoPropertyAccessor.Local
namespace Squidex.Config.Domain
{
public class InfrastructureModule : Module
public sealed class InfrastructureModule : Module
{
private IConfiguration Configuration { get; }
@ -35,6 +39,53 @@ namespace Squidex.Config.Domain
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)
.As<IClock>()
.SingleInstance();
@ -63,7 +114,7 @@ namespace Squidex.Config.Domain
.As<ICommandBus>()
.SingleInstance();
builder.RegisterType<DefaultMemoryEventNotifier>()
builder.RegisterType<DefaultEventNotifier>()
.As<IEventNotifier>()
.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)
{
var storeType = Configuration.GetValue<string>("squidex:stores:type");
var storeType = Configuration.GetValue<string>("store:type");
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))
@ -37,7 +37,7 @@ namespace Squidex.Config.Domain
}
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
{
private const string MongoDatabaseName = "MongoDatabaseName";
private const string MongoDatabaseNameContent = "MongoDatabaseNameContent";
private const string MongoClientRegistration = "StoreMongoClient";
private const string MongoDatabaseRegistration = "StoreMongoDatabaseName";
private const string MongoContentDatabaseRegistration = "StoreMongoDatabaseNameContent";
private IConfiguration Configuration { get; }
@ -46,42 +47,42 @@ namespace Squidex.Config.Domain
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))
.As<IMongoClient>()
builder.Register(c => Singletons<IMongoClient>.GetOrAdd(configuration, s => new MongoClient(s)))
.Named<IMongoClient>(MongoClientRegistration)
.SingleInstance();
builder.Register(c => c.Resolve<IMongoClient>().GetDatabase(databaseName))
.Named<IMongoDatabase>(MongoDatabaseName)
builder.Register(c => c.ResolveNamed<IMongoClient>(MongoClientRegistration).GetDatabase(database))
.Named<IMongoDatabase>(MongoDatabaseRegistration)
.SingleInstance();
builder.Register(c => c.Resolve<IMongoClient>().GetDatabase(databaseNameContent))
.Named<IMongoDatabase>(MongoDatabaseNameContent)
builder.Register(c => c.ResolveNamed<IMongoClient>(MongoClientRegistration).GetDatabase(contentDatabase))
.Named<IMongoDatabase>(MongoContentDatabaseRegistration)
.SingleInstance();
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.EnsureUniqueIndexOnNormalizedUserName(usersCollection);
@ -92,7 +93,7 @@ namespace Squidex.Config.Domain
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);
@ -105,25 +106,25 @@ namespace Squidex.Config.Domain
.InstancePerLifetimeScope();
builder.RegisterType<MongoPersistedGrantStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IPersistedGrantStore>()
.SingleInstance();
builder.RegisterType<MongoEventConsumerInfoRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IEventConsumerInfoRepository>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoContentRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseNameContent))
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoContentDatabaseRegistration))
.As<IContentRepository>()
.As<IEventConsumer>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoHistoryEventRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IHistoryEventRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()
@ -131,14 +132,14 @@ namespace Squidex.Config.Domain
.SingleInstance();
builder.RegisterType<MongoSchemaRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<ISchemaRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoAppRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IAppRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()

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

@ -1,4 +1,5 @@
// ==========================================================================

// ==========================================================================
// IdentityServices.cs
// Squidex Headless CMS
// ==========================================================================
@ -30,29 +31,40 @@ namespace Squidex.Config.Identity
{
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);
}
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;

6
src/Squidex/Config/MyUrlsOptions.cs

@ -7,6 +7,7 @@
// ==========================================================================
using System;
using Squidex.Infrastructure;
namespace Squidex.Config
{
@ -18,6 +19,11 @@ namespace Squidex.Config
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('/')}";
if (trailingSlash &&

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

@ -8,6 +8,7 @@
using Microsoft.Extensions.DependencyInjection;
using Squidex.Config.Domain;
using Squidex.Pipeline;
namespace Squidex.Config.Web
{
@ -15,7 +16,10 @@ namespace Squidex.Config.Web
{
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)
{
app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedProto, RequireHeaderSymmetry = false });
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedProto,
ForwardLimit = null,
RequireHeaderSymmetry = false
});
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.MongoDB;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NSwag.Annotations;
using Microsoft.Extensions.Options;
using Squidex.Config;
using Squidex.Config.Identity;
using Squidex.Core.Identity;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
// ReSharper disable InvertIf
@ -33,12 +33,11 @@ namespace Squidex.Controllers.UI.Account
[SwaggerIgnore]
public sealed class AccountController : Controller
{
private static readonly EventId IdentityEventId = new EventId(8000, "IdentityEventId");
private readonly SignInManager<IdentityUser> signInManager;
private readonly UserManager<IdentityUser> userManager;
private readonly IOptions<MyIdentityOptions> identityOptions;
private readonly IOptions<MyUrlsOptions> urlOptions;
private readonly ILogger<AccountController> logger;
private readonly ISemanticLog log;
private readonly IIdentityServerInteractionService interactions;
public AccountController(
@ -46,10 +45,10 @@ namespace Squidex.Controllers.UI.Account
UserManager<IdentityUser> userManager,
IOptions<MyIdentityOptions> identityOptions,
IOptions<MyUrlsOptions> urlOptions,
ILogger<AccountController> logger,
ISemanticLog log,
IIdentityServerInteractionService interactions)
{
this.logger = logger;
this.log = log;
this.urlOptions = urlOptions;
this.userManager = userManager;
this.interactions = interactions;
@ -264,14 +263,19 @@ namespace Squidex.Controllers.UI.Account
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;
}
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;
}

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

18
src/Squidex/Pipeline/WebpackMiddleware.cs

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

13
src/Squidex/Squidex.csproj

@ -15,7 +15,7 @@
<ItemGroup>
<EmbeddedResource Include="Config\Identity\Cert\*.*;Docs\*.md" />
<None Update="dockerfile">
<None Update="dockerfile">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</None>
</ItemGroup>
@ -23,6 +23,7 @@
<ItemGroup>
<ProjectReference Include="..\Squidex.Core\Squidex.Core.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.MongoDb\Squidex.Infrastructure.MongoDb.csproj" />
<ProjectReference Include="..\Squidex.Infrastructure.Redis\Squidex.Infrastructure.Redis.csproj" />
@ -34,9 +35,9 @@
<ItemGroup>
<PackageReference Include="Autofac" Version="4.4.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.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.Google" 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.Options.ConfigurationExtensions" Version="1.1.1" />
<PackageReference Include="MongoDB.Driver" Version="2.4.3" />
<PackageReference Include="NJsonSchema" Version="8.10.6282.29572" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0-beta20170123" />
<PackageReference Include="NSwag.AspNetCore" Version="9.11.0" />
<PackageReference Include="NJsonSchema" Version="8.27.6302.16041" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.0.0" />
<PackageReference Include="NSwag.AspNetCore" Version="9.12.0" />
<PackageReference Include="OpenCover" Version="4.6.519" />
<PackageReference Include="ReportGenerator" Version="2.5.6" />
<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.Swagger;
using Squidex.Config.Web;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Log.Adapter;
// ReSharper disable ConvertClosureToMethodGroup
// ReSharper disable AccessToModifiedClosure
@ -49,7 +51,7 @@ namespace Squidex
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", true, true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true)
.AddEnvironmentVariables();
.AddEnvironmentVariables("SQUIDEX__");
Configuration = builder.Build();
}
@ -69,15 +71,16 @@ namespace Squidex
services.AddRouting();
services.Configure<MyUrlsOptions>(
Configuration.GetSection("squidex:urls"));
Configuration.GetSection("urls"));
services.Configure<MyIdentityOptions>(
Configuration.GetSection("squidex:identity"));
Configuration.GetSection("identity"));
var builder = new ContainerBuilder();
builder.Populate(services);
builder.RegisterModule(new ClusterModule(Configuration));
builder.RegisterModule(new EventPublishersModule(Configuration));
builder.RegisterModule(new EventStoreModule(Configuration));
builder.RegisterModule(new InfrastructureModule(Configuration));
builder.RegisterModule(new PubSubModule(Configuration));
builder.RegisterModule(new ReadModule(Configuration));
builder.RegisterModule(new StoreModule(Configuration));
builder.RegisterModule(new WebModule(Configuration));
@ -95,8 +98,7 @@ namespace Squidex
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(LogLevel.Debug);
loggerFactory.AddDebug();
loggerFactory.AddSemanticLog(app.ApplicationServices.GetRequiredService<ISemanticLog>());
app.TestExternalSystems();
@ -158,7 +160,8 @@ namespace Squidex
app.UseDeveloperExceptionPage();
app.UseWebpackProxy();
app.Use((context, next) => {
app.Use((context, next) =>
{
if (!Path.HasExtension(context.Request.Path.Value))
{
context.Request.Path = new PathString("/index.html");
@ -168,7 +171,8 @@ namespace Squidex
}
else
{
app.Use((context, next) => {
app.Use((context, next) =>
{
if (!Path.HasExtension(context.Request.Path.Value))
{
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
*/
import { NgModule } from '@angular/core';
import { ApplicationRef, NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
@ -47,6 +48,7 @@ export function configUserReport() {
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
SqxFrameworkModule.forRoot(),
SqxSharedModule.forRoot(),
SqxShellModule,
@ -62,14 +64,14 @@ export function configUserReport() {
{ provide: TitlesConfig, useFactory: configTitles },
{ provide: UserReportConfig, useFactory: configUserReport }
],
bootstrap: [AppComponent]
entryComponents: [AppComponent]
})
export class AppModule {
/*public ngDoBootstrap(appRef: ApplicationRef) {
public ngDoBootstrap(appRef: ApplicationRef) {
try {
appRef.bootstrap(AppComponent);
} catch (e) {
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>
<tbody>
<template ngFor let-eventConsumer [ngForOf]="eventConsumers">
<ng-template ngFor let-eventConsumer [ngForOf]="eventConsumers">
<tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0">
<td>
<span class="truncate">
@ -60,7 +60,7 @@
</td>
</tr>
<tr class="spacer"></tr>
</template>
</ng-template>
</tbody>
</table>
</div>

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

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

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

@ -53,7 +53,7 @@
</thead>
<tbody>
<template ngFor let-content [ngForOf]="contentItems">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [routerLink]="[content.id]" routerLinkActive="active" class="content"
[sqxContent]="content"
[language]="languageSelected"
@ -63,7 +63,7 @@
(publishing)="publishContent(content)"
(deleting)="deleteContent(content)"></tr>
<tr class="spacer"></tr>
</template>
</ng-template>
</tbody>
</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 addClientFormSubmitted = false;
public addClientForm: FormGroup =
this.formBuilder.group({
name: ['',
@ -92,10 +93,12 @@ export class ClientsPageComponent extends AppComponentBase implements OnInit {
}
public resetClientForm() {
this.addClientFormSubmitted = false;
this.addClientForm.reset();
}
public attachClient() {
this.addClientFormSubmitted = true;
this.addClientForm.markAsDirty();
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 reset = () => {
this.addClientFormSubmitted = false;
this.addClientForm.reset();
this.addClientForm.enable();
};

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

@ -40,7 +40,7 @@
</thead>
<tbody>
<template ngFor let-contributor [ngForOf]="appContributors">
<ng-template ngFor let-contributor [ngForOf]="appContributors">
<tr>
<td>
<img class="user-picture" [attr.title]="userName(contributor.contributorId) | async" [attr.src]="userPicture(contributor.contributorId) | async" />
@ -63,14 +63,14 @@
</td>
</tr>
<tr class="spacer"></tr>
</template>
</ng-template>
</tbody>
</table>
<div class="table-items-footer">
<form class="form-inline" [formGroup]="addContributorForm" (ngSubmit)="assignContributor()">
<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>
<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>
<tbody>
<template ngFor let-language [ngForOf]="appLanguages">
<ng-template ngFor let-language [ngForOf]="appLanguages">
<tr>
<td>
<span class="language-code">
@ -60,7 +60,7 @@
</td>
</tr>
<tr class="spacer"></tr>
</template>
</ng-template>
</tbody>
</table>

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

@ -6,6 +6,12 @@
<div class="form-group time-group" *ngIf="showTime">
<input type="text" class="form-control" [formControl]="timeControl" (blur)="touched()" />
</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">
<button class="btn btn-link clear" [disabled]="isDisabled" (click)="reset()">Clear</button>
</div>

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

@ -35,6 +35,10 @@ $form-color: #fff;
}
.time-group {
& {
padding-right: .25rem;
}
.form-control {
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 touchedCallback: () => void = NOOP;
public get showTime() {
return this.mode === 'DateTime';
}
public timeControl = new FormControl();
public dateControl = new FormControl();
@ -45,6 +41,10 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, Af
@Input()
public enforceTime: boolean;
public get showTime() {
return this.mode === 'DateTime';
}
public get hasValue() {
return this.dateValue !== null;
}
@ -133,6 +133,16 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, Af
this.touchedCallback();
}
public writeNow() {
this.writeValue(new Date().toUTCString());
this.updateControls();
this.updateValue();
this.touched();
return false;
}
public reset() {
this.timeControl.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) => {
if (error.status === 401 || error.status === 404) {
if (error.status === 401) {
this.logoutRedirect();
return Observable.empty<Response>();

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

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

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

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

75
src/Squidex/appsettings.json

@ -1,34 +1,53 @@
{
"squidex": {
"urls": {
"baseUrl": "http://localhost:5000"
"urls": {
"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": {
"type": "none",
"consume": true
},
"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": {
"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",
"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: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:clean": "rimraf wwwroot/build"
},
"dependencies": {
"@angular/common": "2.4.9",
"@angular/compiler": "2.4.9",
"@angular/core": "2.4.9",
"@angular/forms": "2.4.9",
"@angular/http": "2.4.9",
"@angular/platform-browser": "2.4.9",
"@angular/platform-browser-dynamic": "2.4.9",
"@angular/router": "3.4.9",
"@angular/animations": "4.0.1",
"@angular/common": "4.0.1",
"@angular/compiler": "4.0.1",
"@angular/core": "4.0.1",
"@angular/forms": "4.0.1",
"@angular/http": "4.0.1",
"@angular/platform-browser": "4.0.1",
"@angular/platform-browser-dynamic": "4.0.1",
"@angular/router": "4.0.1",
"babel-polyfill": "6.23.0",
"bootstrap": "4.0.0-alpha.6",
"core-js": "2.4.1",
"moment": "2.17.1",
"mousetrap": "1.6.0",
"oidc-client": "1.3.0-beta.3",
"moment": "2.18.1",
"mousetrap": "1.6.1",
"oidc-client": "1.3.0",
"pikaday": "1.5.1",
"redoc": "1.10.0",
"rxjs": "5.2.0",
"zone.js": "0.7.7"
"redoc": "1.12.1",
"rxjs": "5.3.0",
"zone.js": "0.8.5"
},
"devDependencies": {
"@angular/compiler-cli": "2.4.9",
"@angular/tsc-wrapped": "0.5.2",
"@ngtools/webpack": "1.2.12",
"@angular/compiler-cli": "4.0.1",
"@angular/tsc-wrapped": "4.0.1",
"@ngtools/webpack": "1.3.0",
"@types/core-js": "0.9.35",
"@types/jasmine": "2.5.43",
"@types/mousetrap": "1.5.33",
@ -45,10 +47,10 @@
"angular2-template-loader": "0.6.2",
"awesome-typescript-loader": "3.1.2",
"cpx": "1.5.0",
"css-loader": "0.27.2",
"css-loader": "0.28.0",
"exports-loader": "0.6.4",
"extract-text-webpack-plugin": "2.1.0",
"file-loader": "0.10.1",
"file-loader": "0.11.1",
"html-loader": "0.4.5",
"html-webpack-plugin": "2.28.0",
"istanbul-instrumenter-loader": "0.2.0",
@ -59,25 +61,25 @@
"karma-coverage": "1.1.1",
"karma-htmlfile-reporter": "0.3.5",
"karma-jasmine": "1.1.0",
"karma-mocha-reporter": "2.2.2",
"karma-mocha-reporter": "2.2.3",
"karma-phantomjs-launcher": "1.0.4",
"karma-sourcemap-loader": "0.3.7",
"karma-webpack": "2.0.2",
"node-sass": "4.5.0",
"karma-webpack": "2.0.3",
"node-sass": "4.5.2",
"null-loader": "0.1.1",
"phantomjs-prebuilt": "2.1.14",
"raw-loader": "0.5.1",
"rimraf": "2.6.1",
"sass-lint": "1.10.2",
"sass-loader": "6.0.3",
"style-loader": "0.13.2",
"style-loader": "0.16.1",
"tslint": "4.5.1",
"tslint-loader": "3.4.3",
"typemoq": "1.3.1",
"typescript": "2.2.1",
"typemoq": "1.4.1",
"typescript": "2.2.2",
"underscore": "1.8.3",
"webpack": "2.2.1",
"webpack-dev-server": "2.4.1",
"webpack-merge": "4.0.0"
"webpack": "2.3.3",
"webpack-dev-server": "2.4.2",
"webpack-merge": "4.1.0"
}
}

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

@ -12,7 +12,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="4.19.2" />
<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.runner.visualstudio" Version="2.2.0" />
</ItemGroup>

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

@ -74,7 +74,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
[Fact]
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));
}
@ -94,7 +94,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
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(eventData2)).Returns(new Envelope<IEvent>(event2));
@ -119,7 +119,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
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(eventData2)).Returns(new Envelope<IEvent>(event2));

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

@ -8,43 +8,38 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Xunit;
using System.Collections.Generic;
using System.Linq;
using Moq;
using Squidex.Infrastructure.Log;
namespace Squidex.Infrastructure.CQRS.Commands
{
public class LogExceptionHandlerTests
{
private readonly MyLogger logger = new MyLogger();
private readonly MyLog log = new MyLog();
private readonly LogExceptionHandler sut;
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);
}
public bool IsEnabled(LogLevel logLevel)
public ISemanticLog CreateScope(Action<IObjectWriter> objectWriter)
{
return false;
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
throw new NotSupportedException();
}
}
public LogExceptionHandlerTests()
{
sut = new LogExceptionHandler(logger);
sut = new LogExceptionHandler(log);
}
[Fact]
@ -57,7 +52,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
var isHandled = await sut.HandleAsync(context);
Assert.False(isHandled);
Assert.Equal(0, logger.LogLevels.Count);
Assert.Equal(0, log.LogLevels.Count);
}
[Fact]
@ -70,7 +65,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
var isHandled = await sut.HandleAsync(context);
Assert.False(isHandled);
Assert.Equal(new[] { LogLevel.Error }, logger.LogLevels.ToArray());
Assert.Equal(new[] { SemanticLogLevel.Error }, log.LogLevels.ToArray());
}
[Fact]
@ -81,7 +76,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
var isHandled = await sut.HandleAsync(context);
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.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Moq;
using Squidex.Infrastructure.Log;
using Xunit;
namespace Squidex.Infrastructure.CQRS.Commands
{
public class LogExecutingHandlerTests
{
private readonly MyLogger logger = new MyLogger();
private readonly MyLog log = new MyLog();
private readonly LogExecutingHandler sut;
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 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++;
}
public bool IsEnabled(LogLevel logLevel)
public ISemanticLog CreateScope(Action<IObjectWriter> objectWriter)
{
return false;
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
throw new NotSupportedException();
}
}
public LogExecutingHandlerTests()
{
sut = new LogExecutingHandler(logger);
sut = new LogExecutingHandler(log);
}
[Fact]
@ -53,7 +48,7 @@ namespace Squidex.Infrastructure.CQRS.Commands
var isHandled = await sut.HandleAsync(context);
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]
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);
Assert.Equal(consumer1.Object.GetType().Name, sut.Name);
Assert.Equal(filter, sut.EventsFilter);
}
[Fact]

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

Loading…
Cancel
Save