diff --git a/libs/orleansdashboard/2.0.0-rc1/orleansdashboard.2.0.0-rc1.nupkg b/libs/orleansdashboard/2.0.0-rc1/orleansdashboard.2.0.0-rc1.nupkg new file mode 100644 index 000000000..b2edaddf6 Binary files /dev/null and b/libs/orleansdashboard/2.0.0-rc1/orleansdashboard.2.0.0-rc1.nupkg differ diff --git a/libs/orleansdashboard/2.0.0-rc1/orleansdashboard.2.0.0-rc1.nupkg.sha512 b/libs/orleansdashboard/2.0.0-rc1/orleansdashboard.2.0.0-rc1.nupkg.sha512 new file mode 100644 index 000000000..540b97178 --- /dev/null +++ b/libs/orleansdashboard/2.0.0-rc1/orleansdashboard.2.0.0-rc1.nupkg.sha512 @@ -0,0 +1 @@ +oeHEL1XH6DwEv4Rk6JjAABzcpTdBI3Zmoz3tyn+20vBUcvsdmKQMFp8I1rBZmAeOJ9NSvvRYf8LHDM2UtRTbvw== \ No newline at end of file diff --git a/libs/orleansdashboard/2.0.0-rc1/orleansdashboard.nuspec b/libs/orleansdashboard/2.0.0-rc1/orleansdashboard.nuspec new file mode 100644 index 000000000..e31eec4a6 --- /dev/null +++ b/libs/orleansdashboard/2.0.0-rc1/orleansdashboard.nuspec @@ -0,0 +1,25 @@ + + + + OrleansDashboard + 2.0.0-rc1 + OrleansContrib + OrleansContrib + false + https://opensource.org/licenses/MIT + https://github.com/OrleansContrib/OrleansDashboard + http://dotnet.github.io/orleans/assets/logo.png + An admin dashboard for Microsoft Orleans + Copyright © 2017 + orleans dashboard metrics monitor + + + + + + + + + + + \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj index 91ab5c926..56ec1fe60 100644 --- a/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj +++ b/src/Squidex.Domain.Apps.Core.Model/Squidex.Domain.Apps.Core.Model.csproj @@ -8,7 +8,7 @@ True - + diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs index 5fe661d4f..bac9ef7fd 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/WebhookActionHandler.cs @@ -63,7 +63,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions var responseString = await response.Content.ReadAsStringAsync(); var requestDump = DumpFormatter.BuildDump(requestMsg, response, requestBody, responseString, TimeSpan.Zero, false); - return (requestDump, null); + Exception ex = null; + + if (!response.IsSuccessStatusCode) + { + ex = new HttpRequestException($"Response code does not indicate success: {(int)response.StatusCode} ({response.StatusCode})."); + } + + return (requestDump, ex); } catch (Exception ex) { diff --git a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs index 13ca5f459..6914f3d44 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs +++ b/src/Squidex.Domain.Apps.Core.Operations/HandleRules/Triggers/ContentChangedTriggerHandler.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Triggers { protected override bool Triggers(Envelope @event, ContentChangedTrigger trigger) { - if (trigger.HandleAll) + if (trigger.HandleAll && @event.Payload is ContentEvent) { return true; } diff --git a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj index 4c836c4ee..413191771 100644 --- a/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj +++ b/src/Squidex.Domain.Apps.Core.Operations/Squidex.Domain.Apps.Core.Operations.csproj @@ -13,18 +13,18 @@ - - + + - - - - + + + + - + ..\..\Squidex.ruleset diff --git a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj index 1095818b8..c63141855 100644 --- a/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj +++ b/src/Squidex.Domain.Apps.Entities.MongoDb/Squidex.Domain.Apps.Entities.MongoDb.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Squidex.Domain.Apps.Entities/AppProvider.cs b/src/Squidex.Domain.Apps.Entities/AppProvider.cs index 7439e4a1e..29a4e9283 100644 --- a/src/Squidex.Domain.Apps.Entities/AppProvider.cs +++ b/src/Squidex.Domain.Apps.Entities/AppProvider.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Orleans; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Domain.Apps.Entities.Rules; @@ -16,52 +17,50 @@ using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities { public sealed class AppProvider : IAppProvider { + private readonly IGrainFactory grainFactory; private readonly IAppRepository appRepository; private readonly IRuleRepository ruleRepository; private readonly ISchemaRepository schemaRepository; - private readonly IStateFactory stateFactory; public AppProvider( + IGrainFactory grainFactory, IAppRepository appRepository, ISchemaRepository schemaRepository, - IStateFactory stateFactory, IRuleRepository ruleRepository) { + Guard.NotNull(grainFactory, nameof(grainFactory)); Guard.NotNull(appRepository, nameof(appRepository)); Guard.NotNull(schemaRepository, nameof(schemaRepository)); - Guard.NotNull(stateFactory, nameof(stateFactory)); Guard.NotNull(ruleRepository, nameof(ruleRepository)); + this.grainFactory = grainFactory; this.appRepository = appRepository; this.schemaRepository = schemaRepository; - this.stateFactory = stateFactory; this.ruleRepository = ruleRepository; } public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id) { - var app = await stateFactory.GetSingleAsync(appId); + var app = await grainFactory.GetGrain(appId).GetStateAsync(); - if (!IsFound(app)) + if (!IsFound(app.Value)) { return (null, null); } - var schema = await stateFactory.GetSingleAsync(id); + var schema = await grainFactory.GetGrain(id).GetStateAsync(); - if (!IsFound(schema) || schema.Snapshot.IsDeleted) + if (!IsFound(schema.Value) || schema.Value.IsDeleted) { return (null, null); } - return (app.Snapshot, schema.Snapshot); + return (app.Value, schema.Value); } public async Task GetAppAsync(string appName) @@ -73,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities return null; } - return (await stateFactory.GetSingleAsync(appId)).Snapshot; + return (await grainFactory.GetGrain(appId).GetStateAsync()).Value; } public async Task GetSchemaAsync(Guid appId, string name) @@ -85,19 +84,19 @@ namespace Squidex.Domain.Apps.Entities return null; } - return (await stateFactory.GetSingleAsync(schemaId)).Snapshot; + return (await grainFactory.GetGrain(schemaId).GetStateAsync()).Value; } public async Task GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false) { - var schema = await stateFactory.GetSingleAsync(id); + var schema = await grainFactory.GetGrain(id).GetStateAsync(); - if (!IsFound(schema) || (schema.Snapshot.IsDeleted && !allowDeleted) || schema.Snapshot.AppId.Id != appId) + if (!IsFound(schema.Value) || (schema.Value.IsDeleted && !allowDeleted) || schema.Value.AppId.Id != appId) { return null; } - return schema.Snapshot; + return schema.Value; } public async Task> GetSchemasAsync(Guid appId) @@ -106,9 +105,9 @@ namespace Squidex.Domain.Apps.Entities var schemas = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id))); + ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); - return schemas.Where(IsFound).Select(s => (ISchemaEntity)s.Snapshot).ToList(); + return schemas.Where(s => IsFound(s.Value)).Select(s => (ISchemaEntity)s.Value).ToList(); } public async Task> GetRulesAsync(Guid appId) @@ -117,9 +116,9 @@ namespace Squidex.Domain.Apps.Entities var rules = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id))); + ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); - return rules.Where(IsFound).Select(r => (IRuleEntity)r.Snapshot).ToList(); + return rules.Where(r => IsFound(r.Value)).Select(r => (IRuleEntity)r.Value).ToList(); } public async Task> GetUserApps(string userId) @@ -128,9 +127,9 @@ namespace Squidex.Domain.Apps.Entities var apps = await Task.WhenAll( - ids.Select(id => stateFactory.GetSingleAsync(id))); + ids.Select(id => grainFactory.GetGrain(id).GetStateAsync())); - return apps.Where(IsFound).Select(a => (IAppEntity)a.Snapshot).ToList(); + return apps.Where(a => IsFound(a.Value)).Select(a => (IAppEntity)a.Value).ToList(); } private Task GetAppIdAsync(string name) @@ -143,9 +142,9 @@ namespace Squidex.Domain.Apps.Entities return await schemaRepository.FindSchemaIdAsync(appId, name); } - private static bool IsFound(IDomainObjectGrain app) + private static bool IsFound(IEntityWithVersion entity) { - return app.Version > EtagVersion.Empty; + return entity.Version > EtagVersion.Empty; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs index 50a342d7e..eb9a3b886 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs @@ -18,13 +18,14 @@ using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Apps { - public class AppGrain : DomainObjectGrain + public class AppGrain : DomainObjectGrain, IAppGrain { private readonly InitialPatterns initialPatterns; private readonly IAppProvider appProvider; @@ -54,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps this.initialPatterns = initialPatterns; } - public override Task ExecuteAsync(IAggregateCommand command) + protected override Task ExecuteAsync(IAggregateCommand command) { switch (command) { @@ -310,5 +311,10 @@ namespace Squidex.Domain.Apps.Entities.Apps { ApplySnapshot(Snapshot.Apply(@event)); } + + public Task> GetStateAsync() + { + return Task.FromResult(new J(Snapshot)); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs index facea56c0..88b6b7b17 100644 --- a/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs +++ b/src/Squidex.Domain.Apps.Entities/Apps/IAppEntity.cs @@ -9,7 +9,11 @@ using Squidex.Domain.Apps.Core.Apps; namespace Squidex.Domain.Apps.Entities.Apps { - public interface IAppEntity : IEntity, IEntityWithVersion + public interface IAppEntity : + IEntity, + IEntityWithCreatedBy, + IEntityWithLastModifiedBy, + IEntityWithVersion { string Name { get; } diff --git a/src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs b/src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs new file mode 100644 index 000000000..d98dd68b8 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Apps/IAppGrain.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Apps +{ + public interface IAppGrain : IDomainObjectGrain + { + Task> GetStateAsync(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs index 734ddb57c..a50f1196d 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetCommandMiddleware.cs @@ -7,24 +7,24 @@ using System; using System.Threading.Tasks; +using Orleans; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetCommandMiddleware : GrainCommandMiddleware + public sealed class AssetCommandMiddleware : GrainCommandMiddleware { private readonly IAssetStore assetStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; public AssetCommandMiddleware( - IStateFactory stateFactory, + IGrainFactory grainFactory, IAssetStore assetStore, IAssetThumbnailGenerator assetThumbnailGenerator) - : base(stateFactory) + : base(grainFactory) { Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetThumbnailGenerator, nameof(assetThumbnailGenerator)); diff --git a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs index 19858fbf1..cf7b15ff0 100644 --- a/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs @@ -15,19 +15,20 @@ using Squidex.Domain.Apps.Events.Assets; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetGrain : DomainObjectGrain + public class AssetGrain : DomainObjectGrain, IAssetGrain { public AssetGrain(IStore store) : base(store) { } - public override Task ExecuteAsync(IAggregateCommand command) + protected override Task ExecuteAsync(IAggregateCommand command) { switch (command) { @@ -137,5 +138,10 @@ namespace Squidex.Domain.Apps.Entities.Assets { ApplySnapshot(Snapshot.Apply(@event)); } + + public Task> GetStateAsync() + { + return Task.FromResult(new J(Snapshot)); + } } } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/GetStatesRequest.cs b/src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs similarity index 63% rename from src/Squidex.Infrastructure/EventSourcing/Grains/Messages/GetStatesRequest.cs rename to src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs index d193d7ebb..864c0256a 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/GetStatesRequest.cs +++ b/src/Squidex.Domain.Apps.Entities/Assets/IAssetGrain.cs @@ -1,13 +1,15 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing.Grains.Messages +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class GetStatesRequest + public interface IAssetGrain : IDomainObjectGrain { } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs index e1ef9b9ba..97334f087 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs @@ -24,7 +24,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Contents { - public class ContentGrain : DomainObjectGrain + public class ContentGrain : DomainObjectGrain, IContentGrain { private readonly IAppProvider appProvider; private readonly IAssetRepository assetRepository; @@ -50,7 +50,7 @@ namespace Squidex.Domain.Apps.Entities.Contents this.contentRepository = contentRepository; } - public override Task ExecuteAsync(IAggregateCommand command) + protected override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); diff --git a/src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs similarity index 67% rename from src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs rename to src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs index 23d3c05a9..a109042d2 100644 --- a/src/Squidex.Domain.Apps.Entities/Contents/ContentScheduler.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs @@ -5,24 +5,26 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; using System.Threading.Tasks; using NodaTime; +using Orleans; +using Orleans.Runtime; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Timers; +using Squidex.Infrastructure.Tasks; namespace Squidex.Domain.Apps.Entities.Contents { - public sealed class ContentScheduler : IRunnable + public sealed class ContentSchedulerGrain : Grain, IContentSchedulerGrain, IRemindable { - private readonly CompletionTimer timer; private readonly IContentRepository contentRepository; private readonly ICommandBus commandBus; private readonly IClock clock; - public ContentScheduler( + public ContentSchedulerGrain( IContentRepository contentRepository, ICommandBus commandBus, IClock clock) @@ -34,15 +36,24 @@ namespace Squidex.Domain.Apps.Entities.Contents this.contentRepository = contentRepository; this.commandBus = commandBus; this.clock = clock; + } + + public override Task OnActivateAsync() + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => PublishAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); - timer = new CompletionTimer(5000, x => PublishAsync()); + return Task.FromResult(true); } - public void Run() + public Task ActivateAsync() { + return TaskHelper.Done; } - private Task PublishAsync() + public Task PublishAsync() { var now = clock.GetCurrentInstant(); @@ -53,5 +64,10 @@ namespace Squidex.Domain.Apps.Entities.Contents return commandBus.PublishAsync(command); }); } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } } } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/StopConsumerMessage.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs similarity index 61% rename from src/Squidex.Infrastructure/EventSourcing/Grains/Messages/StopConsumerMessage.cs rename to src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs index 5a354a468..0b2d547c1 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/StopConsumerMessage.cs +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentGrain.cs @@ -1,14 +1,15 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing.Grains.Messages +using Squidex.Infrastructure.Commands; + +namespace Squidex.Domain.Apps.Entities.Contents { - public sealed class StopConsumerMessage + public interface IContentGrain : IDomainObjectGrain { - public string ConsumerName { get; set; } } } diff --git a/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs b/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs new file mode 100644 index 000000000..77f7fcc0e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs @@ -0,0 +1,15 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Contents +{ + public interface IContentSchedulerGrain : IBackgroundGrain + { + } +} diff --git a/src/Squidex.Infrastructure/States/InvalidateMessage.cs b/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs similarity index 63% rename from src/Squidex.Infrastructure/States/InvalidateMessage.cs rename to src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs index b55f77145..793e43617 100644 --- a/src/Squidex.Infrastructure/States/InvalidateMessage.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs @@ -1,14 +1,15 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.States +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Rules { - public sealed class InvalidateMessage + public interface IRuleDequeuerGrain : IBackgroundGrain { - public string Key { get; set; } } -} \ No newline at end of file +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs new file mode 100644 index 000000000..4ba5432c2 --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Rules/IRuleGrain.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Rules +{ + public interface IRuleGrain : IDomainObjectGrain + { + Task> GetStateAsync(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs similarity index 82% rename from src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs rename to src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs index 693d661bb..79b6726a9 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuer.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs @@ -7,31 +7,30 @@ using System; using System.Collections.Concurrent; -using System.Threading; using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using NodaTime; +using Orleans; +using Orleans.Runtime; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Tasks; -using Squidex.Infrastructure.Timers; namespace Squidex.Domain.Apps.Entities.Rules { - public class RuleDequeuer : DisposableObjectBase, IRunnable + public class RuleDequeuerGrain : Grain, IRuleDequeuerGrain, IRemindable { private readonly ITargetBlock requestBlock; private readonly IRuleEventRepository ruleEventRepository; private readonly RuleService ruleService; - private readonly CompletionTimer timer; private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); private readonly IClock clock; private readonly ISemanticLog log; - public RuleDequeuer(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) + public RuleDequeuerGrain(RuleService ruleService, IRuleEventRepository ruleEventRepository, ISemanticLog log, IClock clock) { Guard.NotNull(ruleEventRepository, nameof(ruleEventRepository)); Guard.NotNull(ruleService, nameof(ruleService)); @@ -48,37 +47,37 @@ namespace Squidex.Domain.Apps.Entities.Rules requestBlock = new PartitionedActionBlock(HandleAsync, x => x.Job.AggregateId.GetHashCode(), new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); - - timer = new CompletionTimer(5000, QueryAsync); } - protected override void DisposeObject(bool disposing) + public override Task OnActivateAsync() { - if (disposing) - { - timer.StopAsync().Wait(); + DelayDeactivation(TimeSpan.FromDays(1)); - requestBlock.Complete(); - requestBlock.Completion.Wait(); - } + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + RegisterTimer(x => QueryAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + + return Task.FromResult(true); } - public void Run() + public override Task OnDeactivateAsync() { + requestBlock.Complete(); + + return requestBlock.Completion; } - public void Next() + public Task ActivateAsync() { - timer.SkipCurrentDelay(); + return TaskHelper.Done; } - private async Task QueryAsync(CancellationToken ct) + public async Task QueryAsync() { try { var now = clock.GetCurrentInstant(); - await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, ct); + await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync); } catch (Exception ex) { @@ -153,5 +152,10 @@ namespace Squidex.Domain.Apps.Entities.Rules return null; } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return TaskHelper.Done; + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs index e74e2b6ab..ba316f6a2 100644 --- a/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs @@ -15,12 +15,13 @@ using Squidex.Domain.Apps.Events.Rules; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Rules { - public class RuleGrain : DomainObjectGrain + public sealed class RuleGrain : DomainObjectGrain, IRuleGrain { private readonly IAppProvider appProvider; @@ -32,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Rules this.appProvider = appProvider; } - public override Task ExecuteAsync(IAggregateCommand command) + protected override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); @@ -125,5 +126,10 @@ namespace Squidex.Domain.Apps.Entities.Rules { ApplySnapshot(Snapshot.Apply(@event)); } + + public Task> GetStateAsync() + { + return Task.FromResult(new J(Snapshot)); + } } } diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs new file mode 100644 index 000000000..ab41ae13e --- /dev/null +++ b/src/Squidex.Domain.Apps.Entities/Schemas/ISchemaGrain.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Domain.Apps.Entities.Schemas +{ + public interface ISchemaGrain : IDomainObjectGrain + { + Task> GetStateAsync(); + } +} diff --git a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs index 2f12f1336..baf1ef9ec 100644 --- a/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs +++ b/src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs @@ -18,12 +18,13 @@ using Squidex.Domain.Apps.Events.Schemas; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Schemas { - public class SchemaGrain : DomainObjectGrain + public class SchemaGrain : DomainObjectGrain, ISchemaGrain { private readonly IAppProvider appProvider; private readonly FieldRegistry registry; @@ -39,7 +40,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas this.registry = registry; } - public override Task ExecuteAsync(IAggregateCommand command) + protected override Task ExecuteAsync(IAggregateCommand command) { VerifyNotDeleted(); @@ -300,5 +301,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas { ApplySnapshot(Snapshot.Apply(@event, registry)); } + + public Task> GetStateAsync() + { + return Task.FromResult(new J(Snapshot)); + } } } \ No newline at end of file diff --git a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index 431c71718..79b7bc932 100644 --- a/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -14,7 +14,8 @@ - + + diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/ResetConsumerMessage.cs b/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs similarity index 58% rename from src/Squidex.Infrastructure/EventSourcing/Grains/Messages/ResetConsumerMessage.cs rename to src/Squidex.Domain.Apps.Entities/SquidexEntities.cs index 012cfd2e1..c65786a8d 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/ResetConsumerMessage.cs +++ b/src/Squidex.Domain.Apps.Entities/SquidexEntities.cs @@ -1,14 +1,16 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing.Grains.Messages +using System.Reflection; + +namespace Squidex.Domain.Apps.Entities { - public sealed class ResetConsumerMessage + public sealed class SquidexEntities { - public string ConsumerName { get; set; } + public static readonly Assembly Assembly = typeof(SquidexEntities).Assembly; } } diff --git a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj index 456d82e06..9c578c330 100644 --- a/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj +++ b/src/Squidex.Domain.Apps.Events/Squidex.Domain.Apps.Events.csproj @@ -12,8 +12,8 @@ - - + + diff --git a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj index 50c66cb45..fce1bc788 100644 --- a/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj +++ b/src/Squidex.Domain.Users.MongoDb/Squidex.Domain.Users.MongoDb.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj index 10b6e916a..0ca9385ae 100644 --- a/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj +++ b/src/Squidex.Domain.Users/Squidex.Domain.Users.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj index 7f009bb3d..3e4bd2895 100644 --- a/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj +++ b/src/Squidex.Infrastructure.Azure/Squidex.Infrastructure.Azure.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs index cbf1559f5..b3cfed490 100644 --- a/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ b/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -45,6 +45,10 @@ namespace Squidex.Infrastructure.EventSourcing return TaskHelper.Done; } + public void WakeUp() + { + } + private EventStoreCatchUpSubscription SubscribeToStream(string streamName) { var settings = CatchUpSubscriptionSettings.Default; diff --git a/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj b/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj index 4ebcb4f28..8a0943f3c 100644 --- a/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj +++ b/src/Squidex.Infrastructure.GetEventStore/Squidex.Infrastructure.GetEventStore.csproj @@ -8,7 +8,7 @@ True - + diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs index c33e7aee7..395fa594f 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEvent.cs @@ -7,6 +7,7 @@ using MongoDB.Bson.Serialization.Attributes; using Newtonsoft.Json.Linq; +using Squidex.Infrastructure.MongoDb; namespace Squidex.Infrastructure.EventSourcing { @@ -16,7 +17,7 @@ namespace Squidex.Infrastructure.EventSourcing [BsonRequired] public string Type { get; set; } - [BsonElement] + [BsonJson] [BsonRequired] public string Payload { get; set; } diff --git a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs index 175b74942..d81be0421 100644 --- a/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs +++ b/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Reader.cs @@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.EventSourcing Guard.NotNull(subscriber, nameof(subscriber)); Guard.NotNullOrEmpty(streamFilter, nameof(streamFilter)); - return new PollingSubscription(this, notifier, subscriber, streamFilter, position); + return new PollingSubscription(this, subscriber, streamFilter, position); } public async Task> QueryAsync(string streamName, long streamPosition = 0) diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs index 2a8d6e572..77019a716 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonConvention.cs @@ -49,4 +49,4 @@ namespace Squidex.Infrastructure.MongoDb ConventionRegistry.Register("json", pack, t => true); } } -} +} \ No newline at end of file diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs index d06df02c9..eefb3e3e5 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonSerializer.cs @@ -12,7 +12,7 @@ using Newtonsoft.Json; namespace Squidex.Infrastructure.MongoDb { - public class BsonJsonSerializer : ClassSerializerBase where T : class + public sealed class BsonJsonSerializer : ClassSerializerBase where T : class { private readonly JsonSerializer serializer; diff --git a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs index dcf598616..52b3ac5d3 100644 --- a/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs +++ b/src/Squidex.Infrastructure.MongoDb/MongoDb/BsonJsonWriter.cs @@ -6,6 +6,7 @@ // ========================================================================== using System; +using System.Globalization; using MongoDB.Bson.IO; using NewtonsoftJSonWriter = Newtonsoft.Json.JsonWriter; @@ -134,12 +135,12 @@ namespace Squidex.Infrastructure.MongoDb public override void WriteValue(DateTime value) { - bsonWriter.WriteString(value.ToString()); + bsonWriter.WriteString(value.ToString(CultureInfo.InvariantCulture)); } public override void WriteValue(DateTimeOffset value) { - bsonWriter.WriteString(value.ToString()); + bsonWriter.WriteString(value.ToString(CultureInfo.InvariantCulture)); } public override void WriteValue(byte[] value) diff --git a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj index e7f9a5feb..bef0bc3ae 100644 --- a/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj +++ b/src/Squidex.Infrastructure.MongoDb/Squidex.Infrastructure.MongoDb.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Squidex.Infrastructure/Assets/AssetFile.cs b/src/Squidex.Infrastructure/Assets/AssetFile.cs index 3b59ba505..bbaa0917d 100644 --- a/src/Squidex.Infrastructure/Assets/AssetFile.cs +++ b/src/Squidex.Infrastructure/Assets/AssetFile.cs @@ -7,6 +7,7 @@ using System; using System.IO; +using Newtonsoft.Json; namespace Squidex.Infrastructure.Assets { @@ -20,11 +21,11 @@ namespace Squidex.Infrastructure.Assets public long FileSize { get; } + [JsonConstructor] public AssetFile(string fileName, string mimeType, long fileSize, Func openAction) { Guard.NotNullOrEmpty(fileName, nameof(fileName)); Guard.NotNullOrEmpty(mimeType, nameof(mimeType)); - Guard.NotNull(openAction, nameof(openAction)); Guard.GreaterEquals(fileSize, 0, nameof(fileSize)); FileName = fileName; diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs index 6d6768581..339c2ebd6 100644 --- a/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs @@ -9,12 +9,13 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.Commands { - public abstract class DomainObjectGrain : IDomainObjectGrain where T : IDomainState, new() + public abstract class DomainObjectGrain : GrainOfGuid, IDomainObjectGrain where T : IDomainState, new() { private readonly List> uncomittedEvents = new List>(); private readonly IStore store; @@ -49,11 +50,11 @@ namespace Squidex.Infrastructure.Commands this.store = store; } - public Task ActivateAsync(Guid key) + public override Task OnActivateAsync(Guid key) { id = key; - persistence = store.WithSnapshotsAndEventSourcing(GetType(), key, ApplySnapshot, ApplyEvent); + persistence = store.WithSnapshotsAndEventSourcing(GetType(), id, ApplySnapshot, ApplyEvent); return persistence.ReadAsync(); } @@ -67,7 +68,7 @@ namespace Squidex.Infrastructure.Commands { Guard.NotNull(@event, nameof(@event)); - @event.SetAggregateId(Id); + @event.SetAggregateId(id); ApplyEvent(@event); @@ -146,12 +147,20 @@ namespace Squidex.Infrastructure.Commands if (command.ExpectedVersion != EtagVersion.Any && command.ExpectedVersion != Version) { - throw new DomainObjectVersionException(Id.ToString(), GetType(), Version, command.ExpectedVersion); + throw new DomainObjectVersionException(id.ToString(), GetType(), Version, command.ExpectedVersion); } if (isUpdate && Version < 0) { - throw new DomainObjectNotFoundException(Id.ToString(), GetType()); + try + { + DeactivateOnIdle(); + } + catch (InvalidOperationException) + { + } + + throw new DomainObjectNotFoundException(id.ToString(), GetType()); } else if (!isUpdate && Version >= 0) { @@ -181,7 +190,7 @@ namespace Squidex.Infrastructure.Commands } else { - result = EntityCreatedResult.Create(Id, Version); + result = EntityCreatedResult.Create(id, Version); } } @@ -195,10 +204,17 @@ namespace Squidex.Infrastructure.Commands } finally { - ClearUncommittedEvents(); + uncomittedEvents.Clear(); } } - public abstract Task ExecuteAsync(IAggregateCommand command); + public async Task> ExecuteAsync(J command) + { + var result = await ExecuteAsync(command.Value); + + return result.AsJ(); + } + + protected abstract Task ExecuteAsync(IAggregateCommand command); } } diff --git a/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs b/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs new file mode 100644 index 000000000..6d0da2184 --- /dev/null +++ b/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs @@ -0,0 +1,36 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Orleans; + +namespace Squidex.Infrastructure.Commands +{ + public static class DomainObjectGrainFormatter + { + public static string Format(IGrainCallContext context) + { + if (context.Method == null) + { + return "Unknown"; + } + + if (string.Equals(context.Method.Name, nameof(IDomainObjectGrain.ExecuteAsync), StringComparison.CurrentCultureIgnoreCase) && + context.Arguments?.Length == 1 && + context.Arguments[0] != null) + { + var argumentFullName = context.Arguments[0].ToString(); + var argumentParts = argumentFullName.Split('.'); + var argumentName = argumentParts[argumentParts.Length - 1]; + + return $"{nameof(IDomainObjectGrain.ExecuteAsync)}({argumentName})"; + } + + return context.Method.Name; + } + } +} diff --git a/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs index a6ef41363..97ebe0b0d 100644 --- a/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs +++ b/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs @@ -7,19 +7,19 @@ using System; using System.Threading.Tasks; -using Squidex.Infrastructure.States; +using Orleans; namespace Squidex.Infrastructure.Commands { public class GrainCommandMiddleware : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain { - private readonly IStateFactory stateFactory; + private readonly IGrainFactory grainFactory; - public GrainCommandMiddleware(IStateFactory stateFactory) + public GrainCommandMiddleware(IGrainFactory grainFactory) { - Guard.NotNull(stateFactory, nameof(stateFactory)); + Guard.NotNull(grainFactory, nameof(grainFactory)); - this.stateFactory = stateFactory; + this.grainFactory = grainFactory; } public async virtual Task HandleAsync(CommandContext context, Func next) @@ -36,11 +36,11 @@ namespace Squidex.Infrastructure.Commands protected async Task ExecuteCommandAsync(TCommand typedCommand) { - var grain = await stateFactory.CreateAsync(typedCommand.AggregateId); + var grain = grainFactory.GetGrain(typedCommand.AggregateId); var result = await grain.ExecuteAsync(typedCommand); - return result; + return result.Value; } } } diff --git a/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs b/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs index 37c80cdb6..500db0245 100644 --- a/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs +++ b/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs @@ -5,18 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Threading.Tasks; -using Squidex.Infrastructure.States; +using Orleans; +using Squidex.Infrastructure.Orleans; namespace Squidex.Infrastructure.Commands { - public interface IDomainObjectGrain : IStatefulObject + public interface IDomainObjectGrain : IGrainWithGuidKey { - Task ExecuteAsync(IAggregateCommand command); - Task WriteSnapshotAsync(); - long Version { get; } + Task> ExecuteAsync(J command); } -} \ No newline at end of file +} diff --git a/src/Squidex.Infrastructure/Commands/SyncedGrainCommandMiddleware.cs b/src/Squidex.Infrastructure/Commands/SyncedGrainCommandMiddleware.cs deleted file mode 100644 index d3d85159f..000000000 --- a/src/Squidex.Infrastructure/Commands/SyncedGrainCommandMiddleware.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; - -namespace Squidex.Infrastructure.Commands -{ - public class SyncedGrainCommandMiddleware : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain - { - private readonly AsyncLockPool lockPool = new AsyncLockPool(10000); - private readonly IStateFactory stateFactory; - - public SyncedGrainCommandMiddleware(IStateFactory stateFactory) - { - Guard.NotNull(stateFactory, nameof(stateFactory)); - - this.stateFactory = stateFactory; - } - - public async virtual Task HandleAsync(CommandContext context, Func next) - { - if (context.Command is TCommand typedCommand) - { - var result = await ExecuteCommandAsync(typedCommand); - - context.Complete(result); - } - - await next(); - } - - protected async Task ExecuteCommandAsync(TCommand typedCommand) - { - var id = typedCommand.AggregateId; - - using (await lockPool.LockAsync(typedCommand.AggregateId)) - { - try - { - var grain = await stateFactory.GetSingleAsync(id); - - var result = await grain.ExecuteAsync(typedCommand); - - stateFactory.Synchronize(id); - - return result; - } - catch - { - stateFactory.Remove(id); - throw; - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/DefaultEventNotifier.cs b/src/Squidex.Infrastructure/EventSourcing/DefaultEventNotifier.cs deleted file mode 100644 index 1e94354e3..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/DefaultEventNotifier.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class DefaultEventNotifier : IEventNotifier - { - private static readonly string ChannelName = typeof(DefaultEventNotifier).Name; - - private readonly IPubSub pubsub; - - public sealed class EventNotification - { - public string StreamName { get; set; } - } - - public DefaultEventNotifier(IPubSub pubsub) - { - Guard.NotNull(pubsub, nameof(pubsub)); - - this.pubsub = pubsub; - } - - public void NotifyEventsStored(string streamName) - { - pubsub.Publish(new EventNotification { StreamName = streamName }, true); - } - - public IDisposable Subscribe(Action handler) - { - return pubsub.Subscribe(x => handler?.Invoke(x.StreamName)); - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs index e05a08c65..93580c7f0 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs @@ -8,25 +8,30 @@ using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Orleans; +using Orleans.Concurrency; using Squidex.Infrastructure.Log; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; namespace Squidex.Infrastructure.EventSourcing.Grains { - public class EventConsumerGrain : DisposableObjectBase, IStatefulObject, IEventSubscriber + public class EventConsumerGrain : GrainOfString, IEventConsumerGrain { - private readonly IEventDataFormatter eventDataFormatter; + private readonly EventConsumerFactory eventConsumerFactory; private readonly IStore store; + private readonly IEventDataFormatter eventDataFormatter; private readonly IEventStore eventStore; private readonly ISemanticLog log; - private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(1); + private TaskScheduler scheduler; + private IPersistence persistence; private IEventSubscription currentSubscription; private IEventConsumer eventConsumer; - private IPersistence persistence; private EventConsumerState state = new EventConsumerState(); public EventConsumerGrain( + EventConsumerFactory eventConsumerFactory, IStore store, IEventStore eventStore, IEventDataFormatter eventDataFormatter, @@ -36,95 +41,54 @@ namespace Squidex.Infrastructure.EventSourcing.Grains Guard.NotNull(store, nameof(store)); Guard.NotNull(eventStore, nameof(eventStore)); Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); + Guard.NotNull(eventConsumerFactory, nameof(eventConsumerFactory)); this.log = log; this.store = store; this.eventStore = eventStore; this.eventDataFormatter = eventDataFormatter; + this.eventConsumerFactory = eventConsumerFactory; } - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - dispatcher.StopAndWaitAsync().Wait(); - } - } - - public Task ActivateAsync(string key) + public override Task OnActivateAsync(string key) { - persistence = store.WithSnapshots(key, s => state = s); - - return persistence.ReadAsync(); - } + scheduler = TaskScheduler.Current; - protected virtual IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position) - { - return new RetrySubscription(eventStore, this, streamFilter, position); - } + eventConsumer = eventConsumerFactory(key); - public virtual EventConsumerInfo GetState() - { - return state.ToInfo(this.eventConsumer.Name); - } + persistence = store.WithSnapshots(GetType(), eventConsumer.Name, s => state = s); - public virtual void Stop() - { - dispatcher.DispatchAsync(HandleStopAsync).Forget(); - } - - public virtual void Start() - { - dispatcher.DispatchAsync(HandleStartAsync).Forget(); + return persistence.ReadAsync(); } - public virtual void Reset() + public Task> GetStateAsync() { - dispatcher.DispatchAsync(HandleResetAsync).Forget(); + return Task.FromResult(state.ToInfo(this.eventConsumer.Name).AsImmutable()); } - public virtual void Activate(IEventConsumer eventConsumer) + public Task OnEventAsync(Immutable subscription, Immutable storedEvent) { - Guard.NotNull(eventConsumer, nameof(eventConsumer)); - - dispatcher.DispatchAsync(() => HandleSetupAsync(eventConsumer)).Forget(); - } - - private Task HandleSetupAsync(IEventConsumer consumer) - { - eventConsumer = consumer; - - if (!state.IsStopped) - { - Subscribe(state.Position); - } - - return TaskHelper.Done; - } - - private Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) - { - if (subscription != currentSubscription) + if (subscription.Value != currentSubscription) { return TaskHelper.Done; } return DoAndUpdateStateAsync(async () => { - var @event = ParseKnownEvent(storedEvent); + var @event = ParseKnownEvent(storedEvent.Value); if (@event != null) { await DispatchConsumerAsync(@event); } - state = state.Handled(storedEvent.EventPosition); + state = state.Handled(storedEvent.Value.EventPosition); }); } - private Task HandleErrorAsync(IEventSubscription subscription, Exception exception) + public Task OnErrorAsync(Immutable subscription, Immutable exception) { - if (subscription != currentSubscription) + if (subscription.Value != currentSubscription) { return TaskHelper.Done; } @@ -133,11 +97,21 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { Unsubscribe(); - state = state.Failed(exception); + state = state.Failed(exception.Value); }); } - private Task HandleStartAsync() + public Task ActivateAsync() + { + if (!state.IsStopped) + { + Subscribe(state.Position); + } + + return TaskHelper.Done; + } + + public Task StartAsync() { if (!state.IsStopped) { @@ -152,7 +126,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains }); } - private Task HandleStopAsync() + public Task StopAsync() { if (state.IsStopped) { @@ -167,7 +141,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains }); } - private Task HandleResetAsync() + public Task ResetAsync() { return DoAndUpdateStateAsync(async () => { @@ -181,16 +155,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains }); } - 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)); - } - private Task DoAndUpdateStateAsync(Action action, [CallerMemberName] string caller = null) { return DoAndUpdateStateAsync(() => { action(); return TaskHelper.Done; }, caller); @@ -283,7 +247,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains if (currentSubscription == null) { currentSubscription?.StopAsync().Forget(); - currentSubscription = CreateSubscription(eventStore, eventConsumer.EventsFilter, position); + currentSubscription = CreateSubscription(eventConsumer.EventsFilter, position); + } + else + { + currentSubscription.WakeUp(); } } @@ -305,5 +273,20 @@ namespace Squidex.Infrastructure.EventSourcing.Grains return null; } } + + protected virtual IEventConsumerGrain GetSelf() + { + return this.AsReference(); + } + + protected virtual IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string streamFilter, string position) + { + return new RetrySubscription(store, subscriber, streamFilter, position); + } + + private IEventSubscription CreateSubscription(string streamFilter, string position) + { + return CreateSubscription(eventStore, new WrapperSubscription(GetSelf(), scheduler), streamFilter, position); + } } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs deleted file mode 100644 index e833aa5b4..000000000 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrainManager.cs +++ /dev/null @@ -1,90 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Squidex.Infrastructure.EventSourcing.Grains.Messages; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public sealed class EventConsumerGrainManager : DisposableObjectBase, IRunnable - { - private readonly IStateFactory factory; - private readonly IPubSub pubSub; - private readonly List consumers; - private readonly List subscriptions = new List(); - - public EventConsumerGrainManager(IEnumerable consumers, IPubSub pubSub, IStateFactory factory) - { - Guard.NotNull(pubSub, nameof(pubSub)); - Guard.NotNull(factory, nameof(factory)); - Guard.NotNull(consumers, nameof(consumers)); - - this.pubSub = pubSub; - this.factory = factory; - this.consumers = consumers.ToList(); - } - - public void Run() - { - var actors = new Dictionary(); - - foreach (var consumer in consumers) - { - var actor = factory.CreateAsync(consumer.Name).Result; - - actors[consumer.Name] = actor; - actor.Activate(consumer); - } - - subscriptions.Add(pubSub.Subscribe(m => - { - if (actors.TryGetValue(m.ConsumerName, out var actor)) - { - actor.Start(); - } - })); - - subscriptions.Add(pubSub.Subscribe(m => - { - if (actors.TryGetValue(m.ConsumerName, out var actor)) - { - actor.Stop(); - } - })); - - subscriptions.Add(pubSub.Subscribe(m => - { - if (actors.TryGetValue(m.ConsumerName, out var actor)) - { - actor.Reset(); - } - })); - - subscriptions.Add(pubSub.ReceiveAsync(request => - { - var states = actors.Values.Select(x => x.GetState()).ToArray(); - - return Task.FromResult(new GetStatesResponse { States = states }); - })); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing) - { - foreach (var subscription in subscriptions) - { - subscription.Dispose(); - } - } - } - } -} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs new file mode 100644 index 000000000..97b3e93ca --- /dev/null +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs @@ -0,0 +1,104 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Orleans; +using Orleans.Concurrency; +using Orleans.Core; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerManagerGrain : Grain, IEventConsumerManagerGrain, IRemindable + { + private readonly IEnumerable eventConsumers; + + public EventConsumerManagerGrain(IEnumerable eventConsumers) + : this(eventConsumers, null, null) + { + } + + protected EventConsumerManagerGrain( + IEnumerable eventConsumers, + IGrainIdentity identity, + IGrainRuntime runtime) + : base(identity, runtime) + { + Guard.NotNull(eventConsumers, nameof(eventConsumers)); + + this.eventConsumers = eventConsumers; + } + + public override Task OnActivateAsync() + { + DelayDeactivation(TimeSpan.FromDays(1)); + + RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + 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 async Task>> GetConsumersAsync() + { + var tasks = + eventConsumers + .Select(c => GrainFactory.GetGrain(c.Name)) + .Select(c => c.GetStateAsync()); + + var consumerInfos = await Task.WhenAll(tasks); + + return new Immutable>(consumerInfos.Select(r => r.Value).ToList()); + } + + public Task ResetAsync(string consumerName) + { + var eventConsumer = GrainFactory.GetGrain(consumerName); + + return eventConsumer.ResetAsync(); + } + + public Task StartAsync(string consumerName) + { + var eventConsumer = GrainFactory.GetGrain(consumerName); + + return eventConsumer.StartAsync(); + } + + public Task StopAsync(string consumerName) + { + var eventConsumer = GrainFactory.GetGrain(consumerName); + + return eventConsumer.StopAsync(); + } + + public Task ActivateAsync() + { + return ActivateAsync(null); + } + + public Task ReceiveReminder(string reminderName, TickStatus status) + { + return ActivateAsync(null); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs new file mode 100644 index 000000000..58b7bf2fb --- /dev/null +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs @@ -0,0 +1,29 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans.Concurrency; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public interface IEventConsumerGrain : IBackgroundGrain + { + Task> GetStateAsync(); + + Task StopAsync(); + + Task StartAsync(); + + Task ResetAsync(); + + Task OnEventAsync(Immutable subscription, Immutable storedEvent); + + Task OnErrorAsync(Immutable subscription, Immutable exception); + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs new file mode 100644 index 000000000..1730b7b30 --- /dev/null +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using Orleans.Concurrency; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public interface IEventConsumerManagerGrain : IBackgroundGrain + { + Task ActivateAsync(string streamName); + + Task StopAsync(string consumerName); + + Task StartAsync(string consumerName); + + Task ResetAsync(string consumerName); + + Task>> GetConsumersAsync(); + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs new file mode 100644 index 000000000..0e97746e4 --- /dev/null +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Orleans; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public sealed class OrleansEventNotifier : IEventNotifier, IInitializable + { + private readonly IGrainFactory factory; + private IEventConsumerManagerGrain eventConsumerManagerGrain; + + public OrleansEventNotifier(IGrainFactory factory) + { + Guard.NotNull(factory, nameof(factory)); + + this.factory = factory; + } + + public void Initialize() + { + eventConsumerManagerGrain = factory.GetGrain("Default"); + } + + public void NotifyEventsStored(string streamName) + { + eventConsumerManagerGrain?.ActivateAsync(streamName); + } + + public IDisposable Subscribe(Action handler) + { + return null; + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs b/src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs new file mode 100644 index 000000000..012179f1f --- /dev/null +++ b/src/Squidex.Infrastructure/EventSourcing/Grains/WrapperSubscription.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading; +using System.Threading.Tasks; +using Orleans.Concurrency; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + internal sealed class WrapperSubscription : IEventSubscriber + { + private readonly IEventConsumerGrain grain; + private readonly TaskScheduler scheduler; + + public WrapperSubscription(IEventConsumerGrain grain, TaskScheduler scheduler) + { + this.grain = grain; + + this.scheduler = scheduler ?? TaskScheduler.Default; + } + + public Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + { + return Dispatch(() => grain.OnEventAsync(subscription.AsImmutable(), storedEvent.AsImmutable())); + } + + public Task OnErrorAsync(IEventSubscription subscription, Exception exception) + { + return Dispatch(() => grain.OnErrorAsync(subscription.AsImmutable(), exception.AsImmutable())); + } + + private Task Dispatch(Func task) + { + return Task.Factory.StartNew(() => task(), CancellationToken.None, TaskCreationOptions.None, scheduler).Unwrap(); + } + } +} diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs b/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs index ce28bb491..6e5bbe94f 100644 --- a/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs +++ b/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs @@ -5,14 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; - namespace Squidex.Infrastructure.EventSourcing { public interface IEventNotifier { void NotifyEventsStored(string streamName); - - IDisposable Subscribe(Action handler); } } diff --git a/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs b/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs index a33b1f22b..48ead1da9 100644 --- a/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs +++ b/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs @@ -11,6 +11,8 @@ namespace Squidex.Infrastructure.EventSourcing { public interface IEventSubscription { + void WakeUp(); + Task StopAsync(); } } \ No newline at end of file diff --git a/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs b/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs index 7cbb556b9..10c908ad5 100644 --- a/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs +++ b/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs @@ -14,10 +14,8 @@ namespace Squidex.Infrastructure.EventSourcing { public sealed class PollingSubscription : 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 string streamFilter; @@ -25,17 +23,14 @@ namespace Squidex.Infrastructure.EventSourcing public PollingSubscription( 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; @@ -61,20 +56,15 @@ namespace Squidex.Infrastructure.EventSourcing } } }); + } - notification = eventNotifier.Subscribe(streamName => - { - if (streamRegex.IsMatch(streamName)) - { - timer.SkipCurrentDelay(); - } - }); + public void WakeUp() + { + timer.SkipCurrentDelay(); } public Task StopAsync() { - notification?.Dispose(); - return timer.StopAsync(); } } diff --git a/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs b/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs index d023eead5..60a9f5679 100644 --- a/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs +++ b/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs @@ -57,6 +57,11 @@ namespace Squidex.Infrastructure.EventSourcing currentSubscription = null; } + public void WakeUp() + { + currentSubscription?.WakeUp(); + } + private async Task HandleEventAsync(IEventSubscription subscription, StoredEvent storedEvent) { if (subscription == currentSubscription) diff --git a/src/Squidex.Infrastructure/Orleans/Bootstrap.cs b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs new file mode 100644 index 000000000..79abd18b9 --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/Bootstrap.cs @@ -0,0 +1,33 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using Orleans; +using Orleans.Runtime; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class Bootstrap : IStartupTask where T : IBackgroundGrain + { + private readonly IGrainFactory grainFactory; + + public Bootstrap(IGrainFactory grainFactory) + { + Guard.NotNull(grainFactory, nameof(grainFactory)); + + this.grainFactory = grainFactory; + } + + public Task Execute(CancellationToken cancellationToken) + { + var grain = grainFactory.GetGrain("Default"); + + return grain.ActivateAsync(); + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs b/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs new file mode 100644 index 000000000..b014a9c63 --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Orleans +{ + public abstract class GrainOfGuid : Grain + { + public override Task OnActivateAsync() + { + return OnActivateAsync(this.GetPrimaryKey()); + } + + public virtual Task OnActivateAsync(Guid key) + { + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Infrastructure/Orleans/GrainOfString.cs b/src/Squidex.Infrastructure/Orleans/GrainOfString.cs new file mode 100644 index 000000000..4ed33bacc --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/GrainOfString.cs @@ -0,0 +1,26 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Orleans; +using Squidex.Infrastructure.Tasks; + +namespace Squidex.Infrastructure.Orleans +{ + public abstract class GrainOfString : Grain + { + public override Task OnActivateAsync() + { + return OnActivateAsync(this.GetPrimaryKeyString()); + } + + public virtual Task OnActivateAsync(string key) + { + return TaskHelper.Done; + } + } +} diff --git a/src/Squidex.Infrastructure/States/IStatefulObject.cs b/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs similarity index 65% rename from src/Squidex.Infrastructure/States/IStatefulObject.cs rename to src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs index 45769b2ce..e30295b8a 100644 --- a/src/Squidex.Infrastructure/States/IStatefulObject.cs +++ b/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs @@ -1,16 +1,17 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== using System.Threading.Tasks; +using Orleans; -namespace Squidex.Infrastructure.States +namespace Squidex.Infrastructure.Orleans { - public interface IStatefulObject + public interface IBackgroundGrain : IGrainWithStringKey { - Task ActivateAsync(TKey key); + Task ActivateAsync(); } } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/StartConsumerMessage.cs b/src/Squidex.Infrastructure/Orleans/J.cs similarity index 55% rename from src/Squidex.Infrastructure/EventSourcing/Grains/Messages/StartConsumerMessage.cs rename to src/Squidex.Infrastructure/Orleans/J.cs index 8d8378653..cd1190790 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/StartConsumerMessage.cs +++ b/src/Squidex.Infrastructure/Orleans/J.cs @@ -1,14 +1,18 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing.Grains.Messages +using Newtonsoft.Json; + +#pragma warning disable SA1401 // Fields must be private + +namespace Squidex.Infrastructure.Orleans { - public sealed class StartConsumerMessage + public static class J { - public string ConsumerName { get; set; } + public static JsonSerializer Serializer = new JsonSerializer(); } } diff --git a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/GetStatesResponse.cs b/src/Squidex.Infrastructure/Orleans/JExtensions.cs similarity index 59% rename from src/Squidex.Infrastructure/EventSourcing/Grains/Messages/GetStatesResponse.cs rename to src/Squidex.Infrastructure/Orleans/JExtensions.cs index 922116d82..49125b346 100644 --- a/src/Squidex.Infrastructure/EventSourcing/Grains/Messages/GetStatesResponse.cs +++ b/src/Squidex.Infrastructure/Orleans/JExtensions.cs @@ -1,14 +1,17 @@ // ========================================================================== // Squidex Headless CMS // ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) +// Copyright (c) Squidex UG (haftungsbeschraenkt) // All rights reserved. Licensed under the MIT license. // ========================================================================== -namespace Squidex.Infrastructure.EventSourcing.Grains.Messages +namespace Squidex.Infrastructure.Orleans { - public sealed class GetStatesResponse + public static class JExtensions { - public EventConsumerInfo[] States { get; set; } + public static J AsJ(this T value) + { + return new J(value); + } } } diff --git a/src/Squidex.Infrastructure/Orleans/J{T}.cs b/src/Squidex.Infrastructure/Orleans/J{T}.cs new file mode 100644 index 000000000..c6f28cd45 --- /dev/null +++ b/src/Squidex.Infrastructure/Orleans/J{T}.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.IO; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Orleans.CodeGeneration; +using Orleans.Serialization; + +namespace Squidex.Infrastructure.Orleans +{ + public struct J + { + private readonly T value; + + public T Value + { + get { return value; } + } + + [JsonConstructor] + public J(T value) + { + this.value = value; + } + + public static implicit operator T(J value) + { + return value.value; + } + + public static implicit operator J(T d) + { + return new J(d); + } + + public override string ToString() + { + return value?.ToString() ?? string.Empty; + } + + public static Task> AsTask(T value) + { + return Task.FromResult>(value); + } + + [CopierMethod] + public static object Copy(object input, ICopyContext context) + { + return input; + } + + [SerializerMethod] + public static void Serialize(object input, ISerializationContext context, Type expected) + { + var stream = new MemoryStream(); + + using (var writer = new JsonTextWriter(new StreamWriter(stream))) + { + J.Serializer.Serialize(writer, input); + + writer.Flush(); + } + + var outBytes = stream.ToArray(); + + context.StreamWriter.Write(outBytes.Length); + context.StreamWriter.Write(outBytes); + } + + [DeserializerMethod] + public static object Deserialize(Type expected, IDeserializationContext context) + { + var outLength = context.StreamReader.ReadInt(); + var outBytes = context.StreamReader.ReadBytes(outLength); + + var stream = new MemoryStream(outBytes); + + using (var reader = new JsonTextReader(new StreamReader(stream))) + { + return J.Serializer.Deserialize(reader, expected); + } + } + } +} diff --git a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index ba723caf1..77029bf52 100644 --- a/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -8,10 +8,13 @@ True - - - - + + + + + + + diff --git a/src/Squidex.Infrastructure/States/IStateFactory.cs b/src/Squidex.Infrastructure/States/IStateFactory.cs deleted file mode 100644 index cc10879ec..000000000 --- a/src/Squidex.Infrastructure/States/IStateFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; - -namespace Squidex.Infrastructure.States -{ - public interface IStateFactory - { - Task GetSingleAsync(string key) where T : IStatefulObject; - - Task GetSingleAsync(Guid key) where T : IStatefulObject; - - Task GetSingleAsync(TKey key) where T : IStatefulObject; - - Task CreateAsync(string key) where T : IStatefulObject; - - Task CreateAsync(Guid key) where T : IStatefulObject; - - Task CreateAsync(TKey key) where T : IStatefulObject; - - void Remove(TKey key) where T : IStatefulObject; - - void Synchronize(TKey key) where T : IStatefulObject; - } -} diff --git a/src/Squidex.Infrastructure/States/StateFactory.cs b/src/Squidex.Infrastructure/States/StateFactory.cs deleted file mode 100644 index 6e196beba..000000000 --- a/src/Squidex.Infrastructure/States/StateFactory.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Memory; - -#pragma warning disable RECS0096 // Type parameter is never used - -namespace Squidex.Infrastructure.States -{ - public sealed class StateFactory : DisposableObjectBase, IInitializable, IStateFactory - { - private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); - private readonly IPubSub pubSub; - private readonly IMemoryCache statesCache; - private readonly IServiceProvider services; - private readonly object lockObject = new object(); - private IDisposable pubSubSubscription; - - public sealed class ObjectHolder where T : IStatefulObject - { - private readonly Task activationTask; - private readonly T obj; - - public ObjectHolder(T obj, TKey key) - { - this.obj = obj; - - activationTask = obj.ActivateAsync(key); - } - - public async Task ActivateAsync() - { - await activationTask; - - return obj; - } - } - - public StateFactory(IPubSub pubSub, IMemoryCache statesCache, IServiceProvider services) - { - Guard.NotNull(pubSub, nameof(pubSub)); - Guard.NotNull(services, nameof(services)); - Guard.NotNull(statesCache, nameof(statesCache)); - - this.pubSub = pubSub; - this.services = services; - this.statesCache = statesCache; - } - - public void Initialize() - { - pubSubSubscription = pubSub.Subscribe(m => - { - lock (lockObject) - { - statesCache.Remove(m.Key); - } - }); - } - - public Task CreateAsync(string key) where T : IStatefulObject - { - return CreateAsync(key); - } - - public Task CreateAsync(Guid key) where T : IStatefulObject - { - return CreateAsync(key); - } - - public async Task CreateAsync(TKey key) where T : IStatefulObject - { - Guard.NotNull(key, nameof(key)); - - var state = (T)services.GetService(typeof(T)); - - await state.ActivateAsync(key); - - return state; - } - - public Task GetSingleAsync(string key) where T : IStatefulObject - { - return GetSingleAsync(key); - } - - public Task GetSingleAsync(Guid key) where T : IStatefulObject - { - return GetSingleAsync(key); - } - - public Task GetSingleAsync(TKey key) where T : IStatefulObject - { - Guard.NotNull(key, nameof(key)); - - lock (lockObject) - { - if (statesCache.TryGetValue>(key, out var stateObj)) - { - return stateObj.ActivateAsync(); - } - - var state = (T)services.GetService(typeof(T)); - - stateObj = new ObjectHolder(state, key); - - statesCache.CreateEntry(key) - .SetValue(stateObj) - .SetAbsoluteExpiration(CacheDuration) - .Dispose(); - - return stateObj.ActivateAsync(); - } - } - - public void Remove(TKey key) where T : IStatefulObject - { - statesCache.Remove(key); - } - - public void Synchronize(TKey key) where T : IStatefulObject - { - pubSub.Publish(new InvalidateMessage { Key = key.ToString() }, false); - } - - protected override void DisposeObject(bool disposing) - { - if (disposing && pubSubSubscription != null) - { - pubSubSubscription.Dispose(); - } - } - } -} diff --git a/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs b/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs index 16fd0c779..a649ce53f 100644 --- a/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs +++ b/src/Squidex.Infrastructure/Tasks/PartitionedActionBlock.cs @@ -24,7 +24,7 @@ namespace Squidex.Infrastructure.Tasks } public PartitionedActionBlock(Action action, Func partitioner) - : this (ToAsync(action), partitioner, new ExecutionDataflowBlockOptions()) + : this (action?.ToAsync(), partitioner, new ExecutionDataflowBlockOptions()) { } @@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.Tasks } public PartitionedActionBlock(Action action, Func partitioner, ExecutionDataflowBlockOptions dataflowBlockOptions) - : this(ToAsync(action), partitioner, dataflowBlockOptions) + : this(action?.ToAsync(), partitioner, dataflowBlockOptions) { } @@ -94,17 +94,5 @@ namespace Squidex.Infrastructure.Tasks { distributor.Fault(exception); } - - private static Func ToAsync(Action action) - { - Guard.NotNull(action, nameof(action)); - - return x => - { - action(x); - - return TaskHelper.Done; - }; - } } } diff --git a/src/Squidex/AppConfiguration.cs b/src/Squidex/AppConfiguration.cs deleted file mode 100644 index 6c707bafb..000000000 --- a/src/Squidex/AppConfiguration.cs +++ /dev/null @@ -1,26 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -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 index baa614181..3a6e0e91f 100644 --- a/src/Squidex/AppServices.cs +++ b/src/Squidex/AppServices.cs @@ -26,17 +26,19 @@ namespace Squidex services.AddMyAssetServices(config); services.AddMyAuthentication(config); + services.AddMyEntitiesServices(config); services.AddMyEventPublishersServices(config); services.AddMyEventStoreServices(config); services.AddMyIdentityServer(); - services.AddMyInfrastructureServices(config); + services.AddMyInfrastructureServices(); + services.AddMyLoggingServices(config); + services.AddMyMigrationServices(); services.AddMyMvc(); - services.AddMyPubSubServices(config); - services.AddMyReadServices(config); + services.AddMyRuleServices(); services.AddMySerializers(); services.AddMyStoreServices(config); services.AddMySwaggerSettings(); - services.AddMyWriteServices(); + services.AddMySubscriptionServices(config); services.Configure( config.GetSection("urls")); diff --git a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs index 41e232307..6dee278b9 100644 --- a/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs +++ b/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs @@ -5,15 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; +using Orleans; using Squidex.Areas.Api.Controllers.EventConsumers.Models; -using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing.Grains.Messages; +using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; @@ -25,12 +24,12 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [SwaggerIgnore] public sealed class EventConsumersController : ApiController { - private readonly IPubSub pubSub; + private readonly IEventConsumerManagerGrain eventConsumerManagerGrain; - public EventConsumersController(ICommandBus commandBus, IPubSub pubSub) + public EventConsumersController(ICommandBus commandBus, IGrainFactory grainFactory) : base(commandBus) { - this.pubSub = pubSub; + eventConsumerManagerGrain = grainFactory.GetGrain("Default"); } [HttpGet] @@ -38,9 +37,9 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [ApiCosts(0)] public async Task GetEventConsumers() { - var entities = await pubSub.RequestAsync(new GetStatesRequest(), TimeSpan.FromSeconds(2), true); + var entities = await eventConsumerManagerGrain.GetConsumersAsync(); - var models = entities.States.Select(x => SimpleMapper.Map(x, new EventConsumerDto())).ToList(); + var models = entities.Value.Select(x => SimpleMapper.Map(x, new EventConsumerDto())).ToList(); return Ok(models); } @@ -48,9 +47,9 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [HttpPut] [Route("event-consumers/{name}/start/")] [ApiCosts(0)] - public IActionResult Start(string name) + public async Task Start(string name) { - pubSub.Publish(new StartConsumerMessage { ConsumerName = name }, true); + await eventConsumerManagerGrain.StartAsync(name); return NoContent(); } @@ -58,9 +57,9 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [HttpPut] [Route("event-consumers/{name}/stop/")] [ApiCosts(0)] - public IActionResult Stop(string name) + public async Task Stop(string name) { - pubSub.Publish(new StopConsumerMessage { ConsumerName = name }, true); + await eventConsumerManagerGrain.StopAsync(name); return NoContent(); } @@ -68,9 +67,9 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [HttpPut] [Route("event-consumers/{name}/reset/")] [ApiCosts(0)] - public IActionResult Reset(string name) + public async Task Reset(string name) { - pubSub.Publish(new ResetConsumerMessage { ConsumerName = name }, true); + await eventConsumerManagerGrain.ResetAsync(name); return NoContent(); } diff --git a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs index 24b5e1e93..aeea54e9c 100644 --- a/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs +++ b/src/Squidex/Areas/IdentityServer/Config/LazyClientStore.cs @@ -130,7 +130,8 @@ namespace Squidex.Areas.IdentityServer.Config ClientSecrets = new List { new Secret(Constants.InternalClientSecret) }, RedirectUris = new List { - urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-oidc", false) + urlsOptions.BuildUrl($"{Constants.PortalPrefix}/signin-oidc", false), + urlsOptions.BuildUrl($"{Constants.OrleansPrefix}/signin-oidc", false) }, AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, AllowedGrantTypes = GrantTypes.ImplicitAndClientCredentials, diff --git a/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs b/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs new file mode 100644 index 000000000..db67abdd3 --- /dev/null +++ b/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs @@ -0,0 +1,43 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Http; +using Squidex.Shared.Identity; + +namespace Squidex.Areas.OrleansDashboard.Middlewares +{ + public sealed class OrleansDashboardAuthenticationMiddleware + { + private readonly RequestDelegate next; + + public OrleansDashboardAuthenticationMiddleware(RequestDelegate next) + { + this.next = next; + } + + public async Task Invoke(HttpContext context) + { + var authentication = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + + if (!authentication.Succeeded || !authentication.Principal.IsInRole(SquidexRoles.Administrator)) + { + await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties + { + RedirectUri = context.Request.PathBase + context.Request.Path + }); + } + else + { + await next(context); + } + } + } +} diff --git a/src/Squidex/Areas/OrleansDashboard/Startup.cs b/src/Squidex/Areas/OrleansDashboard/Startup.cs new file mode 100644 index 000000000..d8b5da6f8 --- /dev/null +++ b/src/Squidex/Areas/OrleansDashboard/Startup.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.AspNetCore.Builder; +using Orleans; +using Squidex.Areas.OrleansDashboard.Middlewares; +using Squidex.Config; + +namespace Squidex.Areas.OrleansDashboard +{ + public static class Startup + { + public static void ConfigureOrleansDashboard(this IApplicationBuilder app) + { + app.Map(Constants.OrleansPrefix, orleansApp => + { + orleansApp.UseAuthentication(); + orleansApp.UseMiddleware(); + orleansApp.UseOrleansDashboard(); + }); + } + } +} diff --git a/src/Squidex/Config/Constants.cs b/src/Squidex/Config/Constants.cs index 9c97c3d20..bd08faef8 100644 --- a/src/Squidex/Config/Constants.cs +++ b/src/Squidex/Config/Constants.cs @@ -17,6 +17,8 @@ namespace Squidex.Config public static readonly string ApiScope = "squidex-api"; + public static readonly string OrleansPrefix = "/orleans"; + public static readonly string PortalPrefix = "/portal"; public static readonly string RoleScope = "role"; diff --git a/src/Squidex/Config/Domain/AssetServices.cs b/src/Squidex/Config/Domain/AssetServices.cs index d2579c2ca..59297c8b6 100644 --- a/src/Squidex/Config/Domain/AssetServices.cs +++ b/src/Squidex/Config/Domain/AssetServices.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Squidex.Infrastructure; using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.Assets.ImageSharp; using Squidex.Infrastructure.Log; namespace Squidex.Config.Domain @@ -45,6 +46,9 @@ namespace Squidex.Config.Domain .As(); } }); + + services.AddSingletonAs() + .As(); } } } diff --git a/src/Squidex/Config/Domain/WriteServices.cs b/src/Squidex/Config/Domain/EntitiesServices.cs similarity index 62% rename from src/Squidex/Config/Domain/WriteServices.cs rename to src/Squidex/Config/Domain/EntitiesServices.cs index d77e4588a..cabc20e5c 100644 --- a/src/Squidex/Config/Domain/WriteServices.cs +++ b/src/Squidex/Config/Domain/EntitiesServices.cs @@ -6,10 +6,12 @@ // ========================================================================== using System; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Migrate_01; using Migrate_01.Migrations; +using Orleans; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities; @@ -19,25 +21,55 @@ using Squidex.Domain.Apps.Entities.Apps.Templates; using Squidex.Domain.Apps.Entities.Assets; using Squidex.Domain.Apps.Entities.Contents; using Squidex.Domain.Apps.Entities.Contents.Commands; +using Squidex.Domain.Apps.Entities.Contents.Edm; +using Squidex.Domain.Apps.Entities.Contents.GraphQL; +using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Users; +using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Migrations; +using Squidex.Pipeline; using Squidex.Pipeline.CommandMiddlewares; namespace Squidex.Config.Domain { - public static class WriteServices + public static class EntitiesServices { - public static void AddMyWriteServices(this IServiceCollection services) + public static void AddMyEntitiesServices(this IServiceCollection services, IConfiguration config) { - services.AddSingletonAs() - .As(); + var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); - services.AddSingletonAs() - .As(); + services.AddSingletonAs(c => new GraphQLUrlGenerator( + c.GetRequiredService>(), + c.GetRequiredService(), + exposeSourceUrl)) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .As(); services.AddSingletonAs() .As(); @@ -54,19 +86,19 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs>() + services.AddSingletonAs() .As(); - services.AddSingletonAs() + services.AddSingletonAs>() .As(); - services.AddSingletonAs>() + services.AddSingletonAs>() .As(); - services.AddSingletonAs>() + services.AddSingletonAs>() .As(); - services.AddSingletonAs>() + services.AddSingletonAs>() .As(); services.AddSingletonAs() @@ -75,26 +107,10 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); + services.AddSingletonAs() + .As(); - services.AddTransientAs() - .AsSelf(); + services.AddSingleton>(DomainObjectGrainFormatter.Format); services.AddTransientAs() .AsSelf(); @@ -111,13 +127,13 @@ namespace Squidex.Config.Domain services.AddTransientAs() .AsSelf(); - services.AddSingleton(c => + services.AddSingleton(c => { - var config = c.GetRequiredService>(); + var uiOptions = c.GetRequiredService>(); var result = new InitialPatterns(); - foreach (var pattern in config.Value.RegexSuggestions) + foreach (var pattern in uiOptions.Value.RegexSuggestions) { if (!string.IsNullOrWhiteSpace(pattern.Key) && !string.IsNullOrWhiteSpace(pattern.Value)) @@ -129,5 +145,32 @@ namespace Squidex.Config.Domain return result; }); } + + public static void AddMyMigrationServices(this IServiceCollection services) + { + services.AddSingletonAs() + .AsSelf(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .AsSelf(); + } } } diff --git a/src/Squidex/Config/Domain/EventStoreServices.cs b/src/Squidex/Config/Domain/EventStoreServices.cs index 85f28033e..baa744514 100644 --- a/src/Squidex/Config/Domain/EventStoreServices.cs +++ b/src/Squidex/Config/Domain/EventStoreServices.cs @@ -5,12 +5,15 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Linq; using EventStore.ClientAPI; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using MongoDB.Driver; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.States; namespace Squidex.Config.Domain { @@ -48,6 +51,23 @@ namespace Squidex.Config.Domain .As(); } }); + + services.AddSingletonAs() + .As() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs(c => + { + var allEventConsumers = c.GetServices(); + + return new EventConsumerFactory(n => allEventConsumers.FirstOrDefault(x => x.Name == n)); + }); } } } diff --git a/src/Squidex/Config/Domain/InfrastructureServices.cs b/src/Squidex/Config/Domain/InfrastructureServices.cs index 108d9791b..085e84e61 100644 --- a/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -5,66 +5,20 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; using NodaTime; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.Assets.ImageSharp; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.States; using Squidex.Infrastructure.UsageTracking; -using Squidex.Pipeline; + +#pragma warning disable RECS0092 // Convert field to readonly namespace Squidex.Config.Domain { public static class InfrastructureServices { - public static void AddMyInfrastructureServices(this IServiceCollection services, IConfiguration config) + public static void AddMyInfrastructureServices(this IServiceCollection services) { - if (config.GetValue("logging:human")) - { - services.AddSingletonAs(c => new Func(() => new JsonLogWriter(Formatting.Indented, true))); - } - else - { - services.AddSingletonAs(c => new Func(() => new JsonLogWriter())); - } - - var loggingFile = config.GetValue("logging:file"); - - if (!string.IsNullOrWhiteSpace(loggingFile)) - { - services.AddSingletonAs(new FileChannel(loggingFile)) - .As() - .As(); - } - - services.AddSingletonAs(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - services.AddSingletonAs(SystemClock.Instance) .As(); @@ -76,23 +30,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingleton(typeof(IStore<>), typeof(Store<>)); } } } diff --git a/src/Squidex/Config/Domain/LoggingServices.cs b/src/Squidex/Config/Domain/LoggingServices.cs new file mode 100644 index 000000000..e4ee51850 --- /dev/null +++ b/src/Squidex/Config/Domain/LoggingServices.cs @@ -0,0 +1,64 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Log; +using Squidex.Pipeline; + +#pragma warning disable RECS0092 // Convert field to readonly + +namespace Squidex.Config.Domain +{ + public static class LoggingServices + { + private static ILogChannel console = new ConsoleLogChannel(); + private static ILogChannel file; + + public static void AddMyLoggingServices(this IServiceCollection services, IConfiguration config) + { + if (config.GetValue("logging:human")) + { + services.AddSingletonAs(c => new Func(() => new JsonLogWriter(Formatting.Indented, true))); + } + else + { + services.AddSingletonAs(c => new Func(() => new JsonLogWriter())); + } + + var loggingFile = config.GetValue("logging:file"); + + if (!string.IsNullOrWhiteSpace(loggingFile)) + { + services.AddSingletonAs(file ?? (file = new FileChannel(loggingFile))) + .As() + .As(); + } + + services.AddSingletonAs(console) + .As(); + + services.AddSingletonAs(c => new ApplicationInfoLogAppender(typeof(Program).Assembly, Guid.NewGuid())) + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + } + } +} diff --git a/src/Squidex/Config/Domain/PubSubServices.cs b/src/Squidex/Config/Domain/PubSubServices.cs deleted file mode 100644 index e35611f06..000000000 --- a/src/Squidex/Config/Domain/PubSubServices.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -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.AddSingletonAs() - .As(); - }, - ["Redis"] = () => - { - var configuration = config.GetRequiredValue("pubsub:redis:configuration"); - - var redis = Singletons.GetOrAddLazy(configuration, s => ConnectionMultiplexer.Connect(s)); - - services.AddSingletonAs(c => new RedisPubSub(redis, c.GetRequiredService())) - .As() - .As(); - } - }); - } - } -} diff --git a/src/Squidex/Config/Domain/ReadServices.cs b/src/Squidex/Config/Domain/ReadServices.cs deleted file mode 100644 index 1fb5baaef..000000000 --- a/src/Squidex/Config/Domain/ReadServices.cs +++ /dev/null @@ -1,141 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -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.Actions; -using Squidex.Domain.Apps.Core.HandleRules.Triggers; -using Squidex.Domain.Apps.Entities; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Apps.Services; -using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; -using Squidex.Domain.Apps.Entities.Contents; -using Squidex.Domain.Apps.Entities.Contents.Edm; -using Squidex.Domain.Apps.Entities.Contents.GraphQL; -using Squidex.Domain.Apps.Entities.History; -using Squidex.Domain.Apps.Entities.Rules; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Domain.Users; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Assets; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.States; -using Squidex.Pipeline; - -namespace Squidex.Config.Domain -{ - public static class ReadServices - { - public static void AddMyReadServices(this IServiceCollection services, IConfiguration config) - { - var consumeEvents = config.GetOptionalValue("eventStore:consume", false); - - if (consumeEvents) - { - services.AddTransient(); - - services.AddSingletonAs() - .As(); - services.AddSingletonAs() - .As(); - services.AddSingletonAs() - .As(); - } - - var exposeSourceUrl = config.GetOptionalValue("assetStore:exposeSourceUrl", true); - - services.AddSingletonAs(c => new GraphQLUrlGenerator( - c.GetRequiredService>(), - c.GetRequiredService(), - exposeSourceUrl)) - .As(); - - services.AddSingletonAs() - .As() - .As(); - - services.AddSingletonAs(c => c.GetService>()?.Value?.Plans.OrEmpty()); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs(c => - { - var allEventConsumers = c.GetServices(); - - return new EventConsumerFactory(n => allEventConsumers.FirstOrDefault(x => x.Name == n)); - }); - - services.AddSingletonAs() - .AsSelf(); - - services.AddSingletonAs() - .AsSelf(); - } - } -} diff --git a/src/Squidex/Config/Domain/RuleServices.cs b/src/Squidex/Config/Domain/RuleServices.cs new file mode 100644 index 000000000..888d1eee4 --- /dev/null +++ b/src/Squidex/Config/Domain/RuleServices.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Core.HandleRules.Actions; +using Squidex.Domain.Apps.Core.HandleRules.Triggers; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Config.Domain +{ + public static class RuleServices + { + public static void AddMyRuleServices(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + } + } +} diff --git a/src/Squidex/Config/Domain/SerializationServices.cs b/src/Squidex/Config/Domain/SerializationServices.cs index f783f2601..45e7ad011 100644 --- a/src/Squidex/Config/Domain/SerializationServices.cs +++ b/src/Squidex/Config/Domain/SerializationServices.cs @@ -27,15 +27,17 @@ namespace Squidex.Config.Domain public static class SerializationServices { private static readonly TypeNameRegistry TypeNameRegistry = - new TypeNameRegistry() - .MapUnmapped(SquidexCoreModel.Assembly) - .MapUnmapped(SquidexEvents.Assembly) - .MapUnmapped(SquidexInfrastructure.Assembly) - .MapUnmapped(SquidexMigrations.Assembly); - + new TypeNameRegistry() + .MapUnmapped(SquidexCoreModel.Assembly) + .MapUnmapped(SquidexEvents.Assembly) + .MapUnmapped(SquidexInfrastructure.Assembly) + .MapUnmapped(SquidexMigrations.Assembly); private static readonly FieldRegistry FieldRegistry = new FieldRegistry(TypeNameRegistry); - private static JsonSerializerSettings ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) + public static readonly JsonSerializerSettings DefaultJsonSettings = new JsonSerializerSettings(); + public static readonly JsonSerializer DefaultJsonSerializer; + + private static void ConfigureJson(JsonSerializerSettings settings, TypeNameHandling typeNameHandling) { settings.SerializationBinder = new TypeNameSerializationBinder(TypeNameRegistry); @@ -65,22 +67,24 @@ namespace Squidex.Config.Domain settings.TypeNameHandling = typeNameHandling; settings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + } + + static SerializationServices() + { + ConfigureJson(DefaultJsonSettings, TypeNameHandling.Auto); + + DefaultJsonSerializer = JsonSerializer.Create(DefaultJsonSettings); - return settings; + BsonJsonConvention.Register(DefaultJsonSerializer); } public static IServiceCollection AddMySerializers(this IServiceCollection services) { - var serializerSettings = ConfigureJson(new JsonSerializerSettings(), TypeNameHandling.Auto); - var serializerInstance = JsonSerializer.Create(serializerSettings); - services.AddSingletonAs(t => FieldRegistry); - services.AddSingletonAs(t => serializerSettings); - services.AddSingletonAs(t => serializerInstance); + services.AddSingletonAs(t => DefaultJsonSettings); + services.AddSingletonAs(t => DefaultJsonSerializer); services.AddSingletonAs(t => TypeNameRegistry); - BsonJsonConvention.Register(serializerInstance); - return services; } diff --git a/src/Squidex/Config/Domain/StoreServices.cs b/src/Squidex/Config/Domain/StoreServices.cs index f9b849af6..537b06688 100644 --- a/src/Squidex/Config/Domain/StoreServices.cs +++ b/src/Squidex/Config/Domain/StoreServices.cs @@ -73,17 +73,6 @@ namespace Squidex.Config.Domain .As>() .As(); - services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) - .As>() - .As() - .As() - .As(); - - services.AddSingletonAs(c => new MongoRoleStore(mongoDatabase)) - .As>() - .As() - .As(); - services.AddSingletonAs(c => new MongoPersistedGrantStore(mongoDatabase)) .As() .As(); @@ -96,6 +85,17 @@ namespace Squidex.Config.Domain .As() .As(); + services.AddSingletonAs(c => new MongoUserStore(mongoDatabase)) + .As>() + .As() + .As() + .As(); + + services.AddSingletonAs(c => new MongoRoleStore(mongoDatabase)) + .As>() + .As() + .As(); + services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase)) .As() .As>() @@ -133,6 +133,8 @@ namespace Squidex.Config.Domain .As(); } }); + + services.AddSingleton(typeof(IStore<>), typeof(Store<>)); } } } diff --git a/src/Squidex/Config/Domain/SubscriptionServices.cs b/src/Squidex/Config/Domain/SubscriptionServices.cs new file mode 100644 index 000000000..c38eab838 --- /dev/null +++ b/src/Squidex/Config/Domain/SubscriptionServices.cs @@ -0,0 +1,37 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Squidex.Domain.Apps.Entities.Apps.Services; +using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; +using Squidex.Domain.Users; +using Squidex.Infrastructure; + +namespace Squidex.Config.Domain +{ + public static class SubscriptionServices + { + public static void AddMySubscriptionServices(this IServiceCollection services, IConfiguration config) + { + services.AddSingletonAs(c => c.GetService>()?.Value?.Plans.OrEmpty()); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + + services.AddSingletonAs() + .As(); + } + } +} diff --git a/src/Squidex/Config/Orleans/ClientWrapper.cs b/src/Squidex/Config/Orleans/ClientWrapper.cs new file mode 100644 index 000000000..99a730a04 --- /dev/null +++ b/src/Squidex/Config/Orleans/ClientWrapper.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net; +using Orleans; +using Orleans.Configuration; +using Orleans.Runtime; +using Squidex.Domain.Apps.Entities; +using Squidex.Infrastructure; + +namespace Squidex.Config.Orleans +{ + public sealed class ClientWrapper : DisposableObjectBase, IInitializable, IDisposable + { + private readonly IClusterClient client; + + public IClusterClient Client + { + get { return client; } + } + + public ClientWrapper() + { + client = new ClientBuilder() + .UseDashboard() + .UseStaticClustering(options => + { + options.Gateways.Add(new IPEndPoint(IPAddress.Loopback, 40000).ToGatewayUri()); + }) + .Configure(options => + { + options.ClusterId = "squidex"; + }) + .ConfigureApplicationParts(builder => + { + builder.AddApplicationPart(SquidexEntities.Assembly); + builder.AddApplicationPart(SquidexInfrastructure.Assembly); + }) + .Build(); + } + + public void Initialize() + { + client.Connect().Wait(); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + client.Close().Wait(); + } + } + } +} diff --git a/src/Squidex/Config/Orleans/OrleansServices.cs b/src/Squidex/Config/Orleans/OrleansServices.cs new file mode 100644 index 000000000..8fa4f34c0 --- /dev/null +++ b/src/Squidex/Config/Orleans/OrleansServices.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Orleans; +using Squidex.Infrastructure; + +namespace Squidex.Config.Orleans +{ + public static class OrleansServices + { + public static void AddOrleansSilo(this IServiceCollection services) + { + services.AddSingletonAs() + .As(); + } + + public static void AddOrleansClient(this IServiceCollection services) + { + services.AddServicesForSelfHostedDashboard(null, options => + { + options.HideTrace = true; + }); + + services.AddSingletonAs() + .As() + .AsSelf(); + + services.AddSingletonAs(c => c.GetRequiredService().Client) + .As(); + + services.AddSingletonAs(c => c.GetRequiredService().Client) + .As(); + } + } +} diff --git a/src/Squidex/Config/Orleans/SiloServices.cs b/src/Squidex/Config/Orleans/SiloServices.cs new file mode 100644 index 000000000..737c1ef15 --- /dev/null +++ b/src/Squidex/Config/Orleans/SiloServices.cs @@ -0,0 +1,42 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Orleans.Hosting; + +namespace Squidex.Config.Orleans +{ + public static class SiloServices + { + public static void AddAppSiloServices(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"); + + services.AddMongoDBMembershipTable(c => + { + c.ConnectionString = mongoConfiguration; + c.CollectionPrefix = "Orleans_"; + c.DatabaseName = mongoDatabaseName; + }); + + services.AddMongoDBReminders(c => + { + c.ConnectionString = mongoConfiguration; + c.CollectionPrefix = "Orleans_"; + c.DatabaseName = mongoDatabaseName; + }); + } + }); + } + } +} \ No newline at end of file diff --git a/src/Squidex/Config/Orleans/SiloWrapper.cs b/src/Squidex/Config/Orleans/SiloWrapper.cs new file mode 100644 index 000000000..18f99175e --- /dev/null +++ b/src/Squidex/Config/Orleans/SiloWrapper.cs @@ -0,0 +1,105 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Orleans; +using Orleans.Configuration; +using Orleans.Hosting; +using Squidex.Config.Domain; +using Squidex.Domain.Apps.Entities; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.Log.Adapter; +using Squidex.Infrastructure.Orleans; + +namespace Squidex.Config.Orleans +{ + public class SiloWrapper : DisposableObjectBase, IInitializable, IDisposable + { + private readonly ISiloHost silo; + + internal sealed class Source : IConfigurationSource + { + private readonly IConfigurationProvider configurationProvider; + + public Source(IConfigurationProvider configurationProvider) + { + this.configurationProvider = configurationProvider; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return configurationProvider; + } + } + + public SiloWrapper(IConfiguration configuration) + { + J.Serializer = SerializationServices.DefaultJsonSerializer; + + silo = new SiloHostBuilder() + .UseDashboard(options => options.HostSelf = true) + .AddStartupTask>() + .AddStartupTask>() + .AddStartupTask>() + .ConfigureEndpoints(Dns.GetHostName(), 11111, 40000, listenOnAnyHostAddress: true) + .Configure(options => + { + options.ClusterId = "squidex"; + }) + .ConfigureLogging((hostingContext, builder) => + { + builder.AddConfiguration(hostingContext.Configuration.GetSection("logging")); + builder.AddSemanticLog(); + builder.AddFilter("Orleans.Runtime.SiloControl", LogLevel.Warning); + }) + .ConfigureApplicationParts(builder => + { + builder.AddApplicationPart(SquidexEntities.Assembly); + builder.AddApplicationPart(SquidexInfrastructure.Assembly); + }) + .ConfigureServices((context, services) => + { + services.AddAppSiloServices(context.Configuration); + services.AddAppServices(context.Configuration); + + services.Configure(options => options.FastKillOnProcessExit = false); + }) + .ConfigureAppConfiguration((hostContext, builder) => + { + if (configuration is IConfigurationRoot root) + { + foreach (var provider in root.Providers) + { + builder.Add(new Source(provider)); + } + } + }) + .Build(); + } + + public void Initialize() + { + silo.StartAsync().Wait(); + } + + protected override void DisposeObject(bool disposing) + { + if (disposing) + { + Task.Run(() => silo.StopAsync()).Wait(); + } + } + } +} diff --git a/src/Squidex/Program.cs b/src/Squidex/Program.cs index 0266f9c99..cde637931 100644 --- a/src/Squidex/Program.cs +++ b/src/Squidex/Program.cs @@ -7,6 +7,10 @@ using System.IO; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Orleans; +using Orleans.Hosting; using Squidex.Infrastructure.Log.Adapter; namespace Squidex @@ -20,13 +24,21 @@ namespace Squidex .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup() - .ConfigureLogging(builder => + .ConfigureLogging((hostingContext, builder) => { + builder.AddConfiguration(hostingContext.Configuration.GetSection("logging")); builder.AddSemanticLog(); }) .ConfigureAppConfiguration((hostContext, builder) => { - builder.AddAppConfiguration(hostContext.HostingEnvironment.EnvironmentName, args); + builder.Sources.Clear(); + + builder.AddJsonFile("appsettings.json", true, true); + builder.AddJsonFile($"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", true); + + builder.AddEnvironmentVariables(); + + builder.AddCommandLine(args); }) .Build() .Run(); diff --git a/src/Squidex/Squidex.csproj b/src/Squidex/Squidex.csproj index 3c5d95107..78137e35a 100644 --- a/src/Squidex/Squidex.csproj +++ b/src/Squidex/Squidex.csproj @@ -48,38 +48,41 @@ - - - - - - + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + + + + + + - + - + + + - + @@ -101,4 +104,8 @@ + + + + \ No newline at end of file diff --git a/src/Squidex/WebStartup.cs b/src/Squidex/WebStartup.cs index 7632fba3a..73b2ffb50 100644 --- a/src/Squidex/WebStartup.cs +++ b/src/Squidex/WebStartup.cs @@ -13,8 +13,10 @@ using Microsoft.Extensions.DependencyInjection; using Squidex.Areas.Api; using Squidex.Areas.Frontend; using Squidex.Areas.IdentityServer; +using Squidex.Areas.OrleansDashboard; using Squidex.Areas.Portal; using Squidex.Config.Domain; +using Squidex.Config.Orleans; using Squidex.Config.Web; namespace Squidex @@ -30,6 +32,8 @@ namespace Squidex public IServiceProvider ConfigureServices(IServiceCollection services) { + services.AddOrleansSilo(); + services.AddOrleansClient(); services.AddAppServices(configuration); return services.BuildServiceProvider(); @@ -48,8 +52,8 @@ namespace Squidex app.ConfigureApi(); app.ConfigurePortal(); + app.ConfigureOrleansDashboard(); app.ConfigureIdentityServer(); - app.ConfigureFrontend(); } } diff --git a/src/Squidex/app-config/helpers.js b/src/Squidex/app-config/helpers.js index 30c3534da..acae3e4bb 100644 --- a/src/Squidex/app-config/helpers.js +++ b/src/Squidex/app-config/helpers.js @@ -1,8 +1,4 @@ -// ReSharper disable InconsistentNaming -// ReSharper disable PossiblyUnassignedProperty -// ReSharper disable InconsistentNaming - -var path = require('path'); +var path = require('path'); var appRoot = path.resolve(__dirname, '..'); diff --git a/src/Squidex/app-config/karma.conf.js b/src/Squidex/app-config/karma.conf.js index 852cce536..e95ed7d43 100644 --- a/src/Squidex/app-config/karma.conf.js +++ b/src/Squidex/app-config/karma.conf.js @@ -34,7 +34,7 @@ module.exports = function (config) { }, /* - * leave Jasmine Spec Runner output visible in browser + * Leave Jasmine Spec Runner output visible in browser */ client: { clearContext: false diff --git a/src/Squidex/app-config/webpack.config.js b/src/Squidex/app-config/webpack.config.js index 41b87ba34..06b097999 100644 --- a/src/Squidex/app-config/webpack.config.js +++ b/src/Squidex/app-config/webpack.config.js @@ -1,6 +1,3 @@ -// ReSharper disable InconsistentNaming -// ReSharper disable PossiblyUnassignedProperty - var webpack = require('webpack'), path = require('path'), HtmlWebpackPlugin = require('html-webpack-plugin'), diff --git a/src/Squidex/app-config/webpack.run.base.js b/src/Squidex/app-config/webpack.run.base.js index c9f4cf820..18274c8fc 100644 --- a/src/Squidex/app-config/webpack.run.base.js +++ b/src/Squidex/app-config/webpack.run.base.js @@ -1,7 +1,4 @@ -// ReSharper disable InconsistentNaming -// ReSharper disable PossiblyUnassignedProperty - - var webpack = require('webpack'), + var webpack = require('webpack'), webpackMerge = require('webpack-merge'), HtmlWebpackPlugin = require('html-webpack-plugin'), commonConfig = require('./webpack.config.js'), diff --git a/src/Squidex/app-config/webpack.run.dev.js b/src/Squidex/app-config/webpack.run.dev.js index 224f8d515..21a138e16 100644 --- a/src/Squidex/app-config/webpack.run.dev.js +++ b/src/Squidex/app-config/webpack.run.dev.js @@ -1,7 +1,4 @@ -// ReSharper disable InconsistentNaming -// ReSharper disable PossiblyUnassignedProperty - - var webpackMerge = require('webpack-merge'), + var webpackMerge = require('webpack-merge'), ExtractTextPlugin = require('extract-text-webpack-plugin'), runConfig = require('./webpack.run.base.js'), helpers = require('./helpers'); diff --git a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts index 97b065926..a1f40d5e8 100644 --- a/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts +++ b/src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts @@ -83,7 +83,7 @@ export class RuleEventsPageComponent implements OnInit { } else if (status === 'Failed') { return 'danger'; } else if (status === 'Pending') { - return 'default'; + return 'secondary'; } else { return status.toLowerCase(); } diff --git a/src/Squidex/appsettings.json b/src/Squidex/appsettings.json index 7edf55f44..d2ae2abd2 100644 --- a/src/Squidex/appsettings.json +++ b/src/Squidex/appsettings.json @@ -42,32 +42,11 @@ } }, - "logging": { /* * Setting the flag to true, enables well formatteds json logs. */ - "human": true - }, - - /* - * The pub sub mechanmism distributes messages between the nodes. - */ - "pubSub": { - /* - * Define the type of the read store. - * - * Supported: InMemory (for single node only), Redis (for cluster) - */ - "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" - } + "human": true }, "assetStore": { diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs index 5f4cdef14..6a6e6028e 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs +++ b/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleEventFormatterTests.cs @@ -86,8 +86,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", "Berlin")) + .AddField("city", + new ContentFieldData() + .AddValue("iv", "Berlin")) }; var result = sut.FormatString("$CONTENT_DATA.country.iv", AsEnvelope(@event)); @@ -102,8 +103,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", "Berlin")) + .AddField("city", + new ContentFieldData() + .AddValue("iv", "Berlin")) }; var result = sut.FormatString("$CONTENT_DATA.city.de", AsEnvelope(@event)); @@ -118,8 +120,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", new JArray())) + .AddField("city", + new ContentFieldData() + .AddValue("iv", new JArray())) }; var result = sut.FormatString("$CONTENT_DATA.city.de.10", AsEnvelope(@event)); @@ -134,9 +137,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", new JObject( - new JProperty("name", "Berlin")))) + .AddField("city", + new ContentFieldData() + .AddValue("iv", new JObject( + new JProperty("name", "Berlin")))) }; var result = sut.FormatString("$CONTENT_DATA.city.de.Name", AsEnvelope(@event)); @@ -151,8 +155,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", "Berlin")) + .AddField("city", + new ContentFieldData() + .AddValue("iv", "Berlin")) }; var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); @@ -167,8 +172,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", JValue.CreateNull())) + .AddField("city", + new ContentFieldData() + .AddValue("iv", JValue.CreateNull())) }; var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); @@ -183,8 +189,9 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", JValue.CreateUndefined())) + .AddField("city", + new ContentFieldData() + .AddValue("iv", JValue.CreateUndefined())) }; var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); @@ -199,9 +206,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", new JObject( - new JProperty("name", "Berlin")))) + .AddField("city", + new ContentFieldData() + .AddValue("iv", new JObject( + new JProperty("name", "Berlin")))) }; var result = sut.FormatString("$CONTENT_DATA.city.iv", AsEnvelope(@event)); @@ -216,9 +224,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", new JArray( - "Berlin"))) + .AddField("city", + new ContentFieldData() + .AddValue("iv", new JArray( + "Berlin"))) }; var result = sut.FormatString("$CONTENT_DATA.city.iv.0", AsEnvelope(@event)); @@ -233,9 +242,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules { Data = new NamedContentData() - .AddField("city", new ContentFieldData() - .AddValue("iv", new JObject( - new JProperty("name", "Berlin")))) + .AddField("city", + new ContentFieldData() + .AddValue("iv", new JObject( + new JProperty("name", "Berlin")))) }; var result = sut.FormatString("$CONTENT_DATA.city.iv.name", AsEnvelope(@event)); diff --git a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj index 0f5eb476e..192241dbf 100644 --- a/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Core.Tests/Squidex.Domain.Apps.Core.Tests.csproj @@ -10,9 +10,9 @@ - + - + diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs index a741e778f..2840f841d 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppGrainTests.cs @@ -58,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Apps }; sut = new AppGrain(initialPatterns, Store, appProvider, appPlansProvider, appPlansBillingManager, userResolver); - sut.ActivateAsync(Id).Wait(); + sut.OnActivateAsync(Id).Wait(); } [Fact] @@ -97,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Apps var result = await sut.ExecuteAsync(CreateCommand(command)); - Assert.True(result is PlanChangedResult); + Assert.True(result.Value is PlanChangedResult); Assert.Equal(planId, sut.Snapshot.Plan.PlanId); diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/NoopAppPlanBillingManagerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs similarity index 95% rename from tests/Squidex.Domain.Apps.Entities.Tests/Apps/NoopAppPlanBillingManagerTests.cs rename to tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs index 19a69298f..39172ada7 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Apps/NoopAppPlanBillingManagerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Billing/NoopAppPlanBillingManagerTests.cs @@ -10,7 +10,7 @@ using System.Threading.Tasks; using Squidex.Domain.Apps.Entities.Apps.Services.Implementations; using Xunit; -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Domain.Apps.Entities.Apps.Billing { public class NoopAppPlanBillingManagerTests { diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs index 634514689..6882a0450 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs @@ -9,12 +9,12 @@ using System; using System.IO; using System.Threading.Tasks; using FakeItEasy; +using Orleans; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.Assets.State; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; using Xunit; @@ -24,7 +24,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); private readonly IAssetStore assetStore = A.Fake(); - private readonly IStateFactory stateFactory = A.Fake(); + private readonly IGrainFactory grainFactory = A.Fake(); private readonly Guid assetId = Guid.NewGuid(); private readonly Stream stream = new MemoryStream(); private readonly ImageInfo image = new ImageInfo(2048, 2048); @@ -42,12 +42,12 @@ namespace Squidex.Domain.Apps.Entities.Assets file = new AssetFile("my-image.png", "image/png", 1024, () => stream); asset = new AssetGrain(Store); - asset.ActivateAsync(Id).Wait(); + asset.OnActivateAsync(Id).Wait(); - A.CallTo(() => stateFactory.CreateAsync(Id)) + A.CallTo(() => grainFactory.GetGrain(Id, null)) .Returns(asset); - sut = new AssetCommandMiddleware(stateFactory, assetStore, assetThumbnailGenerator); + sut = new AssetCommandMiddleware(grainFactory, assetStore, assetThumbnailGenerator); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs index 08f58bb11..81e1fa648 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs @@ -34,7 +34,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public AssetGrainTests() { sut = new AssetGrain(Store); - sut.ActivateAsync(Id).Wait(); + sut.OnActivateAsync(Id).Wait(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs index 855fb6c44..4a558d197 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs @@ -108,7 +108,7 @@ namespace Squidex.Domain.Apps.Entities.Contents patched = patch.MergeInto(data); sut = new ContentGrain(Store, appProvider, A.Dummy(), scriptEngine, A.Dummy()); - sut.ActivateAsync(Id).Wait(); + sut.OnActivateAsync(Id).Wait(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs deleted file mode 100644 index 48f5338b0..000000000 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentQueryServiceTests.cs +++ /dev/null @@ -1,224 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.OData.UriParser; -using Squidex.Domain.Apps.Core.Contents; -using Squidex.Domain.Apps.Core.Scripting; -using Squidex.Domain.Apps.Entities.Apps; -using Squidex.Domain.Apps.Entities.Contents.Edm; -using Squidex.Domain.Apps.Entities.Contents.Repositories; -using Squidex.Domain.Apps.Entities.Schemas; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Security; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents -{ - public class ContentQueryServiceTests - { - private readonly IContentRepository contentRepository = A.Fake(); - private readonly IScriptEngine scriptEngine = A.Fake(); - private readonly ISchemaEntity schema = A.Fake(); - private readonly IContentEntity content = A.Fake(); - private readonly IAppEntity app = A.Fake(); - private readonly IAppProvider appProvider = A.Fake(); - private readonly Guid appId = Guid.NewGuid(); - private readonly Guid schemaId = Guid.NewGuid(); - private readonly Guid contentId = Guid.NewGuid(); - private readonly string appName = "my-app"; - private readonly NamedContentData contentData = new NamedContentData(); - private readonly NamedContentData contentTransformed = new NamedContentData(); - private readonly ClaimsPrincipal user; - private readonly ClaimsIdentity identity = new ClaimsIdentity(); - private readonly EdmModelBuilder modelBuilder = A.Fake(); - private readonly ContentQueryService sut; - - public ContentQueryServiceTests() - { - user = new ClaimsPrincipal(identity); - - A.CallTo(() => app.Id).Returns(appId); - A.CallTo(() => app.Name).Returns(appName); - - A.CallTo(() => content.Id).Returns(contentId); - A.CallTo(() => content.Data).Returns(contentData); - A.CallTo(() => content.Status).Returns(Status.Published); - - sut = new ContentQueryService(contentRepository, appProvider, scriptEngine, modelBuilder); - } - - [Fact] - public async Task Should_return_schema_from_id_if_string_is_guid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) - .Returns(schema); - - var result = await sut.FindSchemaAsync(app, schemaId.ToString()); - - Assert.Equal(schema, result); - } - - [Fact] - public async Task Should_return_schema_from_name_if_string_not_guid() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema")) - .Returns(schema); - - var result = await sut.FindSchemaAsync(app, "my-schema"); - - Assert.Equal(schema, result); - } - - [Fact] - public async Task Should_throw_if_schema_not_found() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId, "my-schema")) - .Returns((ISchemaEntity)null); - - await Assert.ThrowsAsync(() => sut.FindSchemaAsync(app, "my-schema")); - } - - [Fact] - public async Task Should_return_content_from_repository_and_transform() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) - .Returns(schema); - A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) - .Returns(content); - - A.CallTo(() => schema.ScriptQuery) - .Returns(""); - - A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, contentData)), "")) - .Returns(contentTransformed); - - var result = await sut.FindContentAsync(app, schemaId.ToString(), user, contentId); - - Assert.Equal(schema, result.Schema); - - Assert.Equal(contentTransformed, result.Content.Data); - Assert.Equal(content.Id, result.Content.Id); - } - - [Fact] - public async Task Should_throw_if_content_to_find_does_not_exist() - { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) - .Returns(schema); - - A.CallTo(() => contentRepository.FindContentAsync(app, schema, contentId)) - .Returns((IContentEntity)null); - - await Assert.ThrowsAsync(async () => await sut.FindContentAsync(app, schemaId.ToString(), user, contentId)); - } - - [Fact] - public async Task Should_return_contents_with_ids_from_repository_and_transform() - { - await TestManyIdRequest(true, false, new HashSet { Guid.NewGuid() }, Status.Draft, Status.Published); - } - - [Fact] - public async Task Should_return_non_archived_contents_from_repository_and_transform() - { - await TestManyRequest(true, false, Status.Draft, Status.Published); - } - - [Fact] - public async Task Should_return_archived_contents_from_repository_and_transform() - { - await TestManyRequest(true, true, Status.Archived); - } - - [Fact] - public async Task Should_return_draft_contents_from_repository_and_transform() - { - await TestManyRequest(false, false, Status.Published); - } - - [Fact] - public async Task Should_return_draft_contents_from_repository_and_transform_when_requesting_archive_as_non_frontend() - { - await TestManyRequest(false, true, Status.Published); - } - - private async Task TestManyRequest(bool isFrontend, bool archive, params Status[] status) - { - SetupClaims(isFrontend); - - SetupFakeWithOdataQuery(status); - SetupFakeWithScripting(); - - var result = await sut.QueryAsync(app, schemaId.ToString(), user, archive, string.Empty); - - Assert.Equal(schema, result.Schema); - - Assert.Equal(contentData, result.Contents[0].Data); - Assert.Equal(content.Id, result.Contents[0].Id); - - Assert.Equal(123, result.Contents.Total); - } - - private async Task TestManyIdRequest(bool isFrontend, bool archive, HashSet ids, params Status[] status) - { - SetupClaims(isFrontend); - - SetupFakeWithIdQuery(status, ids); - SetupFakeWithScripting(); - - var result = await sut.QueryAsync(app, schemaId.ToString(), user, archive, ids); - - Assert.Equal(schema, result.Schema); - - Assert.Equal(contentData, result.Contents[0].Data); - Assert.Equal(content.Id, result.Contents[0].Id); - - Assert.Equal(123, result.Contents.Total); - } - - private void SetupClaims(bool isFrontend) - { - if (isFrontend) - { - identity.AddClaim(new Claim(OpenIdClaims.ClientId, "squidex-frontend")); - } - } - - private void SetupFakeWithIdQuery(Status[] status, HashSet ids) - { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) - .Returns(schema); - - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), ids)) - .Returns(ResultList.Create(Enumerable.Repeat(content, 1), 123)); - } - - private void SetupFakeWithOdataQuery(Status[] status) - { - A.CallTo(() => appProvider.GetSchemaAsync(appId, schemaId, false)) - .Returns(schema); - - A.CallTo(() => contentRepository.QueryAsync(app, schema, A.That.IsSameSequenceAs(status), A.Ignored)) - .Returns(ResultList.Create(Enumerable.Repeat(content, 1), 123)); - } - - private void SetupFakeWithScripting() - { - A.CallTo(() => schema.ScriptQuery) - .Returns(""); - - A.CallTo(() => scriptEngine.Transform(A.That.Matches(x => x.User == user && x.ContentId == contentId && ReferenceEquals(x.Data, contentData)), "")) - .Returns(contentTransformed); - } - } -} diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs index 96cfbb42c..18754535c 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs @@ -97,26 +97,35 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL data = data ?? new NamedContentData() .AddField("my-string", - new ContentFieldData().AddValue("de", "value")) + new ContentFieldData() + .AddValue("de", "value")) .AddField("my-assets", - new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { assetId }))) + new ContentFieldData() + .AddValue("iv", JToken.FromObject(new[] { assetId }))) .AddField("my-number", - new ContentFieldData().AddValue("iv", 1)) + new ContentFieldData() + .AddValue("iv", 1)) .AddField("my-boolean", - new ContentFieldData().AddValue("iv", true)) + new ContentFieldData() + .AddValue("iv", true)) .AddField("my-datetime", - new ContentFieldData().AddValue("iv", now.ToDateTimeUtc())) + new ContentFieldData() + .AddValue("iv", now.ToDateTimeUtc())) .AddField("my-tags", - new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { "tag1", "tag2" }))) + new ContentFieldData() + .AddValue("iv", JToken.FromObject(new[] { "tag1", "tag2" }))) .AddField("my-references", - new ContentFieldData().AddValue("iv", JToken.FromObject(new[] { refId }))) + new ContentFieldData() + .AddValue("iv", JToken.FromObject(new[] { refId }))) .AddField("my-geolocation", - new ContentFieldData().AddValue("iv", JToken.FromObject(new { latitude = 10, longitude = 20 }))); + new ContentFieldData() + .AddValue("iv", JToken.FromObject(new { latitude = 10, longitude = 20 }))); if (!noJson) { data.AddField("my-json", - new ContentFieldData().AddValue("iv", JToken.FromObject(new { value = 1 }))); + new ContentFieldData() + .AddValue("iv", JToken.FromObject(new { value = 1 }))); } var content = new ContentEntity diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs index 68d032bc7..c7303a074 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerTests.cs @@ -27,13 +27,13 @@ namespace Squidex.Domain.Apps.Entities.Rules private readonly IRuleEventRepository ruleEventRepository = A.Fake(); private readonly Instant now = SystemClock.Instance.GetCurrentInstant(); private readonly RuleService ruleService = A.Fake(); - private readonly RuleDequeuer sut; + private readonly RuleDequeuerGrain sut; public RuleDequeuerTests() { A.CallTo(() => clock.GetCurrentInstant()).Returns(now); - sut = new RuleDequeuer( + sut = new RuleDequeuerGrain( ruleService, ruleEventRepository, log, @@ -69,8 +69,6 @@ namespace Squidex.Domain.Apps.Entities.Rules await sut.HandleAsync(@event); - sut.Dispose(); - A.CallTo(() => ruleEventRepository.MarkSentAsync(@event.Id, requestDump, result, jobResult, requestElapsed, nextCall)) .MustHaveHappened(); } diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs index bb5e414c8..a5e2bf969 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleGrainTests.cs @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Rules public RuleGrainTests() { sut = new RuleGrain(Store, appProvider); - sut.ActivateAsync(Id).Wait(); + sut.OnActivateAsync(Id).Wait(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs index e1fd8c7bd..eb2d3dd06 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/SchemaGrainTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas fieldId = new NamedId(1, fieldName); sut = new SchemaGrain(Store, appProvider, registry); - sut.ActivateAsync(Id).Wait(); + sut.OnActivateAsync(Id).Wait(); } [Fact] diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index e8182ece5..bfbe053bc 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -19,9 +19,9 @@ - + - + diff --git a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index e2021304d..67377c1bf 100644 --- a/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -14,6 +14,7 @@ using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; #pragma warning disable IDE0019 // Use pattern matching @@ -93,6 +94,11 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers return command; } + protected static J J(IAggregateCommand command) + { + return command.AsJ(); + } + protected TEvent CreateEvent(TEvent @event) where TEvent : SquidexEvent { @event.Actor = User; diff --git a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj index 26ddcfcc4..579fde7c1 100644 --- a/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj +++ b/tests/Squidex.Domain.Users.Tests/Squidex.Domain.Users.Tests.csproj @@ -11,9 +11,9 @@ - + - + diff --git a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs index 169e59ff8..8102f3a93 100644 --- a/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainTests.cs @@ -11,18 +11,19 @@ using System.Linq; using System.Threading.Tasks; using FakeItEasy; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Infrastructure.Commands { - public class DomainObjectGrainTests + public sealed class DomainObjectGrainTests { private readonly IStore store = A.Fake>(); private readonly IPersistence persistence = A.Fake>(); private readonly Guid id = Guid.NewGuid(); - private readonly MyGrain sut; + private readonly MyDomainObject sut; public sealed class MyDomainState : IDomainState { @@ -56,14 +57,14 @@ namespace Squidex.Infrastructure.Commands public int Value { get; set; } } - public sealed class MyGrain : DomainObjectGrain + public sealed class MyDomainObject : DomainObjectGrain { - public MyGrain(IStore store) + public MyDomainObject(IStore store) : base(store) { } - public override Task ExecuteAsync(IAggregateCommand command) + protected override Task ExecuteAsync(IAggregateCommand command) { switch (command) { @@ -110,10 +111,10 @@ namespace Squidex.Infrastructure.Commands public DomainObjectGrainTests() { - A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyGrain), id, A>.Ignored, A, Task>>.Ignored)) + A.CallTo(() => store.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>.Ignored, A, Task>>.Ignored)) .Returns(persistence); - sut = new MyGrain(store); + sut = new MyDomainObject(store); } [Fact] @@ -127,14 +128,14 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - var result = await sut.ExecuteAsync(new CreateAuto { Value = 5 }); + var result = await sut.ExecuteAsync(C(new CreateAuto { Value = 5 })); A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 5))) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); - Assert.True(result is EntityCreatedResult); + Assert.True(result.Value is EntityCreatedResult); Assert.Empty(sut.GetUncomittedEvents()); Assert.Equal(5, sut.Snapshot.Value); @@ -145,14 +146,14 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - var result = await sut.ExecuteAsync(new UpdateAuto { Value = 5 }); + var result = await sut.ExecuteAsync(C(new UpdateAuto { Value = 5 })); A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 5))) .MustHaveHappened(); A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count() == 1))) .MustHaveHappened(); - Assert.True(result is EntitySavedResult); + Assert.True(result.Value is EntitySavedResult); Assert.Empty(sut.GetUncomittedEvents()); Assert.Equal(5, sut.Snapshot.Value); @@ -163,7 +164,7 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); } [Fact] @@ -171,7 +172,7 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); } [Fact] @@ -179,9 +180,9 @@ namespace Squidex.Infrastructure.Commands { await SetupEmptyAsync(); - var result = await sut.ExecuteAsync(new CreateCustom()); + var result = await sut.ExecuteAsync(C(new CreateCustom())); - Assert.Equal("CREATED", result); + Assert.Equal("CREATED", result.Value); } [Fact] @@ -189,9 +190,9 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - var result = await sut.ExecuteAsync(new UpdateCustom()); + var result = await sut.ExecuteAsync(C(new UpdateCustom())); - Assert.Equal("UPDATED", result); + Assert.Equal("UPDATED", result.Value); } [Fact] @@ -199,7 +200,7 @@ namespace Squidex.Infrastructure.Commands { await SetupCreatedAsync(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 })); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateCustom { ExpectedVersion = 3 }))); } [Fact] @@ -210,7 +211,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new CreateAuto()))); Assert.Empty(sut.GetUncomittedEvents()); Assert.Equal(0, sut.Snapshot.Value); @@ -224,7 +225,7 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(C(new UpdateAuto()))); Assert.Empty(sut.GetUncomittedEvents()); Assert.Equal(0, sut.Snapshot.Value); @@ -232,14 +233,19 @@ namespace Squidex.Infrastructure.Commands private async Task SetupCreatedAsync() { - await sut.ActivateAsync(id); + await sut.OnActivateAsync(id); - await sut.ExecuteAsync(new CreateAuto()); + await sut.ExecuteAsync(C(new CreateAuto())); + } + + private static J C(IAggregateCommand command) + { + return command.AsJ(); } private async Task SetupEmptyAsync() { - await sut.ActivateAsync(id); + await sut.OnActivateAsync(id); } } } diff --git a/tests/Squidex.Infrastructure.Tests/Commands/GrainCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/GrainCommandMiddlewareTests.cs deleted file mode 100644 index ca97ee36f..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/GrainCommandMiddlewareTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class GrainCommandMiddlewareTests - { - private readonly IStateFactory factory = A.Fake(); - private readonly MyGrain grain = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly GrainCommandMiddleware sut; - - public class MatchingCommand : MyCommand - { - } - - public GrainCommandMiddlewareTests() - { - A.CallTo(() => factory.CreateAsync(id)) - .Returns(grain); - - sut = new GrainCommandMiddleware(factory); - } - - [Fact] - public async Task Should_invoke_grain_when_command_is_correct() - { - var command = new MatchingCommand { AggregateId = id }; - var context = new CommandContext(command, A.Fake()); - - A.CallTo(() => grain.ExecuteAsync(command)) - .Returns(100); - - await sut.HandleAsync(context); - - Assert.Equal(100, context.Result()); - } - - [Fact] - public async Task Should_not_invoke_grain_when_command_is_not_correct() - { - var command = new MyCommand { AggregateId = id }; - var context = new CommandContext(command, A.Fake()); - - await sut.HandleAsync(context); - - Assert.Null(context.Result()); - - A.CallTo(() => grain.ExecuteAsync(command)) - .MustNotHaveHappened(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/Commands/SyncedGrainCommandMiddlewareTests.cs b/tests/Squidex.Infrastructure.Tests/Commands/SyncedGrainCommandMiddlewareTests.cs deleted file mode 100644 index d40bddbb9..000000000 --- a/tests/Squidex.Infrastructure.Tests/Commands/SyncedGrainCommandMiddlewareTests.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class SyncedGrainCommandMiddlewareTests - { - private readonly IStateFactory factory = A.Fake(); - private readonly MyGrain grain = A.Fake(); - private readonly Guid id = Guid.NewGuid(); - private readonly SyncedGrainCommandMiddleware sut; - - public class MatchingCommand : MyCommand - { - } - - public SyncedGrainCommandMiddlewareTests() - { - A.CallTo(() => factory.GetSingleAsync(id)) - .Returns(grain); - - sut = new SyncedGrainCommandMiddleware(factory); - } - - [Fact] - public async Task Should_invoke_grain_when_command_is_correct() - { - var command = new MatchingCommand { AggregateId = id }; - var context = new CommandContext(command, A.Fake()); - - A.CallTo(() => grain.ExecuteAsync(command)) - .Returns(100); - - await sut.HandleAsync(context); - - Assert.Equal(100, context.Result()); - - A.CallTo(() => factory.Synchronize(id)) - .MustHaveHappened(); - A.CallTo(() => factory.Remove(id)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_remove_grain_from_cache_when_failed() - { - var command = new MatchingCommand { AggregateId = id }; - var context = new CommandContext(command, A.Fake()); - - A.CallTo(() => grain.ExecuteAsync(command)) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - - A.CallTo(() => factory.Synchronize(id)) - .MustNotHaveHappened(); - A.CallTo(() => factory.Remove(id)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_not_invoke_grain_when_command_is_not_correct() - { - var command = new MyCommand { AggregateId = id }; - var context = new CommandContext(command, A.Fake()); - - await sut.HandleAsync(context); - - Assert.Null(context.Result()); - - A.CallTo(() => grain.ExecuteAsync(command)) - .MustNotHaveHappened(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventNotifierTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventNotifierTests.cs deleted file mode 100644 index 5730499c2..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventNotifierTests.cs +++ /dev/null @@ -1,49 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Generic; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing -{ - public 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/EventSourcing/Grains/EventConsumerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs index 1240ca92a..5339d54dd 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs @@ -9,6 +9,7 @@ using System; using System.Threading.Tasks; using FakeItEasy; using FluentAssertions; +using Orleans.Concurrency; using Squidex.Infrastructure.Log; using Squidex.Infrastructure.States; using Squidex.Infrastructure.TestHelpers; @@ -20,20 +21,29 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { public sealed class MyEventConsumerGrain : EventConsumerGrain { - public MyEventConsumerGrain(IStore store, IEventStore eventStore, IEventDataFormatter eventDataFormatter, ISemanticLog log) - : base(store, eventStore, eventDataFormatter, log) + public MyEventConsumerGrain( + EventConsumerFactory eventConsumerFactory, + IStore store, + IEventStore eventStore, + IEventDataFormatter eventDataFormatter, + ISemanticLog log) + : base(eventConsumerFactory, store, eventStore, eventDataFormatter, log) { } - protected override IEventSubscription CreateSubscription(IEventStore eventStore, string streamFilter, string position) + protected override IEventConsumerGrain GetSelf() { - return eventStore.CreateSubscription(this, streamFilter, position); + return this; + } + + protected override IEventSubscription CreateSubscription(IEventStore store, IEventSubscriber subscriber, string streamFilter, string position) + { + return store.CreateSubscription(subscriber, streamFilter, position); } } private readonly IEventConsumer eventConsumer = A.Fake(); private readonly IEventStore eventStore = A.Fake(); - private readonly IEventSubscriber sutSubscriber; private readonly IEventSubscription eventSubscription = A.Fake(); private readonly IPersistence persistence = A.Fake>(); private readonly ISemanticLog log = A.Fake(); @@ -53,8 +63,8 @@ namespace Squidex.Infrastructure.EventSourcing.Grains consumerName = eventConsumer.GetType().Name; - A.CallTo(() => store.WithSnapshots(typeof(EventConsumerGrain), consumerName, A>.Ignored)) - .Invokes(new Action>((type, key, a) => apply = a)) + A.CallTo(() => store.WithSnapshots(A.Ignored, consumerName, A>.Ignored)) + .Invokes(new Action>((t, key, a) => apply = a)) .Returns(persistence); A.CallTo(() => eventStore.CreateSubscription(A.Ignored, A.Ignored, A.Ignored)) @@ -71,18 +81,21 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => formatter.Parse(eventData, true)).Returns(envelope); - sut = new MyEventConsumerGrain(store, eventStore, formatter, log); - sutSubscriber = sut; + sut = new MyEventConsumerGrain( + x => eventConsumer, + store, + eventStore, + formatter, + log); } [Fact] - public void Should_not_subscribe_to_event_store_when_stopped_in_db() + public async Task Should_not_subscribe_to_event_store_when_stopped_in_db() { state = state.Stopped(); - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - sut.Dispose(); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); @@ -91,11 +104,10 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } [Fact] - public void Should_subscribe_to_event_store_when_not_found_in_db() + public async Task Should_subscribe_to_event_store_when_not_found_in_db() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - sut.Dispose(); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); @@ -104,11 +116,10 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } [Fact] - public void Should_subscribe_to_event_store_when_not_stopped_in_db() + public async Task Should_subscribe_to_event_store_when_not_stopped_in_db() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - sut.Dispose(); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); @@ -117,14 +128,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } [Fact] - public void Should_stop_subscription_when_stopped() + public async Task Should_stop_subscription_when_stopped() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - sut.Stop(); - sut.Stop(); - - sut.Dispose(); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.StopAsync(); + await sut.StopAsync(); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); @@ -136,13 +145,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } [Fact] - public void Should_reset_consumer_when_resetting() + public async Task Should_reset_consumer_when_resetting() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - sut.Stop(); - sut.Reset(); - sut.Dispose(); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.StopAsync(); + await sut.ResetAsync(); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); @@ -165,14 +173,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_invoke_and_update_position_when_event_received() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); - await OnEventAsync(eventSubscription, @event); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); - sut.Dispose(); + await OnEventAsync(eventSubscription, @event); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); @@ -186,17 +192,15 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_ignore_old_events() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - A.CallTo(() => formatter.Parse(eventData, true)) .Throws(new TypeNameNotFoundException()); var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); - await OnEventAsync(eventSubscription, @event); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); - sut.Dispose(); + await OnEventAsync(eventSubscription, @event); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); @@ -210,14 +214,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_not_invoke_and_update_position_when_event_is_from_another_subscription() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); - await OnEventAsync(A.Fake(), @event); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); - sut.Dispose(); + await OnEventAsync(A.Fake(), @event); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); @@ -228,15 +230,13 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_stop_if_consumer_failed() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); var ex = new InvalidOperationException(); await OnErrorAsync(eventSubscription, ex); - sut.Dispose(); - state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); A.CallTo(() => persistence.WriteSnapshotAsync(A.Ignored)) @@ -249,14 +249,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_not_make_error_handling_when_exception_is_from_another_subscription() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - var ex = new InvalidOperationException(); - await OnErrorAsync(A.Fake(), ex); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); - sut.Dispose(); + await OnErrorAsync(A.Fake(), ex); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); @@ -265,18 +263,27 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } [Fact] - public void Should_stop_if_resetting_failed() + public async Task Should_wakeup_when_already_subscribed() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.ActivateAsync(); + + A.CallTo(() => eventSubscription.WakeUp()) + .MustHaveHappened(); + } + [Fact] + public async Task Should_stop_if_resetting_failed() + { var ex = new InvalidOperationException(); A.CallTo(() => eventConsumer.ClearAsync()) .Throws(ex); - sut.Reset(); - sut.Dispose(); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); + await sut.ResetAsync(); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); @@ -290,9 +297,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_stop_if_handling_failed() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - var ex = new InvalidOperationException(); A.CallTo(() => eventConsumer.On(envelope)) @@ -300,9 +304,10 @@ namespace Squidex.Infrastructure.EventSourcing.Grains var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); - await OnEventAsync(eventSubscription, @event); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); - sut.Dispose(); + await OnEventAsync(eventSubscription, @event); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); @@ -314,16 +319,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventSubscription.StopAsync()) .MustHaveHappened(Repeated.Exactly.Once); - - sut.GetState().ShouldBeEquivalentTo(new EventConsumerInfo { Name = consumerName, IsStopped = true, Position = initialPosition, Error = ex.ToString() }); } [Fact] public async Task Should_stop_if_deserialization_failed() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - var ex = new InvalidOperationException(); A.CallTo(() => formatter.Parse(eventData, true)) @@ -331,9 +331,10 @@ namespace Squidex.Infrastructure.EventSourcing.Grains var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); - await OnEventAsync(eventSubscription, @event); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); - sut.Dispose(); + await OnEventAsync(eventSubscription, @event); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); @@ -350,9 +351,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_start_after_stop_when_handling_failed() { - sut.ActivateAsync(consumerName).Wait(); - sut.Activate(eventConsumer); - var exception = new InvalidOperationException(); A.CallTo(() => eventConsumer.On(envelope)) @@ -360,11 +358,14 @@ namespace Squidex.Infrastructure.EventSourcing.Grains var @event = new StoredEvent(Guid.NewGuid().ToString(), 123, eventData); + await sut.OnActivateAsync(consumerName); + await sut.ActivateAsync(); + await OnEventAsync(eventSubscription, @event); - sut.Start(); - sut.Start(); - sut.Dispose(); + await sut.StopAsync(); + await sut.StartAsync(); + await sut.StartAsync(); state.ShouldBeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); @@ -383,12 +384,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) { - return sutSubscriber.OnErrorAsync(subscriber, ex); + return sut.OnErrorAsync(subscriber.AsImmutable(), ex.AsImmutable()); } private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) { - return sutSubscriber.OnEventAsync(subscriber, ev); + return sut.OnEventAsync(subscriber.AsImmutable(), ev.AsImmutable()); } } } \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs new file mode 100644 index 000000000..22433e7fa --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs @@ -0,0 +1,162 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Generic; +using System.Threading.Tasks; +using FakeItEasy; +using FluentAssertions; +using Orleans; +using Orleans.Concurrency; +using Orleans.Core; +using Orleans.Runtime; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class EventConsumerManagerGrainTests + { + public class MyEventConsumerManagerGrain : EventConsumerManagerGrain + { + public MyEventConsumerManagerGrain( + IEnumerable eventConsumers, + IGrainIdentity identity, + IGrainRuntime runtime) + : base(eventConsumers, identity, runtime) + { + } + } + + private readonly IEventConsumer consumerA = A.Fake(); + private readonly IEventConsumer consumerB = A.Fake(); + private readonly IEventConsumerGrain grainA = A.Fake(); + private readonly IEventConsumerGrain grainB = A.Fake(); + private readonly MyEventConsumerManagerGrain sut; + + public EventConsumerManagerGrainTests() + { + var grainRuntime = A.Fake(); + var grainFactory = A.Fake(); + + A.CallTo(() => grainFactory.GetGrain("a", null)).Returns(grainA); + A.CallTo(() => grainFactory.GetGrain("b", null)).Returns(grainB); + A.CallTo(() => grainRuntime.GrainFactory).Returns(grainFactory); + + A.CallTo(() => consumerA.Name).Returns("a"); + A.CallTo(() => consumerA.EventsFilter).Returns("^a-"); + + A.CallTo(() => consumerB.Name).Returns("b"); + A.CallTo(() => consumerB.EventsFilter).Returns("^b-"); + + sut = new MyEventConsumerManagerGrain(new[] { consumerA, consumerB }, A.Fake(), grainRuntime); + } + + [Fact] + public async Task Should_not_activate_all_grains_on_activate() + { + await sut.OnActivateAsync(); + + A.CallTo(() => grainA.ActivateAsync()) + .MustNotHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_activate_all_grains_on_reminder() + { + await sut.ReceiveReminder(null, default(TickStatus)); + + A.CallTo(() => grainA.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_activate_all_grains_on_wakeup_with_null() + { + await sut.ActivateAsync(null); + + A.CallTo(() => grainA.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_activate_matching_grains_when_stream_name_defined() + { + await sut.ActivateAsync("a-123"); + + A.CallTo(() => grainA.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.ActivateAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_start_matching_grain() + { + await sut.StartAsync("a"); + + A.CallTo(() => grainA.StartAsync()) + .MustHaveHappened(); + + A.CallTo(() => grainB.StartAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_stop_matching_grain() + { + await sut.StopAsync("b"); + + A.CallTo(() => grainA.StopAsync()) + .MustNotHaveHappened(); + + A.CallTo(() => grainB.StopAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_reset_matching_grain() + { + await sut.ResetAsync("b"); + + A.CallTo(() => grainA.ResetAsync()) + .MustNotHaveHappened(); + + A.CallTo(() => grainB.ResetAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_fetch_infos_from_all_grains() + { + A.CallTo(() => grainA.GetStateAsync()) + .Returns(new Immutable( + new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" })); + + A.CallTo(() => grainB.GetStateAsync()) + .Returns(new Immutable( + new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" })); + + var infos = await sut.GetConsumersAsync(); + + infos.Value.ShouldBeEquivalentTo( + new List + { + new EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" }, + new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" } + }); + } + } +} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs deleted file mode 100644 index 88571afab..000000000 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using FluentAssertions; -using Squidex.Infrastructure.EventSourcing.Grains.Messages; -using Squidex.Infrastructure.States; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public class EventConsumerManagerTests - { - private readonly EventConsumerGrain actor1 = A.Fake(); - private readonly EventConsumerGrain actor2 = A.Fake(); - private readonly IStateFactory factory = A.Fake(); - private readonly IEventConsumer consumer1 = A.Fake(); - private readonly IEventConsumer consumer2 = A.Fake(); - private readonly IPubSub pubSub = new InMemoryPubSub(); - private readonly string consumerName1 = "Consumer1"; - private readonly string consumerName2 = "Consumer2"; - private readonly EventConsumerGrainManager sut; - - public EventConsumerManagerTests() - { - A.CallTo(() => consumer1.Name).Returns(consumerName1); - A.CallTo(() => consumer2.Name).Returns(consumerName2); - - A.CallTo(() => factory.CreateAsync(consumerName1)).Returns(actor1); - A.CallTo(() => factory.CreateAsync(consumerName2)).Returns(actor2); - - sut = new EventConsumerGrainManager(new IEventConsumer[] { consumer1, consumer2 }, pubSub, factory); - } - - [Fact] - public void Should_activate_all_actors() - { - sut.Run(); - - A.CallTo(() => actor1.Activate(consumer1)) - .MustHaveHappened(); - - A.CallTo(() => actor2.Activate(consumer2)) - .MustHaveHappened(); - } - - [Fact] - public void Should_start_correct_actor() - { - sut.Run(); - - pubSub.Publish(new StartConsumerMessage { ConsumerName = consumerName1 }, true); - - A.CallTo(() => actor1.Start()) - .MustHaveHappened(); - - A.CallTo(() => actor2.Start()) - .MustNotHaveHappened(); - } - - [Fact] - public void Should_stop_correct_actor() - { - sut.Run(); - - pubSub.Publish(new StopConsumerMessage { ConsumerName = consumerName1 }, true); - - A.CallTo(() => actor1.Stop()) - .MustHaveHappened(); - - A.CallTo(() => actor2.Stop()) - .MustNotHaveHappened(); - } - - [Fact] - public void Should_reset_correct_actor() - { - sut.Run(); - - pubSub.Publish(new ResetConsumerMessage { ConsumerName = consumerName2 }, true); - - A.CallTo(() => actor1.Reset()) - .MustNotHaveHappened(); - - A.CallTo(() => actor2.Reset()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_get_state_from_all_actors() - { - sut.Run(); - - A.CallTo(() => actor1.GetState()) - .Returns(new EventConsumerInfo { Name = consumerName1, Position = "123 " }); - - A.CallTo(() => actor2.GetState()) - .Returns(new EventConsumerInfo { Name = consumerName2, Position = "345 " }); - - var response = await pubSub.RequestAsync(new GetStatesRequest(), TimeSpan.FromSeconds(5), true); - - response.States.ShouldAllBeEquivalentTo(new EventConsumerInfo[] - { - new EventConsumerInfo { Name = consumerName1, Position = "123 " }, - new EventConsumerInfo { Name = consumerName2, Position = "345 " } - }); - } - - [Fact] - public void Should_not_dispose_actors() - { - sut.Dispose(); - } - } -} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs new file mode 100644 index 000000000..536e8829f --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs @@ -0,0 +1,39 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschränkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Orleans; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing.Grains +{ + public class OrleansEventNotifierTests + { + private readonly IEventConsumerManagerGrain manager = A.Fake(); + private readonly OrleansEventNotifier sut; + + public OrleansEventNotifierTests() + { + var factory = A.Fake(); + + A.CallTo(() => factory.GetGrain("Default", null)) + .Returns(manager); + + sut = new OrleansEventNotifier(factory); + } + + [Fact] + public void Should_wakeup_manager_with_stream_name() + { + sut.Initialize(); + sut.NotifyEventsStored("my-stream"); + + A.CallTo(() => manager.ActivateAsync("my-stream")) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs index 19baac3de..8f2e5ac6f 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs @@ -16,14 +16,13 @@ namespace Squidex.Infrastructure.EventSourcing public class PollingSubscriptionTests { 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 PollingSubscription(eventStore, eventSubscriber, "^my-stream", position); await WaitAndStopAsync(sut); @@ -39,7 +38,7 @@ namespace Squidex.Infrastructure.EventSourcing A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) .Throws(ex); - var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); + var sut = new PollingSubscription(eventStore, eventSubscriber, "^my-stream", position); await WaitAndStopAsync(sut); @@ -55,7 +54,7 @@ namespace Squidex.Infrastructure.EventSourcing A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) .Throws(ex); - var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); + var sut = new PollingSubscription(eventStore, eventSubscriber, "^my-stream", position); await WaitAndStopAsync(sut); @@ -71,7 +70,7 @@ namespace Squidex.Infrastructure.EventSourcing A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) .Throws(ex); - var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); + var sut = new PollingSubscription(eventStore, eventSubscriber, "^my-stream", position); await WaitAndStopAsync(sut); @@ -80,24 +79,11 @@ namespace Squidex.Infrastructure.EventSourcing } [Fact] - public async Task Should_not_subscribe_on_notify_when_stream_matches() + public async Task Should_wake_up() { - var sut = new PollingSubscription(eventStore, eventNotifier, eventSubscriber, "^my-stream", position); + var sut = new PollingSubscription(eventStore, eventSubscriber, "^my-stream", position); - eventNotifier.NotifyEventsStored("other-stream-123"); - - await WaitAndStopAsync(sut); - - A.CallTo(() => eventStore.QueryAsync(A>.Ignored, "^my-stream", position, A.Ignored)) - .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); - - eventNotifier.NotifyEventsStored("my-stream-123"); + sut.WakeUp(); await WaitAndStopAsync(sut); diff --git a/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs b/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs index bbb285bf8..74a7eca02 100644 --- a/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs +++ b/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs @@ -44,7 +44,7 @@ namespace Squidex.Infrastructure.EventSourcing { await OnErrorAsync(eventSubscription, new InvalidOperationException()); - await Task.Delay(200); + await Task.Delay(400); await sut.StopAsync(); diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs new file mode 100644 index 000000000..153a9d276 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// EventConsumerBootstrapTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System.Threading; +using System.Threading.Tasks; +using FakeItEasy; +using Orleans; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + public sealed class BootstrapTests + { + private readonly IBackgroundGrain grain = A.Fake(); + private readonly Bootstrap sut; + + public BootstrapTests() + { + var factory = A.Fake(); + + sut = new Bootstrap(factory); + + A.CallTo(() => factory.GetGrain("Default", null)) + .Returns(grain); + } + + [Fact] + public async Task Should_activate_grain_on_run() + { + await sut.Execute(CancellationToken.None); + + A.CallTo(() => grain.ActivateAsync()) + .MustHaveHappened(); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs b/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs new file mode 100644 index 000000000..236e0c25b --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs @@ -0,0 +1,80 @@ +// ========================================================================== +// JsonExternalSerializerTests.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using FakeItEasy; +using Orleans.Serialization; +using Xunit; + +namespace Squidex.Infrastructure.Orleans +{ + public class JsonExternalSerializerTests + { + [Fact] + public void Should_not_copy_null() + { + var v = (string)null; + var c = J.Copy(v, null); + + Assert.Null(c); + } + + [Fact] + public void Should_copy_null_json() + { + var v = new J>(null); + var c = (J>)J.Copy(v, null); + + Assert.Null(c.Value); + } + + [Fact] + public void Should_not_copy_immutable_values() + { + var v = new List { 1, 2, 3 }.AsJ(); + var c = (J>)J.Copy(v, null); + + Assert.Same(v.Value, c.Value); + } + + [Fact] + public void Should_serialize_and_deserialize_value() + { + var value = new J>(new List { 1, 2, 3 }); + + var writtenLength = 0; + var writtenBuffer = (byte[])null; + + var writer = A.Fake(); + var writerContext = new SerializationContext(null) { StreamWriter = writer }; + + A.CallTo(() => writer.Write(A.Ignored)) + .Invokes(new Action(x => writtenLength = x)); + + A.CallTo(() => writer.Write(A.Ignored)) + .Invokes(new Action(x => writtenBuffer = x)); + + J.Serialize(value, writerContext, value.GetType()); + + var reader = A.Fake(); + var readerContext = new DeserializationContext(null) { StreamReader = reader }; + + A.CallTo(() => reader.ReadInt()) + .Returns(writtenLength); + + A.CallTo(() => reader.ReadBytes(writtenLength)) + .Returns(writtenBuffer); + + var copy = (J>)J.Deserialize(value.GetType(), readerContext); + + Assert.Equal(value.Value, copy.Value); + Assert.NotSame(value.Value, copy.Value); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 19934fba9..4337579b8 100644 --- a/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -11,12 +11,12 @@ - + - - - + + + diff --git a/tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs b/tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs deleted file mode 100644 index 8e540cf17..000000000 --- a/tests/Squidex.Infrastructure.Tests/States/StateFactoryTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschränkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System; -using System.Threading.Tasks; -using FakeItEasy; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; -using Squidex.Infrastructure.Tasks; -using Xunit; - -#pragma warning disable RECS0002 // Convert anonymous method to method group - -namespace Squidex.Infrastructure.States -{ - public class StateFactoryTests : IDisposable - { - private class MyStatefulObject : IStatefulObject - { - public Task ActivateAsync(string key) - { - return TaskHelper.Done; - } - } - - private readonly string key = Guid.NewGuid().ToString(); - private readonly MyStatefulObject statefulObject = new MyStatefulObject(); - private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions())); - private readonly IPubSub pubSub = new InMemoryPubSub(true); - private readonly IServiceProvider services = A.Fake(); - private readonly StateFactory sut; - - public StateFactoryTests() - { - A.CallTo(() => services.GetService(typeof(MyStatefulObject))) - .Returns(statefulObject); - - sut = new StateFactory(pubSub, cache, services); - sut.Initialize(); - } - - public void Dispose() - { - sut.Dispose(); - } - - [Fact] - public async Task Should_provide_state_from_services_and_add_to_cache() - { - var actualObject = await sut.GetSingleAsync(key); - - Assert.Same(statefulObject, actualObject); - Assert.NotNull(cache.Get(key)); - } - - [Fact] - public async Task Should_serve_next_request_from_cache() - { - var actualObject1 = await sut.GetSingleAsync(key); - - Assert.Same(statefulObject, actualObject1); - Assert.NotNull(cache.Get(key)); - - var actualObject2 = await sut.GetSingleAsync(key); - - A.CallTo(() => services.GetService(typeof(MyStatefulObject))) - .MustHaveHappened(Repeated.Exactly.Once); - } - - [Fact] - public async Task Should_not_serve_next_request_from_cache_when_detached() - { - var actualObject1 = await sut.CreateAsync(key); - - Assert.Same(statefulObject, actualObject1); - Assert.Null(cache.Get(key)); - - var actualObject2 = await sut.CreateAsync(key); - - A.CallTo(() => services.GetService(typeof(MyStatefulObject))) - .MustHaveHappened(Repeated.Exactly.Twice); - } - - [Fact] - public async Task Should_remove_from_cache_when_invalidation_message_received() - { - var actualObject = await sut.GetSingleAsync(key); - - await InvalidateCacheAsync(); - - Assert.False(cache.TryGetValue(key, out var t)); - } - - [Fact] - public async Task Should_remove_from_cache_when_method_called() - { - var actualObject = await sut.GetSingleAsync(key); - - sut.Remove(key); - - Assert.False(cache.TryGetValue(key, out var t)); - } - - [Fact] - public void Should_send_invalidation_message_on_refresh() - { - InvalidateMessage message = null; - - pubSub.Subscribe(m => - { - message = m; - }); - - sut.Synchronize(key); - - Assert.NotNull(message); - Assert.Equal(key, message.Key); - } - - private async Task RemoveFromCacheAsync() - { - cache.Remove(key); - - await Task.Delay(400); - } - - private async Task InvalidateCacheAsync() - { - pubSub.Publish(new InvalidateMessage { Key = key }, true); - - await Task.Delay(400); - } - } -} \ No newline at end of file diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs b/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs new file mode 100644 index 000000000..8253783e3 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLockPoolTests + { + [Fact] + public async Task Should_lock() + { + var sut = new AsyncLockPool(1); + + var value = 0; + + await Task.WhenAll( + Enumerable.Repeat(0, 100).Select(x => new Func(async () => + { + using (await sut.LockAsync(1)) + { + await Task.Yield(); + + value++; + } + })())); + + Assert.Equal(100, value); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs b/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs new file mode 100644 index 000000000..58765fb66 --- /dev/null +++ b/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs @@ -0,0 +1,38 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLockTests + { + [Fact] + public async Task Should_lock() + { + var sut = new AsyncLock(); + + var value = 0; + + await Task.WhenAll( + Enumerable.Repeat(0, 100).Select(x => new Func(async () => + { + using (await sut.LockAsync()) + { + await Task.Yield(); + + value++; + } + })())); + + Assert.Equal(100, value); + } + } +} diff --git a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs index 4a50e4744..4991571e4 100644 --- a/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs +++ b/tests/Squidex.Infrastructure.Tests/TestHelpers/MyGrain.cs @@ -19,7 +19,7 @@ namespace Squidex.Infrastructure.TestHelpers { } - public override Task ExecuteAsync(IAggregateCommand command) + protected override Task ExecuteAsync(IAggregateCommand command) { return Task.FromResult(null); } diff --git a/tools/Migrate_01/Migrations/AddPatterns.cs b/tools/Migrate_01/Migrations/AddPatterns.cs index 8b1724c92..f07b2d20f 100644 --- a/tools/Migrate_01/Migrations/AddPatterns.cs +++ b/tools/Migrate_01/Migrations/AddPatterns.cs @@ -7,25 +7,25 @@ using System; using System.Threading.Tasks; +using Orleans; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.States; namespace Migrate_01.Migrations { public sealed class AddPatterns : IMigration { private readonly InitialPatterns initialPatterns; - private readonly IStateFactory stateFactory; + private readonly IGrainFactory grainFactory; private readonly IAppRepository appRepository; - public AddPatterns(InitialPatterns initialPatterns, IAppRepository appRepository, IStateFactory stateFactory) + public AddPatterns(InitialPatterns initialPatterns, IAppRepository appRepository, IGrainFactory grainFactory) { this.initialPatterns = initialPatterns; this.appRepository = appRepository; - this.stateFactory = stateFactory; + this.grainFactory = grainFactory; } public async Task UpdateAsync() @@ -34,17 +34,19 @@ namespace Migrate_01.Migrations foreach (var id in ids) { - var app = await stateFactory.GetSingleAsync(id); + var app = grainFactory.GetGrain(id); - if (app.Snapshot.Patterns.Count == 0) + var state = await app.GetStateAsync(); + + if (state.Value.Patterns.Count == 0) { foreach (var pattern in initialPatterns.Values) { var command = new AddPattern { - Actor = app.Snapshot.CreatedBy, - AppId = app.Snapshot.Id, + Actor = state.Value.CreatedBy, + AppId = state.Value.Id, Name = pattern.Name, PatternId = Guid.NewGuid(), Pattern = pattern.Pattern, @@ -53,8 +55,10 @@ namespace Migrate_01.Migrations await app.ExecuteAsync(command); } + + await app.WriteSnapshotAsync(); } } } } -} +} \ No newline at end of file diff --git a/tools/Migrate_01/Rebuilder.cs b/tools/Migrate_01/Rebuilder.cs index af94ccfa6..7d27b1861 100644 --- a/tools/Migrate_01/Rebuilder.cs +++ b/tools/Migrate_01/Rebuilder.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Orleans; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps.State; @@ -39,7 +40,7 @@ namespace Migrate_01 private readonly ISnapshotStore snapshotContentStore; private readonly ISnapshotStore snapshotRuleStore; private readonly ISnapshotStore snapshotSchemaStore; - private readonly IStateFactory stateFactory; + private readonly IGrainFactory grainFactory; public Rebuilder( FieldRegistry fieldRegistry, @@ -50,7 +51,7 @@ namespace Migrate_01 ISnapshotStore snapshotAssetStore, ISnapshotStore snapshotRuleStore, ISnapshotStore snapshotSchemaStore, - IStateFactory stateFactory) + IGrainFactory grainFactory) { this.fieldRegistry = fieldRegistry; this.eventDataFormatter = eventDataFormatter; @@ -60,7 +61,7 @@ namespace Migrate_01 this.snapshotContentStore = snapshotContentStore; this.snapshotRuleStore = snapshotRuleStore; this.snapshotSchemaStore = snapshotSchemaStore; - this.stateFactory = stateFactory; + this.grainFactory = grainFactory; } public async Task RebuildAssetsAsync() @@ -79,9 +80,7 @@ namespace Migrate_01 { if (@event.Payload is AssetEvent assetEvent && handledIds.Add(assetEvent.AssetId)) { - var asset = await stateFactory.CreateAsync(assetEvent.AssetId); - - asset.ApplySnapshot(asset.Snapshot.Apply(@event)); + var asset = grainFactory.GetGrain(assetEvent.AssetId); await asset.WriteSnapshotAsync(); } @@ -107,19 +106,19 @@ namespace Migrate_01 { if (@event.Payload is SchemaEvent schemaEvent && handledIds.Add(schemaEvent.SchemaId.Id)) { - var schema = await stateFactory.GetSingleAsync(schemaEvent.SchemaId.Id); + var schema = grainFactory.GetGrain(schemaEvent.SchemaId.Id); await schema.WriteSnapshotAsync(); } else if (@event.Payload is RuleEvent ruleEvent && handledIds.Add(ruleEvent.RuleId)) { - var rule = await stateFactory.GetSingleAsync(ruleEvent.RuleId); + var rule = grainFactory.GetGrain(ruleEvent.RuleId); await rule.WriteSnapshotAsync(); } else if (@event.Payload is AppEvent appEvent && handledIds.Add(appEvent.AppId.Id)) { - var app = await stateFactory.GetSingleAsync(appEvent.AppId.Id); + var app = grainFactory.GetGrain(appEvent.AppId.Id); await app.WriteSnapshotAsync(); } @@ -176,4 +175,4 @@ namespace Migrate_01 } } } -} +} \ No newline at end of file