Browse Source

Merge branch 'master' into orleans

# Conflicts:
#	src/Squidex/Config/Domain/ReadModule.cs
pull/169/head
Sebastian Stehle 9 years ago
parent
commit
481c6d0c15
  1. 19
      Squidex.sln
  2. 3
      src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs
  3. 3
      src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs
  4. 3
      src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs
  5. 3
      src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs
  6. 11
      src/Squidex.Domain.Apps.Read/Apps/IAppEventConsumer.cs
  7. 11
      src/Squidex.Domain.Apps.Read/Assets/IAssetEventConsumer.cs
  8. 9
      src/Squidex.Domain.Apps.Read/Schemas/ISchemaEventConsumer.cs
  9. 14
      src/Squidex.Domain.Users/DataProtection/Orleans/Grains/IXmlRepositoryGrain.cs
  10. 33
      src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs
  11. 41
      src/Squidex.Domain.Users/DataProtection/Orleans/OrleansXmlRepository.cs
  12. 2
      src/Squidex.Domain.Users/Squidex.Domain.Users.csproj
  13. 33
      src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfo.cs
  14. 74
      src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfoRepository.cs
  15. 2
      src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs
  16. 88
      src/Squidex.Infrastructure/Actors/DefaultRemoteActorChannel.cs
  17. 60
      src/Squidex.Infrastructure/Actors/RemoteActors.cs
  18. 323
      src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs
  19. 15
      src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StartConsumerMessage.cs
  20. 16
      src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs
  21. 36
      src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs
  22. 36
      src/Squidex.Infrastructure/CQRS/Events/EventConsumerCleaner.cs
  23. 2
      src/Squidex.Infrastructure/CQRS/Events/EventConsumerInfo.cs
  24. 38
      src/Squidex.Infrastructure/CQRS/Events/EventStoreSubscription.cs
  25. 22
      src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerRegistryGrainState.cs
  26. 21
      src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfo.cs
  27. 24
      src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfoRepository.cs
  28. 2
      src/Squidex.Infrastructure/CQRS/Events/IEventNotifier.cs
  29. 2
      src/Squidex.Infrastructure/CQRS/Events/IEventSubscriber.cs
  30. 31
      src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/EventConsumerBootstrap.cs
  31. 4
      src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/IEventConsumerGrain.cs
  32. 4
      src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/IEventConsumerRegistryGrain.cs
  33. 74
      src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs
  34. 52
      src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrainState.cs
  35. 44
      src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs
  36. 30
      src/Squidex.Infrastructure/CQRS/Events/Orleans/OrleansClientEventNotifier.cs
  37. 30
      src/Squidex.Infrastructure/CQRS/Events/Orleans/OrleansSiloEventNotifier.cs
  38. 20
      src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs
  39. 10
      src/Squidex.Infrastructure/CollectionExtensions.cs
  40. 12
      src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs
  41. 2
      src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs
  42. 3
      src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs
  43. 27
      src/Squidex/AppConfiguration.cs
  44. 54
      src/Squidex/AppServices.cs
  45. 51
      src/Squidex/Config/Domain/AssetServices.cs
  46. 91
      src/Squidex/Config/Domain/AssetStoreModule.cs
  47. 28
      src/Squidex/Config/Domain/EventPublishersServices.cs
  48. 112
      src/Squidex/Config/Domain/EventStoreModule.cs
  49. 61
      src/Squidex/Config/Domain/EventStoreServices.cs
  50. 155
      src/Squidex/Config/Domain/InfrastructureModule.cs
  51. 110
      src/Squidex/Config/Domain/InfrastructureServices.cs
  52. 36
      src/Squidex/Config/Domain/LoggingExtensions.cs
  53. 69
      src/Squidex/Config/Domain/PubSubModule.cs
  54. 41
      src/Squidex/Config/Domain/PubSubServices.cs
  55. 148
      src/Squidex/Config/Domain/ReadModule.cs
  56. 119
      src/Squidex/Config/Domain/ReadServices.cs
  57. 8
      src/Squidex/Config/Domain/SerializationServices.cs
  58. 44
      src/Squidex/Config/Domain/StoreModule.cs
  59. 208
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  60. 118
      src/Squidex/Config/Domain/StoreServices.cs
  61. 28
      src/Squidex/Config/Domain/SystemExtensions.cs
  62. 56
      src/Squidex/Config/Domain/Usages.cs
  63. 113
      src/Squidex/Config/Domain/WriteModule.cs
  64. 79
      src/Squidex/Config/Domain/WriteServices.cs
  65. 4
      src/Squidex/Config/Identity/AuthenticationExtensions.cs
  66. 12
      src/Squidex/Config/Identity/AuthenticationServices.cs
  67. 4
      src/Squidex/Config/Identity/IdentityExtensions.cs
  68. 93
      src/Squidex/Config/Identity/IdentityServerServices.cs
  69. 134
      src/Squidex/Config/Identity/IdentityServices.cs
  70. 14
      src/Squidex/Config/Options.cs
  71. 40
      src/Squidex/Config/Orleans/ClientServices.cs
  72. 11
      src/Squidex/Config/Orleans/IOrleansRunner.cs
  73. 26
      src/Squidex/Config/Orleans/SiloExtensions.cs
  74. 76
      src/Squidex/Config/Orleans/SiloServices.cs
  75. 100
      src/Squidex/Config/ServiceExtensions.cs
  76. 6
      src/Squidex/Config/Swagger/SwaggerExtensions.cs
  77. 2
      src/Squidex/Config/Web/WebExtensions.cs
  78. 35
      src/Squidex/Config/Web/WebModule.cs
  79. 8
      src/Squidex/Config/Web/WebServices.cs
  80. 4
      src/Squidex/Config/Web/WebpackExtensions.cs
  81. 34
      src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs
  82. 70
      src/Squidex/Program.cs
  83. 3
      src/Squidex/Squidex.csproj
  84. 101
      src/Squidex/WebStartup.cs
  85. 27
      src/Squidex/appsettings.json
  86. 77
      tests/Squidex.Infrastructure.Tests/Actors/ActorRemoteTests.cs
  87. 351
      tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs
  88. 50
      tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultEventNotifierTests.cs
  89. 36
      tests/Squidex.Infrastructure.Tests/CQRS/Events/EventConsumerCleanerTests.cs
  90. 45
      tests/Squidex.Infrastructure.Tests/CQRS/Events/EventSubscriptionTests.cs
  91. 4
      tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs
  92. 2
      tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs

19
Squidex.sln

@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27004.2002
VisualStudioVersion = 15.0.27004.2006
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex", "src\Squidex\Squidex.csproj", "{61F6BBCE-A080-4400-B194-70E2F5D2096E}"
EndProject
@ -34,10 +34,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.Rabb
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squidex.Infrastructure.GoogleCloud", "src\Squidex.Infrastructure.GoogleCloud\Squidex.Infrastructure.GoogleCloud.csproj", "{945871B1-77B8-43FB-B53C-27CF385AB756}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{B56EBCEC-9C50-46A7-848C-65502DE69C5C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmarks", "tests\Benchmarks\Benchmarks.csproj", "{D48A03DF-BCD3-4667-8747-2F251347E2B6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "migrations", "migrations", "{94207AA6-4923-4183-A558-E0F8196B8CA3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Migrate_01", "tools\Migrate_01\Migrate_01.csproj", "{B51126A8-0D75-4A79-867D-10724EC6AC84}"
@ -197,18 +193,6 @@ Global
{945871B1-77B8-43FB-B53C-27CF385AB756}.Release|x64.Build.0 = Release|Any CPU
{945871B1-77B8-43FB-B53C-27CF385AB756}.Release|x86.ActiveCfg = Release|Any CPU
{945871B1-77B8-43FB-B53C-27CF385AB756}.Release|x86.Build.0 = Release|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|x64.ActiveCfg = Debug|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|x64.Build.0 = Debug|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|x86.ActiveCfg = Debug|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Debug|x86.Build.0 = Debug|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|Any CPU.Build.0 = Release|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|x64.ActiveCfg = Release|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|x64.Build.0 = Release|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|x86.ActiveCfg = Release|Any CPU
{D48A03DF-BCD3-4667-8747-2F251347E2B6}.Release|x86.Build.0 = Release|Any CPU
{B51126A8-0D75-4A79-867D-10724EC6AC84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B51126A8-0D75-4A79-867D-10724EC6AC84}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B51126A8-0D75-4A79-867D-10724EC6AC84}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -335,7 +319,6 @@ Global
{D7166C56-178A-4457-B56A-C615C7450DEE} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{C1E5BBB6-6B6A-4DE5-B19D-0538304DE343} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{945871B1-77B8-43FB-B53C-27CF385AB756} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
{D48A03DF-BCD3-4667-8747-2F251347E2B6} = {B56EBCEC-9C50-46A7-848C-65502DE69C5C}
{B51126A8-0D75-4A79-867D-10724EC6AC84} = {94207AA6-4923-4183-A558-E0F8196B8CA3}
{5E75AB7D-6F01-4313-AFF1-7F7128FFD71F} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
{C9809D59-6665-471E-AD87-5AC624C65892} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}

3
src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs

