diff --git a/Squidex.sln b/Squidex.sln index 81f85c0f9..5989e5092 100644 --- a/Squidex.sln +++ b/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} diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs index c6f475e2e..f55b7e494 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Apps/MongoAppRepository.cs +++ b/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, IAppRepository, IEventConsumer + public partial class MongoAppRepository : MongoRepositoryBase, IAppRepository, IAppEventConsumer { public MongoAppRepository(IMongoDatabase database) : base(database) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs index 87b055d15..2caca0755 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetRepository.cs +++ b/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, IAssetRepository, IEventConsumer + public partial class MongoAssetRepository : MongoRepositoryBase, IAssetRepository, IAssetEventConsumer { public MongoAssetRepository(IMongoDatabase database) : base(database) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs index 9b0495efe..290c754e7 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Assets/MongoAssetStatsRepository.cs +++ b/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, IAssetStatsRepository, IEventConsumer + public partial class MongoAssetStatsRepository : MongoRepositoryBase, IAssetStatsRepository, IAssetEventConsumer { public MongoAssetStatsRepository(IMongoDatabase database) : base(database) diff --git a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs b/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs index a43da664f..f17434021 100644 --- a/src/Squidex.Domain.Apps.Read.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/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, ISchemaRepository, IEventConsumer + public partial class MongoSchemaRepository : MongoRepositoryBase, ISchemaRepository, ISchemaEventConsumer { private readonly FieldRegistry registry; diff --git a/src/Squidex.Infrastructure/Actors/IActors.cs b/src/Squidex.Domain.Apps.Read/Apps/IAppEventConsumer.cs similarity index 66% rename from src/Squidex.Infrastructure/Actors/IActors.cs rename to src/Squidex.Domain.Apps.Read/Apps/IAppEventConsumer.cs index b5f1c84bb..c34d229cf 100644 --- a/src/Squidex.Infrastructure/Actors/IActors.cs +++ b/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); } -} \ No newline at end of file +} diff --git a/src/Squidex.Infrastructure/Actors/IActor.cs b/src/Squidex.Domain.Apps.Read/Assets/IAssetEventConsumer.cs similarity index 66% rename from src/Squidex.Infrastructure/Actors/IActor.cs rename to src/Squidex.Domain.Apps.Read/Assets/IAssetEventConsumer.cs index 7975a90ab..28ebcf5d5 100644 --- a/src/Squidex.Infrastructure/Actors/IActor.cs +++ b/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); } -} \ No newline at end of file +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StopConsumerMessage.cs b/src/Squidex.Domain.Apps.Read/Schemas/ISchemaEventConsumer.cs similarity index 65% rename from src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StopConsumerMessage.cs rename to src/Squidex.Domain.Apps.Read/Schemas/ISchemaEventConsumer.cs index 071269b8c..f94bdee68 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StopConsumerMessage.cs +++ b/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 { } } diff --git a/src/Squidex.Infrastructure/Actors/IRemoteActorChannel.cs b/src/Squidex.Domain.Users/DataProtection/Orleans/Grains/IXmlRepositoryGrain.cs similarity index 56% rename from src/Squidex.Infrastructure/Actors/IRemoteActorChannel.cs rename to src/Squidex.Domain.Users/DataProtection/Orleans/Grains/IXmlRepositoryGrain.cs index 260a9d2ff..0089f0d3d 100644 --- a/src/Squidex.Infrastructure/Actors/IRemoteActorChannel.cs +++ b/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 GetAllElementsAsync(); - void Subscribe(string recipient, Action handler); + Task StoreElementAsync(string element, string friendlyName); } -} \ No newline at end of file +} diff --git a/src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs b/src/Squidex.Domain.Users/DataProtection/Orleans/Grains/Implementations/XmlRepositoryGrain.cs new file mode 100644 index 000000000..21af2a50f --- /dev/null +++ b/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>, IXmlRepositoryGrain + { + public Task GetAllElementsAsync() + { + return Task.FromResult(State.Values.ToArray()); + } + + public Task StoreElementAsync(string element, string friendlyName) + { + State[friendlyName] = element; + + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Domain.Users/DataProtection/Orleans/OrleansXmlRepository.cs b/src/Squidex.Domain.Users/DataProtection/Orleans/OrleansXmlRepository.cs new file mode 100644 index 000000000..edce8fc3f --- /dev/null +++ b/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 grain; + + public OrleansXmlRepository(IClusterClient orleans) + { + Guard.NotNull(orleans, nameof(orleans)); + + grain = new Lazy(() => orleans.GetGrain("Default")); + } + + public IReadOnlyCollection 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(); + } + } +} diff --git a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 11b5070ec..d4872e060 100644 --- a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -12,6 +12,8 @@ + + diff --git a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfo.cs b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfo.cs deleted file mode 100644 index 07abec5cc..000000000 --- a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfo.cs +++ /dev/null @@ -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; } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfoRepository.cs b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfoRepository.cs deleted file mode 100644 index 1d7f9acef..000000000 --- a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventConsumerInfoRepository.cs +++ /dev/null @@ -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, IEventConsumerInfoRepository - { - private static readonly FieldDefinition NameField = Fields.Build(x => x.Name); - private static readonly FieldDefinition ErrorField = Fields.Build(x => x.Error); - private static readonly FieldDefinition PositionField = Fields.Build(x => x.Position); - private static readonly FieldDefinition IsStoppedField = Fields.Build(x => x.IsStopped); - - public MongoEventConsumerInfoRepository(IMongoDatabase database) - : base(database) - { - } - - protected override string CollectionName() - { - return "EventPositions"; - } - - public async Task> QueryAsync() - { - var entities = await Collection.Find(new BsonDocument()).SortBy(x => x.Name).ToListAsync(); - - return entities.OfType().ToList(); - } - - public async Task FindAsync(string consumerName) - { - var entity = await Collection.Find(Filter.Eq(NameField, consumerName)).FirstOrDefaultAsync(); - - return entity; - } - - public Task ClearAsync(IEnumerable 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; - } - } - } - } -} diff --git a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs index 2abb9c2cb..10059cf56 100644 --- a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs +++ b/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> GetEventsAsync(string streamName) diff --git a/src/Squidex.Infrastructure/Actors/DefaultRemoteActorChannel.cs b/src/Squidex.Infrastructure/Actors/DefaultRemoteActorChannel.cs deleted file mode 100644 index b7a4b94fc..000000000 --- a/src/Squidex.Infrastructure/Actors/DefaultRemoteActorChannel.cs +++ /dev/null @@ -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 handler) - { - Guard.NotNullOrEmpty(recipient, nameof(recipient)); - - pubSub.Subscribe(ChannelName, json => - { - var envelope = JsonConvert.DeserializeObject(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); - } - } -} diff --git a/src/Squidex.Infrastructure/Actors/RemoteActors.cs b/src/Squidex.Infrastructure/Actors/RemoteActors.cs deleted file mode 100644 index 268c765ce..000000000 --- a/src/Squidex.Infrastructure/Actors/RemoteActors.cs +++ /dev/null @@ -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 senders = new ConcurrentDictionary(); - private readonly ConcurrentDictionary receivers = new ConcurrentDictionary(); - 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); - } - } -} diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs deleted file mode 100644 index b89fb0957..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/EventConsumerActor.cs +++ /dev/null @@ -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 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 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 @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 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; - } - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StartConsumerMessage.cs b/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StartConsumerMessage.cs deleted file mode 100644 index 89850d713..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/StartConsumerMessage.cs +++ /dev/null @@ -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 - { - } -} diff --git a/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs b/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs index ae167f3a5..463348dde 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs +++ b/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)); diff --git a/src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs b/src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs deleted file mode 100644 index 69ab25e88..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/DefaultEventNotifier.cs +++ /dev/null @@ -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 handler) - { - return pubsub.Subscribe(ChannelName, x => handler?.Invoke(x)); - } - } -} diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventConsumerCleaner.cs b/src/Squidex.Infrastructure/CQRS/Events/EventConsumerCleaner.cs deleted file mode 100644 index c51c7b043..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/EventConsumerCleaner.cs +++ /dev/null @@ -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 eventConsumers; - private readonly IEventConsumerInfoRepository eventConsumerInfoRepository; - - public EventConsumerCleaner(IEnumerable 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); - } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Events/Grains/EventConsumerInfo.cs b/src/Squidex.Infrastructure/CQRS/Events/EventConsumerInfo.cs similarity index 91% rename from src/Squidex.Infrastructure/CQRS/Events/Grains/EventConsumerInfo.cs rename to src/Squidex.Infrastructure/CQRS/Events/EventConsumerInfo.cs index f994d9851..08fbdf24c 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Grains/EventConsumerInfo.cs +++ b/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 { diff --git a/src/Squidex.Infrastructure/CQRS/Events/PollingSubscription.cs b/src/Squidex.Infrastructure/CQRS/Events/EventStoreSubscription.cs similarity index 61% rename from src/Squidex.Infrastructure/CQRS/Events/PollingSubscription.cs rename to src/Squidex.Infrastructure/CQRS/Events/EventStoreSubscription.cs index e69234d47..68a9960d5 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/PollingSubscription.cs +++ b/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; } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerRegistryGrainState.cs b/src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerRegistryGrainState.cs deleted file mode 100644 index af54c01c5..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerRegistryGrainState.cs +++ /dev/null @@ -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 EventConsumerNames { get; set; } - - public EventConsumerRegistryGrainState() - { - EventConsumerNames = new HashSet(); - } - } -} diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfo.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfo.cs deleted file mode 100644 index f074ee5ef..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfo.cs +++ /dev/null @@ -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; } - } -} \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfoRepository.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfoRepository.cs deleted file mode 100644 index f44d4de57..000000000 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventConsumerInfoRepository.cs +++ /dev/null @@ -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> QueryAsync(); - - Task FindAsync(string consumerName); - - Task ClearAsync(IEnumerable currentConsumerNames); - - Task SetAsync(string consumerName, string position, bool isStopped, string error = null); - } -} diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventNotifier.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventNotifier.cs index 72e61b3d9..fe3150775 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventNotifier.cs +++ b/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 handler); } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/IEventSubscriber.cs b/src/Squidex.Infrastructure/CQRS/Events/IEventSubscriber.cs index 6957f83c1..7f7e68eb8 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/IEventSubscriber.cs +++ b/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); } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/EventConsumerBootstrap.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/EventConsumerBootstrap.cs new file mode 100644 index 000000000..38e5952f9 --- /dev/null +++ b/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("Default").ActivateAsync(null); + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/Grains/IEventConsumerGrain.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/IEventConsumerGrain.cs similarity index 87% rename from src/Squidex.Infrastructure/CQRS/Events/Grains/IEventConsumerGrain.cs rename to src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/IEventConsumerGrain.cs index 82d0e5560..24091ade3 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Grains/IEventConsumerGrain.cs +++ b/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 GetStateAsync(); + Task ActivateAsync(); + Task StopAsync(); Task StartAsync(); diff --git a/src/Squidex.Infrastructure/CQRS/Events/Grains/IEventConsumerRegistryGrain.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/IEventConsumerRegistryGrain.cs similarity index 87% rename from src/Squidex.Infrastructure/CQRS/Events/Grains/IEventConsumerRegistryGrain.cs rename to src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/IEventConsumerRegistryGrain.cs index 11ee25581..aa96490b8 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Grains/IEventConsumerRegistryGrain.cs +++ b/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); diff --git a/src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerGrain.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs similarity index 84% rename from src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerGrain.cs rename to src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrain.cs index 1bd4829f9..2ec816fc8 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerGrain.cs +++ b/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, IEventSubscriber, IEventConsumerGrain + [StorageProvider(ProviderName = "Default")] + public class EventConsumerGrain : Grain, 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(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 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 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); + } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrainState.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerGrainState.cs new file mode 100644 index 000000000..97bb8ad90 --- /dev/null +++ b/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 }); + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerRegistryGrain.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs similarity index 50% rename from src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerRegistryGrain.cs rename to src/Squidex.Infrastructure/CQRS/Events/Orleans/Grains/Implementation/EventConsumerRegistryGrain.cs index 7ce41b49d..5ff0da2b3 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Grains/Implementation/EventConsumerRegistryGrain.cs +++ b/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, IEventConsumerRegistryGrain + public sealed class EventConsumerRegistryGrain : Grain, IEventConsumerRegistryGrain, IRemindable { + private readonly IEnumerable eventConsumers; + + public EventConsumerRegistryGrain(IEnumerable 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(c.Name)) + .Select(c => c.ActivateAsync()); + + return Task.WhenAll(tasks); + } + public Task> GetConsumersAsync() { var tasks = - State.EventConsumerNames - .Select(n => GrainFactory.GetGrain(n)) + eventConsumers + .Select(c => GrainFactory.GetGrain(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; } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Orleans/OrleansClientEventNotifier.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/OrleansClientEventNotifier.cs new file mode 100644 index 000000000..178b19810 --- /dev/null +++ b/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("Default"); + } + + public void NotifyEventsStored(string streamName) + { + eventConsumerRegistryGrain.ActivateAsync(streamName); + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/Orleans/OrleansSiloEventNotifier.cs b/src/Squidex.Infrastructure/CQRS/Events/Orleans/OrleansSiloEventNotifier.cs new file mode 100644 index 000000000..aaf976b1b --- /dev/null +++ b/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("Default"); + } + + public void NotifyEventsStored(string streamName) + { + eventConsumerRegistryGrain.ActivateAsync(streamName); + } + } +} diff --git a/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs b/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs index 5c232455b..92ecd5e16 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/RetrySubscription.cs +++ b/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()); diff --git a/src/Squidex.Infrastructure/CollectionExtensions.cs b/src/Squidex.Infrastructure/CollectionExtensions.cs index da9268948..ea0f8362e 100644 --- a/src/Squidex.Infrastructure/CollectionExtensions.cs +++ b/src/Squidex.Infrastructure/CollectionExtensions.cs @@ -30,6 +30,16 @@ namespace Squidex.Infrastructure } } + public static IEnumerable OrEmpty(this IEnumerable source) + { + return source ?? Enumerable.Empty(); + } + + public static IEnumerable Concat(this IEnumerable source, T value) + { + return source.Concat(Enumerable.Repeat(value, 1)); + } + public static int SequentialHashCode(this IEnumerable collection) { return collection.SequentialHashCode(EqualityComparer.Default); diff --git a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs b/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs index 662da7078..f4c6114d2 100644 --- a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerFactoryExtensions.cs +++ b/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(); + + return builder; + } + + public static ILoggerFactory AddSemanticLog(this ILoggerFactory factory, ISemanticLog log) + { + factory.AddProvider(new SemanticLogLoggerProvider(log)); return factory; } diff --git a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs b/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs index 3bd84a2b1..7e2ad2956 100644 --- a/src/Squidex.Infrastructure/Log/Adapter/SemanticLogLoggerProvider.cs +++ b/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; } diff --git a/src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs b/src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs similarity index 95% rename from src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs rename to src/Squidex.Infrastructure/Tasks/SingleThreadedDispatcher.cs index 993ae84f1..6a58699f6 100644 --- a/src/Squidex.Infrastructure/Actors/SingleThreadedDispatcher.cs +++ b/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 { diff --git a/src/Squidex/AppConfiguration.cs b/src/Squidex/AppConfiguration.cs new file mode 100644 index 000000000..16a9fa09e --- /dev/null +++ b/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); + } + } +} diff --git a/src/Squidex/AppServices.cs b/src/Squidex/AppServices.cs new file mode 100644 index 000000000..71246b8bf --- /dev/null +++ b/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( + config.GetSection("urls")); + services.Configure( + config.GetSection("identity")); + services.Configure( + config.GetSection("ui")); + services.Configure( + config.GetSection("usage")); + } + } +} diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs new file mode 100644 index 000000000..cca549879 --- /dev/null +++ b/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())) + .As() + .As(); + }, + ["GoogleCloud"] = () => + { + var bucketName = config.GetRequiredValue("assetStore:googleCloud:bucket"); + + services.AddSingleton(c => new GoogleCloudAssetStore(bucketName)) + .As() + .As(); + }, + ["AzureBlob"] = () => + { + var connectionString = config.GetRequiredValue("assetStore:azureBlob:connectionString"); + var containerName = config.GetRequiredValue("assetStore:azureBlob:containerName"); + + services.AddSingleton(c => new AzureBlobAssetStore(connectionString, containerName)) + .As() + .As(); + } + }); + } + } +} diff --git a/src/Squidex/Config/Domain/AssetStoreModule.cs b/src/Squidex/Config/Domain/AssetStoreModule.cs deleted file mode 100644 index 683dbe70e..000000000 --- a/src/Squidex/Config/Domain/AssetStoreModule.cs +++ /dev/null @@ -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("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("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())) - .As() - .As() - .SingleInstance(); - } - else if (string.Equals(assetStoreType, "GoogleCloud", StringComparison.OrdinalIgnoreCase)) - { - var bucketName = Configuration.GetValue("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() - .As() - .SingleInstance(); - } - else if (string.Equals(assetStoreType, "AzureBlob", StringComparison.OrdinalIgnoreCase)) - { - var connectionString = Configuration.GetValue("assetStore:azureBlob:connectionString"); - - if (string.IsNullOrWhiteSpace(connectionString)) - { - throw new ConfigurationException("Configure AssetStore AzureBlob connection string with 'assetStore:azureBlob:connectionString'."); - } - - var containerName = Configuration.GetValue("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() - .As() - .SingleInstance(); - } - else - { - throw new ConfigurationException($"Unsupported value '{assetStoreType}' for 'assetStore:type', supported: AzureBlob, Folder, GoogleCloud."); - } - } - } -} diff --git a/src/Squidex/Config/Domain/EventPublishersModule.cs b/src/Squidex/Config/Domain/EventPublishersServices.cs similarity index 69% rename from src/Squidex/Config/Domain/EventPublishersModule.cs rename to src/Squidex/Config/Domain/EventPublishersServices.cs index bafc5b3cf..0d3edbacb 100644 --- a/src/Squidex/Config/Domain/EventPublishersModule.cs +++ b/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("eventsFilter"); + var eventsFilter = config.GetValue("eventsFilter"); var enabled = child.GetValue("enabled"); if (string.Equals(eventPublisherType, "RabbitMq", StringComparison.OrdinalIgnoreCase)) { - var configuration = child.GetValue("configuration"); + var publisherConfig = child.GetValue("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(), name, configuration, exchange, eventsFilter)) + services.AddSingleton(c => new RabbitMqEventConsumer(c.GetRequiredService(), name, publisherConfig, exchange, eventsFilter)) .As() - .As() - .SingleInstance(); + .As(); } } else diff --git a/src/Squidex/Config/Domain/EventStoreModule.cs b/src/Squidex/Config/Domain/EventStoreModule.cs deleted file mode 100644 index ea282e94b..000000000 --- a/src/Squidex/Config/Domain/EventStoreModule.cs +++ /dev/null @@ -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("eventStore:consume"); - - if (consumeEvents) - { - builder.RegisterType() - .AsSelf() - .InstancePerDependency(); - } - - var eventStoreType = Configuration.GetValue("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("eventStore:mongoDb:configuration"); - - if (string.IsNullOrWhiteSpace(configuration)) - { - throw new ConfigurationException("Configure EventStore MongoDb configuration with 'eventStore:mongoDb:configuration'."); - } - - var database = Configuration.GetValue("eventStore:mongoDb:database"); - - if (string.IsNullOrWhiteSpace(database)) - { - throw new ConfigurationException("Configure EventStore MongoDb Database name with 'eventStore:mongoDb:database'."); - } - - builder.Register(c => Singletons.GetOrAdd(configuration, s => new MongoClient(s))) - .Named(MongoClientRegistration) - .SingleInstance(); - - builder.Register(c => c.ResolveNamed(MongoClientRegistration).GetDatabase(database)) - .Named(MongoDatabaseRegistration) - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .SingleInstance(); - } - else if (string.Equals(eventStoreType, "GetEventStore", StringComparison.OrdinalIgnoreCase)) - { - var configuration = Configuration.GetValue("eventStore:getEventStore:configuration"); - - if (string.IsNullOrWhiteSpace(configuration)) - { - throw new ConfigurationException("Configure GetEventStore EventStore configuration with 'eventStore:getEventStore:configuration'."); - } - - var projectionHost = Configuration.GetValue("eventStore:getEventStore:projectionHost"); - - if (string.IsNullOrWhiteSpace(projectionHost)) - { - throw new ConfigurationException("Configure GetEventStore EventStore projection host with 'eventStore:getEventStore:projectionHost'."); - } - - var prefix = Configuration.GetValue("eventStore:getEventStore:prefix"); - - var connection = EventStoreConnection.Create(configuration); - - builder.Register(c => new GetEventStore(connection, prefix, projectionHost)) - .As() - .As() - .SingleInstance(); - } - else - { - throw new ConfigurationException($"Unsupported value '{eventStoreType}' for 'eventStore:type', supported: MongoDb, GetEventStore."); - } - } - } -} diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs new file mode 100644 index 000000000..5038f07c1 --- /dev/null +++ b/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.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); + var mongDatabase = mongoClient.GetDatabase(mongoDatabaseName); + + return new MongoEventStore(mongDatabase, c.GetRequiredService()); + }) + .As() + .As(); + }, + ["GetEventStore"] = () => + { + var eventStoreConfiguration = config.GetRequiredValue("eventStore:getEventStore:configuration"); + var eventStoreProjectionHost = config.GetRequiredValue("eventStore:getEventStore:projectionHost"); + var eventStorePrefix = config.GetValue("eventStore:getEventStore:prefix"); + + var connection = EventStoreConnection.Create(eventStoreConfiguration); + + services.AddSingleton(c => new GetEventStore(connection, eventStorePrefix, eventStoreProjectionHost)) + .As() + .As(); + } + }); + } + } +} diff --git a/src/Squidex/Config/Domain/InfrastructureModule.cs b/src/Squidex/Config/Domain/InfrastructureModule.cs deleted file mode 100644 index 67e0e731c..000000000 --- a/src/Squidex/Config/Domain/InfrastructureModule.cs +++ /dev/null @@ -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("logging:human")) - { - builder.Register(c => new Func(() => new JsonLogWriter(Formatting.Indented, true))) - .AsSelf() - .SingleInstance(); - } - else - { - builder.Register(c => new Func(() => new JsonLogWriter())) - .AsSelf() - .SingleInstance(); - } - - var loggingFile = Configuration.GetValue("logging:file"); - - if (!string.IsNullOrWhiteSpace(loggingFile)) - { - builder.RegisterInstance(new FileChannel(loggingFile)) - .As() - .As() - .SingleInstance(); - } - - builder.Register(c => new ApplicationInfoLogAppender(GetType(), Guid.NewGuid())) - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.Register(c => SystemClock.Instance) - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.Register(c => new InvalidatingMemoryCache(new MemoryCache(c.Resolve>()), c.Resolve())) - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .AsSelf() - .SingleInstance(); - } - } -} diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs new file mode 100644 index 000000000..3fb21a523 --- /dev/null +++ b/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("logging:human")) + { + services.AddSingleton(c => new Func(() => new JsonLogWriter(Formatting.Indented, true))); + } + else + { + services.AddSingleton(c => new Func(() => new JsonLogWriter())); + } + + var loggingFile = config.GetValue("logging:file"); + + if (!string.IsNullOrWhiteSpace(loggingFile)) + { + services.AddSingleton(new FileChannel(loggingFile)) + .As() + .As(); + } + + services.AddSingleton(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton(SystemClock.Instance) + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton(); + + services.AddSingleton(c => new InvalidatingMemoryCache( + new MemoryCache( + c.GetRequiredService>()), + c.GetRequiredService())) + .As(); + } + } +} diff --git a/src/Squidex/Config/Domain/LoggingExtensions.cs b/src/Squidex/Config/Domain/LoggingExtensions.cs new file mode 100644 index 000000000..6571256e5 --- /dev/null +++ b/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(); + + var config = services.GetRequiredService(); + + 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); + } + })); + } + } +} diff --git a/src/Squidex/Config/Domain/PubSubModule.cs b/src/Squidex/Config/Domain/PubSubModule.cs deleted file mode 100644 index 07b07ea43..000000000 --- a/src/Squidex/Config/Domain/PubSubModule.cs +++ /dev/null @@ -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("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("pubsub:redis:configuration"); - - if (string.IsNullOrWhiteSpace(configuration)) - { - throw new ConfigurationException("Configure PubSub Redis configuration with 'pubSub:redis:configuration'."); - } - - builder.Register(c => Singletons.GetOrAddLazy(configuration, s => ConnectionMultiplexer.Connect(s))) - .Named>(RedisRegistration) - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed>(RedisRegistration)) - .As() - .As() - .SingleInstance(); - } - else if (string.Equals(pubSubType, "InMemory", StringComparison.OrdinalIgnoreCase)) - { - builder.RegisterType() - .As() - .SingleInstance(); - } - else - { - throw new ConfigurationException($"Unsupported value '{pubSubType}' for 'pubSub:type', supported: Redis, InMemory."); - } - } - } -} diff --git a/src/Squidex/Config/Domain/PubSubServices.cs b/src/Squidex/Config/Domain/PubSubServices.cs new file mode 100644 index 000000000..dc251043e --- /dev/null +++ b/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() + .As(); + }, + ["Redis"] = () => + { + var configuration = config.GetRequiredValue("pubsub:redis:configuration"); + + var redis = Singletons.GetOrAddLazy(configuration, s => ConnectionMultiplexer.Connect(s)); + + services.AddSingleton(c => new RedisPubSub(redis, c.GetRequiredService())) + .As() + .As(); + } + }); + } + } +} diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs deleted file mode 100644 index 5135fc0f1..000000000 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ /dev/null @@ -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>().Value?.Plans ?? Enumerable.Empty()) - .As>() - .AsSelf() - .SingleInstance(); - - builder.Register(c => new GraphQLUrlGenerator( - c.Resolve>(), - c.Resolve(), - Configuration.GetValue("assetStore:exposeSourceUrl"))) - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .InstancePerDependency(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .InstancePerDependency(); - - builder.RegisterType() - .As() - .AsSelf() - .InstancePerDependency(); - - builder.RegisterType() - .As() - .AsSelf() - .InstancePerDependency(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .AsSelf() - .SingleInstance(); - - builder.Register(c => - { - var eventConsumers = c.Resolve>(); - - return new EventConsumerFactory(x => eventConsumers.First(e => e.Name == x)); - }) - .AsSelf() - .SingleInstance(); - } - } -} diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs new file mode 100644 index 000000000..09aabc32f --- /dev/null +++ b/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>(), + c.GetRequiredService(), + exposeSourceUrl)) + .As(); + + services.AddSingleton(c => c.GetService>()?.Value?.Plans.OrEmpty()); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton(c => new CompoundEventConsumer(c.GetServices().ToArray())) + .As(); + + services.AddSingleton(c => + new CompoundEventConsumer( + c.GetServices().OfType() + .Concat(c.GetRequiredService()).ToArray())) + .As(); + + services.AddSingleton(c => + new CompoundEventConsumer( + c.GetServices().OfType() + .Concat(c.GetRequiredService()) + .Concat(c.GetRequiredService()).ToArray())) + .As(); + + services.AddSingleton(c => + { + var allEventConsumers = c.GetServices(); + + return new EventConsumerFactory(n => allEventConsumers.FirstOrDefault(x => x.Name == n)); + }); + + services.AddSingleton(); + services.AddSingleton(); + } + } +} diff --git a/src/Squidex/Config/Domain/Serializers.cs b/src/Squidex/Config/Domain/SerializationServices.cs similarity index 94% rename from src/Squidex/Config/Domain/Serializers.cs rename to src/Squidex/Config/Domain/SerializationServices.cs index 97c3d3139..aa9c9aa62 100644 --- a/src/Squidex/Config/Domain/Serializers.cs +++ b/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); diff --git a/src/Squidex/Config/Domain/StoreModule.cs b/src/Squidex/Config/Domain/StoreModule.cs deleted file mode 100644 index 85a17d57d..000000000 --- a/src/Squidex/Config/Domain/StoreModule.cs +++ /dev/null @@ -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("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."); - } - } - } -} diff --git a/src/Squidex/Config/Domain/StoreMongoDbModule.cs b/src/Squidex/Config/Domain/StoreMongoDbModule.cs deleted file mode 100644 index faa0c355f..000000000 --- a/src/Squidex/Config/Domain/StoreMongoDbModule.cs +++ /dev/null @@ -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("store:mongoDb:configuration"); - - if (string.IsNullOrWhiteSpace(configuration)) - { - throw new ConfigurationException("Configure the Store MongoDb configuration with 'store:mongoDb:configuration'."); - } - - var database = Configuration.GetValue("store:mongoDb:database"); - - if (string.IsNullOrWhiteSpace(database)) - { - throw new ConfigurationException("Configure the Store MongoDb database with 'store:mongoDb:database'."); - } - - var contentDatabase = Configuration.GetValue("store:mongoDb:contentDatabase"); - - if (string.IsNullOrWhiteSpace(contentDatabase)) - { - contentDatabase = database; - } - - builder.Register(c => Singletons.GetOrAdd(configuration, s => new MongoClient(s))) - .Named(MongoClientRegistration) - .SingleInstance(); - - builder.Register(c => c.ResolveNamed(MongoClientRegistration).GetDatabase(database)) - .Named(MongoDatabaseRegistration) - .SingleInstance(); - - builder.Register(c => c.ResolveNamed(MongoClientRegistration).GetDatabase(contentDatabase)) - .Named(MongoContentDatabaseRegistration) - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As>() - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As>() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoContentDatabaseRegistration)) - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) - .As() - .As() - .As() - .AsSelf() - .SingleInstance(); - - builder.Register(c => - new CompoundEventConsumer( - c.Resolve(), - c.Resolve())) - .As() - .AsSelf() - .SingleInstance(); - - builder.Register(c => - new CompoundEventConsumer( - c.Resolve(), - c.Resolve(), - c.Resolve())) - .As() - .AsSelf() - .SingleInstance(); - - builder.Register(c => - new CompoundEventConsumer( - c.Resolve(), - c.Resolve())) - .As() - .AsSelf() - .SingleInstance(); - } - } -} diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs new file mode 100644 index 000000000..7e2f64f3b --- /dev/null +++ b/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.GetOrAdd(mongoConfiguration, s => new MongoClient(s)); + var mongoDatabase = mongoClient.GetDatabase(mongoDatabaseName); + var mongoContentDatabase = mongoClient.GetDatabase(mongoContentDatabaseName); + + services.AddSingleton(c => new MongoUserStore(mongoDatabase)) + .As>() + .As() + .As() + .As(); + + services.AddSingleton(c => new MongoRoleStore(mongoDatabase)) + .As>() + .As() + .As(); + + services.AddSingleton(c => new MongoPersistedGrantStore(mongoDatabase)) + .As() + .As(); + + services.AddSingleton(c => new MongoUsageStore(mongoDatabase)) + .As() + .As(); + + services.AddSingleton(c => new MongoContentRepository(mongoContentDatabase, c.GetService())) + .As() + .As(); + + services.AddSingleton(c => new MongoRuleEventRepository(mongoDatabase)) + .As() + .As(); + + services.AddSingleton(c => new MongoHistoryEventRepository(mongoDatabase, c.GetServices())) + .As() + .As() + .As(); + + services.AddSingleton(c => new MongoAppRepository(mongoDatabase)) + .As() + .As() + .As(); + + services.AddSingleton(c => new MongoSchemaRepository(mongoDatabase, c.GetRequiredService())) + .As() + .As() + .As(); + + services.AddSingleton(c => new MongoAssetStatsRepository(mongoDatabase)) + .As() + .As() + .As(); + + services.AddSingleton(c => new MongoAssetRepository(mongoDatabase)) + .As() + .As() + .As(); + + services.AddSingleton(c => new MongoRuleRepository(mongoDatabase)) + .As() + .As() + .As(); + } + }); + } + } +} diff --git a/src/Squidex/Config/Domain/SystemExtensions.cs b/src/Squidex/Config/Domain/SystemExtensions.cs new file mode 100644 index 000000000..ed8a4bccd --- /dev/null +++ b/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>(); + + foreach (var system in systems) + { + system.Connect(); + } + } + } +} diff --git a/src/Squidex/Config/Domain/Usages.cs b/src/Squidex/Config/Domain/Usages.cs deleted file mode 100644 index d9574bfa6..000000000 --- a/src/Squidex/Config/Domain/Usages.cs +++ /dev/null @@ -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().CleanAsync().Wait(); - - var consumers = services.GetServices(); - - foreach (var consumer in consumers) - { - var actor = services.GetService(); - - if (actor != null) - { - actor.SubscribeAsync(consumer); - - services.GetService().Connect(consumer.Name, actor); - } - } - - return app; - } - - public static IApplicationBuilder TestExternalSystems(this IApplicationBuilder app) - { - var systems = app.ApplicationServices.GetRequiredService>(); - - foreach (var system in systems) - { - system.Connect(); - } - - return app; - } - } -} diff --git a/src/Squidex/Config/Domain/WriteModule.cs b/src/Squidex/Config/Domain/WriteModule.cs deleted file mode 100644 index e6ce99535..000000000 --- a/src/Squidex/Config/Domain/WriteModule.cs +++ /dev/null @@ -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() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.RegisterType() - .As() - .SingleInstance(); - - builder.Register>(c => (id => new AppDomainObject(id, -1))) - .AsSelf() - .SingleInstance(); - - builder.Register>(c => (id => new AssetDomainObject(id, -1))) - .AsSelf() - .SingleInstance(); - - builder.Register>(c => (id => new ContentDomainObject(id, -1))) - .AsSelf() - .SingleInstance(); - - builder.Register>(c => (id => new RuleDomainObject(id, -1))) - .AsSelf() - .SingleInstance(); - - builder.Register>(c => - { - var fieldRegistry = c.Resolve(); - - return id => new SchemaDomainObject(id, -1, fieldRegistry); - }) - .AsSelf() - .SingleInstance(); - } - } -} diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/WriteServices.cs new file mode 100644 index 000000000..6282d19ab --- /dev/null +++ b/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() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton() + .As(); + + services.AddSingleton>(c => (id => new AppDomainObject(id, -1))); + services.AddSingleton>(c => (id => new RuleDomainObject(id, -1))); + services.AddSingleton>(c => (id => new AssetDomainObject(id, -1))); + services.AddSingleton>(c => (id => new ContentDomainObject(id, -1))); + + services.AddSingleton>(c => + { + var fieldRegistry = c.GetRequiredService(); + + return id => new SchemaDomainObject(id, -1, fieldRegistry); + }); + } + } +} diff --git a/src/Squidex/Config/Identity/AuthenticationUsage.cs b/src/Squidex/Config/Identity/AuthenticationExtensions.cs similarity index 87% rename from src/Squidex/Config/Identity/AuthenticationUsage.cs rename to src/Squidex/Config/Identity/AuthenticationExtensions.cs index b9dd4ad8f..69fe4f294 100644 --- a/src/Squidex/Config/Identity/AuthenticationUsage.cs +++ b/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) { diff --git a/src/Squidex/Config/Identity/AuthenticationServices.cs b/src/Squidex/Config/Identity/AuthenticationServices.cs index aeb2743b9..38cec3b7b 100644 --- a/src/Squidex/Config/Identity/AuthenticationServices.cs +++ b/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(); + var identityOptions = config.GetSection("identity").Get(); 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(); + var urlsOptions = config.GetSection("urls").Get(); if (!string.IsNullOrWhiteSpace(urlsOptions.BaseUrl)) { diff --git a/src/Squidex/Config/Identity/IdentityUsage.cs b/src/Squidex/Config/Identity/IdentityExtensions.cs similarity index 97% rename from src/Squidex/Config/Identity/IdentityUsage.cs rename to src/Squidex/Config/Identity/IdentityExtensions.cs index 22db07e35..6bcee01ee 100644 --- a/src/Squidex/Config/Identity/IdentityUsage.cs +++ b/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) { diff --git a/src/Squidex/Config/Identity/IdentityServerServices.cs b/src/Squidex/Config/Identity/IdentityServerServices.cs new file mode 100644 index 000000000..53f3ade0a --- /dev/null +++ b/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, + UserClaimsPrincipalFactoryWithEmail>(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddIdentityServer(options => + { + options.UserInteraction.ErrorUrl = "/error/"; + }) + .AddAspNetIdentity() + .AddInMemoryApiResources(GetApiResources()) + .AddInMemoryIdentityResources(GetIdentityResources()) + .AddSigningCredential(certificate); + } + + private static IEnumerable GetApiResources() + { + yield return new ApiResource(Constants.ApiScope) + { + UserClaims = new List + { + JwtClaimTypes.Email, + JwtClaimTypes.Role + } + }; + } + + private static IEnumerable 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 + }); + } + } +} diff --git a/src/Squidex/Config/Identity/IdentityServices.cs b/src/Squidex/Config/Identity/IdentityServices.cs index cdd8421a2..621a585b3 100644 --- a/src/Squidex/Config/Identity/IdentityServices.cs +++ b/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("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("identity:keysStore:redis:configuration"); - - if (string.IsNullOrWhiteSpace(redisConfiguration)) - { - throw new ConfigurationException("Configure KeyStore Redis configuration with 'identity:keysStore:redis:configuration'."); - } - - var connectionMultiplexer = Singletons.GetOrAdd(redisConfiguration, s => ConnectionMultiplexer.Connect(s)); - - dataProtection.PersistKeysToRedis(connectionMultiplexer); - } - else if (string.Equals(keyStoreType, "Folder", StringComparison.OrdinalIgnoreCase)) - { - var folderPath = configuration.GetValue("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, - UserClaimsPrincipalFactoryWithEmail>(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddIdentityServer(options => - { - options.UserInteraction.ErrorUrl = "/error/"; - }) - .AddAspNetIdentity() - .AddInMemoryApiResources(GetApiResources()) - .AddInMemoryIdentityResources(GetIdentityResources()) - .AddSigningCredential(certificate); - - return services; - } - - public static IServiceCollection AddMyIdentity(this IServiceCollection services) + public static void AddMyIdentity(this IServiceCollection services) { services.AddIdentity() .AddDefaultTokenProviders(); - - return services; } - private static IEnumerable GetApiResources() + public static void AddMyDataProtectection(this IServiceCollection services, IConfiguration config) { - yield return new ApiResource(Constants.ApiScope) - { - UserClaims = new List - { - JwtClaimTypes.Email, - JwtClaimTypes.Role - } - }; - } + var dataProtection = services.AddDataProtection().SetApplicationName("Squidex"); - private static IEnumerable 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(); + + services.AddSingleton>(s => + { + return new ConfigureOptions(options => { - SquidexClaimTypes.SquidexDisplayName, - SquidexClaimTypes.SquidexPictureUrl + options.XmlRepository = s.GetRequiredService(); }); + }); } } } diff --git a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/ResetConsumerMessage.cs b/src/Squidex/Config/Options.cs similarity index 57% rename from src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/ResetConsumerMessage.cs rename to src/Squidex/Config/Options.cs index 562514bc8..e1e5fb45b 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/Actors/Messages/ResetConsumerMessage.cs +++ b/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 { + public Options() + : base(StringComparer.OrdinalIgnoreCase) + { + } } } diff --git a/src/Squidex/Config/Orleans/ClientServices.cs b/src/Squidex/Config/Orleans/ClientServices.cs new file mode 100644 index 000000000..3ac53da25 --- /dev/null +++ b/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() + .As(); + + services.AddSingleton(c => + { + var client = new ClientBuilder() + .UseConfiguration(ClientConfiguration.LocalhostSilo()) + .AddApplicationPartsFromReferences(typeof(EventConsumerGrain).Assembly) + .AddApplicationPartsFromReferences(typeof(XmlRepositoryGrain).Assembly) + .Build(); + + client.Connect().Wait(); + + return client; + }); + } + } +} diff --git a/src/Squidex/Config/Orleans/IOrleansRunner.cs b/src/Squidex/Config/Orleans/IOrleansRunner.cs deleted file mode 100644 index e79a9c00e..000000000 --- a/src/Squidex/Config/Orleans/IOrleansRunner.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Squidex.Config.Orleans -{ - public class IOrleansRunner - { - } -} diff --git a/src/Squidex/Config/Orleans/SiloExtensions.cs b/src/Squidex/Config/Orleans/SiloExtensions.cs new file mode 100644 index 000000000..f251d2343 --- /dev/null +++ b/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; + } + } +} diff --git a/src/Squidex/Config/Orleans/SiloServices.cs b/src/Squidex/Config/Orleans/SiloServices.cs index f2c750bf6..499cf99e3 100644 --- a/src/Squidex/Config/Orleans/SiloServices.cs +++ b/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(); + var mongoConfiguration = config.GetRequiredValue("store:mongoDb:configuration"); + var mongoDatabaseName = config.GetRequiredValue("store:mongoDb:database"); - var hostBuilderContext = new HostBuilderContext(new Dictionary()) + var clusterConfiguration = + services.Where(x => x.ServiceType == typeof(ClusterConfiguration)) + .Select(x => x.ImplementationInstance) + .Select(x => (ClusterConfiguration)x) + .FirstOrDefault(); + + if (clusterConfiguration != null) + { + clusterConfiguration.Globals.RegisterBootstrapProvider("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() + .As(); } } -} +} \ No newline at end of file diff --git a/src/Squidex/Config/ServiceExtensions.cs b/src/Squidex/Config/ServiceExtensions.cs new file mode 100644 index 000000000..d669bb64a --- /dev/null +++ b/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 + { + private readonly IServiceCollection services; + + public InterfaceRegistrator(IServiceCollection services) + { + this.services = services; + } + + public InterfaceRegistrator As() + { + if (typeof(TInterface) != typeof(T)) + { + this.services.AddSingleton(typeof(TInterface), c => + { + return c.GetRequiredService(); + }); + } + + return this; + } + } + + public static InterfaceRegistrator AddSingleton(this IServiceCollection services, Func factory) where T : class + { + services.AddSingleton(typeof(T), factory); + + return new InterfaceRegistrator(services); + } + + public static InterfaceRegistrator AddSingleton(this IServiceCollection services, T instance) where T : class + { + services.AddSingleton(typeof(T), instance); + + return new InterfaceRegistrator(services); + } + + public static InterfaceRegistrator AddSingleton(this IServiceCollection services) where T : class + { + services.AddSingleton(); + + return new InterfaceRegistrator(services); + } + + public static T GetOptionalValue(this IConfiguration config, string path, T defaultValue = default(T)) + { + var value = config.GetValue(path, defaultValue); + + return value; + } + + public static string GetRequiredValue(this IConfiguration config, string path) + { + var value = config.GetValue(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; + } + } +} diff --git a/src/Squidex/Config/Swagger/SwaggerUsage.cs b/src/Squidex/Config/Swagger/SwaggerExtensions.cs similarity index 81% rename from src/Squidex/Config/Swagger/SwaggerUsage.cs rename to src/Squidex/Config/Swagger/SwaggerExtensions.cs index b0c1ecc07..c15976c81 100644 --- a/src/Squidex/Config/Swagger/SwaggerUsage.cs +++ b/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(); - app.UseSwagger(typeof(SwaggerUsage).GetTypeInfo().Assembly, settings); + app.UseSwagger(typeof(SwaggerExtensions).GetTypeInfo().Assembly, settings); } } } diff --git a/src/Squidex/Config/Web/WebUsages.cs b/src/Squidex/Config/Web/WebExtensions.cs similarity index 98% rename from src/Squidex/Config/Web/WebUsages.cs rename to src/Squidex/Config/Web/WebExtensions.cs index 75357fe80..314360c5a 100644 --- a/src/Squidex/Config/Web/WebUsages.cs +++ b/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) { diff --git a/src/Squidex/Config/Web/WebModule.cs b/src/Squidex/Config/Web/WebModule.cs deleted file mode 100644 index 0b83f7e63..000000000 --- a/src/Squidex/Config/Web/WebModule.cs +++ /dev/null @@ -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() - .AsSelf() - .SingleInstance(); - - builder.RegisterType() - .AsSelf() - .SingleInstance(); - } - } -} diff --git a/src/Squidex/Config/Web/WebDependencies.cs b/src/Squidex/Config/Web/WebServices.cs similarity index 71% rename from src/Squidex/Config/Web/WebDependencies.cs rename to src/Squidex/Config/Web/WebServices.cs index 019def3c5..bb1d994fd 100644 --- a/src/Squidex/Config/Web/WebDependencies.cs +++ b/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(); + services.AddSingleton(); + services.AddMvc().AddMySerializers(); + services.AddCors(); + services.AddRouting(); } } } diff --git a/src/Squidex/Config/Web/WebpackUsages.cs b/src/Squidex/Config/Web/WebpackExtensions.cs similarity index 92% rename from src/Squidex/Config/Web/WebpackUsages.cs rename to src/Squidex/Config/Web/WebpackExtensions.cs index 01ff6278b..b33a7979d 100644 --- a/src/Squidex/Config/Web/WebpackUsages.cs +++ b/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) { diff --git a/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs b/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs index 4d2228cf7..f020395aa 100644 --- a/src/Squidex/Controllers/Api/EventConsumers/EventConsumersController.cs +++ b/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("Default"); } [HttpGet] @@ -40,7 +36,7 @@ namespace Squidex.Controllers.Api.EventConsumers [ApiCosts(0)] public async Task 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 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 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 Reset(string name) { - var actor = actors.Get(name); - - actor?.Tell(new ResetConsumerMessage()); + await eventConsumerRegistryGrain.ResetAsync(name); return NoContent(); } diff --git a/src/Squidex/Program.cs b/src/Squidex/Program.cs index 35d74a6e0..9af35940e 100644 --- a/src/Squidex/Program.cs +++ b/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() - .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() + .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"; } } } diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index c99422e47..66e0cfcd1 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -48,8 +48,6 @@ - - @@ -68,6 +66,7 @@ + diff --git a/src/Squidex/Startup.cs b/src/Squidex/WebStartup.cs similarity index 50% rename from src/Squidex/Startup.cs rename to src/Squidex/WebStartup.cs index deb1601d0..1af37eab2 100644 --- a/src/Squidex/Startup.cs +++ b/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( - Configuration.GetSection("urls")); - services.Configure( - Configuration.GetSection("identity")); - services.Configure( - Configuration.GetSection("ui")); - services.Configure( - 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().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()); - - 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(); - - 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(); diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 0fc439776..2709ed69d 100644 --- a/src/Squidex/appsettings.json +++ b/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 } } \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Actors/ActorRemoteTests.cs b/tests/Squidex.Infrastructure.Tests/Actors/ActorRemoteTests.cs deleted file mode 100644 index e53df6eac..000000000 --- a/tests/Squidex.Infrastructure.Tests/Actors/ActorRemoteTests.cs +++ /dev/null @@ -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 Invokes { get; } = new List(); - - 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 - { - new SuccessMessage { Counter = 1 }, - new SuccessMessage { Counter = 2 }, - new SuccessMessage { Counter = 3 } - }); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs deleted file mode 100644 index 1dde168d1..000000000 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/Actors/EventConsumerActorTests.cs +++ /dev/null @@ -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(); - private readonly IEventConsumer eventConsumer = A.Fake(); - private readonly IEventStore eventStore = A.Fake(); - private readonly IEventSubscription eventSubscription = A.Fake(); - private readonly ISemanticLog log = A.Fake(); - private readonly IActor sutActor; - private readonly IEventSubscriber sutSubscriber; - private readonly EventDataFormatter formatter = A.Fake(); - private readonly EventData eventData = new EventData(); - private readonly Envelope envelope = new Envelope(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.Ignored, A.Ignored, A.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.Ignored, A.Ignored, A.Ignored)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_subscribe_to_event_store_when_not_found_in_db() - { - A.CallTo(() => eventConsumerInfoRepository.FindAsync(consumerName)).Returns(Task.FromResult(null)); - - await OnSubscribeAsync(); - - sut.Dispose(); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.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.Ignored, A.Ignored, A.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.Ignored, A.Ignored, consumerInfo.Position)) - .MustHaveHappened(Repeated.Exactly.Once); - - A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.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(), @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(), 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.Ignored, A.Ignored, A.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); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultEventNotifierTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultEventNotifierTests.cs deleted file mode 100644 index 8810c42a1..000000000 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/DefaultEventNotifierTests.cs +++ /dev/null @@ -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(); - - 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" }); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventConsumerCleanerTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventConsumerCleanerTests.cs deleted file mode 100644 index ea60aa0e7..000000000 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventConsumerCleanerTests.cs +++ /dev/null @@ -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(); - var eventConsumer2 = A.Fake(); - - A.CallTo(() => eventConsumer1.Name).Returns("consumer1"); - A.CallTo(() => eventConsumer2.Name).Returns("consumer2"); - - var repository = A.Fake(); - - var sut = new EventConsumerCleaner(new[] { eventConsumer1, eventConsumer2 }, repository); - - await sut.CleanAsync(); - - A.CallTo(() => repository.ClearAsync(A>.That.IsSameSequenceAs(new string[] { "consumer1", "consumer2" }))).MustHaveHappened(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/PollingSubscriptionTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventSubscriptionTests.cs similarity index 67% rename from tests/Squidex.Infrastructure.Tests/CQRS/Events/PollingSubscriptionTests.cs rename to tests/Squidex.Infrastructure.Tests/CQRS/Events/EventSubscriptionTests.cs index e5e2c063b..15a3a74c3 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/PollingSubscriptionTests.cs +++ b/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(); - private readonly IEventNotifier eventNotifier = new DefaultEventNotifier(new InMemoryPubSub()); private readonly IEventSubscriber eventSubscriber = A.Fake(); 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>.Ignored, A.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>.Ignored, A.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>.Ignored, A.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>.Ignored, A.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>.Ignored, A.Ignored, "^my-stream", position)) - .MustHaveHappened(Repeated.Exactly.Twice); + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) + .MustNotHaveHappened(); } private static async Task WaitAndStopAsync(IEventSubscription sut) diff --git a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs b/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs index b91e7deef..2fffa7273 100644 --- a/tests/Squidex.Infrastructure.Tests/Log/SemanticLogTests.cs +++ b/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(); loggerInstance.LogCritical(new EventId(123, "EventName"), exception, "Log {0}", 123); diff --git a/tests/Squidex.Infrastructure.Tests/Actors/SingleThreadedDispatcherTests.cs b/tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs similarity index 98% rename from tests/Squidex.Infrastructure.Tests/Actors/SingleThreadedDispatcherTests.cs rename to tests/Squidex.Infrastructure.Tests/Tasks/SingleThreadedDispatcherTests.cs index 4ec54c725..a04966154 100644 --- a/tests/Squidex.Infrastructure.Tests/Actors/SingleThreadedDispatcherTests.cs +++ b/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 {