@ -13,12 +13,11 @@ using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Apps.Repositories;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Read.MongoDb.Apps
{
public partial class MongoAppRepository : MongoRepositoryBase<MongoAppEntity>, IAppRepository, IEventConsumer
public partial class MongoAppRepository : MongoRepositoryBase<MongoAppEntity>, IAppRepository, IAppEventConsumer
{
public MongoAppRepository(IMongoDatabase database)
: base(database)

3
src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs

@ -14,12 +14,11 @@ using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Read.MongoDb.Assets
{
public partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository, IEventConsumer
public partial class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository, IAssetEventConsumer
{
public MongoAssetRepository(IMongoDatabase database)
: base(database)

3
src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs

@ -14,12 +14,11 @@ using MongoDB.Driver;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Read.MongoDb.Assets
{
public partial class MongoAssetStatsRepository : MongoRepositoryBase<MongoAssetStatsEntity>, IAssetStatsRepository, IEventConsumer
public partial class MongoAssetStatsRepository : MongoRepositoryBase<MongoAssetStatsEntity>, IAssetStatsRepository, IAssetEventConsumer
{
public MongoAssetStatsRepository(IMongoDatabase database)
: base(database)

3
src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs

@ -15,12 +15,11 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Read.MongoDb.Schemas
{
public partial class MongoSchemaRepository : MongoRepositoryBase<MongoSchemaEntity>, ISchemaRepository, IEventConsumer
public partial class MongoSchemaRepository : MongoRepositoryBase<MongoSchemaEntity>, ISchemaRepository, ISchemaEventConsumer
{
private readonly FieldRegistry registry;

11
src/Squidex.Infrastructure/Actors/IActors.cs → src/Squidex.Domain.Apps.Read/Apps/IAppEventConsumer.cs

@ -1,15 +1,16 @@
// ==========================================================================
// IActors.cs
// IAppEventConsumer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.Actors
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Read.Apps
{
public interface IActors
public interface IAppEventConsumer : IEventConsumer
{
IActor Get(string id);
}
}
}

11
src/Squidex.Infrastructure/Actors/IActor.cs → src/Squidex.Domain.Apps.Read/Assets/IAssetEventConsumer.cs

@ -1,15 +1,16 @@
// ==========================================================================
// IActor.cs
// IAssetEventConsumer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.Actors
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Read.Assets
{
public interface IActor
public interface IAssetEventConsumer : IEventConsumer
{
void Tell(object message);
}
}
}

9
src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StopConsumerMessage.cs → src/Squidex.Domain.Apps.Read/Schemas/ISchemaEventConsumer.cs

@ -1,15 +1,16 @@
// ==========================================================================
// StopConsumerMessage.cs
// ISchemaEventConsumer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Domain.Apps.Read.Schemas
{
[TypeName(nameof(StopConsumerMessage))]
public sealed class StopConsumerMessage
public interface ISchemaEventConsumer : IEventConsumer
{
}
}

14
src/Squidex.Infrastructure/Actors/IRemoteActorChannel.cs → src/Squidex.Domain.Users/DataProtection/Orleans/Grains/IXmlRepositoryGrain.cs

@ -1,20 +1,20 @@
// ==========================================================================
// IRemoteActorChannel.cs
// IXmlRepositoryGrain.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Infrastructure.Actors
namespace Squidex.Domain.Users.DataProtection.Orleans.Grains
{
public interface IRemoteActorChannel
public interface IXmlRepositoryGrain : IGrainWithStringKey
{
Task SendAsync(string recipient, object message);
Task<string[]> GetAllElementsAsync();
void Subscribe(string recipient, Action<object> handler);
Task StoreElementAsync(string element, string friendlyName);
}
}
}

33
src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs

@ -0,0 +1,33 @@
// ==========================================================================
// XmlRepositoryGrain.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orleans;
using Orleans.Providers;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Users.DataProtection.Orleans.Grains.Implementations
{
[StorageProvider(ProviderName = "Default")]
public sealed class XmlRepositoryGrain : Grain<Dictionary<string, string>>, IXmlRepositoryGrain
{
public Task<string[]> GetAllElementsAsync()
{
return Task.FromResult(State.Values.ToArray());
}
public Task StoreElementAsync(string element, string friendlyName)
{
State[friendlyName] = element;
return TaskHelper.Done;
}
}
}

41
src/Squidex.Domain.Users/DataProtection/Orleans/OrleansXmlRepository.cs

@ -0,0 +1,41 @@
// ==========================================================================
// OrleansXmlRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Orleans;
using Squidex.Domain.Users.DataProtection.Orleans.Grains;
using Squidex.Infrastructure;
namespace Squidex.Domain.Users.DataProtection.Orleans
{
public sealed class OrleansXmlRepository : IXmlRepository
{
private readonly Lazy<IXmlRepositoryGrain> grain;
public OrleansXmlRepository(IClusterClient orleans)
{
Guard.NotNull(orleans, nameof(orleans));
grain = new Lazy<IXmlRepositoryGrain>(() => orleans.GetGrain<IXmlRepositoryGrain>("Default"));
}
public IReadOnlyCollection<XElement> GetAllElements()
{
return grain.Value.GetAllElementsAsync().ContinueWith(x => x.Result.Select(XElement.Parse).ToList()).Result;
}
public void StoreElement(XElement element, string friendlyName)
{
grain.Value.StoreElementAsync(element.ToString(), friendlyName).Wait();
}
}
}

2
src/Squidex.Domain.Users/Squidex.Domain.Users.csproj

@ -12,6 +12,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.0.0" />
<PackageReference Include="Microsoft.Orleans.Client" Version="2.0.0-beta1" />
<PackageReference Include="Microsoft.Orleans.OrleansCodeGenerator.Build" Version="2.0.0-beta1-fix" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.4.0" />
<PackageReference Include="RefactoringEssentials" Version="5.2.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />

33
src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfo.cs

@ -1,33 +0,0 @@
// ==========================================================================
// MongoEventConsumerInfo.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
namespace Squidex.Infrastructure.CQRS.Events
{
[BsonIgnoreExtraElements]
public sealed class MongoEventConsumerInfo : IEventConsumerInfo
{
[BsonId]
[BsonRepresentation(BsonType.String)]
public string Name { get; set; }
[BsonElement]
[BsonIgnoreIfNull]
public string Error { get; set; }
[BsonElement]
[BsonIgnoreIfDefault]
public bool IsStopped { get; set; }
[BsonElement]
[BsonRequired]
public string Position { get; set; }
}
}

74
src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfoRepository.cs

@ -1,74 +0,0 @@
// ==========================================================================
// MongoEventConsumerInfoRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.CQRS.Events
{
public sealed class MongoEventConsumerInfoRepository : MongoRepositoryBase<MongoEventConsumerInfo>, IEventConsumerInfoRepository
{
private static readonly FieldDefinition<MongoEventConsumerInfo, string> NameField = Fields.Build(x => x.Name);
private static readonly FieldDefinition<MongoEventConsumerInfo, string> ErrorField = Fields.Build(x => x.Error);
private static readonly FieldDefinition<MongoEventConsumerInfo, string> PositionField = Fields.Build(x => x.Position);
private static readonly FieldDefinition<MongoEventConsumerInfo, bool> IsStoppedField = Fields.Build(x => x.IsStopped);
public MongoEventConsumerInfoRepository(IMongoDatabase database)
: base(database)
{
}
protected override string CollectionName()
{
return "EventPositions";
}
public async Task<IReadOnlyList<IEventConsumerInfo>> QueryAsync()
{
var entities = await Collection.Find(new BsonDocument()).SortBy(x => x.Name).ToListAsync();
return entities.OfType<IEventConsumerInfo>().ToList();
}
public async Task<IEventConsumerInfo> FindAsync(string consumerName)
{
var entity = await Collection.Find(Filter.Eq(NameField, consumerName)).FirstOrDefaultAsync();
return entity;
}
public Task ClearAsync(IEnumerable<string> currentConsumerNames)
{
return Collection.DeleteManyAsync(Filter.Not(Filter.In(NameField, currentConsumerNames)));
}
public async Task SetAsync(string consumerName, string position, bool isStopped = false, string error = null)
{
try
{
await Collection.UpdateOneAsync(Filter.Eq(NameField, consumerName),
Update
.Set(ErrorField, error)
.Set(PositionField, position)
.Set(IsStoppedField, isStopped),
new UpdateOptions { IsUpsert = true });
}
catch (MongoWriteException ex)
{
if (ex.WriteError?.Category != ServerErrorCategory.DuplicateKey)
{
throw;
}
}
}
}
}

2
src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs

@ -60,7 +60,7 @@ namespace Squidex.Infrastructure.CQRS.Events
Guard.NotNull(subscriber, nameof(subscriber));
Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter));
return new PollingSubscription(this, notifier, subscriber, streamFilter, position);
return new EventStoreSubscription(this, subscriber, streamFilter, position);
}
public async Task<IReadOnlyList<StoredEvent>> GetEventsAsync(string streamName)

88
src/Squidex.Infrastructure/Actors/DefaultRemoteActorChannel.cs

@ -1,88 +0,0 @@
// ==========================================================================
// DefaultRemoteActorChannel.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Actors
{
public sealed class DefaultRemoteActorChannel : IRemoteActorChannel
{
private static readonly string ChannelName = typeof(DefaultRemoteActorChannel).Name;
private readonly IPubSub pubSub;
private readonly JsonSerializer serializer;
private readonly TypeNameRegistry typeNameRegistry;
private sealed class Envelope
{
public string Recipient { get; set; }
public string PayloadType { get; set; }
public JToken Payload { get; set; }
}
public DefaultRemoteActorChannel(IPubSub pubSub, TypeNameRegistry typeNameRegistry, JsonSerializerSettings serializerSettings = null)
{
Guard.NotNull(pubSub, nameof(pubSub));
Guard.NotNull(typeNameRegistry, nameof(typeNameRegistry));
this.pubSub = pubSub;
this.typeNameRegistry = typeNameRegistry;
serializer = JsonSerializer.Create(serializerSettings ?? new JsonSerializerSettings());
}
public Task SendAsync(string recipient, object message)
{
Guard.NotNullOrEmpty(recipient, nameof(recipient));
Guard.NotNull(message, nameof(message));
var messageType = typeNameRegistry.GetName(message.GetType());
var messageBody = WriteJson(message);
var envelope = new Envelope { Recipient = recipient, Payload = messageBody, PayloadType = messageType };
pubSub.Publish(ChannelName, JsonConvert.SerializeObject(envelope), true);
return TaskHelper.Done;
}
public void Subscribe(string recipient, Action<object> handler)
{
Guard.NotNullOrEmpty(recipient, nameof(recipient));
pubSub.Subscribe(ChannelName, json =>
{
var envelope = JsonConvert.DeserializeObject<Envelope>(json);
if (string.Equals(envelope.Recipient, recipient, StringComparison.OrdinalIgnoreCase))
{
var messageType = typeNameRegistry.GetType(envelope.PayloadType);
var messageBody = ReadJson(envelope.Payload, messageType);
handler?.Invoke(messageBody);
}
});
}
private object ReadJson(JToken token, Type type)
{
return token.ToObject(type, serializer);
}
private JToken WriteJson(object value)
{
return JToken.FromObject(value, serializer);
}
}
}

60
src/Squidex.Infrastructure/Actors/RemoteActors.cs

@ -1,60 +0,0 @@
// ==========================================================================
// RemoteActors.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Concurrent;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Actors
{
public sealed class RemoteActors : IActors
{
private readonly ConcurrentDictionary<string, IActor> senders = new ConcurrentDictionary<string, IActor>();
private readonly ConcurrentDictionary<string, bool> receivers = new ConcurrentDictionary<string, bool>();
private readonly IRemoteActorChannel channel;
private sealed class Sender : IActor
{
private readonly IRemoteActorChannel channel;
private readonly string recipient;
public Sender(IRemoteActorChannel channel, string recipient)
{
this.recipient = recipient;
this.channel = channel;
}
public void Tell(object message)
{
channel.SendAsync(recipient, message).Forget();
}
}
public RemoteActors(IRemoteActorChannel channel)
{
Guard.NotNull(channel, nameof(channel));
this.channel = channel;
}
public IActor Get(string id)
{
Guard.NotNullOrEmpty(id, nameof(id));
return senders.GetOrAdd(id, k => new Sender(channel, id));
}
public void Connect(string id, IActor actor)
{
Guard.NotNullOrEmpty(id, nameof(id));
Guard.NotNull(actor, nameof(actor));
channel.Subscribe(id, actor.Tell);
}
}
}

323
src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs

@ -1,323 +0,0 @@
// ==========================================================================
// EventConsumerActor.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.Actors;
using Squidex.Infrastructure.CQRS.Events.Actors.Messages;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Events.Actors
{
public class EventConsumerActor : DisposableObjectBase, IEventSubscriber, IActor
{
private readonly EventDataFormatter formatter;
private readonly IEventStore eventStore;
private readonly IEventConsumerInfoRepository eventConsumerInfoRepository;
private readonly ISemanticLog log;
private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(1);
private IEventSubscription currentSubscription;
private IEventConsumer eventConsumer;
private bool statusIsRunning = true;
private string statusPosition;
private string statusError;
private static Func<IEventStore, IEventSubscriber, string, string, IEventSubscription> DefaultFactory
{
get { return (e, s, t, p) => new RetrySubscription(e, s, t, p); }
}
public EventConsumerActor(
EventDataFormatter formatter,
IEventStore eventStore,
IEventConsumerInfoRepository eventConsumerInfoRepository,
ISemanticLog log)
{
Guard.NotNull(log, nameof(log));
Guard.NotNull(formatter, nameof(formatter));
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventConsumerInfoRepository, nameof(eventConsumerInfoRepository));
this.log = log;
this.formatter = formatter;
this.eventStore = eventStore;
this.eventConsumerInfoRepository = eventConsumerInfoRepository;
}
protected override void DisposeObject(bool disposing)
{
if (disposing)
{
dispatcher.StopAndWaitAsync().Wait();
}
}
protected virtual IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position)
{
return new RetrySubscription(eventStore, this, streamFilter, position);
}
public Task SubscribeAsync(IEventConsumer eventConsumer)
{
Guard.NotNull(eventConsumer, nameof(eventConsumer));
return dispatcher.DispatchAsync(() => HandleSetupAsync(eventConsumer));
}
private async Task HandleSetupAsync(IEventConsumer consumer)
{
eventConsumer = consumer;
var status = await eventConsumerInfoRepository.FindAsync(eventConsumer.Name);
if (status != null)
{
statusError = status.Error;
statusPosition = status.Position;
statusIsRunning = !status.IsStopped;
}
if (statusIsRunning)
{
Subscribe(statusPosition);
}
}
private Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent)
{
if (subscription != currentSubscription)
{
return TaskHelper.Done;
}
return DoAndUpdateStateAsync(async () =>
{
var @event = ParseKnownEvent(storedEvent);
if (@event != null)
{
await DispatchConsumerAsync(@event);
}
statusError = null;
statusPosition = storedEvent.EventPosition;
});
}
private Task HandleErrorAsync(IEventSubscription subscription, Exception exception)
{
if (subscription != currentSubscription)
{
return TaskHelper.Done;
}
return DoAndUpdateStateAsync(() =>
{
Unsubscribe();
statusError = exception.ToString();
statusIsRunning = false;
});
}
private Task HandleStartAsync()
{
if (statusIsRunning)
{
return TaskHelper.Done;
}
return DoAndUpdateStateAsync(() =>
{
Subscribe(statusPosition);
statusError = null;
statusIsRunning = true;
});
}
private Task HandleStopAsync()
{
if (!statusIsRunning)
{
return TaskHelper.Done;
}
return DoAndUpdateStateAsync(() =>
{
Unsubscribe();
statusError = null;
statusIsRunning = false;
});
}
private Task HandleResetInternalAsync()
{
return DoAndUpdateStateAsync(async () =>
{
Unsubscribe();
await ClearAsync();
Subscribe(null);
statusError = null;
statusPosition = null;
statusIsRunning = true;
});
}
Task IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent)
{
return dispatcher.DispatchAsync(() => HandleEventAsync(subscription, storedEvent));
}
Task IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception)
{
return dispatcher.DispatchAsync(() => HandleErrorAsync(subscription, exception));
}
void IActor.Tell(object message)
{
switch (message)
{
case StopConsumerMessage stop:
dispatcher.DispatchAsync(() => HandleStopAsync()).Forget();
break;
case StartConsumerMessage stop:
dispatcher.DispatchAsync(() => HandleStartAsync()).Forget();
break;
case ResetConsumerMessage stop:
dispatcher.DispatchAsync(() => HandleResetInternalAsync()).Forget();
break;
}
}
private Task DoAndUpdateStateAsync(Action action)
{
return DoAndUpdateStateAsync(() => { action(); return TaskHelper.Done; });
}
private async Task DoAndUpdateStateAsync(Func<Task> action)
{
try
{
await action();
await eventConsumerInfoRepository.SetAsync(eventConsumer.Name, statusPosition, !statusIsRunning, statusError);
}
catch (Exception ex)
{
try
{
Unsubscribe();
}
catch (Exception unsubscribeException)
{
ex = new AggregateException(ex, unsubscribeException);
}
log.LogFatal(ex, w => w
.WriteProperty("action", "HandleEvent")
.WriteProperty("state", "Failed")
.WriteProperty("eventConsumer", eventConsumer.Name));
statusError = ex.ToString();
statusIsRunning = false;
await eventConsumerInfoRepository.SetAsync(eventConsumer.Name, statusPosition, !statusIsRunning, statusError);
}
}
private async Task ClearAsync()
{
var actionId = Guid.NewGuid().ToString();
log.LogInformation(w => w
.WriteProperty("action", "EventConsumerReset")
.WriteProperty("actionId", actionId)
.WriteProperty("state", "Started")
.WriteProperty("eventConsumer", eventConsumer.Name));
using (log.MeasureTrace(w => w
.WriteProperty("action", "EventConsumerReset")
.WriteProperty("actionId", actionId)
.WriteProperty("state", "Completed")
.WriteProperty("eventConsumer", eventConsumer.Name)))
{
await eventConsumer.ClearAsync();
}
}
private async Task DispatchConsumerAsync(Envelope<IEvent> @event)
{
var eventId = @event.Headers.EventId().ToString();
var eventType = @event.Payload.GetType().Name;
log.LogInformation(w => w
.WriteProperty("action", "HandleEvent")
.WriteProperty("actionId", eventId)
.WriteProperty("state", "Started")
.WriteProperty("eventId", eventId)
.WriteProperty("eventType", eventType)
.WriteProperty("eventConsumer", eventConsumer.Name));
using (log.MeasureTrace(w => w
.WriteProperty("action", "HandleEvent")
.WriteProperty("actionId", eventId)
.WriteProperty("state", "Completed")
.WriteProperty("eventId", eventId)
.WriteProperty("eventType", eventType)
.WriteProperty("eventConsumer", eventConsumer.Name)))
{
await eventConsumer.On(@event);
}
}
private void Unsubscribe()
{
if (currentSubscription != null)
{
currentSubscription.StopAsync().Forget();
currentSubscription = null;
}
}
private void Subscribe(string position)
{
if (currentSubscription == null)
{
currentSubscription?.StopAsync().Forget();
currentSubscription = CreateSubscription(eventStore, eventConsumer.EventsFilter, position);
}
}
private Envelope<IEvent> ParseKnownEvent(StoredEvent message)
{
try
{
var @event = formatter.Parse(message.Data);
@event.SetEventPosition(message.EventPosition);
@event.SetEventStreamNumber(message.EventStreamNumber);
return @event;
}
catch (TypeNameNotFoundException)
{
log.LogDebug(w => w.WriteProperty("oldEventFound", message.Data.Type));
return null;
}
}
}
}

15
src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StartConsumerMessage.cs

@ -1,15 +0,0 @@
// ==========================================================================
// StartConsumerMessage.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages
{
[TypeName(nameof(StartConsumerMessage))]
public sealed class StartConsumerMessage
{
}
}

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

@ -24,6 +24,22 @@ namespace Squidex.Infrastructure.CQRS.Events
{
}
public CompoundEventConsumer(IEventConsumer[] inners)
{
Guard.NotNull(inners, nameof(inners));
Guard.NotEmpty(inners, nameof(inners));
this.inners = inners;
Name = inners.First().Name;
var innerFilters =
this.inners.Where(x => !string.IsNullOrWhiteSpace(x.EventsFilter))
.Select(x => $"({x.EventsFilter})");
EventsFilter = string.Join("|", innerFilters);
}
public CompoundEventConsumer(string name, IEventConsumer first, params IEventConsumer[] inners)
{
Guard.NotNull(first, nameof(first));

36
src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs

@ -1,36 +0,0 @@
// ==========================================================================
// DefaultEventNotifier.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.CQRS.Events
{
public sealed class DefaultEventNotifier : IEventNotifier
{
private static readonly string ChannelName = typeof(DefaultEventNotifier).Name;
private readonly IPubSub pubsub;
public DefaultEventNotifier(IPubSub pubsub)
{
Guard.NotNull(pubsub, nameof(pubsub));
this.pubsub = pubsub;
}
public void NotifyEventsStored(string streamName)
{
pubsub.Publish(ChannelName, streamName, true);
}
public IDisposable Subscribe(Action<string> handler)
{
return pubsub.Subscribe(ChannelName, x => handler?.Invoke(x));
}
}
}

36
src/Squidex.Infrastructure/CQRS/Events/EventConsumerCleaner.cs

@ -1,36 +0,0 @@
// ==========================================================================
// EventConsumerCleaner.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.CQRS.Events
{
public sealed class EventConsumerCleaner
{
private readonly IEnumerable<IEventConsumer> eventConsumers;
private readonly IEventConsumerInfoRepository eventConsumerInfoRepository;
public EventConsumerCleaner(IEnumerable<IEventConsumer> eventConsumers, IEventConsumerInfoRepository eventConsumerInfoRepository)
{
Guard.NotNull(eventConsumers, nameof(eventConsumers));
Guard.NotNull(eventConsumerInfoRepository, nameof(eventConsumerInfoRepository));
this.eventConsumers = eventConsumers;
this.eventConsumerInfoRepository = eventConsumerInfoRepository;
}
public Task CleanAsync()
{
var names = eventConsumers.Select(x => x.Name).ToArray();
return eventConsumerInfoRepository.ClearAsync(names);
}
}
}

2
src/Squidex.Infrastructure/CQRS/Events/Grains/EventConsumerInfo.cs → src/Squidex.Infrastructure/CQRS/Events/EventConsumerInfo.cs

@ -6,7 +6,7 @@
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.CQRS.Events.Grains
namespace Squidex.Infrastructure.CQRS.Events
{
public sealed class EventConsumerInfo
{

38
src/Squidex.Infrastructure/CQRS/Events/PollingSubscription.cs → src/Squidex.Infrastructure/CQRS/Events/EventStoreSubscription.cs

@ -1,5 +1,5 @@
// ==========================================================================
// PollingSubscription.cs
// EventStoreSubscription.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -7,43 +7,33 @@
// ==========================================================================
using System;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure.Timers;
namespace Squidex.Infrastructure.CQRS.Events
{
public sealed class PollingSubscription : IEventSubscription
public sealed class EventStoreSubscription : IEventSubscription
{
private readonly IEventNotifier eventNotifier;
private readonly IEventStore eventStore;
private readonly IEventSubscriber eventSubscriber;
private readonly IDisposable notification;
private readonly CompletionTimer timer;
private readonly Regex streamRegex;
private readonly CancellationTokenSource cts = new CancellationTokenSource();
private readonly Task task;
private readonly string streamFilter;
private string position;
public PollingSubscription(
public EventStoreSubscription(
IEventStore eventStore,
IEventNotifier eventNotifier,
IEventSubscriber eventSubscriber,
string streamFilter,
string position)
{
Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventNotifier, nameof(eventNotifier));
Guard.NotNull(eventSubscriber, nameof(eventSubscriber));
this.position = position;
this.eventNotifier = eventNotifier;
this.eventStore = eventStore;
this.eventSubscriber = eventSubscriber;
this.streamFilter = streamFilter;
streamRegex = new Regex(streamFilter);
timer = new CompletionTimer(5000, async ct =>
task = Task.Run(async () =>
{
try
{
@ -52,7 +42,7 @@ namespace Squidex.Infrastructure.CQRS.Events
await eventSubscriber.OnEventAsync(this, storedEvent);
position = storedEvent.EventPosition;
}, ct, streamFilter, position);
}, cts.Token, streamFilter, position);
}
catch (Exception ex)
{
@ -61,22 +51,18 @@ namespace Squidex.Infrastructure.CQRS.Events
await eventSubscriber.OnErrorAsync(this, ex);
}
}
});
notification = eventNotifier.Subscribe(streamName =>
{
if (streamRegex.IsMatch(streamName))
finally
{
timer.SkipCurrentDelay();
await eventSubscriber.OnClosedAsync(this);
}
});
}
public Task StopAsync()
{
notification?.Dispose();
cts.Cancel();
return timer.StopAsync();
return task;
}
}
}

22
src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerRegistryGrainState.cs

@ -1,22 +0,0 @@
// ==========================================================================
// EventConsumerRegistryGrainState.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
namespace Squidex.Infrastructure.CQRS.Events.Grains.Implementation
{
public sealed class EventConsumerRegistryGrainState
{
public HashSet<string> EventConsumerNames { get; set; }
public EventConsumerRegistryGrainState()
{
EventConsumerNames = new HashSet<string>();
}
}
}

21
src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfo.cs

@ -1,21 +0,0 @@
// ==========================================================================
// IEventConsumerInfo.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.CQRS.Events
{
public interface IEventConsumerInfo
{
bool IsStopped { get; }
string Name { get; }
string Error { get; }
string Position { get; }
}
}

24
src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfoRepository.cs

@ -1,24 +0,0 @@
// ==========================================================================
// IEventConsumerInfoRepository.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.CQRS.Events
{
public interface IEventConsumerInfoRepository
{
Task<IReadOnlyList<IEventConsumerInfo>> QueryAsync();
Task<IEventConsumerInfo> FindAsync(string consumerName);
Task ClearAsync(IEnumerable<string> currentConsumerNames);
Task SetAsync(string consumerName, string position, bool isStopped, string error = null);
}
}

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

@ -13,7 +13,5 @@ namespace Squidex.Infrastructure.CQRS.Events
public interface IEventNotifier
{
void NotifyEventsStored(string streamName);
IDisposable Subscribe(Action<string> handler);
}
}

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

@ -16,5 +16,7 @@ namespace Squidex.Infrastructure.CQRS.Events
Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent);
Task OnErrorAsync(IEventSubscription subscription, Exception exception);
Task OnClosedAsync(IEventSubscription subscription);
}
}

31
src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/EventConsumerBootstrap.cs

@ -0,0 +1,31 @@
// ==========================================================================
// EventConsumerBootstrap.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Orleans.Providers;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains
{
public sealed class EventConsumerBootstrap : IBootstrapProvider
{
public string Name { get; private set; }
public Task Close()
{
return TaskHelper.Done;
}
public Task Init(string name, IProviderRuntime providerRuntime, IProviderConfiguration config)
{
Name = name;
return providerRuntime.GrainFactory.GetGrain<IEventConsumerRegistryGrain>("Default").ActivateAsync(null);
}
}
}

4
src/Squidex.Infrastructure/CQRS/Events/Grains/IEventConsumerGrain.cs → src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/IEventConsumerGrain.cs

@ -9,12 +9,14 @@
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Infrastructure.CQRS.Events.Grains
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains
{
public interface IEventConsumerGrain : IGrainWithStringKey, IEventSubscriber
{
Task<EventConsumerInfo> GetStateAsync();
Task ActivateAsync();
Task StopAsync();
Task StartAsync();

4
src/Squidex.Infrastructure/CQRS/Events/Grains/IEventConsumerRegistryGrain.cs → src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/IEventConsumerRegistryGrain.cs

@ -10,11 +10,11 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Infrastructure.CQRS.Events.Grains
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains
{
public interface IEventConsumerRegistryGrain : IGrainWithStringKey
{
Task RegisterAsync(string consumerName);
Task ActivateAsync(string streamName);
Task StopAsync(string consumerName);

74
src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerGrain.cs → src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs

@ -9,20 +9,22 @@
using System;
using System.Threading.Tasks;
using Orleans;
using Orleans.Providers;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Events.Grains
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation
{
public class EventConsumerGrain : Grain<EventConsumerInfo>, IEventSubscriber, IEventConsumerGrain
[StorageProvider(ProviderName = "Default")]
public class EventConsumerGrain : Grain<EventConsumerGrainState>, IEventSubscriber, IEventConsumerGrain
{
private readonly EventDataFormatter eventFormatter;
private readonly EventConsumerFactory eventConsumerFactory;
private readonly IEventStore eventStore;
private readonly ISemanticLog log;
private TaskFactory dispatcher;
private IEventSubscription currentSubscription;
private IEventConsumer eventConsumer;
private TaskFactory dispatcher;
public EventConsumerGrain(
EventDataFormatter eventFormatter,
@ -42,23 +44,23 @@ namespace Squidex.Infrastructure.CQRS.Events.Grains
this.eventConsumerFactory = eventConsumerFactory;
}
public override async Task OnActivateAsync()
public override Task OnActivateAsync()
{
dispatcher = new TaskFactory(TaskScheduler.Current);
eventConsumer = eventConsumerFactory(this.GetPrimaryKeyString());
await GrainFactory.GetGrain<IEventConsumerRegistryGrain>(string.Empty).RegisterAsync(this.IdentityString);
dispatcher = new TaskFactory(TaskScheduler.Current);
eventConsumer = eventConsumerFactory(this.IdentityString);
return TaskHelper.Done;
}
public Task ActivateAsync()
{
if (!State.IsStopped)
{
Subscribe(State.Position);
}
}
protected virtual IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position)
{
return new RetrySubscription(eventStore, this, streamFilter, position);
return TaskHelper.Done;
}
private Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent)
@ -77,12 +79,11 @@ namespace Squidex.Infrastructure.CQRS.Events.Grains
await DispatchConsumerAsync(@event);
}
State.Error = null;
State.Position = storedEvent.EventPosition;
State = EventConsumerGrainState.Handled(storedEvent.EventPosition);
});
}
private Task HandleErrorAsync(IEventSubscription subscription, Exception exception)
private Task HandleClosedAsync(IEventSubscription subscription)
{
if (subscription != currentSubscription)
{
@ -92,15 +93,22 @@ namespace Squidex.Infrastructure.CQRS.Events.Grains
return DoAndUpdateStateAsync(() =>
{
Unsubscribe();
State.Error = exception.ToString();
State.IsStopped = true;
});
}
public Task<EventConsumerInfo> GetStateAsync()
private Task HandleErrorAsync(IEventSubscription subscription, Exception exception)
{
return Task.FromResult(State);
if (subscription != currentSubscription)
{
return TaskHelper.Done;
}
return DoAndUpdateStateAsync(() =>
{
Unsubscribe();
State = EventConsumerGrainState.Failed(exception);
});
}
public Task StartAsync()
@ -114,8 +122,7 @@ namespace Squidex.Infrastructure.CQRS.Events.Grains
{
Subscribe(State.Position);
State.Error = null;
State.IsStopped = false;
State = State.Started();
});
}
@ -130,8 +137,7 @@ namespace Squidex.Infrastructure.CQRS.Events.Grains
{
Unsubscribe();
State.Error = null;
State.IsStopped = true;
State = State.Stopped();
});
}
@ -145,9 +151,7 @@ namespace Squidex.Infrastructure.CQRS.Events.Grains
Subscribe(null);
State.Error = null;
State.Position = null;
State.IsStopped = false;
State = EventConsumerGrainState.Initial();
});
}
@ -161,6 +165,16 @@ namespace Squidex.Infrastructure.CQRS.Events.Grains
return dispatcher.StartNew(() => this.HandleErrorAsync(subscription, exception)).Unwrap();
}
Task IEventSubscriber.OnClosedAsync(IEventSubscription subscription)
{
return dispatcher.StartNew(() => this.HandleClosedAsync(subscription)).Unwrap();
}
public Task<EventConsumerInfo> GetStateAsync()
{
return Task.FromResult(State.ToInfo(this.GetPrimaryKeyString()));
}
private Task DoAndUpdateStateAsync(Action action)
{
return DoAndUpdateStateAsync(() => { action(); return TaskHelper.Done; });
@ -188,8 +202,7 @@ namespace Squidex.Infrastructure.CQRS.Events.Grains
.WriteProperty("state", "Failed")
.WriteProperty("eventConsumer", eventConsumer.Name));
State.Error = ex.ToString();
State.IsStopped = true;
State = EventConsumerGrainState.Failed(ex);
}
await WriteStateAsync();
@ -276,5 +289,10 @@ namespace Squidex.Infrastructure.CQRS.Events.Grains
return null;
}
}
protected virtual IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position)
{
return new RetrySubscription(eventStore, this, streamFilter, position);
}
}
}

52
src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrainState.cs

@ -0,0 +1,52 @@
// ==========================================================================
// EventConsumerGrainState.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Squidex.Infrastructure.Reflection;
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation
{
public sealed class EventConsumerGrainState
{
public bool IsStopped { get; set; }
public string Error { get; set; }
public string Position { get; set; }
public static EventConsumerGrainState Initial()
{
return new EventConsumerGrainState();
}
public static EventConsumerGrainState Handled(string position)
{
return new EventConsumerGrainState { Position = position };
}
public static EventConsumerGrainState Failed(Exception ex)
{
return new EventConsumerGrainState { IsStopped = true, Error = ex?.ToString() };
}
public EventConsumerGrainState Stopped()
{
return new EventConsumerGrainState { Position = Position, IsStopped = true };
}
public EventConsumerGrainState Started()
{
return new EventConsumerGrainState { Position = Position, IsStopped = false };
}
public EventConsumerInfo ToInfo(string name)
{
return SimpleMapper.Map(this, new EventConsumerInfo { Name = name });
}
}
}

44
src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerRegistryGrain.cs → src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs

@ -6,30 +6,60 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Orleans;
using Orleans.Runtime;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Events.Grains.Implementation
namespace Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation
{
public sealed class EventConsumerRegistryGrain : Grain<EventConsumerRegistryGrainState>, IEventConsumerRegistryGrain
public sealed class EventConsumerRegistryGrain : Grain, IEventConsumerRegistryGrain, IRemindable
{
private readonly IEnumerable<IEventConsumer> eventConsumers;
public EventConsumerRegistryGrain(IEnumerable<IEventConsumer> eventConsumers)
{
Guard.NotNull(eventConsumers, nameof(eventConsumers));
this.eventConsumers = eventConsumers;
}
public override Task OnActivateAsync()
{
DelayDeactivation(TimeSpan.FromDays(1));
RegisterTimer(x => ActivateAsync(null), null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
return Task.FromResult(true);
}
public Task ActivateAsync(string streamName)
{
var tasks =
eventConsumers
.Where(c => streamName == null || Regex.IsMatch(streamName, c.EventsFilter))
.Select(c => GrainFactory.GetGrain<IEventConsumerGrain>(c.Name))
.Select(c => c.ActivateAsync());
return Task.WhenAll(tasks);
}
public Task<List<EventConsumerInfo>> GetConsumersAsync()
{
var tasks =
State.EventConsumerNames
.Select(n => GrainFactory.GetGrain<IEventConsumerGrain>(n))
eventConsumers
.Select(c => GrainFactory.GetGrain<IEventConsumerGrain>(c.Name))
.Select(c => c.GetStateAsync());
return Task.WhenAll(tasks).ContinueWith(x => x.Result.ToList());
}
public Task RegisterAsync(string consumerName)
public Task ReceiveReminder(string reminderName, TickStatus status)
{
State.EventConsumerNames.Add(consumerName);
return TaskHelper.Done;
}

30
src/Squidex.Infrastructure/CQRS/Events/Orleans/OrleansClientEventNotifier.cs

@ -0,0 +1,30 @@
// ==========================================================================
// OrleansClientEventNotifier.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Orleans;
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains;
namespace Squidex.Infrastructure.CQRS.Events.Orleans
{
public sealed class OrleansClientEventNotifier : IEventNotifier
{
private readonly IEventConsumerRegistryGrain eventConsumerRegistryGrain;
public OrleansClientEventNotifier(IClusterClient orleans)
{
Guard.NotNull(orleans, nameof(orleans));
eventConsumerRegistryGrain = orleans.GetGrain<IEventConsumerRegistryGrain>("Default");
}
public void NotifyEventsStored(string streamName)
{
eventConsumerRegistryGrain.ActivateAsync(streamName);
}
}
}

30
src/Squidex.Infrastructure/CQRS/Events/Orleans/OrleansSiloEventNotifier.cs

@ -0,0 +1,30 @@
// ==========================================================================
// OrleansEventNotifier.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Orleans;
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains;
namespace Squidex.Infrastructure.CQRS.Events.Orleans
{
public sealed class OrleansSiloEventNotifier : IEventNotifier
{
private readonly IEventConsumerRegistryGrain eventConsumerRegistryGrain;
public OrleansSiloEventNotifier(IGrainFactory orleans)
{
Guard.NotNull(orleans, nameof(orleans));
eventConsumerRegistryGrain = orleans.GetGrain<IEventConsumerRegistryGrain>("Default");
}
public void NotifyEventsStored(string streamName)
{
eventConsumerRegistryGrain.ActivateAsync(streamName);
}
}
}

20
src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs

@ -9,7 +9,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure.Actors;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.CQRS.Events
@ -50,6 +49,7 @@ namespace Squidex.Infrastructure.CQRS.Events
private void Unsubscribe()
{
currentSubscription?.StopAsync().Forget();
currentSubscription = null;
}
private async Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent)
@ -62,12 +62,21 @@ namespace Squidex.Infrastructure.CQRS.Events
}
}
private async Task HandleClosedAsync(IEventSubscription subscription)
{
if (subscription == currentSubscription)
{
await eventSubscriber.OnClosedAsync(this);
Unsubscribe();
}
}
private async Task HandleErrorAsync(IEventSubscription subscription, Exception exception)
{
if (subscription == currentSubscription)
{
subscription.StopAsync().Forget();
subscription = null;
Unsubscribe();
if (retryWindow.CanRetryAfterFailure())
{
@ -93,6 +102,11 @@ namespace Squidex.Infrastructure.CQRS.Events
return dispatcher.DispatchAsync(() => HandleErrorAsync(subscription, exception));
}
Task IEventSubscriber.OnClosedAsync(IEventSubscription subscription)
{
return dispatcher.DispatchAsync(() => HandleClosedAsync(subscription));
}
public async Task StopAsync()
{
await dispatcher.DispatchAsync(() => Unsubscribe());

10
src/Squidex.Infrastructure/CollectionExtensions.cs

@ -30,6 +30,16 @@ namespace Squidex.Infrastructure
}
}
public static IEnumerable<T> OrEmpty<T>(this IEnumerable<T> source)
{
return source ?? Enumerable.Empty<T>();
}
public static IEnumerable<T> Concat<T>(this IEnumerable<T> source, T value)
{
return source.Concat(Enumerable.Repeat(value, 1));
}
public static int SequentialHashCode<T>(this IEnumerable<T> collection)
{
return collection.SequentialHashCode(EqualityComparer<T>.Default);

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

@ -6,15 +6,23 @@
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Squidex.Infrastructure.Log.Adapter
{
public static class SemanticLogLoggerFactoryExtensions
{
public static ILoggerFactory AddSemanticLog(this ILoggerFactory factory, ISemanticLog semanticLog)
public static ILoggingBuilder AddSemanticLog(this ILoggingBuilder builder)
{
factory.AddProvider(new SemanticLogLoggerProvider(semanticLog));
builder.Services.AddSingleton<ILoggerProvider, SemanticLogLoggerProvider>();
return builder;
}
public static ILoggerFactory AddSemanticLog(this ILoggerFactory factory, ISemanticLog log)
{
factory.AddProvider(new SemanticLogLoggerProvider(log));
return factory;
}

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

@ -16,6 +16,8 @@ namespace Squidex.Infrastructure.Log.Adapter
public SemanticLogLoggerProvider(ISemanticLog semanticLog)
{
Guard.NotNull(semanticLog, nameof(semanticLog));
this.semanticLog = semanticLog;
}

3
src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs → src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs

@ -9,9 +9,8 @@
using System;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Actors
namespace Squidex.Infrastructure.Tasks
{
public sealed class SingleThreadedDispatcher
{

27
src/Squidex/AppConfiguration.cs

@ -0,0 +1,27 @@
// ==========================================================================
// AppConfiguration.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Configuration;
namespace Squidex
{
public static class AppConfiguration
{
public static void AddAppConfiguration(this IConfigurationBuilder builder, string environmentName, string[] args)
{
builder.Sources.Clear();
builder.AddJsonFile("appsettings.json", true, true);
builder.AddJsonFile($"appsettings.{environmentName}.json", true);
builder.AddEnvironmentVariables();
builder.AddCommandLine(args);
}
}
}

54
src/Squidex/AppServices.cs

@ -0,0 +1,54 @@
// ==========================================================================
// Services.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Config;
using Squidex.Config.Domain;
using Squidex.Config.Identity;
using Squidex.Config.Orleans;
using Squidex.Config.Swagger;
using Squidex.Config.Web;
namespace Squidex
{
public static class AppServices
{
public static void AddAppServices(this IServiceCollection services, IConfiguration config)
{
services.AddLogging();
services.AddMemoryCache();
services.AddOptions();
services.AddMyAssetServices(config);
services.AddMyAuthentication(config);
services.AddMyDataProtectection(config);
services.AddMyEventPublishersServices(config);
services.AddMyEventStoreServices(config);
services.AddMyIdentity();
services.AddMyIdentityServer();
services.AddMyInfrastructureServices(config);
services.AddMyMvc();
services.AddMyPubSubServices(config);
services.AddMyReadServices(config);
services.AddMySerializers();
services.AddMyStoreServices(config);
services.AddMySwaggerSettings();
services.AddMyWriteServices();
services.Configure<MyUrlsOptions>(
config.GetSection("urls"));
services.Configure<MyIdentityOptions>(
config.GetSection("identity"));
services.Configure<MyUIOptions>(
config.GetSection("ui"));
services.Configure<MyUsageOptions>(
config.GetSection("usage"));
}
}
}

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

@ -0,0 +1,51 @@
// ==========================================================================
// AssetServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Log;
namespace Squidex.Config.Domain
{
public static class AssetServices
{
public static void AddMyAssetServices(this IServiceCollection services, IConfiguration config)
{
config.ConfigureByOption("assetStore:type", new Options
{
["Folder"] = () =>
{
var path = config.GetRequiredValue("assetStore:folder:path");
services.AddSingleton(c => new FolderAssetStore(path, c.GetRequiredService<ISemanticLog>()))
.As<IAssetStore>()
.As<IExternalSystem>();
},
["GoogleCloud"] = () =>
{
var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket");
services.AddSingleton(c => new GoogleCloudAssetStore(bucketName))
.As<IAssetStore>()
.As<IExternalSystem>();
},
["AzureBlob"] = () =>
{
var connectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString");
var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName");
services.AddSingleton(c => new AzureBlobAssetStore(connectionString, containerName))
.As<IAssetStore>()
.As<IExternalSystem>();
}
});
}
}
}

91
src/Squidex/Config/Domain/AssetStoreModule.cs

@ -1,91 +0,0 @@
// ==========================================================================
// AssetStoreModule.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.Assets;
using Squidex.Infrastructure.Log;
namespace Squidex.Config.Domain
{
public sealed class AssetStoreModule : Module
{
private IConfiguration Configuration { get; }
public AssetStoreModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
var assetStoreType = Configuration.GetValue<string>("assetStore:type");
if (string.IsNullOrWhiteSpace(assetStoreType))
{
throw new ConfigurationException("Configure the AssetStore type with 'assetStore:type'.");
}
if (string.Equals(assetStoreType, "Folder", StringComparison.OrdinalIgnoreCase))
{
var path = Configuration.GetValue<string>("assetStore:folder:path");
if (string.IsNullOrWhiteSpace(path))
{
throw new ConfigurationException("Configure AssetStore Folder path with 'assetStore:folder:path'.");
}
builder.Register(c => new FolderAssetStore(path, c.Resolve<ISemanticLog>()))
.As<IAssetStore>()
.As<IExternalSystem>()
.SingleInstance();
}
else if (string.Equals(assetStoreType, "GoogleCloud", StringComparison.OrdinalIgnoreCase))
{
var bucketName = Configuration.GetValue<string>("assetStore:googleCloud:bucket");
if (string.IsNullOrWhiteSpace(bucketName))
{
throw new ConfigurationException("Configure AssetStore GoogleCloud bucket with 'assetStore:googleCloud:bucket'.");
}
builder.Register(c => new GoogleCloudAssetStore(bucketName))
.As<IAssetStore>()
.As<IExternalSystem>()
.SingleInstance();
}
else if (string.Equals(assetStoreType, "AzureBlob", StringComparison.OrdinalIgnoreCase))
{
var connectionString = Configuration.GetValue<string>("assetStore:azureBlob:connectionString");
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ConfigurationException("Configure AssetStore AzureBlob connection string with 'assetStore:azureBlob:connectionString'.");
}
var containerName = Configuration.GetValue<string>("assetStore:azureBlob:containerName");
if (string.IsNullOrWhiteSpace(containerName))
{
throw new ConfigurationException("Configure AssetStore AzureBlob container with 'assetStore:azureBlob:containerName'.");
}
builder.Register(c => new AzureBlobAssetStore(connectionString, containerName))
.As<IAssetStore>()
.As<IExternalSystem>()
.SingleInstance();
}
else
{
throw new ConfigurationException($"Unsupported value '{assetStoreType}' for 'assetStore:type', supported: AzureBlob, Folder, GoogleCloud.");
}
}
}
}

28
src/Squidex/Config/Domain/EventPublishersModule.cs → src/Squidex/Config/Domain/EventPublishersServices.cs

@ -1,5 +1,5 @@
// ==========================================================================
// EventPublishersModule.cs
// EventPublishersServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -7,26 +7,19 @@
// ==========================================================================
using System;
using Autofac;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Config.Domain
{
public sealed class EventPublishersModule : Module
public static class EventPublishersServices
{
private IConfiguration Configuration { get; }
public EventPublishersModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
public static void AddMyEventPublishersServices(this IServiceCollection services, IConfiguration config)
{
var eventPublishers = Configuration.GetSection("eventPublishers");
var eventPublishers = config.GetSection("eventPublishers");
foreach (var child in eventPublishers.GetChildren())
{
@ -37,15 +30,15 @@ namespace Squidex.Config.Domain
throw new ConfigurationException($"Configure EventPublisher type with 'eventPublishers:{child.Key}:type'.");
}
var eventsFilter = Configuration.GetValue<string>("eventsFilter");
var eventsFilter = config.GetValue<string>("eventsFilter");
var enabled = child.GetValue<bool>("enabled");
if (string.Equals(eventPublisherType, "RabbitMq", StringComparison.OrdinalIgnoreCase))
{
var configuration = child.GetValue<string>("configuration");
var publisherConfig = child.GetValue<string>("configuration");
if (string.IsNullOrWhiteSpace(configuration))
if (string.IsNullOrWhiteSpace(publisherConfig))
{
throw new ConfigurationException($"Configure EventPublisher RabbitMq configuration with 'eventPublishers:{child.Key}:configuration'.");
}
@ -61,10 +54,9 @@ namespace Squidex.Config.Domain
if (enabled)
{
builder.Register(c => new RabbitMqEventConsumer(c.Resolve<JsonSerializerSettings>(), name, configuration, exchange, eventsFilter))
services.AddSingleton(c => new RabbitMqEventConsumer(c.GetRequiredService<JsonSerializerSettings>(), name, publisherConfig, exchange, eventsFilter))
.As<IEventConsumer>()
.As<IExternalSystem>()
.SingleInstance();
.As<IExternalSystem>();
}
}
else

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

@ -1,112 +0,0 @@
// ==========================================================================
// EventStoreModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Autofac;
using Autofac.Core;
using EventStore.ClientAPI;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Events.Actors;
namespace Squidex.Config.Domain
{
public sealed class EventStoreModule : Module
{
private const string MongoClientRegistration = "EventStoreMongoClient";
private const string MongoDatabaseRegistration = "EventStoreMongoDatabase";
private IConfiguration Configuration { get; }
public EventStoreModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
var consumeEvents = Configuration.GetValue<bool>("eventStore:consume");
if (consumeEvents)
{
builder.RegisterType<EventConsumerActor>()
.AsSelf()
.InstancePerDependency();
}
var eventStoreType = Configuration.GetValue<string>("eventStore:type");
if (string.IsNullOrWhiteSpace(eventStoreType))
{
throw new ConfigurationException("Configure EventStore type with 'eventStore:type'.");
}
if (string.Equals(eventStoreType, "MongoDb", StringComparison.OrdinalIgnoreCase))
{
var configuration = Configuration.GetValue<string>("eventStore:mongoDb:configuration");
if (string.IsNullOrWhiteSpace(configuration))
{
throw new ConfigurationException("Configure EventStore MongoDb configuration with 'eventStore:mongoDb:configuration'.");
}
var database = Configuration.GetValue<string>("eventStore:mongoDb:database");
if (string.IsNullOrWhiteSpace(database))
{
throw new ConfigurationException("Configure EventStore MongoDb Database name with 'eventStore:mongoDb:database'.");
}
builder.Register(c => Singletons<IMongoClient>.GetOrAdd(configuration, s => new MongoClient(s)))
.Named<IMongoClient>(MongoClientRegistration)
.SingleInstance();
builder.Register(c => c.ResolveNamed<IMongoClient>(MongoClientRegistration).GetDatabase(database))
.Named<IMongoDatabase>(MongoDatabaseRegistration)
.SingleInstance();
builder.RegisterType<MongoEventStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IExternalSystem>()
.As<IEventStore>()
.SingleInstance();
}
else if (string.Equals(eventStoreType, "GetEventStore", StringComparison.OrdinalIgnoreCase))
{
var configuration = Configuration.GetValue<string>("eventStore:getEventStore:configuration");
if (string.IsNullOrWhiteSpace(configuration))
{
throw new ConfigurationException("Configure GetEventStore EventStore configuration with 'eventStore:getEventStore:configuration'.");
}
var projectionHost = Configuration.GetValue<string>("eventStore:getEventStore:projectionHost");
if (string.IsNullOrWhiteSpace(projectionHost))
{
throw new ConfigurationException("Configure GetEventStore EventStore projection host with 'eventStore:getEventStore:projectionHost'.");
}
var prefix = Configuration.GetValue<string>("eventStore:getEventStore:prefix");
var connection = EventStoreConnection.Create(configuration);
builder.Register(c => new GetEventStore(connection, prefix, projectionHost))
.As<IExternalSystem>()
.As<IEventStore>()
.SingleInstance();
}
else
{
throw new ConfigurationException($"Unsupported value '{eventStoreType}' for 'eventStore:type', supported: MongoDb, GetEventStore.");
}
}
}
}

61
src/Squidex/Config/Domain/EventStoreServices.cs

@ -0,0 +1,61 @@
// ==========================================================================
// EventStoreServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using EventStore.ClientAPI;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
namespace Squidex.Config.Domain
{
public static class EventStoreServices
{
public static void AddMyEventStoreServices(this IServiceCollection services, IConfiguration config)
{
var consumeEvents = config.GetOptionalValue("eventStore:consume", false);
if (!consumeEvents)
{
return;
}
config.ConfigureByOption("eventStore:type", new Options
{
["MongoDb"] = () =>
{
var mongoConfiguration = config.GetRequiredValue("eventStore:mongoDb:configuration");
var mongoDatabaseName = config.GetRequiredValue("eventStore:mongoDb:database");
services.AddSingleton(c =>
{
var mongoClient = Singletons<IMongoClient>.GetOrAdd(mongoConfiguration, s => new MongoClient(s));
var mongDatabase = mongoClient.GetDatabase(mongoDatabaseName);
return new MongoEventStore(mongDatabase, c.GetRequiredService<IEventNotifier>());
})
.As<IExternalSystem>()
.As<IEventStore>();
},
["GetEventStore"] = () =>
{
var eventStoreConfiguration = config.GetRequiredValue("eventStore:getEventStore:configuration");
var eventStoreProjectionHost = config.GetRequiredValue("eventStore:getEventStore:projectionHost");
var eventStorePrefix = config.GetValue<string>("eventStore:getEventStore:prefix");
var connection = EventStoreConnection.Create(eventStoreConfiguration);
services.AddSingleton(c => new GetEventStore(connection, eventStorePrefix, eventStoreProjectionHost))
.As<IExternalSystem>()
.As<IEventStore>();
}
});
}
}
}

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

@ -1,155 +0,0 @@
// ==========================================================================
// InfrastructureModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// 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.Infrastructure;
using Squidex.Infrastructure.Actors;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Assets.ImageSharp;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline;
namespace Squidex.Config.Domain
{
public sealed class InfrastructureModule : Module
{
private IConfiguration Configuration { get; }
public InfrastructureModule(IConfiguration configuration)
{
Configuration = configuration;
}
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();
builder.RegisterType<BackgroundUsageTracker>()
.As<IUsageTracker>()
.SingleInstance();
builder.RegisterType<HttpContextAccessor>()
.As<IHttpContextAccessor>()
.SingleInstance();
builder.RegisterType<ActionContextAccessor>()
.As<IActionContextAccessor>()
.SingleInstance();
builder.RegisterType<DefaultDomainObjectRepository>()
.As<IDomainObjectRepository>()
.SingleInstance();
builder.RegisterType<DefaultDomainObjectFactory>()
.As<IDomainObjectFactory>()
.SingleInstance();
builder.RegisterType<AggregateHandler>()
.As<IAggregateHandler>()
.SingleInstance();
builder.RegisterType<InMemoryCommandBus>()
.As<ICommandBus>()
.SingleInstance();
builder.RegisterType<DefaultEventNotifier>()
.As<IEventNotifier>()
.SingleInstance();
builder.RegisterType<DefaultStreamNameResolver>()
.As<IStreamNameResolver>()
.SingleInstance();
builder.RegisterType<ImageSharpAssetThumbnailGenerator>()
.As<IAssetThumbnailGenerator>()
.SingleInstance();
builder.Register(c => new InvalidatingMemoryCache(new MemoryCache(c.Resolve<IOptions<MemoryCacheOptions>>()), c.Resolve<IPubSub>()))
.As<IMemoryCache>()
.SingleInstance();
builder.RegisterType<DefaultRemoteActorChannel>()
.As<IRemoteActorChannel>()
.SingleInstance();
builder.RegisterType<RemoteActors>()
.As<IActors>()
.AsSelf()
.SingleInstance();
builder.RegisterType<EventConsumerCleaner>()
.AsSelf()
.SingleInstance();
builder.RegisterType<EventDataFormatter>()
.AsSelf()
.SingleInstance();
}
}
}

110
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -0,0 +1,110 @@
// ==========================================================================
// InfrastructureServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using NodaTime;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Assets.ImageSharp;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Events.Orleans;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Pipeline;
namespace Squidex.Config.Domain
{
public static class InfrastructureServices
{
public static void AddMyInfrastructureServices(this IServiceCollection services, IConfiguration config)
{
if (config.GetValue<bool>("logging:human"))
{
services.AddSingleton(c => new Func<IObjectWriter>(() => new JsonLogWriter(Formatting.Indented, true)));
}
else
{
services.AddSingleton(c => new Func<IObjectWriter>(() => new JsonLogWriter()));
}
var loggingFile = config.GetValue<string>("logging:file");
if (!string.IsNullOrWhiteSpace(loggingFile))
{
services.AddSingleton(new FileChannel(loggingFile))
.As<ILogChannel>()
.As<IExternalSystem>();
}
services.AddSingleton(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid()))
.As<ILogAppender>();
services.AddSingleton<ActionContextLogAppender>()
.As<ILogAppender>();
services.AddSingleton<TimestampLogAppender>()
.As<ILogAppender>();
services.AddSingleton<DebugLogChannel>()
.As<ILogChannel>();
services.AddSingleton<ConsoleLogChannel>()
.As<ILogChannel>();
services.AddSingleton<SemanticLog>()
.As<ISemanticLog>();
services.AddSingleton(SystemClock.Instance)
.As<IClock>();
services.AddSingleton<BackgroundUsageTracker>()
.As<IUsageTracker>();
services.AddSingleton<HttpContextAccessor>()
.As<IHttpContextAccessor>();
services.AddSingleton<ActionContextAccessor>()
.As<IActionContextAccessor>();
services.AddSingleton<DefaultDomainObjectRepository>()
.As<IDomainObjectRepository>();
services.AddSingleton<DefaultDomainObjectFactory>()
.As<IDomainObjectFactory>();
services.AddSingleton<AggregateHandler>()
.As<IAggregateHandler>();
services.AddSingleton<InMemoryCommandBus>()
.As<ICommandBus>();
services.AddSingleton<DefaultStreamNameResolver>()
.As<IStreamNameResolver>();
services.AddSingleton<ImageSharpAssetThumbnailGenerator>()
.As<IAssetThumbnailGenerator>();
services.AddSingleton<EventDataFormatter>();
services.AddSingleton(c => new InvalidatingMemoryCache(
new MemoryCache(
c.GetRequiredService<IOptions<MemoryCacheOptions>>()),
c.GetRequiredService<IPubSub>()))
.As<IMemoryCache>();
}
}
}

36
src/Squidex/Config/Domain/LoggingExtensions.cs

@ -0,0 +1,36 @@
// ==========================================================================
// LoggingExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure.Log;
namespace Squidex.Config.Domain
{
public static class LoggingExtensions
{
public static void LogConfiguration(this IServiceProvider services)
{
var log = services.GetRequiredService<ISemanticLog>();
var config = services.GetRequiredService<IConfiguration>();
log.LogInformation(w => w
.WriteProperty("message", "Application started")
.WriteObject("environment", c =>
{
foreach (var kvp in config.AsEnumerable().Where(kvp => kvp.Value != null))
{
c.WriteProperty(kvp.Key, kvp.Value);
}
}));
}
}
}

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

@ -1,69 +0,0 @@
// ==========================================================================
// PubSubModule.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 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>.GetOrAddLazy(configuration, s => ConnectionMultiplexer.Connect(s)))
.Named<Lazy<IConnectionMultiplexer>>(RedisRegistration)
.SingleInstance();
builder.RegisterType<RedisPubSub>()
.WithParameter(ResolvedParameter.ForNamed<Lazy<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.");
}
}
}
}

41
src/Squidex/Config/Domain/PubSubServices.cs

@ -0,0 +1,41 @@
// ==========================================================================
// PubSubServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using StackExchange.Redis;
namespace Squidex.Config.Domain
{
public static class PubSubServices
{
public static void AddMyPubSubServices(this IServiceCollection services, IConfiguration config)
{
config.ConfigureByOption("pubSub:type", new Options
{
["InMemory"] = () =>
{
services.AddSingleton<InMemoryPubSub>()
.As<IPubSub>();
},
["Redis"] = () =>
{
var configuration = config.GetRequiredValue("pubsub:redis:configuration");
var redis = Singletons<IConnectionMultiplexer>.GetOrAddLazy(configuration, s => ConnectionMultiplexer.Connect(s));
services.AddSingleton(c => new RedisPubSub(redis, c.GetRequiredService<ISemanticLog>()))
.As<IPubSub>()
.As<IExternalSystem>();
}
});
}
}
}

148
src/Squidex/Config/Domain/ReadModule.cs

@ -1,148 +0,0 @@
// ==========================================================================
// ReadModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using Autofac;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.ActionHandlers;
using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Read.Apps.Services.Implementations;
using Squidex.Domain.Apps.Read.Contents;
using Squidex.Domain.Apps.Read.Contents.Edm;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Read.History;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Read.Schemas.Services.Implementations;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Pipeline;
namespace Squidex.Config.Domain
{
public sealed class ReadModule : Module
{
private IConfiguration Configuration { get; }
public ReadModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
builder.Register(c => c.Resolve<IOptions<MyUsageOptions>>().Value?.Plans ?? Enumerable.Empty<ConfigAppLimitsPlan>())
.As<IEnumerable<ConfigAppLimitsPlan>>()
.AsSelf()
.SingleInstance();
builder.Register(c => new GraphQLUrlGenerator(
c.Resolve<IOptions<MyUrlsOptions>>(),
c.Resolve<IAssetStore>(),
Configuration.GetValue<bool>("assetStore:exposeSourceUrl")))
.As<IGraphQLUrlGenerator>()
.AsSelf()
.SingleInstance();
builder.RegisterType<CachingGraphQLService>()
.As<IGraphQLService>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<ContentQueryService>()
.As<IContentQueryService>()
.AsSelf()
.SingleInstance();
builder.RegisterType<CachingAppProvider>()
.As<IAppProvider>()
.AsSelf()
.SingleInstance();
builder.RegisterType<ConfigAppPlansProvider>()
.As<IAppPlansProvider>()
.AsSelf()
.SingleInstance();
builder.RegisterType<CachingSchemaProvider>()
.As<ISchemaProvider>()
.AsSelf()
.SingleInstance();
builder.RegisterType<AssetUserPictureStore>()
.As<IUserPictureStore>()
.AsSelf()
.SingleInstance();
builder.RegisterType<AppHistoryEventsCreator>()
.As<IHistoryEventsCreator>()
.AsSelf()
.SingleInstance();
builder.RegisterType<ContentHistoryEventsCreator>()
.As<IHistoryEventsCreator>()
.AsSelf()
.SingleInstance();
builder.RegisterType<SchemaHistoryEventsCreator>()
.As<IHistoryEventsCreator>()
.AsSelf()
.SingleInstance();
builder.RegisterType<NoopAppPlanBillingManager>()
.As<IAppPlanBillingManager>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<RuleDequeuer>()
.As<IExternalSystem>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<RuleEnqueuer>()
.As<IEventConsumer>()
.AsSelf()
.InstancePerDependency();
builder.RegisterType<ContentChangedTriggerHandler>()
.As<IRuleTriggerHandler>()
.AsSelf()
.SingleInstance();
builder.RegisterType<WebhookActionHandler>()
.As<IRuleActionHandler>()
.AsSelf()
.SingleInstance();
builder.RegisterType<RuleService>()
.AsSelf()
.SingleInstance();
builder.RegisterType<EdmModelBuilder>()
.AsSelf()
.SingleInstance();
builder.Register(c =>
{
var eventConsumers = c.Resolve<IEnumerable<IEventConsumer>>();
return new EventConsumerFactory(x => eventConsumers.First(e => e.Name == x));
})
.AsSelf()
.SingleInstance();
}
}
}

119
src/Squidex/Config/Domain/ReadServices.cs

@ -0,0 +1,119 @@
// ==========================================================================
// ReadServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.HandleRules.ActionHandlers;
using Squidex.Domain.Apps.Core.HandleRules.Triggers;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Apps.Services;
using Squidex.Domain.Apps.Read.Apps.Services.Implementations;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Contents;
using Squidex.Domain.Apps.Read.Contents.Edm;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Read.History;
using Squidex.Domain.Apps.Read.Rules;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Apps.Read.Schemas.Services.Implementations;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Pipeline;
namespace Squidex.Config.Domain
{
public static class ReadServices
{
public static void AddMyReadServices(this IServiceCollection services, IConfiguration config)
{
var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true);
services.AddSingleton(c => new GraphQLUrlGenerator(
c.GetRequiredService<IOptions<MyUrlsOptions>>(),
c.GetRequiredService<IAssetStore>(),
exposeSourceUrl))
.As<IGraphQLUrlGenerator>();
services.AddSingleton(c => c.GetService<IOptions<MyUsageOptions>>()?.Value?.Plans.OrEmpty());
services.AddSingleton<CachingGraphQLService>()
.As<IGraphQLService>();
services.AddSingleton<ContentQueryService>()
.As<IContentQueryService>();
services.AddSingleton<CachingAppProvider>()
.As<IAppProvider>();
services.AddSingleton<ConfigAppPlansProvider>()
.As<IAppPlansProvider>();
services.AddSingleton<CachingSchemaProvider>()
.As<ISchemaProvider>();
services.AddSingleton<AssetUserPictureStore>()
.As<IUserPictureStore>();
services.AddSingleton<AppHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
services.AddSingleton<ContentHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
services.AddSingleton<SchemaHistoryEventsCreator>()
.As<IHistoryEventsCreator>();
services.AddSingleton<NoopAppPlanBillingManager>()
.As<IAppPlanBillingManager>();
services.AddSingleton<RuleDequeuer>()
.As<IExternalSystem>();
services.AddSingleton<RuleEnqueuer>()
.As<IEventConsumer>();
services.AddSingleton<ContentChangedTriggerHandler>()
.As<IRuleTriggerHandler>();
services.AddSingleton<WebhookActionHandler>()
.As<IRuleActionHandler>();
services.AddSingleton(c => new CompoundEventConsumer(c.GetServices<IAssetEventConsumer>().ToArray()))
.As<IEventConsumer>();
services.AddSingleton(c =>
new CompoundEventConsumer(
c.GetServices<IAppEventConsumer>().OfType<IEventConsumer>()
.Concat(c.GetRequiredService<CachingAppProvider>()).ToArray()))
.As<IEventConsumer>();
services.AddSingleton(c =>
new CompoundEventConsumer(
c.GetServices<ISchemaEventConsumer>().OfType<IEventConsumer>()
.Concat(c.GetRequiredService<CachingGraphQLService>())
.Concat(c.GetRequiredService<CachingSchemaProvider>()).ToArray()))
.As<IEventConsumer>();
services.AddSingleton(c =>
{
var allEventConsumers = c.GetServices<IEventConsumer>();
return new EventConsumerFactory(n => allEventConsumers.FirstOrDefault(x => x.Name == n));
});
services.AddSingleton<RuleService>();
services.AddSingleton<EdmModelBuilder>();
}
}
}

8
src/Squidex/Config/Domain/Serializers.cs → src/Squidex/Config/Domain/SerializationServices.cs

@ -1,5 +1,5 @@
// ==========================================================================
// Serializers.cs
// SerializationServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -23,7 +23,7 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Config.Domain
{
public static class Serializers
public static class SerializationServices
{
private static readonly TypeNameRegistry TypeNameRegistry = new TypeNameRegistry();
private static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings();
@ -61,7 +61,7 @@ namespace Squidex.Config.Domain
return settings;
}
static Serializers()
static SerializationServices()
{
TypeNameRegistry.MapUnmapped(typeof(SquidexCoreModel).Assembly);
TypeNameRegistry.MapUnmapped(typeof(SquidexEvents).Assembly);
@ -72,7 +72,7 @@ namespace Squidex.Config.Domain
BsonJsonConvention.Register(JsonSerializer.Create(SerializerSettings));
}
public static IServiceCollection AddMyEventFormatter(this IServiceCollection services)
public static IServiceCollection AddMySerializers(this IServiceCollection services)
{
services.AddSingleton(t => TypeNameRegistry);
services.AddSingleton(t => FieldRegistry);

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

@ -1,44 +0,0 @@
// ==========================================================================
// StoreModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using Autofac;
using Microsoft.Extensions.Configuration;
using Squidex.Infrastructure;
namespace Squidex.Config.Domain
{
public class StoreModule : Module
{
private IConfiguration Configuration { get; }
public StoreModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
var storeType = Configuration.GetValue<string>("store:type");
if (string.IsNullOrWhiteSpace(storeType))
{
throw new ConfigurationException("Configure the Store type with 'store:type'.");
}
if (string.Equals(storeType, "MongoDB", StringComparison.OrdinalIgnoreCase))
{
builder.RegisterModule(new StoreMongoDbModule(Configuration));
}
else
{
throw new ConfigurationException($"Unsupported value '{storeType}' for 'stores:type', supported: MongoDb.");
}
}
}
}

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

@ -1,208 +0,0 @@
// ==========================================================================
// StoreMongoDbModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Autofac;
using Autofac.Core;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using MongoDB.Driver;
using Squidex.Domain.Apps.Read.Apps.Repositories;
using Squidex.Domain.Apps.Read.Apps.Services.Implementations;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Domain.Apps.Read.Contents.GraphQL;
using Squidex.Domain.Apps.Read.Contents.Repositories;
using Squidex.Domain.Apps.Read.History.Repositories;
using Squidex.Domain.Apps.Read.MongoDb.Apps;
using Squidex.Domain.Apps.Read.MongoDb.Assets;
using Squidex.Domain.Apps.Read.MongoDb.Contents;
using Squidex.Domain.Apps.Read.MongoDb.History;
using Squidex.Domain.Apps.Read.MongoDb.Rules;
using Squidex.Domain.Apps.Read.MongoDb.Schemas;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Domain.Apps.Read.Schemas.Services.Implementations;
using Squidex.Domain.Users;
using Squidex.Domain.Users.MongoDb;
using Squidex.Domain.Users.MongoDb.Infrastructure;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Shared.Users;
namespace Squidex.Config.Domain
{
public class StoreMongoDbModule : Module
{
private const string MongoClientRegistration = "StoreMongoClient";
private const string MongoDatabaseRegistration = "StoreMongoDatabaseName";
private const string MongoContentDatabaseRegistration = "StoreMongoDatabaseNameContent";
private IConfiguration Configuration { get; }
public StoreMongoDbModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
var configuration = Configuration.GetValue<string>("store:mongoDb:configuration");
if (string.IsNullOrWhiteSpace(configuration))
{
throw new ConfigurationException("Configure the Store MongoDb configuration with 'store:mongoDb:configuration'.");
}
var database = Configuration.GetValue<string>("store:mongoDb:database");
if (string.IsNullOrWhiteSpace(database))
{
throw new ConfigurationException("Configure the Store MongoDb database with 'store:mongoDb:database'.");
}
var contentDatabase = Configuration.GetValue<string>("store:mongoDb:contentDatabase");
if (string.IsNullOrWhiteSpace(contentDatabase))
{
contentDatabase = database;
}
builder.Register(c => Singletons<IMongoClient>.GetOrAdd(configuration, s => new MongoClient(s)))
.Named<IMongoClient>(MongoClientRegistration)
.SingleInstance();
builder.Register(c => c.ResolveNamed<IMongoClient>(MongoClientRegistration).GetDatabase(database))
.Named<IMongoDatabase>(MongoDatabaseRegistration)
.SingleInstance();
builder.Register(c => c.ResolveNamed<IMongoClient>(MongoClientRegistration).GetDatabase(contentDatabase))
.Named<IMongoDatabase>(MongoContentDatabaseRegistration)
.SingleInstance();
builder.RegisterType<MongoUserStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IUserStore<IUser>>()
.As<IUserFactory>()
.As<IUserResolver>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoRoleStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IRoleStore<IRole>>()
.As<IRoleFactory>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoPersistedGrantStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IPersistedGrantStore>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoUsageStore>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IUsageStore>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoHistoryEventRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IHistoryEventRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoEventConsumerInfoRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IEventConsumerInfoRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoContentRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoContentDatabaseRegistration))
.As<IContentRepository>()
.As<IEventConsumer>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoRuleEventRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IRuleEventRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoAppRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IAppRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoSchemaRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<ISchemaRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoAssetStatsRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IAssetStatsRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoAssetRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IAssetRepository>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.RegisterType<MongoRuleRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IRuleRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>()
.AsSelf()
.SingleInstance();
builder.Register(c =>
new CompoundEventConsumer(
c.Resolve<MongoAssetRepository>(),
c.Resolve<MongoAssetStatsRepository>()))
.As<IEventConsumer>()
.AsSelf()
.SingleInstance();
builder.Register(c =>
new CompoundEventConsumer(
c.Resolve<MongoSchemaRepository>(),
c.Resolve<CachingGraphQLService>(),
c.Resolve<CachingSchemaProvider>()))
.As<IEventConsumer>()
.AsSelf()
.SingleInstance();
builder.Register(c =>
new CompoundEventConsumer(
c.Resolve<MongoAppRepository>(),
c.Resolve<CachingAppProvider>()))
.As<IEventConsumer>()
.AsSelf()
.SingleInstance();
}
}
}

118
src/Squidex/Config/Domain/StoreServices.cs

@ -0,0 +1,118 @@
// ==========================================================================
// StoreServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Read.Apps;
using Squidex.Domain.Apps.Read.Apps.Repositories;
using Squidex.Domain.Apps.Read.Assets;
using Squidex.Domain.Apps.Read.Assets.Repositories;
using Squidex.Domain.Apps.Read.Contents.Repositories;
using Squidex.Domain.Apps.Read.History;
using Squidex.Domain.Apps.Read.History.Repositories;
using Squidex.Domain.Apps.Read.MongoDb.Apps;
using Squidex.Domain.Apps.Read.MongoDb.Assets;
using Squidex.Domain.Apps.Read.MongoDb.Contents;
using Squidex.Domain.Apps.Read.MongoDb.History;
using Squidex.Domain.Apps.Read.MongoDb.Rules;
using Squidex.Domain.Apps.Read.MongoDb.Schemas;
using Squidex.Domain.Apps.Read.Rules.Repositories;
using Squidex.Domain.Apps.Read.Schemas;
using Squidex.Domain.Apps.Read.Schemas.Repositories;
using Squidex.Domain.Apps.Read.Schemas.Services;
using Squidex.Domain.Users;
using Squidex.Domain.Users.MongoDb;
using Squidex.Domain.Users.MongoDb.Infrastructure;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.UsageTracking;
using Squidex.Shared.Users;
namespace Squidex.Config.Domain
{
public static class StoreServices
{
public static void AddMyStoreServices(this IServiceCollection services, IConfiguration config)
{
config.ConfigureByOption("store:type", new Options
{
["MongoDB"] = () =>
{
var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration");
var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database");
var mongoContentDatabaseName = config.GetOptionalValue("store:mongoDb:contentDatabase", mongoDatabaseName);
var mongoClient = Singletons<IMongoClient>.GetOrAdd(mongoConfiguration, s => new MongoClient(s));
var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName);
var mongoContentDatabase = mongoClient.GetDatabase(mongoContentDatabaseName);
services.AddSingleton(c => new MongoUserStore(mongoDatabase))
.As<IUserStore<IUser>>()
.As<IUserFactory>()
.As<IUserResolver>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoRoleStore(mongoDatabase))
.As<IRoleStore<IRole>>()
.As<IRoleFactory>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoPersistedGrantStore(mongoDatabase))
.As<IPersistedGrantStore>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoUsageStore(mongoDatabase))
.As<IUsageStore>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoContentRepository(mongoContentDatabase, c.GetService<ISchemaProvider>()))
.As<IContentRepository>()
.As<IEventConsumer>();
services.AddSingleton(c => new MongoRuleEventRepository(mongoDatabase))
.As<IRuleEventRepository>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices<IHistoryEventsCreator>()))
.As<IHistoryEventRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoAppRepository(mongoDatabase))
.As<IAppRepository>()
.As<IAppEventConsumer>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoSchemaRepository(mongoDatabase, c.GetRequiredService<FieldRegistry>()))
.As<ISchemaRepository>()
.As<ISchemaEventConsumer>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoAssetStatsRepository(mongoDatabase))
.As<IAssetStatsRepository>()
.As<IAssetEventConsumer>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoAssetRepository(mongoDatabase))
.As<IAssetRepository>()
.As<IAssetEventConsumer>()
.As<IExternalSystem>();
services.AddSingleton(c => new MongoRuleRepository(mongoDatabase))
.As<IRuleRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>();
}
});
}
}
}

28
src/Squidex/Config/Domain/SystemExtensions.cs

@ -0,0 +1,28 @@
// ==========================================================================
// SystemExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure;
namespace Squidex.Config.Domain
{
public static class SystemExtensions
{
public static void TestExternalSystems(this IServiceProvider services)
{
var systems = services.GetRequiredService<IEnumerable<IExternalSystem>>();
foreach (var system in systems)
{
system.Connect();
}
}
}
}

56
src/Squidex/Config/Domain/Usages.cs

@ -1,56 +0,0 @@
// ==========================================================================
// Usages.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Actors;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Events.Actors;
namespace Squidex.Config.Domain
{
public static class Usages
{
public static IApplicationBuilder UseMyEventStore(this IApplicationBuilder app)
{
var services = app.ApplicationServices;
services.GetService<EventConsumerCleaner>().CleanAsync().Wait();
var consumers = services.GetServices<IEventConsumer>();
foreach (var consumer in consumers)
{
var actor = services.GetService<EventConsumerActor>();
if (actor != null)
{
actor.SubscribeAsync(consumer);
services.GetService<RemoteActors>().Connect(consumer.Name, actor);
}
}
return app;
}
public static IApplicationBuilder TestExternalSystems(this IApplicationBuilder app)
{
var systems = app.ApplicationServices.GetRequiredService<IEnumerable<IExternalSystem>>();
foreach (var system in systems)
{
system.Connect();
}
return app;
}
}
}

113
src/Squidex/Config/Domain/WriteModule.cs

@ -1,113 +0,0 @@
// ==========================================================================
// WriteModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Autofac;
using Microsoft.Extensions.Configuration;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Write.Apps;
using Squidex.Domain.Apps.Write.Assets;
using Squidex.Domain.Apps.Write.Contents;
using Squidex.Domain.Apps.Write.Rules;
using Squidex.Domain.Apps.Write.Schemas;
using Squidex.Domain.Users;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Pipeline.CommandMiddlewares;
namespace Squidex.Config.Domain
{
public class WriteModule : Module
{
private IConfiguration Configuration { get; }
public WriteModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<NoopUserEvents>()
.As<IUserEvents>()
.SingleInstance();
builder.RegisterType<JintScriptEngine>()
.As<IScriptEngine>()
.SingleInstance();
builder.RegisterType<ContentVersionLoader>()
.As<IContentVersionLoader>()
.SingleInstance();
builder.RegisterType<ETagCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<EnrichWithTimestampCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<EnrichWithActorCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<EnrichWithAppIdCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<EnrichWithSchemaIdCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<AppCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<AssetCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<ContentCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<SchemaCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.RegisterType<RuleCommandMiddleware>()
.As<ICommandMiddleware>()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, -1)))
.AsSelf()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<AssetDomainObject>>(c => (id => new AssetDomainObject(id, -1)))
.AsSelf()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<ContentDomainObject>>(c => (id => new ContentDomainObject(id, -1)))
.AsSelf()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<RuleDomainObject>>(c => (id => new RuleDomainObject(id, -1)))
.AsSelf()
.SingleInstance();
builder.Register<DomainObjectFactoryFunction<SchemaDomainObject>>(c =>
{
var fieldRegistry = c.Resolve<FieldRegistry>();
return id => new SchemaDomainObject(id, -1, fieldRegistry);
})
.AsSelf()
.SingleInstance();
}
}
}

79
src/Squidex/Config/Domain/WriteServices.cs

@ -0,0 +1,79 @@
// ==========================================================================
// WriteServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Write.Apps;
using Squidex.Domain.Apps.Write.Assets;
using Squidex.Domain.Apps.Write.Contents;
using Squidex.Domain.Apps.Write.Rules;
using Squidex.Domain.Apps.Write.Schemas;
using Squidex.Domain.Users;
using Squidex.Infrastructure.CQRS.Commands;
using Squidex.Pipeline.CommandMiddlewares;
namespace Squidex.Config.Domain
{
public static class WriteServices
{
public static void AddMyWriteServices(this IServiceCollection services)
{
services.AddSingleton<NoopUserEvents>()
.As<IUserEvents>();
services.AddSingleton<JintScriptEngine>()
.As<IScriptEngine>();
services.AddSingleton<ContentVersionLoader>()
.As<IContentVersionLoader>();
services.AddSingleton<ETagCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<EnrichWithTimestampCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<EnrichWithActorCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<EnrichWithAppIdCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<EnrichWithSchemaIdCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<AppCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<AssetCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<ContentCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<SchemaCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<RuleCommandMiddleware>()
.As<ICommandMiddleware>();
services.AddSingleton<DomainObjectFactoryFunction<AppDomainObject>>(c => (id => new AppDomainObject(id, -1)));
services.AddSingleton<DomainObjectFactoryFunction<RuleDomainObject>>(c => (id => new RuleDomainObject(id, -1)));
services.AddSingleton<DomainObjectFactoryFunction<AssetDomainObject>>(c => (id => new AssetDomainObject(id, -1)));
services.AddSingleton<DomainObjectFactoryFunction<ContentDomainObject>>(c => (id => new ContentDomainObject(id, -1)));
services.AddSingleton<DomainObjectFactoryFunction<SchemaDomainObject>>(c =>
{
var fieldRegistry = c.GetRequiredService<FieldRegistry>();
return id => new SchemaDomainObject(id, -1, fieldRegistry);
});
}
}
}

4
src/Squidex/Config/Identity/AuthenticationUsage.cs → src/Squidex/Config/Identity/AuthenticationExtensions.cs

@ -1,5 +1,5 @@
// ==========================================================================
// AuthenticationUsage.cs
// AuthenticationExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Builder;
namespace Squidex.Config.Identity
{
public static class AuthenticationUsage
public static class AuthenticationExtensions
{
public static IApplicationBuilder UseMyAuthentication(this IApplicationBuilder app)
{

12
src/Squidex/Config/Identity/AuthenticationServices.cs

@ -16,23 +16,21 @@ namespace Squidex.Config.Identity
{
public static class AuthenticationServices
{
public static IServiceCollection AddMyAuthentication(this IServiceCollection services, IConfiguration configuration)
public static void AddMyAuthentication(this IServiceCollection services, IConfiguration config)
{
var identityOptions = configuration.GetSection("identity").Get<MyIdentityOptions>();
var identityOptions = config.GetSection("identity").Get<MyIdentityOptions>();
services.AddAuthentication()
.AddMyGoogleAuthentication(identityOptions)
.AddMyMicrosoftAuthentication(identityOptions)
.AddMyApiProtection(identityOptions, configuration);
return services;
.AddMyApiProtection(identityOptions, config);
}
public static AuthenticationBuilder AddMyApiProtection(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration configuration)
public static AuthenticationBuilder AddMyApiProtection(this AuthenticationBuilder authBuilder, MyIdentityOptions identityOptions, IConfiguration config)
{
var apiScope = Constants.ApiScope;
var urlsOptions = configuration.GetSection("urls").Get<MyUrlsOptions>();
var urlsOptions = config.GetSection("urls").Get<MyUrlsOptions>();
if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl))
{

4
src/Squidex/Config/Identity/IdentityUsage.cs → src/Squidex/Config/Identity/IdentityExtensions.cs

@ -1,5 +1,5 @@
// ==========================================================================
// IdentityUsage.cs
// IdentityExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -20,7 +20,7 @@ using Squidex.Shared.Users;
namespace Squidex.Config.Identity
{
public static class IdentityUsage
public static class IdentityExtensions
{
public static IApplicationBuilder UseMyIdentityServer(this IApplicationBuilder app)
{

93
src/Squidex/Config/Identity/IdentityServerServices.cs

@ -0,0 +1,93 @@
// ==========================================================================
// IdentityServerServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Domain.Users;
using Squidex.Shared.Identity;
using Squidex.Shared.Users;
namespace Squidex.Config.Identity
{
public static class IdentityServerServices
{
public static void AddMyIdentityServer(this IServiceCollection services)
{
X509Certificate2 certificate;
var assembly = typeof(IdentityServices).GetTypeInfo().Assembly;
using (var certStream = assembly.GetManifestResourceStream("Squidex.Config.Identity.Cert.IdentityCert.pfx"))
{
var certData = new byte[certStream.Length];
certStream.Read(certData, 0, certData.Length);
certificate = new X509Certificate2(certData, "password",
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.Exportable);
}
services.AddSingleton(
GetApiResources());
services.AddSingleton(
GetIdentityResources());
services.AddSingleton<IUserClaimsPrincipalFactory<IUser>,
UserClaimsPrincipalFactoryWithEmail>();
services.AddSingleton<IClientStore,
LazyClientStore>();
services.AddSingleton<IResourceStore,
InMemoryResourcesStore>();
services.AddIdentityServer(options =>
{
options.UserInteraction.ErrorUrl = "/error/";
})
.AddAspNetIdentity<IUser>()
.AddInMemoryApiResources(GetApiResources())
.AddInMemoryIdentityResources(GetIdentityResources())
.AddSigningCredential(certificate);
}
private static IEnumerable<ApiResource> GetApiResources()
{
yield return new ApiResource(Constants.ApiScope)
{
UserClaims = new List<string>
{
JwtClaimTypes.Email,
JwtClaimTypes.Role
}
};
}
private static IEnumerable<IdentityResource> GetIdentityResources()
{
yield return new IdentityResources.OpenId();
yield return new IdentityResources.Profile();
yield return new IdentityResources.Email();
yield return new IdentityResource(Constants.RoleScope,
new[]
{
JwtClaimTypes.Role
});
yield return new IdentityResource(Constants.ProfileScope,
new[]
{
SquidexClaimTypes.SquidexDisplayName,
SquidexClaimTypes.SquidexPictureUrl
});
}
}
}

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

@ -6,147 +6,39 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using IdentityModel;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Squidex.Domain.Users;
using Squidex.Infrastructure;
using Squidex.Shared.Identity;
using Squidex.Domain.Users.DataProtection.Orleans;
using Squidex.Shared.Users;
using StackExchange.Redis;
namespace Squidex.Config.Identity
{
public static class IdentityServices
{
public static IServiceCollection AddMyDataProtectection(this IServiceCollection services, IConfiguration configuration)
{
var dataProtection = services.AddDataProtection().SetApplicationName("Squidex");
var keyStoreType = configuration.GetValue<string>("identity:keysStore:type");
if (string.IsNullOrWhiteSpace(keyStoreType))
{
throw new ConfigurationException("Configure KeyStore type with 'identity:keysStore:type'.");
}
if (string.Equals(keyStoreType, "Redis", StringComparison.OrdinalIgnoreCase))
{
var redisConfiguration = configuration.GetValue<string>("identity:keysStore:redis:configuration");
if (string.IsNullOrWhiteSpace(redisConfiguration))
{
throw new ConfigurationException("Configure KeyStore Redis configuration with 'identity:keysStore:redis:configuration'.");
}
var connectionMultiplexer = Singletons<ConnectionMultiplexer>.GetOrAdd(redisConfiguration, s => ConnectionMultiplexer.Connect(s));
dataProtection.PersistKeysToRedis(connectionMultiplexer);
}
else if (string.Equals(keyStoreType, "Folder", StringComparison.OrdinalIgnoreCase))
{
var folderPath = configuration.GetValue<string>("identity:keysStore:folder:path");
if (string.IsNullOrWhiteSpace(folderPath))
{
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;
}
public static IServiceCollection AddMyIdentityServer(this IServiceCollection services)
{
X509Certificate2 certificate;
var assembly = typeof(IdentityServices).GetTypeInfo().Assembly;
using (var certStream = assembly.GetManifestResourceStream("Squidex.Config.Identity.Cert.IdentityCert.pfx"))
{
var certData = new byte[certStream.Length];
certStream.Read(certData, 0, certData.Length);
certificate = new X509Certificate2(certData, "password",
X509KeyStorageFlags.MachineKeySet |
X509KeyStorageFlags.PersistKeySet |
X509KeyStorageFlags.Exportable);
}
services.AddSingleton(
GetApiResources());
services.AddSingleton(
GetIdentityResources());
services.AddSingleton<IUserClaimsPrincipalFactory<IUser>,
UserClaimsPrincipalFactoryWithEmail>();
services.AddSingleton<IClientStore,
LazyClientStore>();
services.AddSingleton<IResourceStore,
InMemoryResourcesStore>();
services.AddIdentityServer(options =>
{
options.UserInteraction.ErrorUrl = "/error/";
})
.AddAspNetIdentity<IUser>()
.AddInMemoryApiResources(GetApiResources())
.AddInMemoryIdentityResources(GetIdentityResources())
.AddSigningCredential(certificate);
return services;
}
public static IServiceCollection AddMyIdentity(this IServiceCollection services)
public static void AddMyIdentity(this IServiceCollection services)
{
services.AddIdentity<IUser, IRole>()
.AddDefaultTokenProviders();
return services;
}
private static IEnumerable<ApiResource> GetApiResources()
public static void AddMyDataProtectection(this IServiceCollection services, IConfiguration config)
{
yield return new ApiResource(Constants.ApiScope)
{
UserClaims = new List<string>
{
JwtClaimTypes.Email,
JwtClaimTypes.Role
}
};
}
var dataProtection = services.AddDataProtection().SetApplicationName("Squidex");
private static IEnumerable<IdentityResource> GetIdentityResources()
{
yield return new IdentityResources.OpenId();
yield return new IdentityResources.Profile();
yield return new IdentityResources.Email();
yield return new IdentityResource(Constants.RoleScope,
new[]
{
JwtClaimTypes.Role
});
yield return new IdentityResource(Constants.ProfileScope,
new[]
services.AddSingleton<OrleansXmlRepository>();
services.AddSingleton<IConfigureOptions<KeyManagementOptions>>(s =>
{
return new ConfigureOptions<KeyManagementOptions>(options =>
{
SquidexClaimTypes.SquidexDisplayName,
SquidexClaimTypes.SquidexPictureUrl
options.XmlRepository = s.GetRequiredService<OrleansXmlRepository>();
});
});
}
}
}

14
src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/ResetConsumerMessage.cs → src/Squidex/Config/Options.cs

@ -1,15 +1,21 @@
// ==========================================================================
// ResetConsumerMessage.cs
// Options.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
namespace Squidex.Infrastructure.CQRS.Events.Actors.Messages
using System;
using System.Collections.Generic;
namespace Squidex.Config
{
[TypeName(nameof(ResetConsumerMessage))]
public sealed class ResetConsumerMessage
public sealed class Options : Dictionary<string, Action>
{
public Options()
: base(StringComparer.OrdinalIgnoreCase)
{
}
}
}

40
src/Squidex/Config/Orleans/ClientServices.cs

@ -0,0 +1,40 @@
// ==========================================================================
// ClientServices.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.DependencyInjection;
using Orleans;
using Orleans.Runtime.Configuration;
using Squidex.Domain.Users.DataProtection.Orleans.Grains.Implementations;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Events.Orleans;
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation;
namespace Squidex.Config.Orleans
{
public static class ClientServices
{
public static void AddAppClient(this IServiceCollection services)
{
services.AddSingleton<OrleansClientEventNotifier>()
.As<IEventNotifier>();
services.AddSingleton(c =>
{
var client = new ClientBuilder()
.UseConfiguration(ClientConfiguration.LocalhostSilo())
.AddApplicationPartsFromReferences(typeof(EventConsumerGrain).Assembly)
.AddApplicationPartsFromReferences(typeof(XmlRepositoryGrain).Assembly)
.Build();
client.Connect().Wait();
return client;
});
}
}
}

11
src/Squidex/Config/Orleans/IOrleansRunner.cs

@ -1,11 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Squidex.Config.Orleans
{
public class IOrleansRunner
{
}
}

26
src/Squidex/Config/Orleans/SiloExtensions.cs

@ -0,0 +1,26 @@
// ==========================================================================
// SiloExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Microsoft.Extensions.Configuration;
using Orleans.Hosting;
namespace Squidex.Config.Orleans
{
public static class SiloExtensions
{
public static ISiloHostBuilder UseContentRoot(this ISiloHostBuilder builder, string path)
{
builder.ConfigureAppConfiguration(config =>
{
config.SetBasePath(path);
});
return builder;
}
}
}

76
src/Squidex/Config/Orleans/SiloServices.cs

@ -6,36 +6,82 @@
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Microsoft.AspNetCore.Hosting;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Orleans;
using Orleans.Hosting;
using Orleans.Runtime.Configuration;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Events.Orleans;
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains;
namespace Squidex.Config.Orleans
{
public static class SiloServices
{
public static IServiceCollection AddOrleans(this IServiceCollection services, IConfiguration configuration, IHostingEnvironment hostingEnvironment)
public static void AddAppSiloServices(this IServiceCollection services, IConfiguration config)
{
var properties = new Dictionary<object, object>();
var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration");
var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database");
var hostBuilderContext = new HostBuilderContext(new Dictionary<object, object>())
var clusterConfiguration =
services.Where(x => x.ServiceType == typeof(ClusterConfiguration))
.Select(x => x.ImplementationInstance)
.Select(x => (ClusterConfiguration)x)
.FirstOrDefault();
if (clusterConfiguration != null)
{
clusterConfiguration.Globals.RegisterBootstrapProvider<EventConsumerBootstrap>("EventConsumers");
}
config.ConfigureByOption("store:type", new Options
{
Configuration = configuration
};
["MongoDB"] = () =>
{
if (clusterConfiguration != null)
{
clusterConfiguration.AddMongoDBStorageProvider("Default", c =>
{
c.ConnectionString = mongoConfiguration;
c.CollectionPrefix = "Orleans_";
c.DatabaseName = mongoDatabaseName;
c.UseJsonFormat = true;
});
clusterConfiguration.AddMongoDBStatisticsProvider("Default", c =>
{
c.ConnectionString = mongoConfiguration;
c.CollectionPrefix = "Orleans_";
c.DatabaseName = mongoDatabaseName;
});
}
var orleansConfig = configuration.GetSection("orleans");
services.UseMongoDBGatewayListProvider(c =>
{
c.ConnectionString = mongoConfiguration;
c.CollectionPrefix = "Orleans_";
c.DatabaseName = mongoDatabaseName;
});
var clusterConfiguration = ClusterConfiguration.LocalhostPrimarySilo(33333);
clusterConfiguration.AddMongoDBStatisticsProvider("Default", orleansConfig);
clusterConfiguration.AddMongoDBStorageProvider("Default", orleansConfig);
services.UseMongoDBMembershipTable(c =>
{
c.ConnectionString = mongoConfiguration;
c.CollectionPrefix = "Orleans_";
c.DatabaseName = mongoDatabaseName;
});
services.AddD
services.UseMongoDBReminders(c =>
{
c.ConnectionString = mongoConfiguration;
c.CollectionPrefix = "Orleans_";
c.DatabaseName = mongoDatabaseName;
});
}
});
return services;
services.AddSingleton<OrleansSiloEventNotifier>()
.As<IEventNotifier>();
}
}
}
}

100
src/Squidex/Config/ServiceExtensions.cs

@ -0,0 +1,100 @@
// ==========================================================================
// ServiceExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure;
namespace Squidex.Config
{
public static class ServiceExtensions
{
public sealed class InterfaceRegistrator<T>
{
private readonly IServiceCollection services;
public InterfaceRegistrator(IServiceCollection services)
{
this.services = services;
}
public InterfaceRegistrator<T> As<TInterface>()
{
if (typeof(TInterface) != typeof(T))
{
this.services.AddSingleton(typeof(TInterface), c =>
{
return c.GetRequiredService<T>();
});
}
return this;
}
}
public static InterfaceRegistrator<T> AddSingleton<T>(this IServiceCollection services, Func<IServiceProvider, T> factory) where T : class
{
services.AddSingleton(typeof(T), factory);
return new InterfaceRegistrator<T>(services);
}
public static InterfaceRegistrator<T> AddSingleton<T>(this IServiceCollection services, T instance) where T : class
{
services.AddSingleton(typeof(T), instance);
return new InterfaceRegistrator<T>(services);
}
public static InterfaceRegistrator<T> AddSingleton<T>(this IServiceCollection services) where T : class
{
services.AddSingleton<T, T>();
return new InterfaceRegistrator<T>(services);
}
public static T GetOptionalValue<T>(this IConfiguration config, string path, T defaultValue = default(T))
{
var value = config.GetValue<T>(path, defaultValue);
return value;
}
public static string GetRequiredValue(this IConfiguration config, string path)
{
var value = config.GetValue<string>(path);
if (string.IsNullOrWhiteSpace(value))
{
var name = string.Join(' ', path.Split(':').Select(x => x.ToPascalCase()));
throw new ConfigurationException($"Configure the {name} with '{path}'.");
}
return value;
}
public static string ConfigureByOption(this IConfiguration config, string path, Options options)
{
var value = config.GetRequiredValue(path);
if (options.TryGetValue(value, out var action))
{
action();
}
else
{
throw new ConfigurationException($"Unsupported value '{value}' for '{path}', supported: {string.Join(' ', options.Keys)}.");
}
return value;
}
}
}

6
src/Squidex/Config/Swagger/SwaggerUsage.cs → src/Squidex/Config/Swagger/SwaggerExtensions.cs

@ -1,5 +1,5 @@
// ==========================================================================
// SwaggerUsage.cs
// SwaggerExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -13,13 +13,13 @@ using NSwag.AspNetCore;
namespace Squidex.Config.Swagger
{
public static class SwaggerUsage
public static class SwaggerExtensions
{
public static void UseMySwagger(this IApplicationBuilder app)
{
var settings = app.ApplicationServices.GetService<SwaggerSettings>();
app.UseSwagger(typeof(SwaggerUsage).GetTypeInfo().Assembly, settings);
app.UseSwagger(typeof(SwaggerExtensions).GetTypeInfo().Assembly, settings);
}
}
}

2
src/Squidex/Config/Web/WebUsages.cs → src/Squidex/Config/Web/WebExtensions.cs

@ -15,7 +15,7 @@ using Squidex.Pipeline;
namespace Squidex.Config.Web
{
public static class WebUsages
public static class WebExtensions
{
public static void UseMyCors(this IApplicationBuilder app)
{

35
src/Squidex/Config/Web/WebModule.cs

@ -1,35 +0,0 @@
// ==========================================================================
// WebModule.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using Autofac;
using Microsoft.Extensions.Configuration;
using Squidex.Pipeline;
namespace Squidex.Config.Web
{
public class WebModule : Module
{
private IConfiguration Configuration { get; }
public WebModule(IConfiguration configuration)
{
Configuration = configuration;
}
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<AppApiFilter>()
.AsSelf()
.SingleInstance();
builder.RegisterType<FileCallbackResultExecutor>()
.AsSelf()
.SingleInstance();
}
}
}

8
src/Squidex/Config/Web/WebDependencies.cs → src/Squidex/Config/Web/WebServices.cs

@ -8,14 +8,20 @@
using Microsoft.Extensions.DependencyInjection;
using Squidex.Config.Domain;
using Squidex.Pipeline;
namespace Squidex.Config.Web
{
public static class WebDependencies
public static class WebServices
{
public static void AddMyMvc(this IServiceCollection services)
{
services.AddSingleton<AppApiFilter>();
services.AddSingleton<FileCallbackResultExecutor>();
services.AddMvc().AddMySerializers();
services.AddCors();
services.AddRouting();
}
}
}

4
src/Squidex/Config/Web/WebpackUsages.cs → src/Squidex/Config/Web/WebpackExtensions.cs

@ -1,5 +1,5 @@
// ==========================================================================
// WebpackUsages.cs
// WebpackExtensions.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -11,7 +11,7 @@ using Squidex.Pipeline;
namespace Squidex.Config.Web
{
public static class WebpackUsages
public static class WebpackExtensions
{
public static IApplicationBuilder UseWebpackProxy(this IApplicationBuilder app)
{

34
src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs

@ -10,10 +10,9 @@ using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NSwag.Annotations;
using Orleans;
using Squidex.Controllers.Api.EventConsumers.Models;
using Squidex.Infrastructure.Actors;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.CQRS.Events.Actors.Messages;
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains;
using Squidex.Infrastructure.Reflection;
using Squidex.Pipeline;
@ -25,14 +24,11 @@ namespace Squidex.Controllers.Api.EventConsumers
[SwaggerIgnore]
public sealed class EventConsumersController : Controller
{
private readonly IEventConsumerInfoRepository eventConsumerRepository;
private readonly IActors actors;
private readonly IEventConsumerRegistryGrain eventConsumerRegistryGrain;
public EventConsumersController(IEventConsumerInfoRepository eventConsumerRepository, IActors actors)
public EventConsumersController(IClusterClient orleans)
{
this.eventConsumerRepository = eventConsumerRepository;
this.actors = actors;
eventConsumerRegistryGrain = orleans.GetGrain<IEventConsumerRegistryGrain>("Default");
}
[HttpGet]
@ -40,7 +36,7 @@ namespace Squidex.Controllers.Api.EventConsumers
[ApiCosts(0)]
public async Task<IActionResult> GetEventConsumers()
{
var entities = await eventConsumerRepository.QueryAsync();
var entities = await eventConsumerRegistryGrain.GetConsumersAsync();
var models = entities.Select(x => SimpleMapper.Map(x, new EventConsumerDto())).ToList();
@ -50,11 +46,9 @@ namespace Squidex.Controllers.Api.EventConsumers
[HttpPut]
[Route("event-consumers/{name}/start/")]
[ApiCosts(0)]
public IActionResult Start(string name)
public async Task<IActionResult> Start(string name)
{
var actor = actors.Get(name);
actor?.Tell(new StartConsumerMessage());
await eventConsumerRegistryGrain.StartAsync(name);
return NoContent();
}
@ -62,11 +56,9 @@ namespace Squidex.Controllers.Api.EventConsumers
[HttpPut]
[Route("event-consumers/{name}/stop/")]
[ApiCosts(0)]
public IActionResult Stop(string name)
public async Task<IActionResult> Stop(string name)
{
var actor = actors.Get(name);
actor?.Tell(new StopConsumerMessage());
await eventConsumerRegistryGrain.StopAsync(name);
return NoContent();
}
@ -74,11 +66,9 @@ namespace Squidex.Controllers.Api.EventConsumers
[HttpPut]
[Route("event-consumers/{name}/reset/")]
[ApiCosts(0)]
public IActionResult Reset(string name)
public async Task<IActionResult> Reset(string name)
{
var actor = actors.Get(name);
actor?.Tell(new ResetConsumerMessage());
await eventConsumerRegistryGrain.ResetAsync(name);
return NoContent();
}

70
src/Squidex/Program.cs

@ -6,31 +6,73 @@
// All rights reserved.
// ==========================================================================
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Orleans.Hosting;
using Orleans.Runtime.Configuration;
using Squidex.Config.Orleans;
using Squidex.Domain.Users.DataProtection.Orleans.Grains.Implementations;
using Squidex.Infrastructure.CQRS.Events.Orleans.Grains.Implementation;
using Squidex.Infrastructure.Log.Adapter;
namespace Squidex
{
public class Program
public static class Program
{
public static void Main(string[] args)
{
new WebHostBuilder()
.UseKestrel(k => { k.AddServerHeader = false; })
var silo = new SiloHostBuilder()
.AddApplicationPartsFromReferences(typeof(EventConsumerGrain).Assembly)
.AddApplicationPartsFromReferences(typeof(XmlRepositoryGrain).Assembly)
.UseConfiguration(ClusterConfiguration.LocalhostPrimarySilo(33333))
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.ConfigureAppConfiguration((hostContext, options) =>
.ConfigureServices((context, services) =>
{
options.Sources.Clear();
options.AddJsonFile("appsettings.json", true, true);
options.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true);
options.AddEnvironmentVariables();
options.AddCommandLine(args);
services.AddAppSiloServices(context.Configuration);
services.AddAppServices(context.Configuration);
})
.Build()
.Run();
.ConfigureLogging(builder =>
{
builder.AddSemanticLog();
})
.ConfigureAppConfiguration((hostContext, builder) =>
{
builder.AddAppConfiguration(GetEnvironment(), args);
})
.Build();
silo.StartAsync().Wait();
try
{
new WebHostBuilder()
.UseKestrel(k => { k.AddServerHeader = false; })
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<WebStartup>()
.ConfigureLogging(builder =>
{
builder.AddSemanticLog();
})
.ConfigureAppConfiguration((hostContext, builder) =>
{
builder.AddAppConfiguration(hostContext.HostingEnvironment.EnvironmentName, args);
})
.Build()
.Run();
}
finally
{
silo.StopAsync().Wait();
}
}
private static string GetEnvironment()
{
var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
return environment ?? "Development";
}
}
}

3
src/Squidex/Squidex.csproj

@ -48,8 +48,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="4.6.2" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="4.2.0" />
<PackageReference Include="EventStore.ClientAPI.NetCore" Version="4.0.2-rc" />
<PackageReference Include="IdentityServer4" Version="2.0.1" />
<PackageReference Include="IdentityServer4.AccessTokenValidation" Version="2.0.0" />
@ -68,6 +66,7 @@
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="2.0.0" />
<PackageReference Include="Microsoft.Data.Edm" Version="5.8.2" />
<PackageReference Include="Microsoft.OData.Core" Version="7.3.1" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0-beta1" />
<PackageReference Include="Microsoft.Orleans.OrleansRuntime" Version="2.0.0-beta1" />
<PackageReference Include="MongoDB.Driver" Version="2.4.4" />
<PackageReference Include="NJsonSchema" Version="9.8.3" />

101
src/Squidex/Startup.cs → src/Squidex/WebStartup.cs

@ -1,5 +1,5 @@
// ==========================================================================
// Startup.cs
// WebStartup.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
@ -9,28 +9,26 @@
using System;
using System.IO;
using System.Linq;
using Autofac;
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Squidex.Config;
using Squidex.Config.Domain;
using Squidex.Config.Identity;
using Squidex.Config.Orleans;
using Squidex.Config.Swagger;
using Squidex.Config.Web;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Log.Adapter;
#pragma warning disable RECS0002 // Convert anonymous method to method group
namespace Squidex
{
public class Startup
public class WebStartup : IStartup
{
private readonly IConfiguration configuration;
private readonly IHostingEnvironment environment;
private static readonly string[] IdentityServerPaths =
{
"/client-callback-popup",
@ -39,98 +37,39 @@ namespace Squidex
"/error"
};
private IConfiguration Configuration { get; }
private IHostingEnvironment Environment { get; }
public Startup(IHostingEnvironment env, IConfiguration config)
public WebStartup(IConfiguration configuration, IHostingEnvironment environment)
{
Environment = env;
Configuration = config;
this.configuration = configuration;
this.environment = environment;
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMySwaggerSettings();
services.AddMyEventFormatter();
services.AddMyDataProtectection(Configuration);
services.AddMyAuthentication(Configuration);
services.AddMyIdentity();
services.AddMyIdentityServer();
services.AddMyMvc();
services.AddCors();
services.AddLogging();
services.AddMemoryCache();
services.AddOptions();
services.AddRouting();
services.Configure<MyUrlsOptions>(
Configuration.GetSection("urls"));
services.Configure<MyIdentityOptions>(
Configuration.GetSection("identity"));
services.Configure<MyUIOptions>(
Configuration.GetSection("ui"));
services.Configure<MyUsageOptions>(
Configuration.GetSection("usage"));
var builder = new ContainerBuilder();
builder.Populate(services);
builder.RegisterModule(new AssetStoreModule(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));
builder.RegisterModule(new WriteModule(Configuration));
var container = builder.Build();
container.Resolve<IApplicationLifetime>().ApplicationStopping.Register(() =>
{
container.Dispose();
});
services.AddAppClient();
services.AddAppServices(configuration);
return new AutofacServiceProvider(container);
return services.BuildServiceProvider();
}
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
public void Configure(IApplicationBuilder app)
{
loggerFactory.AddSemanticLog(app.ApplicationServices.GetRequiredService<ISemanticLog>());
app.TestExternalSystems();
app.ApplicationServices.LogConfiguration();
app.ApplicationServices.TestExternalSystems();
app.UseMyCors();
app.UseMyForwardingRules();
app.UseMyTracking();
MapAndUseIdentity(app);
MapAndUseIdentityServer(app);
MapAndUseApi(app);
MapAndUseFrontend(app);
app.UseMyEventStore();
var log = app.ApplicationServices.GetRequiredService<ISemanticLog>();
log.LogInformation(w => w
.WriteProperty("message", "Application started")
.WriteObject("environment", c =>
{
foreach (var kvp in Configuration.AsEnumerable().Where(kvp => kvp.Value != null))
{
c.WriteProperty(kvp.Key, kvp.Value);
}
}));
}
private void MapAndUseIdentity(IApplicationBuilder app)
private void MapAndUseIdentityServer(IApplicationBuilder app)
{
app.Map(Constants.IdentityPrefix, identityApp =>
{
if (Environment.IsDevelopment())
if (environment.IsDevelopment())
{
identityApp.UseDeveloperExceptionPage();
}
@ -145,7 +84,7 @@ namespace Squidex
identityApp.UseMyAdmin();
identityApp.UseStaticFiles();
identityApp.MapWhen(x => IsIdentityRequest(x), mvcApp =>
identityApp.MapWhen(IsIdentityRequest, mvcApp =>
{
mvcApp.UseMvc();
});
@ -156,7 +95,7 @@ namespace Squidex
{
app.Map(Constants.ApiPrefix, appApi =>
{
if (Environment.IsDevelopment())
if (environment.IsDevelopment())
{
appApi.UseDeveloperExceptionPage();
}
@ -172,7 +111,7 @@ namespace Squidex
private void MapAndUseFrontend(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
if (environment.IsDevelopment())
{
app.UseWebpackProxy();

27
src/Squidex/appsettings.json

@ -189,31 +189,6 @@
/*
* Lock new users automatically, the administrator must unlock them.
*/
"lockAutomatically": false,
"keysStore": {
/*
* Define the type of the key store.
*
* Supported: InMemory (development only), Folder (shared or local), Redis
*
* Read More: https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-storage-providers
*/
"type": "InMemory",
"redis": {
/*
* Connection string to your redis server.
*
* Read More: https://github.com/ServiceStack/ServiceStack.Redis#redis-connection-strings
*/
"configuration": "localhost:6379,resolveDns=1"
},
"folder": {
/*
* Relative or absolute path to your encryption keys folder.
*/
"path": "keys"
}
}
"lockAutomatically": false
}
}

77
tests/Squidex.Infrastructure.Tests/Actors/ActorRemoteTests.cs

@ -1,77 +0,0 @@
// ==========================================================================
// ActorRemoteTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Squidex.Infrastructure.Tasks;
using Xunit;
namespace Squidex.Infrastructure.Actors
{
public class ActorRemoteTests
{
[TypeName(nameof(SuccessMessage))]
public class SuccessMessage : object
{
public int Counter { get; set; }
}
private sealed class MyActor : IActor
{
private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher();
public List<object> Invokes { get; } = new List<object>();
public Task StopAndWaitAsync()
{
return dispatcher.StopAndWaitAsync();
}
public void Tell(object message)
{
dispatcher.DispatchAsync(() =>
{
Invokes.Add(message);
}).Forget();
}
}
private readonly MyActor actor = new MyActor();
private readonly TypeNameRegistry registry = new TypeNameRegistry();
private readonly RemoteActors actors;
private readonly IActor remoteActor;
public ActorRemoteTests()
{
registry.Map(typeof(SuccessMessage));
actors = new RemoteActors(new DefaultRemoteActorChannel(new InMemoryPubSub(), registry));
actors.Connect("my", actor);
remoteActor = actors.Get("my");
}
[Fact]
public async Task Should_handle_messages_sequentially()
{
remoteActor.Tell(new SuccessMessage { Counter = 1 });
remoteActor.Tell(new SuccessMessage { Counter = 2 });
remoteActor.Tell(new SuccessMessage { Counter = 3 });
await actor.StopAndWaitAsync();
actor.Invokes.ShouldBeEquivalentTo(new List<object>
{
new SuccessMessage { Counter = 1 },
new SuccessMessage { Counter = 2 },
new SuccessMessage { Counter = 3 }
});
}
}
}

351
tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs

@ -1,351 +0,0 @@
// ==========================================================================
// EventConsumerActorTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Infrastructure.Actors;
using Squidex.Infrastructure.CQRS.Events.Actors.Messages;
using Squidex.Infrastructure.Log;
using Xunit;
namespace Squidex.Infrastructure.CQRS.Events.Actors
{
public class EventConsumerActorTests
{
public sealed class MyEvent : IEvent
{
}
private sealed class MyEventConsumerInfo : IEventConsumerInfo
{
public bool IsStopped { get; set; }
public string Name { get; set; }
public string Error { get; set; }
public string Position { get; set; }
}
public sealed class MyEventConsumerActor : EventConsumerActor
{
public MyEventConsumerActor(
EventDataFormatter formatter,
IEventStore eventStore,
IEventConsumerInfoRepository eventConsumerInfoRepository,
ISemanticLog log)
: base(formatter, eventStore, eventConsumerInfoRepository, log)
{
}
protected override IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position)
{
return eventStore.CreateSubscription(this, streamFilter, position);
}
}
private readonly IEventConsumerInfoRepository eventConsumerInfoRepository = A.Fake<IEventConsumerInfoRepository>();
private readonly IEventConsumer eventConsumer = A.Fake<IEventConsumer>();
private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>();
private readonly ISemanticLog log = A.Fake<ISemanticLog>();
private readonly IActor sutActor;
private readonly IEventSubscriber sutSubscriber;
private readonly EventDataFormatter formatter = A.Fake<EventDataFormatter>();
private readonly EventData eventData = new EventData();
private readonly Envelope<IEvent> envelope = new Envelope<IEvent>(new MyEvent());
private readonly EventConsumerActor sut;
private readonly MyEventConsumerInfo consumerInfo = new MyEventConsumerInfo();
private readonly string consumerName;
public EventConsumerActorTests()
{
consumerInfo.Position = Guid.NewGuid().ToString();
consumerName = eventConsumer.GetType().Name;
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)).Returns(eventSubscription);
A.CallTo(() => eventConsumer.Name).Returns(consumerName);
A.CallTo(() => eventConsumerInfoRepository.FindAsync(consumerName)).Returns(consumerInfo);
A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope);
sut = new MyEventConsumerActor(
formatter,
eventStore,
eventConsumerInfoRepository,
log);
sutActor = sut;
sutSubscriber = sut;
}
[Fact]
public async Task Should_not_subscribe_to_event_store_when_stopped_in_db()
{
consumerInfo.IsStopped = true;
await OnSubscribeAsync();
sut.Dispose();
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_subscribe_to_event_store_when_not_found_in_db()
{
A.CallTo(() => eventConsumerInfoRepository.FindAsync(consumerName)).Returns(Task.FromResult<IEventConsumerInfo>(null));
await OnSubscribeAsync();
sut.Dispose();
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored))
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_subscribe_to_event_store_when_not_stopped_in_db()
{
await OnSubscribeAsync();
sut.Dispose();
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored))
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_stop_subscription_when_stopped()
{
await OnSubscribeAsync();
sutActor.Tell(new StopConsumerMessage());
sutActor.Tell(new StopConsumerMessage());
sut.Dispose();
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, true, null))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventSubscription.StopAsync())
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_reset_consumer_when_resetting()
{
await OnSubscribeAsync();
sutActor.Tell(new StopConsumerMessage());
sutActor.Tell(new ResetConsumerMessage());
sut.Dispose();
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, true, null))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, null, false, null))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventConsumer.ClearAsync())
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventSubscription.StopAsync())
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, consumerInfo.Position))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, null))
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_invoke_and_update_position_when_event_received()
{
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData);
await OnSubscribeAsync();
await OnEventAsync(eventSubscription, @event);
sut.Dispose();
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, @event.EventPosition, false, null))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventConsumer.On(envelope))
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_ignore_old_events()
{
A.CallTo(() => formatter.Parse(eventData, true))
.Throws(new TypeNameNotFoundException());
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData);
await OnSubscribeAsync();
await OnEventAsync(eventSubscription, @event);
sut.Dispose();
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, @event.EventPosition, false, null))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventConsumer.On(envelope))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription()
{
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData);
await OnSubscribeAsync();
await OnEventAsync(A.Fake<IEventSubscription>(), @event);
sut.Dispose();
A.CallTo(() => eventConsumer.On(envelope))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription()
{
var ex = new InvalidOperationException();
await OnSubscribeAsync();
await OnErrorAsync(A.Fake<IEventSubscription>(), ex);
sut.Dispose();
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, false, ex.ToString()))
.MustNotHaveHappened();
}
[Fact]
public async Task Should_stop_if_resetting_failed()
{
var ex = new InvalidOperationException();
A.CallTo(() => eventConsumer.ClearAsync())
.Throws(ex);
await OnSubscribeAsync();
sutActor.Tell(new ResetConsumerMessage());
sut.Dispose();
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, true, ex.ToString()))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventSubscription.StopAsync())
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_stop_if_handling_failed()
{
var ex = new InvalidOperationException();
A.CallTo(() => eventConsumer.On(envelope))
.Throws(ex);
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData);
await OnSubscribeAsync();
await OnEventAsync(eventSubscription, @event);
sut.Dispose();
A.CallTo(() => eventConsumer.On(envelope))
.MustHaveHappened();
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, true, ex.ToString()))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventSubscription.StopAsync())
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_stop_if_deserialization_failed()
{
var ex = new InvalidOperationException();
A.CallTo(() => formatter.Parse(eventData, true))
.Throws(ex);
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData);
await OnSubscribeAsync();
await OnEventAsync(eventSubscription, @event);
sut.Dispose();
A.CallTo(() => eventConsumer.On(envelope))
.MustNotHaveHappened();
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, true, ex.ToString()))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventSubscription.StopAsync())
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_start_after_stop_when_handling_failed()
{
var exception = new InvalidOperationException();
A.CallTo(() => eventConsumer.On(envelope))
.Throws(exception);
var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData);
await OnSubscribeAsync();
await OnEventAsync(eventSubscription, @event);
sutActor.Tell(new StartConsumerMessage());
sutActor.Tell(new StartConsumerMessage());
sut.Dispose();
A.CallTo(() => eventConsumer.On(envelope))
.MustHaveHappened();
A.CallTo(() => eventConsumerInfoRepository.SetAsync(consumerName, consumerInfo.Position, true, exception.ToString()))
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventSubscription.StopAsync())
.MustHaveHappened(Repeated.Exactly.Once);
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored))
.MustHaveHappened(Repeated.Exactly.Twice);
}
private Task OnErrorAsync(IEventSubscription subscriber, Exception ex)
{
return sutSubscriber.OnErrorAsync(subscriber, ex);
}
private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev)
{
return sutSubscriber.OnEventAsync(subscriber, ev);
}
private Task OnSubscribeAsync()
{
return sut.SubscribeAsync(eventConsumer);
}
}
}

50
tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultEventNotifierTests.cs

@ -1,50 +0,0 @@
// ==========================================================================
// DefaultEventNotifierTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using Xunit;
namespace Squidex.Infrastructure.CQRS.Events
{
public sealed class DefaultEventNotifierTests
{
private readonly DefaultEventNotifier sut = new DefaultEventNotifier(new InMemoryPubSub());
[Fact]
public void Should_invalidate_all_actions()
{
var handler1Handled = 0;
var handler2Handled = 0;
var streamNames = new List<string>();
sut.Subscribe(x =>
{
streamNames.Add(x);
handler1Handled++;
});
sut.NotifyEventsStored("a");
sut.Subscribe(x =>
{
streamNames.Add(x);
handler2Handled++;
});
sut.NotifyEventsStored("b");
Assert.Equal(2, handler1Handled);
Assert.Equal(1, handler2Handled);
Assert.Equal(streamNames.ToArray(), new[] { "a", "b", "b" });
}
}
}

36
tests/Squidex.Infrastructure.Tests/CQRS/Events/EventConsumerCleanerTests.cs

@ -1,36 +0,0 @@
// ==========================================================================
// EventConsumerCleanerTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Xunit;
namespace Squidex.Infrastructure.CQRS.Events
{
public class EventConsumerCleanerTests
{
[Fact]
public async Task Should_call_repository_with_all_names()
{
var eventConsumer1 = A.Fake<IEventConsumer>();
var eventConsumer2 = A.Fake<IEventConsumer>();
A.CallTo(() => eventConsumer1.Name).Returns("consumer1");
A.CallTo(() => eventConsumer2.Name).Returns("consumer2");
var repository = A.Fake<IEventConsumerInfoRepository>();
var sut = new EventConsumerCleaner(new[] { eventConsumer1, eventConsumer2 }, repository);
await sut.CleanAsync();
A.CallTo(() => repository.ClearAsync(A<IEnumerable<string>>.That.IsSameSequenceAs(new string[] { "consumer1", "consumer2" }))).MustHaveHappened();
}
}
}

45
tests/Squidex.Infrastructure.Tests/CQRS/Events/PollingSubscriptionTests.cs → tests/Squidex.Infrastructure.Tests/CQRS/Events/EventSubscriptionTests.cs

@ -14,17 +14,16 @@ using Xunit;
namespace Squidex.Infrastructure.CQRS.Events
{
public class PollingSubscriptionTests
public class EventSubscriptionTests
{
private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IEventNotifier eventNotifier = new DefaultEventNotifier(new InMemoryPubSub());
private readonly IEventSubscriber eventSubscriber = A.Fake<IEventSubscriber>();
private readonly string position = Guid.NewGuid().ToString();
[Fact]
public async Task Should_subscribe_on_start()
{
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
var sut = new EventStoreSubscription(eventStore, eventSubscriber, "^my-stream", position);
await WaitAndStopAsync(sut);
@ -40,7 +39,7 @@ namespace Squidex.Infrastructure.CQRS.Events
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, A<CancellationToken>.Ignored, "^my-stream", position))
.Throws(ex);
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
var sut = new EventStoreSubscription(eventStore, eventSubscriber, "^my-stream", position);
await WaitAndStopAsync(sut);
@ -49,30 +48,30 @@ namespace Squidex.Infrastructure.CQRS.Events
}
[Fact]
public async Task Should_propagate_operation_cancelled_exception_to_subscriber()
public async Task Should_propagate_closed_to_subscriber()
{
var ex = new OperationCanceledException();
var ex = new InvalidOperationException();
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, A<CancellationToken>.Ignored, "^my-stream", position))
.Throws(ex);
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
var sut = new EventStoreSubscription(eventStore, eventSubscriber, "^my-stream", position);
await WaitAndStopAsync(sut);
A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex))
.MustNotHaveHappened();
A.CallTo(() => eventSubscriber.OnClosedAsync(sut))
.MustHaveHappened();
}
[Fact]
public async Task Should_propagate_aggregate_operation_cancelled_exception_to_subscriber()
public async Task Should_propagate_operation_cancelled_exception_to_subscriber()
{
var ex = new AggregateException(new OperationCanceledException());
var ex = new OperationCanceledException();
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, A<CancellationToken>.Ignored, "^my-stream", position))
.Throws(ex);
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
var sut = new EventStoreSubscription(eventStore, eventSubscriber, "^my-stream", position);
await WaitAndStopAsync(sut);
@ -81,29 +80,19 @@ namespace Squidex.Infrastructure.CQRS.Events
}
[Fact]
public async Task Should_not_subscribe_on_notify_when_stream_matches()
public async Task Should_propagate_aggregate_operation_cancelled_exception_to_subscriber()
{
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
eventNotifier.NotifyEventsStored("other-stream-123");
await WaitAndStopAsync(sut);
var ex = new AggregateException(new OperationCanceledException());
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, A<CancellationToken>.Ignored, "^my-stream", position))
.MustHaveHappened(Repeated.Exactly.Once);
}
[Fact]
public async Task Should_subscribe_on_notify_when_stream_matches()
{
var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position);
.Throws(ex);
eventNotifier.NotifyEventsStored("my-stream-123");
var sut = new EventStoreSubscription(eventStore, eventSubscriber, "^my-stream", position);
await WaitAndStopAsync(sut);
A.CallTo(() => eventStore.GetEventsAsync(A<Func<StoredEvent, Task>>.Ignored, A<CancellationToken>.Ignored, "^my-stream", position))
.MustHaveHappened(Repeated.Exactly.Twice);
A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex))
.MustNotHaveHappened();
}
private static async Task WaitAndStopAsync(IEventSubscription sut)

4
tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs

@ -276,7 +276,9 @@ namespace Squidex.Infrastructure.Log
{
var exception = new InvalidOperationException();
var loggerFactory = new LoggerFactory().AddSemanticLog(Log);
var loggerFactory =
new LoggerFactory()
.AddSemanticLog(Log);
var loggerInstance = loggerFactory.CreateLogger<SemanticLogTests>();
loggerInstance.LogCritical(new EventId(123, "EventName"), exception, "Log {0}", 123);

2
tests/Squidex.Infrastructure.Tests/Actors/SingleThreadedDispatcherTests.cs → tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs

@ -12,7 +12,7 @@ using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
using Xunit;
namespace Squidex.Infrastructure.Actors
namespace Squidex.Infrastructure.Tasks
{
public class SingleThreadedDispatcherTests
{
Loading…
Cancel
Save