From 00c53719487a4d064ab6c7f6f607d9bd82fbfd20 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Mon, 11 Jul 2022 13:47:03 +0200 Subject: [PATCH] Refactoring/no orleans (#894) * Temp * Temp. * Rename messages. * Some progress. * Some progress. * Some fixes in registration of dependencies. * Infrastructure tests * More tests. * More tests. * Naming fixes. * Compile fix * More fixes. * Tests green :) * More tests and distributed cache. * Compile and bug fixes. * Fix recreation. * Fix tests and better cancellation token flow. * Flow the cancellation token. * Compile fix. * Build fix. * Fixes and better tests. * Fix threading. * File was not saved. * More cancellation tokens. * Fix method name. * Fix tests. * Update yml. * Better check. * Increase time for text indexer. --- .../Actions/Comment/CommentActionHandler.cs | 2 +- .../CreateContentActionHandler.cs | 2 +- .../Notification/NotificationActionHandler.cs | 2 +- .../Assets/Azure/AzureMetadataSource.cs | 5 +- .../DoubleLinkedContentMiddleware.cs | 23 +- backend/i18n/frontend_en.json | 3 + backend/i18n/frontend_it.json | 3 + backend/i18n/frontend_nl.json | 3 + backend/i18n/frontend_zh.json | 3 + backend/i18n/source/frontend_en.json | 3 + backend/src/Migrations/MigrationPath.cs | 4 - .../Migrations/StartEventConsumers.cs | 30 -- .../Migrations/StopEventConsumers.cs | 30 -- .../ConvertContent/ExcludeChangedTypes.cs | 8 +- .../HandleRules/RuleEventFormatter.cs | 10 +- .../HandleRules/RuleService.cs | 25 +- .../Tags/ITagService.cs | 24 +- .../ValidateContent/JsonValueConverter.cs | 8 +- .../ValidateContent/JsonValueValidator.cs | 8 +- .../ValidateContent/RootContext.cs | 6 +- .../Validators/FieldValidator.cs | 2 +- .../Apps/MongoAppRepository.cs | 43 +- .../Rules/MongoRuleRepository.cs | 16 +- .../Schemas/MongoSchemaRepository.cs | 45 +- .../AlwaysCreateClientCommandMiddleware.cs | 8 +- .../Apps/AppPermanentDeleter.cs | 15 +- .../Apps/AppUISettings.cs | 142 +++++- .../Apps/AppUISettingsGrain.cs | 120 ----- .../Apps/BackupApps.cs | 5 +- .../Diagnostics/OrleansAppsHealthCheck.cs | 37 -- .../Apps/DomainObject/AppCommandMiddleware.cs | 31 +- .../DomainObject/AppDomainObject.State.cs | 2 +- .../Apps/DomainObject/AppDomainObject.cs | 55 ++- .../Apps/DomainObject/AppDomainObjectGrain.cs | 27 - .../Apps/DomainObject/IAppGrain.cs | 16 - .../Apps/IAppUISettings.cs | 12 +- .../Apps/Indexes/AppsCacheGrain.cs | 108 ---- .../Apps/Indexes/AppsIndex.cs | 154 +++--- .../Apps/Indexes/IAppsCacheGrain.cs | 21 - .../Apps/Indexes/IAppsIndex.cs | 3 - .../Invitation/InviteUserCommandMiddleware.cs | 11 +- .../Apps/Plans/IAppPlanBillingManager.cs | 6 +- .../Apps/Plans/IUsageNotifierGrain.cs | 18 - .../{UsageNotification.cs => Messages.cs} | 14 +- .../Apps/Plans/NoopAppPlanBillingManager.cs | 6 +- .../Plans/RestrictAppsCommandMiddleware.cs | 12 +- .../Apps/Plans/UsageGate.cs | 57 +-- ...otifierGrain.cs => UsageNotifierWorker.cs} | 32 +- .../Apps/Repositories/IAppRepository.cs | 7 +- .../Templates/TemplateCommandMiddleware.cs | 5 +- .../Assets/AssetChangedTriggerHandler.cs | 3 +- ...CleanupGrain.cs => AssetCleanupProcess.cs} | 26 +- .../Assets/AssetTagsDeleter.cs | 2 +- .../DomainObject/AssetCommandMiddleware.cs | 67 +-- .../DomainObject/AssetDomainObject.State.cs | 2 +- .../Assets/DomainObject/AssetDomainObject.cs | 42 +- .../DomainObject/AssetDomainObjectGrain.cs | 40 -- .../DomainObject/AssetFolderDomainObject.cs | 21 +- .../AssetFolderDomainObjectGrain.cs | 39 -- .../AssetsBulkUpdateCommandMiddleware.cs | 26 +- .../Assets/DomainObject/IAssetGrain.cs | 17 - .../Assets/FileTagAssetMetadataSource.cs | 3 +- .../Assets/FileTypeAssetMetadataSource.cs | 3 +- .../Assets/IAssetLoader.cs | 5 +- .../Assets/IAssetMetadataSource.cs | 3 +- .../Assets/ImageAssetMetadataSource.cs | 9 +- .../Assets/Queries/AssetEnricher.cs | 15 +- .../Assets/Queries/AssetLoader.cs | 52 +- .../Assets/Queries/AssetQueryParser.cs | 18 +- .../Assets/Queries/AssetQueryService.cs | 2 +- .../Assets/Queries/FilterTagTransformer.cs | 9 +- .../Assets/RecursiveDeleter.cs | 2 +- .../Assets/SvgAssetMetadataSource.cs | 3 +- .../Backup/BackupGrain.cs | 285 ----------- .../Backup/BackupProcessor.cs | 305 ++++++++++++ .../Backup/BackupReader.cs | 6 +- .../Backup/BackupService.cs | 69 ++- .../Backup/BackupWorker.cs | 90 ++++ .../Backup/IBackupArchiveLocation.cs | 6 +- .../Backup/IBackupReader.cs | 4 +- .../Backup/IBackupService.cs | 6 +- .../Backup/{IRestoreGrain.cs => Messages.cs} | 15 +- .../Backup/RestoreGrain.cs | 435 ---------------- .../Backup/RestoreProcessor.cs | 465 ++++++++++++++++++ .../Backup/State/BackupRestoreState.cs | 2 +- .../Backup/State/BackupState.cs | 16 + .../Backup/State/RestoreJob.cs | 9 + .../Backup/TempFolderBackupArchiveLocation.cs | 22 +- .../Comments/Commands/CommentsCommand.cs | 5 +- .../Comments/CommentsLoader.cs | 17 +- .../DomainObject/CommentsCommandMiddleware.cs | 26 +- .../{CommentsGrain.cs => CommentsStream.cs} | 45 +- .../Comments/DomainObject/ICommentsGrain.cs | 21 - .../Comments/GrainWatchingService.cs | 35 -- .../Comments/ICommentsLoader.cs | 5 +- .../Comments/IWatchingGrain.cs | 16 - .../Comments/IWatchingService.cs | 3 +- .../Comments/WatchingGrain.cs | 86 ---- .../Comments/WatchingService.cs | 62 +++ .../Contents/ContentChangedTriggerHandler.cs | 6 +- ...lerGrain.cs => ContentSchedulerProcess.cs} | 49 +- .../Contents/Counter/CounterDeleter.cs | 30 -- .../Contents/Counter/CounterGrain.cs | 53 -- .../Contents/Counter/CounterJintExtension.cs | 23 +- .../Contents/Counter/CounterService.cs | 77 +++ .../{ICounterGrain.cs => ICounterService.cs} | 12 +- .../DomainObject/ContentCommandMiddleware.cs | 18 +- .../DomainObject/ContentDomainObject.State.cs | 2 +- .../DomainObject/ContentDomainObject.cs | 51 +- .../DomainObject/ContentDomainObjectGrain.cs | 31 -- .../ContentsBulkUpdateCommandMiddleware.cs | 29 +- .../Contents/DomainObject/IContentGrain.cs | 17 - .../GraphQL/Types/Contents/ContentActions.cs | 3 +- .../Contents/IContentLoader.cs | 5 +- .../Contents/IContentSchedulerGrain.cs | 15 - .../Contents/Queries/ContentLoader.cs | 52 +- .../Contents/Queries/ContentQueryParser.cs | 8 +- .../Contents/Queries/Steps/CalculateTokens.cs | 8 +- .../Contents/Queries/Steps/ConvertData.cs | 6 +- .../Contents/SingletonCommandMiddleware.cs | 8 +- .../Contents/Text/Extensions.cs | 4 +- .../Contents/Text/TextIndexingProcess.cs | 21 +- .../History/NotifoService.cs | 11 +- .../Rules/DomainObject/IRuleGrain.cs | 16 - .../DomainObject/RuleDomainObject.State.cs | 2 +- .../Rules/DomainObject/RuleDomainObject.cs | 23 +- .../DomainObject/RuleDomainObjectGrain.cs | 27 - .../Rules/IRuleDequeuerGrain.cs | 15 - .../Rules/Indexes/RulesCacheGrain.cs | 56 --- .../Rules/Indexes/RulesIndex.cs | 73 +-- .../Rules/Repositories/IRuleRepository.cs | 2 +- .../Rules/RuleCommandMiddleware.cs | 16 +- ...DequeuerGrain.cs => RuleDequeuerWorker.cs} | 56 +-- .../Rules/Runner/DefaultRuleRunnerService.cs | 41 +- .../Messages.cs} | 15 +- .../Rules/Runner/RuleRunnerGrain.cs | 292 ----------- .../Rules/Runner/RuleRunnerProcessor.cs | 288 +++++++++++ ...IRuleRunnerGrain.cs => RuleRunnerState.cs} | 13 +- .../Rules/Runner/RuleRunnerWorker.cs | 79 +++ .../Rules/UsageTracking/IUsageTrackerGrain.cs | 21 - .../Rules/UsageTracking/Messages.cs | 25 + .../UsageTrackerCommandMiddleware.cs | 46 +- ...eTrackerGrain.cs => UsageTrackerWorker.cs} | 78 +-- .../Schemas/DomainObject/ISchemaGrain.cs | 16 - .../DomainObject/SchemaDomainObject.cs | 43 +- .../DomainObject/SchemaDomainObjectGrain.cs | 27 - .../Schemas/Indexes/ISchemasCacheGrain.cs | 23 - .../Schemas/Indexes/SchemasCacheGrain.cs | 101 ---- .../Schemas/Indexes/SchemasIndex.cs | 160 +++--- .../Schemas/Repositories/ISchemaRepository.cs | 8 +- .../Squidex.Domain.Apps.Entities.csproj | 5 - .../Tags/GrainTagService.cs | 79 --- .../Tags/ITagGrain.cs | 31 -- .../Tags/TagGrain.cs | 212 -------- .../Tags/TagService.cs | 278 +++++++++++ .../GetEventStoreSubscription.cs | 2 +- .../Caching/MongoCacheEntry.cs} | 18 +- .../Caching/MongoDistributedCache.cs | 109 ++++ .../MongoEventStoreSubscription.cs | 2 +- .../EventSourcing/MongoEventStore_Writer.cs | 8 +- .../States/MongoSnapshotStore.cs | 4 +- .../States/MongoSnapshotStoreBase.cs | 10 +- .../CQRS/Events/RabbitMqEventConsumer.cs | 8 +- .../Squidex.Infrastructure.RabbitMq.csproj | 2 +- .../Commands/AggregateCommandMiddleware.cs | 60 +++ .../Commands/CachingDomainObjectMiddleware.cs | 40 ++ .../Commands/CommandExtensions.cs | 7 +- .../Commands/CustomCommandMiddlewareRunner.cs | 7 +- .../Commands/DefaultDomainObjectCache.cs | 92 ++++ .../Commands/DomainObject.Execute.cs | 139 +++--- .../Commands/DomainObject.cs | 236 ++++----- .../Commands/DomainObjectGrain.cs | 38 -- .../Commands/DomainObjectGrainFormatter.cs | 39 -- .../EnrichWithTimestampCommandMiddleware.cs | 14 +- .../Commands/GrainCommandMiddleware.cs | 52 -- .../{IDomainObjectGrain.cs => IAggregate.cs} | 7 +- .../Commands/IAggregateCommand.cs | 2 +- .../Commands/ICommandBus.cs | 3 +- .../Commands/ICommandMiddleware.cs | 5 +- .../Commands/IDomainObjectCache.cs} | 11 +- .../Commands/IDomainState.cs | 2 - .../Commands/InMemoryCommandBus.cs | 49 +- .../Commands/LogCommandMiddleware.cs | 5 +- .../Commands/ReadonlyCommandMiddleware.cs | 7 +- .../Commands/Rebuilder.cs | 2 +- .../Commands/SnapshotList.cs | 123 ----- .../Diagnostics/OrleansHealthCheck.cs | 35 -- .../DomainObjectConflictException.cs | 4 +- .../DomainObjectDeletedException.cs | 6 +- .../DomainObjectNotFoundException.cs | 6 +- .../DomainObjectVersionException.cs | 6 +- .../{Grains => Consume}/BatchSubscriber.cs | 84 +--- .../Consume/EventConsumerManager.cs | 78 +++ .../EventConsumerProcessor.cs} | 152 +++--- .../{Grains => Consume}/EventConsumerState.cs | 4 +- .../Consume/EventConsumerWorker.cs | 83 ++++ .../Consume/IEventConsumerManager.cs | 24 + .../EventSourcing/Consume/Messages.cs} | 19 +- .../DefaultEventConsumerFactory.cs | 31 -- .../EventSourcing/DefaultEventFormatter.cs | 1 - .../EventSourcing/EventCommit.cs | 4 +- .../EventSourcing/EventConsumerInfo.cs | 15 +- .../EventConsumersHealthCheck.cs | 73 --- .../Grains/EventConsumerManagerGrain.cs | 112 ----- .../Grains/IEventConsumerGrain.cs | 22 - .../Grains/IEventConsumerManagerGrain.cs | 29 -- .../Grains/OrleansEventNotifier.cs | 30 -- .../EventSourcing/IEventConsumerFactory.cs | 14 - .../EventSourcing/IEventNotifier.cs | 9 + .../EventSourcing/IEventSubscriber.cs | 4 +- .../EventSourcing/IEventSubscription.cs | 10 +- .../EventSourcing/PollingSubscription.cs | 2 +- .../EventSourcing/RetrySubscription.cs | 91 ++-- .../Squidex.Infrastructure/FileExtensions.cs | 5 + .../Orleans/ActivationLimit.cs | 72 --- .../Orleans/ActivationLimiter.cs | 70 --- .../Orleans/ActivationLimiterFilter.cs | 24 - .../Orleans/ActivityPropagationFilter.cs | 110 ----- .../Orleans/CultureFilter.cs | 39 -- .../Orleans/ExceptionWrapperFilter.cs | 35 -- .../Orleans/GrainBase.cs | 63 --- .../Orleans/GrainBootstrap.cs | 52 -- .../Orleans/GrainState.cs | 85 ---- .../Orleans/IActivationLimit.cs | 18 - .../Orleans/IActivationLimiter.cs | 16 - .../Orleans/IBackgroundGrain.cs | 16 - .../Orleans/IDeactivater.cs | 14 - .../Orleans/IGrainState.cs | 24 - .../Orleans/Indexes/IUniqueNameGrain.cs | 18 - .../Orleans/Indexes/UniqueNameGrain.cs | 48 -- .../Orleans/JsonSerializer.cs | 46 -- .../Orleans/LocalCacheFilter.cs | 37 -- .../Orleans/LoggingFilter.cs | 56 --- .../Orleans/OrleansWrapperException.cs | 50 -- .../Orleans/SingleGrain.cs | 14 - .../Orleans/StateFilter.cs | 58 --- .../Orleans/StreamReaderWrapper.cs | 86 ---- .../Orleans/StreamWriterWrapper.cs | 77 --- .../Queries/Json/QueryParser.cs | 8 +- .../Squidex.Infrastructure.csproj | 7 +- .../States/BatchContext.cs | 22 +- .../States/DefaultEventStreamNames.cs | 2 +- .../States/IPersistenceFactory.cs | 2 + .../States/ISnapshotStore.cs | 3 +- .../States/NameReservation.cs} | 8 +- .../States/NameReservationState.cs | 78 +++ .../States/Persistence.cs | 4 +- .../States/SimpleState.cs | 146 ++++++ .../Squidex.Infrastructure/States/Store.cs | 2 + .../States/StoreExtensions.cs | 5 +- .../Squidex.Infrastructure/Tasks/AsyncLock.cs | 55 +++ .../LimitedConcurrencyLevelTaskScheduler.cs | 132 +++++ .../Tasks/ReentrantScheduler.cs | 41 ++ .../Tasks/TaskExtensions.cs | 5 +- .../Timers/CompletionTimer.cs | 5 + .../UsageTracking/BackgroundUsageTracker.cs | 9 +- backend/src/Squidex.Shared/Permissions.cs | 3 - .../ETagCommandMiddleware.cs | 8 +- .../EnrichWithActorCommandMiddleware.cs | 7 +- .../EnrichWithAppIdCommandMiddleware.cs | 5 +- .../EnrichWithContentIdCommandMiddleware.cs | 5 +- .../EnrichWithSchemaIdCommandMiddleware.cs | 7 +- backend/src/Squidex.Web/Constants.cs | 4 - .../Squidex.Web/Pipeline/ApiCostsFilter.cs | 2 +- .../Squidex.Web/Pipeline/UsageMiddleware.cs | 9 +- backend/src/Squidex.Web/Resources.cs | 4 - .../Controllers/Apps/AppAssetsController.cs | 2 +- .../Controllers/Apps/AppClientsController.cs | 2 +- .../Apps/AppContributorsController.cs | 2 +- .../Apps/AppLanguagesController.cs | 2 +- .../Controllers/Apps/AppRolesController.cs | 2 +- .../Controllers/Apps/AppSettingsController.cs | 2 +- .../Apps/AppWorkflowsController.cs | 2 +- .../Api/Controllers/Apps/AppsController.cs | 6 +- .../Assets/AssetContentController.cs | 2 +- .../Assets/AssetFoldersController.cs | 6 +- .../Controllers/Assets/AssetsController.cs | 10 +- .../Backups/BackupContentController.cs | 6 +- .../Controllers/Backups/BackupsController.cs | 5 +- .../Backups/Models/BackupJobDto.cs | 1 - .../Comments/CommentsController.cs | 10 +- .../UserNotificationsController.cs | 6 +- .../Contents/ContentsController.cs | 10 +- .../EventConsumersController.cs | 25 +- .../Controllers/Plans/AppPlansController.cs | 2 +- .../Api/Controllers/Rules/Models/RulesDto.cs | 3 +- .../Api/Controllers/Rules/RulesController.cs | 8 +- .../Schemas/SchemaFieldsController.cs | 2 +- .../Controllers/Schemas/SchemasController.cs | 6 +- .../Statistics/UsagesController.cs | 10 +- .../Controllers/Users/Models/ResourcesDto.cs | 5 - .../Config/TokenStoreInitializer.cs | 26 +- ...rleansDashboardAuthenticationMiddleware.cs | 51 -- .../Squidex/Config/Domain/AssetServices.cs | 4 - .../Squidex/Config/Domain/BackupsServices.cs | 6 + .../Squidex/Config/Domain/CommandsServices.cs | 9 +- .../Squidex/Config/Domain/CommentsServices.cs | 3 - .../Squidex/Config/Domain/ContentsServices.cs | 8 +- .../Config/Domain/EventSourcingServices.cs | 15 +- .../Config/Domain/HealthCheckServices.cs | 7 +- .../Config/Domain/InfrastructureServices.cs | 15 +- .../Squidex/Config/Domain/LoggingServices.cs | 20 - .../Config/Domain/MigrationServices.cs | 6 - .../src/Squidex/Config/Domain/RuleServices.cs | 4 - .../Squidex/Config/Domain/StoreServices.cs | 5 + .../Config/Messaging/MessagingServices.cs | 89 ++++ backend/src/Squidex/Config/Orleans/Helper.cs | 45 -- .../Squidex/Config/Orleans/OrleansServices.cs | 195 -------- backend/src/Squidex/Program.cs | 18 +- backend/src/Squidex/Squidex.csproj | 10 +- backend/src/Squidex/Startup.cs | 10 +- backend/src/Squidex/appsettings.json | 31 +- .../HandleRules/EventEnricherTests.cs | 2 +- .../HandleRules/RuleServiceTests.cs | 7 +- .../Operations/Tags/TagNormalizerTests.cs | 9 +- .../AppProviderExtensionsTests.cs | 2 +- ...lwaysCreateClientCommandMiddlewareTests.cs | 4 +- .../Apps/AppEventDeleterTests.cs | 2 +- .../Apps/AppPermanentDeleterTests.cs | 16 +- .../Apps/AppUISettingsGrainTests.cs | 138 ------ .../Apps/AppUISettingsTests.cs | 174 +++++-- .../Apps/BackupAppsTests.cs | 11 +- .../Apps/DefaultAppLogStoreTests.cs | 4 +- .../DomainObject/AppCommandMiddlewareTests.cs | 15 +- .../Apps/DomainObject/AppDomainObjectTests.cs | 20 +- .../Guards/GuardAppClientsTests.cs | 6 +- .../Guards/GuardAppContributorsTests.cs | 6 +- .../Guards/GuardAppLanguagesTests.cs | 3 +- .../DomainObject/Guards/GuardAppRolesTests.cs | 9 +- .../Apps/DomainObject/Guards/GuardAppTests.cs | 3 +- .../Guards/GuardAppWorkflowTests.cs | 3 +- .../Apps/Indexes/AppsCacheGrainTests.cs | 153 ------ .../Apps/Indexes/AppsIndexTests.cs | 346 +++++-------- .../InvitationEventConsumerTests.cs | 2 +- .../InviteUserCommandMiddlewareTests.cs | 20 +- .../RestrictAppsCommandMiddlewareTests.cs | 36 +- .../Apps/Plans/UsageGateTests.cs | 47 +- ...ainTests.cs => UsageNotifierWorkerTest.cs} | 43 +- .../Assets/AssetChangedTriggerHandlerTests.cs | 12 +- ...inTests.cs => AssetCleanupProcessTests.cs} | 14 +- .../Assets/AssetPermanentDeleterTests.cs | 4 +- .../Assets/AssetTagsDeleterTests.cs | 2 +- .../Assets/BackupAssetsTests.cs | 18 +- .../AssetCommandMiddlewareTests.cs | 38 +- .../AssetDomainObjectGrainTests.cs | 44 -- .../DomainObject/AssetDomainObjectTests.cs | 4 +- .../AssetFolderDomainObjectGrainTests.cs | 44 -- .../AssetFolderDomainObjectTests.cs | 2 +- .../AssetsBulkUpdateCommandMiddlewareTests.cs | 18 +- .../Guards/GuardAssetFolderTests.cs | 2 +- .../DomainObject/Guards/GuardAssetTests.cs | 4 +- .../Assets/FileTagAssetMetadataSourceTests.cs | 10 +- .../FileTypeAssetMetadataSourceTests.cs | 6 +- .../Assets/ImageAssetMetadataSourceTests.cs | 30 +- .../Assets/MongoDb/AssetsQueryFixture.cs | 4 +- .../Assets/Queries/AssetEnricherTests.cs | 28 +- .../Assets/Queries/AssetLoaderTests.cs | 80 ++- .../Assets/Queries/AssetQueryParserTests.cs | 6 +- .../Assets/Queries/AssetQueryServiceTests.cs | 48 +- .../Assets/RecursiveDeleterTests.cs | 20 +- .../Assets/RepairFilesTests.cs | 8 +- .../Assets/SvgAssetMetadataSourceTests.cs | 8 +- .../Backup/BackupServiceTests.cs | 151 +++--- .../Comments/CommentsLoaderTests.cs | 27 +- .../CommentsCommandMiddlewareTests.cs | 48 +- ...tsGrainTests.cs => CommentsStreamTests.cs} | 42 +- .../Comments/GrainWatchingServiceTests.cs | 45 -- .../Comments/WatchingGrainTests.cs | 73 --- .../Comments/WatchingServiceTests.cs | 90 ++++ .../ContentChangedTriggerHandlerTests.cs | 22 +- ...sts.cs => ContentSchedulerProcessTests.cs} | 27 +- .../Contents/ContentsSearchSourceTests.cs | 4 +- .../Contents/Counter/CounterDeleterTests.cs | 50 -- .../Contents/Counter/CounterGrainTests.cs | 53 -- .../Counter/CounterJintExtensionTests.cs | 26 +- .../Contents/Counter/CounterServiceTests.cs | 77 +++ .../ContentCommandMiddlewareTests.cs | 18 +- .../ContentDomainObjectGrainTests.cs | 44 -- .../DomainObject/ContentDomainObjectTests.cs | 2 +- ...ontentsBulkUpdateCommandMiddlewareTests.cs | 58 ++- .../Contents/GraphQL/GraphQLMutationTests.cs | 167 ++++--- .../Contents/MongoDb/ContentsQueryFixture.cs | 4 +- .../Contents/Queries/CalculateTokensTests.cs | 4 +- .../Contents/Queries/ContentEnricherTests.cs | 4 +- .../Contents/Queries/ContentLoaderTests.cs | 73 ++- .../Queries/ContentQueryServiceTests.cs | 2 +- .../SingletonCommandMiddlewareTests.cs | 13 +- .../Text/CachingTextIndexerStateTests.cs | 2 +- .../RuleCommandMiddlewareTests.cs | 13 +- .../DomainObject/RuleDomainObjectTests.cs | 2 +- .../Rules/Indexes/RulesCacheGrainTests.cs | 103 ---- .../Rules/Indexes/RulesIndexTests.cs | 83 +--- ...ainTests.cs => RuleDequeuerWorkerTests.cs} | 13 +- .../DomainObject/SchemaDomainObjectTests.cs | 2 +- .../Schemas/Indexes/SchemasCacheGrainTests.cs | 121 ----- .../Schemas/Indexes/SchemasIndexTests.cs | 249 ++++------ .../Schemas/MongoDb/SchemasHashTests.cs | 12 +- .../Squidex.Domain.Apps.Entities.Tests.csproj | 1 + .../Tags/GrainTagServiceTests.cs | 112 ----- .../Tags/TagGrainTests.cs | 207 -------- .../Tags/TagServiceTests.cs | 211 ++++++++ .../TestHelpers/HandlerTestBase.cs | 12 +- .../TestHelpers/Mocks.cs | 30 +- .../Commands/CommandRequestTests.cs | 103 ---- .../CustomCommandMiddlewareRunnerTests.cs | 9 +- .../Commands/DefaultDomainObjectCacheTests.cs | 94 ++++ .../DefaultDomainObjectFactoryTests.cs | 60 +++ .../DomainObjectGrainFormatterTests.cs | 66 --- .../Commands/DomainObjectTests.cs | 307 +++++------- ...richWithTimestampCommandMiddlewareTests.cs | 9 +- .../Commands/InMemoryCommandBusTests.cs | 26 +- .../Commands/LogCommandMiddlewareTests.cs | 8 +- .../ReadonlyCommandMiddlewareTests.cs | 6 +- .../Consume/EventConsumerManagerTests.cs | 119 +++++ .../EventConsumerProcessorTests.cs} | 189 ++++--- .../EventConsumerStateTests.cs | 2 +- .../Consume/EventConsumerWorkerTests.cs | 100 ++++ .../DefaultEventConsumerFactoryTests.cs | 46 -- .../EventConsumersHealthCheckTests.cs | 119 ----- .../EventSourcing/EventStoreTests.cs | 12 +- .../Grains/EventConsumerManagerGrainTests.cs | 181 ------- .../Grains/OrleansEventNotifierTests.cs | 39 -- .../EventSourcing/MongoParallelInsertTests.cs | 340 +++---------- .../EventSourcing/PollingSubscriptionTests.cs | 2 +- .../EventSourcing/RetrySubscriptionTests.cs | 67 +-- .../Log/BackgroundRequestLogStoreTests.cs | 2 +- .../Migrations/MigratorTests.cs | 6 +- .../Orleans/ActivationLimiterFilterTests.cs | 80 --- .../Orleans/ActivationLimiterTests.cs | 94 ---- .../Orleans/ActivityPropagationTests.cs | 140 ------ .../Orleans/AsyncLocalTests.cs | 65 --- .../Orleans/BootstrapTests.cs | 72 --- .../Orleans/CultureFilterTests.cs | 102 ---- .../Orleans/ExceptionWrapperFilterTests.cs | 157 ------ .../Orleans/Indexes/UniqueNameGrainTests.cs | 64 --- .../Orleans/JsonExternalSerializationTests.cs | 107 ---- .../Orleans/JsonExternalSerializerTests.cs | 122 ----- .../Orleans/LoggingFilterTests.cs | 71 --- .../Squidex.Infrastructure.Tests.csproj | 5 - .../TestHelpers/BinaryFormatterHelper.cs | 11 +- .../TestHelpers/MyDomainObject.cs | 26 +- .../TestHelpers/TestState.cs | 102 ++++ .../ETagCommandMiddlewareTests.cs | 2 +- .../EnrichWithActorCommandMiddlewareTests.cs | 2 +- .../EnrichWithAppIdCommandMiddlewareTests.cs | 2 +- ...richWithContentIdCommandMiddlewareTests.cs | 2 +- ...nrichWithSchemaIdCommandMiddlewareTests.cs | 2 +- .../Pipeline/ApiCostsFilterTests.cs | 8 +- .../Pipeline/AppResolverTests.cs | 19 +- .../Pipeline/SchemaResolverTests.cs | 7 +- .../Pipeline/UsageMiddlewareTests.cs | 5 +- .../TestSuite.ApiTests/AssetTests.cs | 5 +- .../TestSuite.ApiTests/ContentQueryTests.cs | 175 +++---- .../TestSuite.ApiTests/ContentUpdateTests.cs | 117 ++++- .../TestSuite.ApiTests.csproj | 12 +- .../TestSuite.LoadTests.csproj | 6 +- .../TestSuite.Shared/TestSuite.Shared.csproj | 4 +- .../administration-area.component.html | 5 - .../features/administration/declarations.ts | 1 - .../src/app/features/administration/module.ts | 7 +- .../pages/cluster/cluster-page.component.html | 5 - .../pages/cluster/cluster-page.component.scss | 9 - .../pages/cluster/cluster-page.component.ts | 16 - .../event-consumers-page.component.ts | 2 +- .../state/event-consumers.state.ts | 6 + .../pages/rules/rules-page.component.html | 2 +- frontend/src/app/shared/state/ui.state.ts | 9 +- 467 files changed, 7220 insertions(+), 11061 deletions(-) delete mode 100644 backend/src/Migrations/Migrations/StartEventConsumers.cs delete mode 100644 backend/src/Migrations/Migrations/StopEventConsumers.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObjectGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/IAppGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsCacheGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IUsageNotifierGrain.cs rename backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/{UsageNotification.cs => Messages.cs} (58%) rename backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/{UsageNotifierGrain.cs => UsageNotifierWorker.cs} (73%) rename backend/src/Squidex.Domain.Apps.Entities/Assets/{AssetCleanupGrain.cs => AssetCleanupProcess.cs} (51%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObjectGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObjectGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWorker.cs rename backend/src/Squidex.Domain.Apps.Entities/Backup/{IRestoreGrain.cs => Messages.cs} (50%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs rename backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/{CommentsGrain.cs => CommentsStream.cs} (78%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/ICommentsGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/GrainWatchingService.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/{ContentSchedulerGrain.cs => ContentSchedulerProcess.cs} (68%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterDeleter.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs rename backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/{ICounterGrain.cs => ICounterService.cs} (60%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObjectGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/IContentGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/IRuleGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObjectGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs rename backend/src/Squidex.Domain.Apps.Entities/Rules/{RuleDequeuerGrain.cs => RuleDequeuerWorker.cs} (80%) rename backend/src/Squidex.Domain.Apps.Entities/Rules/{Indexes/IRulesCacheGrain.cs => Runner/Messages.cs} (53%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerProcessor.cs rename backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/{IRuleRunnerGrain.cs => RuleRunnerState.cs} (61%) create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerWorker.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/Messages.cs rename backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/{UsageTrackerGrain.cs => UsageTrackerWorker.cs} (62%) delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/ISchemaGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObjectGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasCacheGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs delete mode 100644 backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs create mode 100644 backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs rename backend/src/{Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs => Squidex.Infrastructure.MongoDb/Caching/MongoCacheEntry.cs} (55%) create mode 100644 backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/AggregateCommandMiddleware.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/CachingDomainObjectMiddleware.cs create mode 100644 backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs delete mode 100644 backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs rename backend/src/Squidex.Infrastructure/Commands/{IDomainObjectGrain.cs => IAggregate.cs} (85%) rename backend/src/{Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetFolderGrain.cs => Squidex.Infrastructure/Commands/IDomainObjectCache.cs} (57%) delete mode 100644 backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs delete mode 100644 backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs rename backend/src/Squidex.Infrastructure/EventSourcing/{Grains => Consume}/BatchSubscriber.cs (60%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerManager.cs rename backend/src/Squidex.Infrastructure/EventSourcing/{Grains/EventConsumerGrain.cs => Consume/EventConsumerProcessor.cs} (61%) rename backend/src/Squidex.Infrastructure/EventSourcing/{Grains => Consume}/EventConsumerState.cs (94%) create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs create mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Consume/IEventConsumerManager.cs rename backend/src/{Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs => Squidex.Infrastructure/EventSourcing/Consume/Messages.cs} (50%) delete mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventConsumerFactory.cs delete mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/EventConsumersHealthCheck.cs delete mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs delete mode 100644 backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumerFactory.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/ActivityPropagationFilter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/CultureFilter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/ExceptionWrapperFilter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/GrainState.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/IDeactivater.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/IGrainState.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/JsonSerializer.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/OrleansWrapperException.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/SingleGrain.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/StateFilter.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs delete mode 100644 backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs rename backend/src/{Squidex.Domain.Apps.Entities/Assets/IAssetCleanupGrain.cs => Squidex.Infrastructure/States/NameReservation.cs} (64%) create mode 100644 backend/src/Squidex.Infrastructure/States/NameReservationState.cs create mode 100644 backend/src/Squidex.Infrastructure/States/SimpleState.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/LimitedConcurrencyLevelTaskScheduler.cs create mode 100644 backend/src/Squidex.Infrastructure/Tasks/ReentrantScheduler.cs delete mode 100644 backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs create mode 100644 backend/src/Squidex/Config/Messaging/MessagingServices.cs delete mode 100644 backend/src/Squidex/Config/Orleans/Helper.cs delete mode 100644 backend/src/Squidex/Config/Orleans/OrleansServices.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsCacheGrainTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/{UsageNotifierGrainTests.cs => UsageNotifierWorkerTest.cs} (80%) rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/{AssetCleanupGrainTests.cs => AssetCleanupProcessTests.cs} (71%) delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectGrainTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectGrainTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/{CommentsGrainTests.cs => CommentsStreamTests.cs} (81%) delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/GrainWatchingServiceTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingGrainTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/{ContentSchedulerGrainTests.cs => ContentSchedulerProcessTests.cs} (88%) delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterDeleterTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterGrainTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterServiceTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectGrainTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesCacheGrainTests.cs rename backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/{RuleDequeuerGrainTests.cs => RuleDequeuerWorkerTests.cs} (94%) delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasCacheGrainTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs delete mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs create mode 100644 backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagServiceTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Commands/CommandRequestTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectFactoryTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs rename backend/tests/Squidex.Infrastructure.Tests/EventSourcing/{Grains/EventConsumerGrainTests.cs => Consume/EventConsumerProcessorTests.cs} (72%) rename backend/tests/Squidex.Infrastructure.Tests/EventSourcing/{Grains => Consume}/EventConsumerStateTests.cs (94%) create mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventConsumerFactoryTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventConsumersHealthCheckTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivityPropagationTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/CultureFilterTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/ExceptionWrapperFilterTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameGrainTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializationTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs delete mode 100644 backend/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs create mode 100644 backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestState.cs delete mode 100644 frontend/src/app/features/administration/pages/cluster/cluster-page.component.html delete mode 100644 frontend/src/app/features/administration/pages/cluster/cluster-page.component.scss delete mode 100644 frontend/src/app/features/administration/pages/cluster/cluster-page.component.ts diff --git a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs index e8374aff5..7a7f945b6 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Comment/CommentActionHandler.cs @@ -64,7 +64,7 @@ namespace Squidex.Extensions.Actions.Comment command.FromRule = true; - await commandBus.PublishAsync(command); + await commandBus.PublishAsync(command, ct); return Result.Success($"Commented: {job.Text}"); } diff --git a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs index 82790f3ac..965577813 100644 --- a/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/CreateContent/CreateContentActionHandler.cs @@ -76,7 +76,7 @@ namespace Squidex.Extensions.Actions.CreateContent command.FromRule = true; - await commandBus.PublishAsync(command); + await commandBus.PublishAsync(command, ct); return Result.Success($"Created to: {job.SchemaId.Name}"); } diff --git a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs index 158be043b..4c22643ba 100644 --- a/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs +++ b/backend/extensions/Squidex.Extensions/Actions/Notification/NotificationActionHandler.cs @@ -84,7 +84,7 @@ namespace Squidex.Extensions.Actions.Notification command.AppId = NoApp; command.FromRule = true; - await commandBus.PublishAsync(command); + await commandBus.PublishAsync(command, ct); return Result.Success($"Notified: {job.Text}"); } diff --git a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs index c8b1b5be8..bd6965d17 100644 --- a/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs +++ b/backend/extensions/Squidex.Extensions/Assets/Azure/AzureMetadataSource.cs @@ -47,7 +47,8 @@ namespace Squidex.Extensions.Assets.Azure this.log = log; } - public async Task EnhanceAsync(UploadAssetCommand command) + public async Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) { try { @@ -55,7 +56,7 @@ namespace Squidex.Extensions.Assets.Azure { await using (var stream = command.File.OpenRead()) { - var result = await client.AnalyzeImageInStreamAsync(stream, features); + var result = await client.AnalyzeImageInStreamAsync(stream, features, cancellationToken: ct); command.Tags ??= new HashSet(); diff --git a/backend/extensions/Squidex.Extensions/Samples/Middleware/DoubleLinkedContentMiddleware.cs b/backend/extensions/Squidex.Extensions/Samples/Middleware/DoubleLinkedContentMiddleware.cs index 0c52bb75d..c9973fd6a 100644 --- a/backend/extensions/Squidex.Extensions/Samples/Middleware/DoubleLinkedContentMiddleware.cs +++ b/backend/extensions/Squidex.Extensions/Samples/Middleware/DoubleLinkedContentMiddleware.cs @@ -23,9 +23,10 @@ namespace Squidex.Extensions.Samples.Middleware this.contentLoader = contentLoader; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { - await next(context); + await next(context, ct); if (context.Command is UpdateContent update && context.IsCompleted && update.SchemaId.Name == "source") { @@ -36,7 +37,8 @@ namespace Squidex.Extensions.Samples.Middleware await contentLoader.GetAsync( content.AppId.Id, content.Id, - content.Version - 1); + content.Version - 1, + ct); // The data might have been changed within the domain object. Therefore we do not use the data fro mthe command. var oldReferenceId = GetReference(contentPrevious?.Data); @@ -50,7 +52,7 @@ namespace Squidex.Extensions.Samples.Middleware if (oldReferenceId != null) { - var oldReferenced = await contentLoader.GetAsync(content.AppId.Id, DomainId.Create(oldReferenceId)); + var oldReferenced = await contentLoader.GetAsync(content.AppId.Id, DomainId.Create(oldReferenceId), ct: ct); if (oldReferenced != null) { @@ -59,13 +61,13 @@ namespace Squidex.Extensions.Samples.Middleware // Remove the reference from the old referenced content. data.Remove("referencing"); - await UpdateReferencing(context, oldReferenced, data); + await UpdateReferencing(context, oldReferenced, data, ct); } } if (newReferenceId != null) { - var newReferenced = await contentLoader.GetAsync(content.AppId.Id, DomainId.Create(newReferenceId)); + var newReferenced = await contentLoader.GetAsync(content.AppId.Id, DomainId.Create(newReferenceId), ct: ct); if (newReferenced != null) { @@ -77,14 +79,16 @@ namespace Squidex.Extensions.Samples.Middleware ["iv"] = JsonValue.Array(content.Id) }; - await UpdateReferencing(context, newReferenced, data); + await UpdateReferencing(context, newReferenced, data, ct); } } } } - private static async Task UpdateReferencing(CommandContext context, IContentEntity reference, ContentData data) + private static async Task UpdateReferencing(CommandContext context, IContentEntity reference, ContentData data, + CancellationToken ct) { + // Also set the expected version, otherwise it will be overriden with the version from the request. await context.CommandBus.PublishAsync(new UpdateContent { AppId = reference.AppId, @@ -93,9 +97,8 @@ namespace Squidex.Extensions.Samples.Middleware DoNotScript = true, DoNotValidate = true, Data = data, - // Also set the expected version, otherwise it will be overriden with the version from the request. ExpectedVersion = reference.Version - }); + }, ct); } private static string GetReference(ContentData data) diff --git a/backend/i18n/frontend_en.json b/backend/i18n/frontend_en.json index dccb7d272..381d9a828 100644 --- a/backend/i18n/frontend_en.json +++ b/backend/i18n/frontend_en.json @@ -582,11 +582,14 @@ "eventConsumers.position": "Position", "eventConsumers.refreshTooltip": "Refresh event consumers", "eventConsumers.reloaded": "Event Consumers reloaded.", + "eventConsumers.reset": "Event consumer will be reset soon", "eventConsumers.resetFailed": "Failed to reset event consumer. Please reload.", "eventConsumers.resetTooltip": "Reset Event Consumer", + "eventConsumers.started": "Event consumer will be started soon", "eventConsumers.startFailed": "Failed to start event consumer. Please reload.", "eventConsumers.startTooltip": "Start Event Consumer", "eventConsumers.stopFailed": "Failed to stop event consumer. Please reload.", + "eventConsumers.stopped": "Event consumer will be stopped soon", "eventConsumers.stopTooltip": "Stop Event Consumer", "features.loadFailed": "Failed to load features. Please reload.", "history.loadFailed": "Failed to load history. Please reload.", diff --git a/backend/i18n/frontend_it.json b/backend/i18n/frontend_it.json index 103deefba..ec12d2512 100644 --- a/backend/i18n/frontend_it.json +++ b/backend/i18n/frontend_it.json @@ -582,11 +582,14 @@ "eventConsumers.position": "Posizione", "eventConsumers.refreshTooltip": "Aggiorna gli eventi degli utenti", "eventConsumers.reloaded": "Eventi degli utenti aggiornati.", + "eventConsumers.reset": "Event consumer will be reset soon", "eventConsumers.resetFailed": "Non è stato possibile ripristinare gli eventi degli utenti. Per favore ricarica.", "eventConsumers.resetTooltip": "Ripristina gli event degli utenti", + "eventConsumers.started": "Event consumer will be started soon", "eventConsumers.startFailed": "Non è stato possibile far partire l'evento dell'utente. Per favore ricarica.", "eventConsumers.startTooltip": "Inizia l'evento utente", "eventConsumers.stopFailed": "Non è stato possibile fermare l'evento dell'utente. Per favore ricarica.", + "eventConsumers.stopped": "Event consumer will be stopped soon", "eventConsumers.stopTooltip": "Interrompi l'evento dell'utente", "features.loadFailed": "Non è stato possibile caricare le funzionalità. Per favore ricarica.", "history.loadFailed": "Non è stato possibile caricare la cronologia. Per favore ricarica.", diff --git a/backend/i18n/frontend_nl.json b/backend/i18n/frontend_nl.json index b48a11b5d..20e6d8b30 100644 --- a/backend/i18n/frontend_nl.json +++ b/backend/i18n/frontend_nl.json @@ -582,11 +582,14 @@ "eventConsumers.position": "Positie", "eventConsumers.refreshTooltip": "Ververs evenementgebruikers", "eventConsumers.reloaded": "Evenementconsumenten herladen.", + "eventConsumers.reset": "Event consumer will be reset soon", "eventConsumers.resetFailed": "Kan gebeurtenisgebruiker niet resetten. Laad opnieuw.", "eventConsumers.resetTooltip": "Reset Event Consumer", + "eventConsumers.started": "Event consumer will be started soon", "eventConsumers.startFailed": "Het starten van een evenementconsument is mislukt. Laad opnieuw.", "eventConsumers.startTooltip": "Start Event Consumer", "eventConsumers.stopFailed": "Kan de gebruiker van het evenement niet stoppen. Laad opnieuw.", + "eventConsumers.stopped": "Event consumer will be stopped soon", "eventConsumers.stopTooltip": "Stop Event Consumer", "features.loadFailed": "Laden van functies is mislukt. Laad opnieuw.", "history.loadFailed": "Kan geschiedenis niet laden. Laad opnieuw.", diff --git a/backend/i18n/frontend_zh.json b/backend/i18n/frontend_zh.json index 9b7fb2608..3c47f0e10 100644 --- a/backend/i18n/frontend_zh.json +++ b/backend/i18n/frontend_zh.json @@ -582,11 +582,14 @@ "eventConsumers.position": "位置", "eventConsumers.refreshTooltip": "刷新事件消费者", "eventConsumers.reloaded": "事件消费者重新加载。", + "eventConsumers.reset": "Event consumer will be reset soon", "eventConsumers.resetFailed": "无法重置事件消费者。请重新加载。", "eventConsumers.resetTooltip": "重置事件消费者", + "eventConsumers.started": "Event consumer will be started soon", "eventConsumers.startFailed": "无法启动事件消费者。请重新加载。", "eventConsumers.startTooltip": "启动事件消费者", "eventConsumers.stopFailed": "无法停止事件消费者。请重新加载。", + "eventConsumers.stopped": "Event consumer will be stopped soon", "eventConsumers.stopTooltip": "停止事件消费者", "features.loadFailed": "加载功能失败。请重新加载。", "history.loadFailed": "加载历史记录失败。请重新加载。", diff --git a/backend/i18n/source/frontend_en.json b/backend/i18n/source/frontend_en.json index dccb7d272..381d9a828 100644 --- a/backend/i18n/source/frontend_en.json +++ b/backend/i18n/source/frontend_en.json @@ -582,11 +582,14 @@ "eventConsumers.position": "Position", "eventConsumers.refreshTooltip": "Refresh event consumers", "eventConsumers.reloaded": "Event Consumers reloaded.", + "eventConsumers.reset": "Event consumer will be reset soon", "eventConsumers.resetFailed": "Failed to reset event consumer. Please reload.", "eventConsumers.resetTooltip": "Reset Event Consumer", + "eventConsumers.started": "Event consumer will be started soon", "eventConsumers.startFailed": "Failed to start event consumer. Please reload.", "eventConsumers.startTooltip": "Start Event Consumer", "eventConsumers.stopFailed": "Failed to stop event consumer. Please reload.", + "eventConsumers.stopped": "Event consumer will be stopped soon", "eventConsumers.stopTooltip": "Stop Event Consumer", "features.loadFailed": "Failed to load features. Please reload.", "history.loadFailed": "Failed to load history. Please reload.", diff --git a/backend/src/Migrations/MigrationPath.cs b/backend/src/Migrations/MigrationPath.cs index acd61401e..705d4ebfb 100644 --- a/backend/src/Migrations/MigrationPath.cs +++ b/backend/src/Migrations/MigrationPath.cs @@ -37,8 +37,6 @@ namespace Migrations private IEnumerable ResolveMigrators(int version) { - yield return serviceProvider.GetRequiredService(); - // Version 06: Convert Event store. Must always be executed first. if (version < 6) { @@ -121,8 +119,6 @@ namespace Migrations { yield return serviceProvider.GetRequiredService(); } - - yield return serviceProvider.GetRequiredService(); } } } diff --git a/backend/src/Migrations/Migrations/StartEventConsumers.cs b/backend/src/Migrations/Migrations/StartEventConsumers.cs deleted file mode 100644 index c02a9e165..000000000 --- a/backend/src/Migrations/Migrations/StartEventConsumers.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Orleans; - -namespace Migrations.Migrations -{ - public sealed class StartEventConsumers : IMigration - { - private readonly IEventConsumerManagerGrain eventConsumerManager; - - public StartEventConsumers(IGrainFactory grainFactory) - { - eventConsumerManager = grainFactory.GetGrain(SingleGrain.Id); - } - - public Task UpdateAsync( - CancellationToken ct) - { - return eventConsumerManager.StartAllAsync(); - } - } -} diff --git a/backend/src/Migrations/Migrations/StopEventConsumers.cs b/backend/src/Migrations/Migrations/StopEventConsumers.cs deleted file mode 100644 index 14d4903e8..000000000 --- a/backend/src/Migrations/Migrations/StopEventConsumers.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Migrations; -using Squidex.Infrastructure.Orleans; - -namespace Migrations.Migrations -{ - public sealed class StopEventConsumers : IMigration - { - private readonly IEventConsumerManagerGrain eventConsumerManager; - - public StopEventConsumers(IGrainFactory grainFactory) - { - eventConsumerManager = grainFactory.GetGrain(SingleGrain.Id); - } - - public Task UpdateAsync( - CancellationToken ct) - { - return eventConsumerManager.StopAllAsync(); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs index 035856bf3..b82cb8cb1 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ConvertContent/ExcludeChangedTypes.cs @@ -15,11 +15,11 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { public sealed class ExcludeChangedTypes : IContentFieldConverter, IContentValueConverter { - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; - public ExcludeChangedTypes(IJsonSerializer jsonSerializer) + public ExcludeChangedTypes(IJsonSerializer serializer) { - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; } public ContentFieldData? ConvertField(IRootField field, ContentFieldData source) @@ -54,7 +54,7 @@ namespace Squidex.Domain.Apps.Core.ConvertContent { try { - return !JsonValueValidator.IsValid(field, source, jsonSerializer); + return !JsonValueValidator.IsValid(field, source, serializer); } catch { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs index 52ddf5712..0125a04aa 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleEventFormatter.cs @@ -27,7 +27,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules private const string GlobalFallback = "null"; private static readonly Regex RegexPatternOld = new Regex(@"^(?(?[^_]*)_(?[^\s]*))", RegexOptions.Compiled | RegexOptions.ExplicitCapture); private static readonly Regex RegexPatternNew = new Regex(@"^\{(?(?[\w]+)_(?[\w\.\-]+))[\s]*(\|[\s]*(?[^\?}]+))?(\?[\s]*(?[^\}\s]+))?[\s]*\}", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; private readonly IEnumerable formatters; private readonly ITemplateEngine templateEngine; private readonly IScriptEngine scriptEngine; @@ -69,9 +69,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules } } - public RuleEventFormatter(IJsonSerializer jsonSerializer, IEnumerable formatters, ITemplateEngine templateEngine, IScriptEngine scriptEngine) + public RuleEventFormatter(IJsonSerializer serializer, IEnumerable formatters, ITemplateEngine templateEngine, IScriptEngine scriptEngine) { - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; this.formatters = formatters; this.templateEngine = templateEngine; this.scriptEngine = scriptEngine; @@ -81,14 +81,14 @@ namespace Squidex.Domain.Apps.Core.HandleRules { var payload = @event; - return jsonSerializer.Serialize(payload); + return serializer.Serialize(payload); } public virtual string ToEnvelope(EnrichedEvent @event) { var payload = new { type = @event.Name, payload = @event, timestamp = @event.Timestamp }; - return jsonSerializer.Serialize(payload); + return serializer.Serialize(payload); } public async ValueTask FormatAsync(string text, EnrichedEvent @event) diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs index a8b50e063..ce2262537 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/HandleRules/RuleService.cs @@ -27,31 +27,26 @@ namespace Squidex.Domain.Apps.Core.HandleRules private readonly TypeNameRegistry typeNameRegistry; private readonly RuleOptions ruleOptions; private readonly IEventEnricher eventEnricher; - private readonly IJsonSerializer jsonSerializer; - private readonly IClock clock; + private readonly IJsonSerializer serializer; private readonly ILogger log; + public IClock Clock { get; set; } = SystemClock.Instance; + public RuleService( IOptions ruleOptions, IEnumerable ruleTriggerHandlers, IEnumerable ruleActionHandlers, IEventEnricher eventEnricher, - IJsonSerializer jsonSerializer, - IClock clock, + IJsonSerializer serializer, ILogger log, TypeNameRegistry typeNameRegistry) { this.typeNameRegistry = typeNameRegistry; - + this.eventEnricher = eventEnricher; this.ruleOptions = ruleOptions.Value; this.ruleTriggerHandlers = ruleTriggerHandlers.ToDictionary(x => x.TriggerType); this.ruleActionHandlers = ruleActionHandlers.ToDictionary(x => x.ActionType); - this.eventEnricher = eventEnricher; - - this.jsonSerializer = jsonSerializer; - - this.clock = clock; - + this.serializer = serializer; this.log = log; } @@ -94,7 +89,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules yield break; } - var now = clock.GetCurrentInstant(); + var now = Clock.GetCurrentInstant(); await foreach (var enrichedEvent in triggerHandler.CreateSnapshotEventsAsync(context, ct)) { @@ -204,7 +199,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules return; } - var now = clock.GetCurrentInstant(); + var now = Clock.GetCurrentInstant(); var eventTime = @event.Headers.ContainsKey(CommonHeaders.Timestamp) ? @@ -321,7 +316,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules { var (description, data) = await actionHandler.CreateJobAsync(enrichedEvent, context.Rule.Action); - var json = jsonSerializer.Serialize(data); + var json = serializer.Serialize(data); job.ActionData = json; job.ActionName = actionName; @@ -367,7 +362,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules var actionType = typeNameRegistry.GetType(actionName); var actionHandler = ruleActionHandlers[actionType]; - var deserialized = jsonSerializer.Deserialize(job, actionHandler.DataType); + var deserialized = serializer.Deserialize(job, actionHandler.DataType); using (var cts = new CancellationTokenSource(GetTimeoutInMs())) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs index 86fdb9827..f8a64343c 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/Tags/ITagService.cs @@ -11,20 +11,28 @@ namespace Squidex.Domain.Apps.Core.Tags { public interface ITagService { - Task> GetTagIdsAsync(DomainId appId, string group, HashSet names); + Task> GetTagIdsAsync(DomainId id, string group, HashSet names, + CancellationToken ct = default); - Task> NormalizeTagsAsync(DomainId appId, string group, HashSet? names, HashSet? ids); + Task> NormalizeTagsAsync(DomainId id, string group, HashSet? names, HashSet? ids, + CancellationToken ct = default); - Task> DenormalizeTagsAsync(DomainId appId, string group, HashSet ids); + Task> DenormalizeTagsAsync(DomainId id, string group, HashSet ids, + CancellationToken ct = default); - Task GetTagsAsync(DomainId appId, string group); + Task GetTagsAsync(DomainId id, string group, + CancellationToken ct = default); - Task GetExportableTagsAsync(DomainId appId, string group); + Task GetExportableTagsAsync(DomainId id, string group, + CancellationToken ct = default); - Task RenameTagAsync(DomainId appId, string group, string name, string newName); + Task RenameTagAsync(DomainId id, string group, string name, string newName, + CancellationToken ct = default); - Task RebuildTagsAsync(DomainId appId, string group, TagsExport export); + Task RebuildTagsAsync(DomainId id, string group, TagsExport export, + CancellationToken ct = default); - Task ClearAsync(DomainId appId, string group); + Task ClearAsync(DomainId id, string group, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs index bc02a6cb0..7d5689891 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueConverter.cs @@ -22,19 +22,19 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { private static readonly JsonValueConverter Instance = new JsonValueConverter(); - public record struct Args(JsonValue Value, IJsonSerializer JsonSerializer, ResolvedComponents Components); + public record struct Args(JsonValue Value, IJsonSerializer Serializer, ResolvedComponents Components); private JsonValueConverter() { } - public static (object? Result, JsonError? Error) ConvertValue(IField field, JsonValue value, IJsonSerializer jsonSerializer, + public static (object? Result, JsonError? Error) ConvertValue(IField field, JsonValue value, IJsonSerializer serializer, ResolvedComponents components) { Guard.NotNull(field); Guard.NotNull(value); - var args = new Args(value, jsonSerializer, components); + var args = new Args(value, serializer, components); return field.Accept(Instance, args); } @@ -128,7 +128,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public (object? Result, JsonError? Error) Visit(IField field, Args args) { - var result = GeoJsonValue.TryParse(args.Value, args.JsonSerializer, out var value); + var result = GeoJsonValue.TryParse(args.Value, args.Serializer, out var value); switch (result) { diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs index e24c71b20..b219a6b77 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/JsonValueValidator.cs @@ -20,18 +20,18 @@ namespace Squidex.Domain.Apps.Core.ValidateContent { private static readonly JsonValueValidator Instance = new JsonValueValidator(); - public record struct Args(JsonValue Value, IJsonSerializer JsonSerializer); + public record struct Args(JsonValue Value, IJsonSerializer Serializer); private JsonValueValidator() { } - public static bool IsValid(IField field, JsonValue value, IJsonSerializer jsonSerializer) + public static bool IsValid(IField field, JsonValue value, IJsonSerializer serializer) { Guard.NotNull(field); Guard.NotNull(value); - var args = new Args(value, jsonSerializer); + var args = new Args(value, serializer); return field.Accept(Instance, args); } @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent public bool Visit(IField field, Args args) { - var result = GeoJsonValue.TryParse(args.Value, args.JsonSerializer, out _); + var result = GeoJsonValue.TryParse(args.Value, args.Serializer, out _); return result == GeoJsonParseResult.Success; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs index 37f42f480..e2c222e1d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/RootContext.cs @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent private readonly ConcurrentBag errors = new ConcurrentBag(); private readonly Scheduler scheduler = new Scheduler(); - public IJsonSerializer JsonSerializer { get; } + public IJsonSerializer Serializer { get; } public DomainId ContentId { get; } @@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent } public RootContext( - IJsonSerializer jsonSerializer, + IJsonSerializer serializer, NamedId appId, NamedId schemaId, Schema schema, @@ -47,7 +47,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent AppId = appId; Components = components; ContentId = contentId; - JsonSerializer = jsonSerializer; + Serializer = serializer; Schema = schema; SchemaId = schemaId; } diff --git a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs index e1c37616c..374c3c04d 100644 --- a/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs +++ b/backend/src/Squidex.Domain.Apps.Core.Operations/ValidateContent/Validators/FieldValidator.cs @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Core.ValidateContent.Validators typedValue = jsonValue.Value; var (json, error) = JsonValueConverter.ConvertValue(field, jsonValue, - context.Root.JsonSerializer, + context.Root.Serializer, context.Root.Components); if (error != null) diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs index 0fb277ae6..13f669d6d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs @@ -18,8 +18,8 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps { public sealed class MongoAppRepository : MongoSnapshotStoreBase, IAppRepository, IDeleter { - public MongoAppRepository(IMongoDatabase database, JsonSerializer jsonSerializer) - : base(database, jsonSerializer) + public MongoAppRepository(IMongoDatabase database, JsonSerializer serializer) + : base(database, serializer) { } @@ -82,5 +82,44 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps return result; } + + public async Task> QueryAllAsync(string contributorId, IEnumerable names, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAppRepository/QueryAllAsync")) + { + var entities = + await Collection.Find(x => (x.IndexedUserIds.Contains(contributorId) || names.Contains(x.IndexedName)) && !x.IndexedDeleted) + .ToListAsync(ct); + + return entities.Select(x => (IAppEntity)x.Document).ToList(); + } + } + + public async Task FindAsync(DomainId id, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAppRepository/FindAsync")) + { + var entity = + await Collection.Find(x => x.DocumentId == id && !x.IndexedDeleted) + .FirstOrDefaultAsync(ct); + + return entity?.Document; + } + } + + public async Task FindAsync(string name, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoAppRepository/FindAsyncByName")) + { + var entity = + await Collection.Find(x => x.IndexedName == name && !x.IndexedDeleted) + .FirstOrDefaultAsync(ct); + + return entity?.Document; + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs index 2bfb20b8d..2aaa55c7a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs @@ -8,18 +8,18 @@ using MongoDB.Driver; using Newtonsoft.Json; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Rules; using Squidex.Domain.Apps.Entities.Rules.DomainObject; using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Rules { public sealed class MongoRuleRepository : MongoSnapshotStoreBase, IRuleRepository, IDeleter { - public MongoRuleRepository(IMongoDatabase database, JsonSerializer jsonSerializer) - : base(database, jsonSerializer) + public MongoRuleRepository(IMongoDatabase database, JsonSerializer serializer) + : base(database, serializer) { } @@ -40,14 +40,16 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); } - public async Task> QueryIdsAsync(DomainId appId, + public async Task> QueryAllAsync(DomainId appId, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("MongoRuleRepository/QueryIdsAsync")) + using (Telemetry.Activities.StartActivity("MongoRuleRepository/QueryAllAsync")) { - var entities = await Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted).Only(x => x.IndexedId).ToListAsync(ct); + var entities = + await Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted) + .ToListAsync(ct); - return entities.Select(x => DomainId.Create(x["_ri"].AsString)).ToList(); + return entities.Select(x => (IRuleEntity)x.Document).ToList(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs index 0480ef56c..08c553d2b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs @@ -8,18 +8,18 @@ using MongoDB.Driver; using Newtonsoft.Json; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Domain.Apps.Entities.Schemas.DomainObject; using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Infrastructure; -using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas { public sealed class MongoSchemaRepository : MongoSnapshotStoreBase, ISchemaRepository, IDeleter { - public MongoSchemaRepository(IMongoDatabase database, JsonSerializer jsonSerializer) - : base(database, jsonSerializer) + public MongoSchemaRepository(IMongoDatabase database, JsonSerializer serializer) + : base(database, serializer) { } @@ -41,33 +41,42 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas return Collection.DeleteManyAsync(Filter.Eq(x => x.IndexedAppId, app.Id), ct); } - public async Task> QueryIdsAsync(DomainId appId, - CancellationToken ct = default) + public async Task> QueryAllAsync(DomainId appId, CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("MongoSchemaRepository/QueryAsync")) + using (Telemetry.Activities.StartActivity("MongoSchemaRepository/QueryAllAsync")) { - var find = Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted); + var entities = + await Collection.Find(x => x.IndexedAppId == appId && !x.IndexedDeleted) + .ToListAsync(ct); - return await QueryAsync(find, ct); + return entities.Select(x => (ISchemaEntity)x.Document).ToList(); } } - private static async Task> QueryAsync(IFindFluent find, - CancellationToken ct) + public async Task FindAsync(DomainId appId, DomainId id, + CancellationToken ct = default) { - var entities = await find.SortBy(x => x.IndexedCreated).Only(x => x.IndexedId, x => x.IndexedName).ToListAsync(ct); + using (Telemetry.Activities.StartActivity("MongoSchemaRepository/FindAsync")) + { + var entity = + await Collection.Find(x => x.IndexedAppId == appId && x.IndexedId == id && !x.IndexedDeleted) + .FirstOrDefaultAsync(ct); - var result = new Dictionary(); + return entity?.Document; + } + } - foreach (var entity in entities) + public async Task FindAsync(DomainId appId, string name, + CancellationToken ct = default) + { + using (Telemetry.Activities.StartActivity("MongoSchemaRepository/FindAsyncByName")) { - var indexedId = DomainId.Create(entity["_si"].AsString); - var indexedName = entity["_sn"].AsString; + var entity = + await Collection.Find(x => x.IndexedAppId == appId && x.IndexedName == name && !x.IndexedDeleted) + .FirstOrDefaultAsync(ct); - result[indexedName] = indexedId; + return entity?.Document; } - - return result; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AlwaysCreateClientCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AlwaysCreateClientCommandMiddleware.cs index 7cbc33387..43d801e1f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AlwaysCreateClientCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AlwaysCreateClientCommandMiddleware.cs @@ -14,9 +14,10 @@ namespace Squidex.Domain.Apps.Entities.Apps { public sealed class AlwaysCreateClientCommandMiddleware : ICommandMiddleware { - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { - await next(context); + await next(context, ct); if (context.IsCompleted && context.Command is CreateApp createApp) { @@ -26,7 +27,8 @@ namespace Squidex.Domain.Apps.Entities.Apps { command.AppId = appId; - var newContext = await context.CommandBus.PublishAsync(command); + // If we have the app already it is not worth to cancel the step here. + var newContext = await context.CommandBus.PublishAsync(command, default); context.Complete(newContext.PlainResult); }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs index 89332f7c6..62c255e5b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppPermanentDeleter.cs @@ -5,10 +5,10 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Apps public sealed class AppPermanentDeleter : IEventConsumer { private readonly IEnumerable deleters; - private readonly IGrainFactory grainFactory; + private readonly IDomainObjectFactory factory; private readonly HashSet consumingTypes; public string Name @@ -30,11 +30,10 @@ namespace Squidex.Domain.Apps.Entities.Apps get => "^app-"; } - public AppPermanentDeleter(IEnumerable deleters, IGrainFactory grainFactory, TypeNameRegistry typeNameRegistry) + public AppPermanentDeleter(IEnumerable deleters, IDomainObjectFactory factory, TypeNameRegistry typeNameRegistry) { this.deleters = deleters.OrderBy(x => x.Order).ToList(); - - this.grainFactory = grainFactory; + this.factory = factory; // Compute the event types names once for performance reasons and use hashset for extensibility. consumingTypes = new HashSet @@ -88,9 +87,9 @@ namespace Squidex.Domain.Apps.Entities.Apps using (Telemetry.Activities.StartActivity("RemoveAppFromSystem")) { // Bypass our normal app resolve process, so that we can also retrieve the deleted app. - var appGrain = grainFactory.GetGrain(appArchived.AppId.Id.ToString()); + var app = factory.Create(appArchived.AppId.Id); - var app = await appGrain.GetStateAsync(); + await app.EnsureLoadedAsync(); // If the app does not exist, the version is lower than zero. if (app.Version < 0) @@ -102,7 +101,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { using (Telemetry.Activities.StartActivity(deleter.GetType().Name)) { - await deleter.DeleteAppAsync(app, default); + await deleter.DeleteAppAsync(app.Snapshot, default); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs index ccfbbef5e..23211c02d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettings.cs @@ -5,66 +5,170 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Apps { public sealed class AppUISettings : IAppUISettings, IDeleter { - private readonly IGrainFactory grainFactory; + private readonly IPersistenceFactory persistanceFactory; - public AppUISettings(IGrainFactory grainFactory) + [CollectionName("UISettings")] + public sealed class State { - this.grainFactory = grainFactory; + public JsonObject Settings { get; set; } = new JsonObject(); + + public void Set(JsonObject settings) + { + Settings = settings; + } + + public bool Set(string path, JsonValue value) + { + var container = GetContainer(path, true, out var key); + + if (container == null) + { + ThrowHelper.InvalidOperationException("Path does not lead to an object."); + return false; + } + + if (!container.TryGetValue(key, out var existing) || !existing.Equals(value)) + { + container[key] = value; + return true; + } + + return false; + } + + public bool Remove(string path) + { + var container = GetContainer(path, false, out var key); + + if (container == null) + { + return false; + } + + return container.Remove(key); + } + + private JsonObject? GetContainer(string path, bool add, out string key) + { + Guard.NotNullOrEmpty(path); + + var segments = path.Split('.'); + + key = segments[^1]; + + var current = Settings; + + if (segments.Length > 1) + { + foreach (var segment in segments.Take(segments.Length - 1)) + { + if (!current.TryGetValue(segment, out var found)) + { + if (add) + { + found = new JsonObject(); + + current[segment] = found; + } + else + { + return null; + } + } + + if (found.Value is JsonObject o) + { + current = o; + } + else + { + return null; + } + } + } + + return current; + } + } + + public AppUISettings(IPersistenceFactory persistanceFactory) + { + this.persistanceFactory = persistanceFactory; } async Task IDeleter.DeleteContributorAsync(DomainId appId, string contributorId, CancellationToken ct) { - await GetGrain(appId, null).ClearAsync(); + await ClearAsync(appId, contributorId, ct); } async Task IDeleter.DeleteAppAsync(IAppEntity app, CancellationToken ct) { - await GetGrain(app.Id, null).ClearAsync(); + await ClearAsync(app.Id, null, ct); foreach (var userId in app.Contributors.Keys) { - await GetGrain(app.Id, userId).ClearAsync(); + await ClearAsync(app.Id, userId, ct); } } - public Task GetAsync(DomainId appId, string? userId) + public async Task GetAsync(DomainId appId, string? userId, + CancellationToken ct = default) { - return GetGrain(appId, userId).GetAsync(); + var state = await GetStateAsync(appId, userId, ct); + + return state.Value.Settings; } - public Task RemoveAsync(DomainId appId, string? userId, string path) + public async Task RemoveAsync(DomainId appId, string? userId, string path, + CancellationToken ct = default) { - return GetGrain(appId, userId).RemoveAsync(path); + var state = await GetStateAsync(appId, userId, ct); + + await state.UpdateIfAsync(s => s.Remove(path), ct: ct); } - public Task SetAsync(DomainId appId, string? userId, string path, JsonValue value) + public async Task SetAsync(DomainId appId, string? userId, string path, JsonValue value, + CancellationToken ct = default) { - return GetGrain(appId, userId).SetAsync(path, value); + var state = await GetStateAsync(appId, userId, ct); + + await state.UpdateIfAsync(s => s.Set(path, value), ct: ct); } - public Task SetAsync(DomainId appId, string? userId, JsonObject settings) + public async Task SetAsync(DomainId appId, string? userId, JsonObject settings, + CancellationToken ct = default) { - return GetGrain(appId, userId).SetAsync(settings); + var state = await GetStateAsync(appId, userId, ct); + + await state.UpdateAsync(s => s.Set(settings), ct: ct); } - public Task ClearAsync(DomainId appId, string? userId) + public async Task ClearAsync(DomainId appId, string? userId, + CancellationToken ct = default) { - return GetGrain(appId, userId).ClearAsync(); + var state = await GetStateAsync(appId, userId, ct); + + await state.ClearAsync(ct); } - private IAppUISettingsGrain GetGrain(DomainId appId, string? userId) + private async Task> GetStateAsync(DomainId appId, string? userId, + CancellationToken ct) { - return grainFactory.GetGrain(GetKey(appId, userId)); + var state = new SimpleState(persistanceFactory, GetType(), GetKey(appId, userId)); + + await state.LoadAsync(ct); + + return state; } private static string GetKey(DomainId appId, string? userId) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs deleted file mode 100644 index 3dc83b0c6..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs +++ /dev/null @@ -1,120 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppUISettingsGrain : GrainBase, IAppUISettingsGrain - { - private readonly IGrainState state; - - [CollectionName("UISettings")] - public sealed class State - { - public JsonObject Settings { get; set; } = new JsonObject(); - } - - public AppUISettingsGrain(IGrainIdentity identity, IGrainState state) - : base(identity) - { - this.state = state; - } - - public Task GetAsync() - { - return Task.FromResult(state.Value.Settings); - } - - public Task ClearAsync() - { - TryDeactivateOnIdle(); - - return state.ClearAsync(); - } - - public Task SetAsync(JsonObject settings) - { - state.Value.Settings = settings; - - return state.WriteAsync(); - } - - public Task SetAsync(string path, JsonValue value) - { - var container = GetContainer(path, true, out var key); - - if (container == null) - { - ThrowHelper.InvalidOperationException("Path does not lead to an object."); - return Task.CompletedTask; - } - - container[key] = value; - - return state.WriteAsync(); - } - - public async Task RemoveAsync(string path) - { - var container = GetContainer(path, false, out var key); - - if (container?.ContainsKey(key) == true) - { - container.Remove(key); - - await state.WriteAsync(); - } - } - - private JsonObject? GetContainer(string path, bool add, out string key) - { - Guard.NotNullOrEmpty(path); - - var segments = path.Split('.'); - - key = segments[^1]; - - var current = state.Value.Settings; - - if (segments.Length > 1) - { - foreach (var segment in segments.Take(segments.Length - 1)) - { - if (!current.TryGetValue(segment, out var found)) - { - if (add) - { - found = new JsonObject(); - - current[segment] = found; - } - else - { - return null; - } - } - - if (found.Value is JsonObject o) - { - current = o; - } - else - { - return null; - } - } - } - - return current; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs index aebc41442..24a140d9a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/BackupApps.cs @@ -59,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Apps public async Task BackupAsync(BackupContext context, CancellationToken ct) { - var json = await appUISettings.GetAsync(context.AppId, null); + var json = await appUISettings.GetAsync(context.AppId, null, ct); await context.Writer.WriteJsonAsync(SettingsFile, json, ct); } @@ -116,7 +116,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { var json = await context.Reader.ReadJsonAsync(SettingsFile, ct); - await appUISettings.SetAsync(context.AppId, null, json); + await appUISettings.SetAsync(context.AppId, null, json, ct); } private async Task ReserveAppAsync(DomainId appId, string appName, @@ -139,7 +139,6 @@ namespace Squidex.Domain.Apps.Entities.Apps { await rebuilder.InsertManyAsync(Enumerable.Repeat(context.AppId, 1), 1, default); - await appsIndex.RegisterAsync(context.AppId, appName); await appsIndex.RemoveReservationAsync(appReservation); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs deleted file mode 100644 index a218c2436..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Diagnostics/OrleansAppsHealthCheck.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Orleans; -using Squidex.Domain.Apps.Entities.Apps.Indexes; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Apps.Diagnostics -{ - public sealed class OrleansAppsHealthCheck : IHealthCheck - { - private readonly IGrainFactory grainFactory; - - public OrleansAppsHealthCheck(IGrainFactory grainFactory) - { - this.grainFactory = grainFactory; - } - - public async Task CheckHealthAsync(HealthCheckContext context, - CancellationToken cancellationToken = default) - { - await GetGrain().GetAppIdsAsync(new[] { "test" }); - - return HealthCheckResult.Healthy("Orleans must establish communication."); - } - - private IAppsCacheGrain GetGrain() - { - return grainFactory.GetGrain(SingleGrain.Id); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs index 20f070088..567a42484 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppCommandMiddleware.cs @@ -5,7 +5,6 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Assets; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Infrastructure.Commands; @@ -14,51 +13,51 @@ using Squidex.Infrastructure.Validation; namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { - public sealed class AppCommandMiddleware : GrainCommandMiddleware + public sealed class AppCommandMiddleware : AggregateCommandMiddleware { private readonly IAppImageStore appImageStore; private readonly IAssetThumbnailGenerator assetThumbnailGenerator; private readonly IContextProvider contextProvider; - public AppCommandMiddleware( - IGrainFactory grainFactory, - IAppImageStore appImageStore, - IAssetThumbnailGenerator assetThumbnailGenerator, - IContextProvider contextProvider) - : base(grainFactory) + public AppCommandMiddleware(IDomainObjectFactory domainObjectFactory, + IAppImageStore appImageStore, IAssetThumbnailGenerator assetThumbnailGenerator, IContextProvider contextProvider) + : base(domainObjectFactory) { this.appImageStore = appImageStore; this.assetThumbnailGenerator = assetThumbnailGenerator; this.contextProvider = contextProvider; } - public override async Task HandleAsync(CommandContext context, NextDelegate next) + public override async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (context.Command is UploadAppImage uploadImage) { - await UploadAsync(uploadImage); + await UploadAsync(uploadImage, ct); } - await base.HandleAsync(context, next); + await base.HandleAsync(context, next, ct); } - protected override Task EnrichResultAsync(CommandContext context, CommandResult result) + protected override Task EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) { if (result.Payload is IAppEntity app) { contextProvider.Context.App = app; } - return base.EnrichResultAsync(context, result); + return base.EnrichResultAsync(context, result, ct); } - private async Task UploadAsync(UploadAppImage uploadImage) + private async Task UploadAsync(UploadAppImage uploadImage, + CancellationToken ct) { var file = uploadImage.File; await using (var uploadStream = file.OpenRead()) { - var image = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, file.MimeType); + var image = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, file.MimeType, ct); if (image == null) { @@ -68,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject await using (var uploadStream = file.OpenRead()) { - await appImageStore.UploadAsync(uploadImage.AppId.Id, uploadStream); + await appImageStore.UploadAsync(uploadImage.AppId.Id, uploadStream, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs index a756e7d83..b4bb1c00a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.State.cs @@ -18,7 +18,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { - public sealed partial class AppDomainObject + public partial class AppDomainObject { [CollectionName("Apps")] public sealed class State : DomainObjectState, IAppEntity diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs index 0f5345fe0..6a25a109f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObject.cs @@ -24,7 +24,7 @@ using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { - public sealed partial class AppDomainObject : DomainObject + public partial class AppDomainObject : DomainObject { private readonly IServiceProvider serviceProvider; @@ -50,7 +50,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return command is AppUpdateCommand update && Equals(update?.AppId?.Id, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) { switch (command) { @@ -62,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject Create(c); return Snapshot; - }); + }, ct); case UpdateApp update: return UpdateReturn(update, c => @@ -72,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject Update(c); return Snapshot; - }); + }, ct); case UpdateAppSettings updateSettings: return UpdateReturn(updateSettings, c => @@ -82,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject UpdateSettings(c); return Snapshot; - }); + }, ct); case UploadAppImage uploadImage: return UpdateReturn(uploadImage, c => @@ -92,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject UploadImage(c); return Snapshot; - }); + }, ct); case RemoveAppImage removeImage: return UpdateReturn(removeImage, c => @@ -102,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject RemoveImage(c); return Snapshot; - }); + }, ct); case ConfigureAssetScripts configureAssetScripts: return UpdateReturn(configureAssetScripts, c => @@ -112,17 +113,17 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject ConfigureAssetScripts(c); return Snapshot; - }); + }, ct); case AssignContributor assignContributor: - return UpdateReturnAsync(assignContributor, async c => + return UpdateReturnAsync(assignContributor, async (c, ct) => { await GuardAppContributors.CanAssign(c, Snapshot, UserResolver(), GetPlan()); AssignContributor(c, !Snapshot.Contributors.ContainsKey(assignContributor.ContributorId)); return Snapshot; - }); + }, ct); case RemoveContributor removeContributor: return UpdateReturn(removeContributor, c => @@ -132,7 +133,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject RemoveContributor(c); return Snapshot; - }); + }, ct); case AttachClient attachClient: return UpdateReturn(attachClient, c => @@ -142,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject AttachClient(c); return Snapshot; - }); + }, ct); case UpdateClient updateClient: return UpdateReturn(updateClient, c => @@ -152,7 +153,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject UpdateClient(c); return Snapshot; - }); + }, ct); case RevokeClient revokeClient: return UpdateReturn(revokeClient, c => @@ -162,7 +163,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject RevokeClient(c); return Snapshot; - }); + }, ct); case AddWorkflow addWorkflow: return UpdateReturn(addWorkflow, c => @@ -172,7 +173,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject AddWorkflow(c); return Snapshot; - }); + }, ct); case UpdateWorkflow updateWorkflow: return UpdateReturn(updateWorkflow, c => @@ -182,7 +183,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject UpdateWorkflow(c); return Snapshot; - }); + }, ct); case DeleteWorkflow deleteWorkflow: return UpdateReturn(deleteWorkflow, c => @@ -192,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject DeleteWorkflow(c); return Snapshot; - }); + }, ct); case AddLanguage addLanguage: return UpdateReturn(addLanguage, c => @@ -202,7 +203,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject AddLanguage(c); return Snapshot; - }); + }, ct); case RemoveLanguage removeLanguage: return UpdateReturn(removeLanguage, c => @@ -212,7 +213,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject RemoveLanguage(c); return Snapshot; - }); + }, ct); case UpdateLanguage updateLanguage: return UpdateReturn(updateLanguage, c => @@ -222,7 +223,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject UpdateLanguage(c); return Snapshot; - }); + }, ct); case AddRole addRole: return UpdateReturn(addRole, c => @@ -232,7 +233,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject AddRole(c); return Snapshot; - }); + }, ct); case DeleteRole deleteRole: return UpdateReturn(deleteRole, c => @@ -242,7 +243,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject DeleteRole(c); return Snapshot; - }); + }, ct); case UpdateRole updateRole: return UpdateReturn(updateRole, c => @@ -252,10 +253,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject UpdateRole(c); return Snapshot; - }); + }, ct); case ChangePlan changePlan: - return UpdateReturnAsync(changePlan, async c => + return UpdateReturnAsync(changePlan, async (c, ct) => { GuardApp.CanChangePlan(c, Snapshot, AppPlansProvider()); @@ -278,15 +279,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject return result; } - }); + }, ct); case DeleteApp delete: - return UpdateAsync(delete, async c => + return UpdateAsync(delete, async (c, ct) => { await AppPlanBillingManager().ChangePlanAsync(c.Actor.Identifier, Snapshot.NamedId(), null, null); DeleteApp(c); - }); + }, ct); default: ThrowHelper.NotSupportedException(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObjectGrain.cs deleted file mode 100644 index b9a8ccf9a..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/AppDomainObjectGrain.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject -{ - public sealed class AppDomainObjectGrain : DomainObjectGrain, IAppGrain - { - public AppDomainObjectGrain(IGrainIdentity identity, IDomainObjectFactory factory) - : base(identity, factory) - { - } - - public async Task GetStateAsync() - { - await DomainObject.EnsureLoadedAsync(); - - return Snapshot; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/IAppGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/IAppGrain.cs deleted file mode 100644 index 00b826750..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/DomainObject/IAppGrain.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Apps.DomainObject -{ - public interface IAppGrain : IDomainObjectGrain - { - Task GetStateAsync(); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs index 243cdaddd..875fb4ff4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettings.cs @@ -12,12 +12,16 @@ namespace Squidex.Domain.Apps.Entities.Apps { public interface IAppUISettings { - Task GetAsync(DomainId appId, string? userId); + Task GetAsync(DomainId appId, string? userId, + CancellationToken ct = default); - Task SetAsync(DomainId appId, string? userId, string path, JsonValue value); + Task SetAsync(DomainId appId, string? userId, string path, JsonValue value, + CancellationToken ct = default); - Task SetAsync(DomainId appId, string? userId, JsonObject settings); + Task SetAsync(DomainId appId, string? userId, JsonObject settings, + CancellationToken ct = default); - Task RemoveAsync(DomainId appId, string? userId, string path); + Task RemoveAsync(DomainId appId, string? userId, string path, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs deleted file mode 100644 index f0de5dca9..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsCacheGrain.cs +++ /dev/null @@ -1,108 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Concurrency; -using Orleans.Core; -using Squidex.Domain.Apps.Entities.Apps.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans.Indexes; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - [Reentrant] - public sealed class AppsCacheGrain : UniqueNameGrain, IAppsCacheGrain - { - private readonly IAppRepository appRepository; - private readonly Dictionary appIds = new Dictionary(); - - public AppsCacheGrain(IGrainIdentity identity, IAppRepository appRepository) - : base(identity) - { - this.appRepository = appRepository; - } - - public override async Task ReserveAsync(DomainId id, string name) - { - var token = await base.ReserveAsync(id, name); - - if (token == null) - { - return null; - } - - var ids = await GetAppIdsAsync(new[] { name }); - - if (ids.Any()) - { - await RemoveReservationAsync(token); - return null; - } - - return token; - } - - public async Task> GetAppIdsAsync(string[] names) - { - var result = new List(); - - List? pendingNames = null; - - foreach (var name in names) - { - if (!appIds.TryGetValue(name, out var cachedId)) - { - pendingNames ??= new List(); - pendingNames.Add(name); - } - else if (cachedId != DomainId.Empty) - { - result.Add(cachedId); - } - } - - if (pendingNames != null) - { - var foundIds = await appRepository.QueryIdsAsync(pendingNames); - - foreach (var name in pendingNames) - { - if (foundIds.TryGetValue(name, out var id)) - { - appIds[name] = id; - - result.Add(id); - } - else - { - appIds[name] = default; - } - } - } - - return result; - } - - public Task AddAsync(DomainId id, string name) - { - appIds[name] = id; - - return Task.CompletedTask; - } - - public Task RemoveAsync(DomainId id) - { - var name = appIds.FirstOrDefault(x => x.Value == id).Key; - - if (name != null) - { - appIds.Remove(name); - } - - return Task.CompletedTask; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs index 5650ccdfe..a142acde0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs @@ -5,51 +5,57 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Caching; using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.Repositories; +using Squidex.Hosting; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.States; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; using Squidex.Shared; namespace Squidex.Domain.Apps.Entities.Apps.Indexes { - public sealed class AppsIndex : IAppsIndex, ICommandMiddleware + public sealed class AppsIndex : IAppsIndex, ICommandMiddleware, IInitializable { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); private readonly IAppRepository appRepository; - private readonly IGrainFactory grainFactory; - private readonly IReplicatedCache grainCache; + private readonly IReplicatedCache appCache; + private readonly NameReservationState namesState; - public AppsIndex(IAppRepository appRepository, IGrainFactory grainFactory, IReplicatedCache grainCache) + public AppsIndex(IAppRepository appRepository, IReplicatedCache appCache, + IPersistenceFactory persistenceFactory) { this.appRepository = appRepository; - this.grainFactory = grainFactory; - this.grainCache = grainCache; + this.appCache = appCache; + + namesState = new NameReservationState(persistenceFactory, "Apps"); } - public Task RegisterAsync(DomainId id, string name, - CancellationToken ct = default) + public Task InitializeAsync( + CancellationToken ct) { - return Cache().AddAsync(id, name); + return namesState.LoadAsync(ct); } public Task RemoveReservationAsync(string? token, CancellationToken ct = default) { - return Cache().RemoveReservationAsync(token); + return namesState.RemoveReservationAsync(token, ct); } public async Task ReserveAsync(DomainId id, string name, CancellationToken ct = default) { - return await Cache().ReserveAsync(id, name); + if (await appRepository.FindAsync(name, ct) != null) + { + return null; + } + + return await namesState.ReserveAsync(id, name, ct); } public async Task> GetAppsForUserAsync(string userId, PermissionSet permissions, @@ -57,17 +63,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { using (Telemetry.Activities.StartActivity("AppsIndex/GetAppsForUserAsync")) { - var ids = - await Task.WhenAll( - GetAppIdsByUserAsync(userId), - GetAppIdsAsync(permissions.ToAppNames())); + var apps = await appRepository.QueryAllAsync(userId, permissions.ToAppNames(), ct); - var apps = - await Task.WhenAll(ids - .SelectMany(x => x).Distinct() - .Select(id => GetAppAsync(id, false, ct))); + foreach (var app in apps.Where(IsValid)) + { + await CacheItAsync(app); + } - return apps.NotNull().ToList(); + return apps.Where(IsValid).ToList(); } } @@ -78,20 +81,25 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { if (canCache) { - if (grainCache.TryGetValue(GetCacheKey(name), out var v) && v is IAppEntity cacheApp) + if (appCache.TryGetValue(GetCacheKey(name), out var v) && v is IAppEntity cacheApp) { return cacheApp; } } - var appId = await GetAppIdAsync(name); + var app = await appRepository.FindAsync(name, ct); - if (appId == DomainId.Empty) + if (!IsValid(app)) { - return null; + app = null; + } + + if (app != null) + { + await CacheItAsync(app); } - return await GetAppAsync(appId, canCache, ct); + return app; } } @@ -102,13 +110,18 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes { if (canCache) { - if (grainCache.TryGetValue(GetCacheKey(appId), out var cached) && cached is IAppEntity cachedApp) + if (appCache.TryGetValue(GetCacheKey(appId), out var cached) && cached is IAppEntity cachedApp) { return cachedApp; } } - var app = await GetAppCoreAsync(appId); + var app = await appRepository.FindAsync(appId, ct); + + if (!IsValid(app)) + { + app = null; + } if (app != null) { @@ -119,57 +132,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes } } - private async Task> GetAppIdsByUserAsync(string userId) - { - using (Telemetry.Activities.StartActivity("AppsIndex/GetAppIdsByUserAsync")) - { - var result = await appRepository.QueryIdsAsync(userId); - - return result.Values; - } - } - - private async Task> GetAppIdsAsync(string[] names) - { - using (Telemetry.Activities.StartActivity("AppsIndex/GetAppIdsAsync")) - { - var result = await Cache().GetAppIdsAsync(names); - - return result; - } - } - - private async Task GetAppIdAsync(string name) - { - using (Telemetry.Activities.StartActivity("AppsIndex/GetAppIdAsync")) - { - var result = await Cache().GetAppIdsAsync(new[] { name }); - - return result.FirstOrDefault(); - } - } - - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { var command = context.Command; if (command is CreateApp createApp) { - var cache = Cache(); - - var token = await CheckAppAsync(cache, createApp); + var token = await CheckAppAsync(createApp, ct); try { - await next(context); + await next(context, ct); } finally { - await cache.RemoveReservationAsync(token); + // Always remove the reservation and therefore do not pass over cancellation token. + await namesState.RemoveReservationAsync(token, default); } } else { - await next(context); + await next(context, ct); } if (context.IsCompleted) @@ -189,9 +172,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes } } - private static async Task CheckAppAsync(IAppsCacheGrain cache, CreateApp command) + private async Task CheckAppAsync(CreateApp command, + CancellationToken ct) { - var token = await cache.ReserveAsync(command.AppId, command.Name); + var token = await ReserveAsync(command.AppId, command.Name, ct); if (token == null) { @@ -204,15 +188,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes private async Task OnCreateAsync(CreateApp create) { await InvalidateItAsync(create.AppId, create.Name); - - await Cache().AddAsync(create.AppId, create.Name); } private async Task OnDeleteAsync(DeleteApp delete) { await InvalidateItAsync(delete.AppId.Id, delete.AppId.Name); - - await Cache().RemoveAsync(delete.AppId.Id); } private async Task OnUpdateAsync(AppUpdateCommand update) @@ -220,23 +200,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes await InvalidateItAsync(update.AppId.Id, update.AppId.Name); } - private IAppsCacheGrain Cache() - { - return grainFactory.GetGrain(SingleGrain.Id); - } - - private async Task GetAppCoreAsync(DomainId id, bool allowArchived = false) - { - var app = await grainFactory.GetGrain(id.ToString()).GetStateAsync(); - - if (app.Version <= EtagVersion.Empty || (app.IsDeleted && !allowArchived)) - { - return null; - } - - return app; - } - private static string GetCacheKey(DomainId id) { return $"{typeof(AppsIndex)}_Apps_Id_{id}"; @@ -247,9 +210,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes return $"{typeof(AppsIndex)}_Apps_Name_{name}"; } + private static bool IsValid(IAppEntity? app) + { + return app != null && app.Version > EtagVersion.Empty && !app.IsDeleted; + } + private Task InvalidateItAsync(DomainId id, string name) { - return grainCache.RemoveAsync( + return appCache.RemoveAsync( GetCacheKey(id), GetCacheKey(name)); } @@ -257,8 +225,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes private Task CacheItAsync(IAppEntity app) { return Task.WhenAll( - grainCache.AddAsync(GetCacheKey(app.Id), app, CacheDuration), - grainCache.AddAsync(GetCacheKey(app.Name), app, CacheDuration)); + appCache.AddAsync(GetCacheKey(app.Id), app, CacheDuration), + appCache.AddAsync(GetCacheKey(app.Name), app, CacheDuration)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsCacheGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsCacheGrain.cs deleted file mode 100644 index b4d095346..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsCacheGrain.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans.Indexes; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public interface IAppsCacheGrain : IUniqueNameGrain - { - Task> GetAppIdsAsync(string[] names); - - Task AddAsync(DomainId id, string name); - - Task RemoveAsync(DomainId id); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs index 73f5d57d4..15cb5163b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs @@ -24,9 +24,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes Task ReserveAsync(DomainId id, string name, CancellationToken ct = default); - Task RegisterAsync(DomainId id, string name, - CancellationToken ct = default); - Task RemoveReservationAsync(string? token, CancellationToken ct = default); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs index 8a0e089aa..b93a27cca 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Invitation/InviteUserCommandMiddleware.cs @@ -21,7 +21,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation this.userResolver = userResolver; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (context.Command is AssignContributor assignContributor && ShouldResolve(assignContributor)) { @@ -31,11 +32,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation if (assignContributor.Invite) { - (user, created) = await userResolver.CreateUserIfNotExistsAsync(assignContributor.ContributorId, true); + (user, created) = await userResolver.CreateUserIfNotExistsAsync(assignContributor.ContributorId, true, ct); } else { - user = await userResolver.FindByIdOrEmailAsync(assignContributor.ContributorId); + user = await userResolver.FindByIdOrEmailAsync(assignContributor.ContributorId, ct); } if (user != null) @@ -43,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation assignContributor.ContributorId = user.Id; } - await next(context); + await next(context, ct); if (created && context.PlainResult is IAppEntity app) { @@ -52,7 +53,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation } else { - await next(context); + await next(context, ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlanBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlanBillingManager.cs index 7975a2638..9f9fb5571 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlanBillingManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IAppPlanBillingManager.cs @@ -13,8 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { bool HasPortal { get; } - Task ChangePlanAsync(string userId, NamedId appId, string? planId, string? referer); + Task ChangePlanAsync(string userId, NamedId appId, string? planId, string? referer, + CancellationToken ct = default); - Task GetPortalLinkAsync(string userId); + Task GetPortalLinkAsync(string userId, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IUsageNotifierGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IUsageNotifierGrain.cs deleted file mode 100644 index 3e41443df..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/IUsageNotifierGrain.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Orleans.Concurrency; - -namespace Squidex.Domain.Apps.Entities.Apps.Plans -{ - public interface IUsageNotifierGrain : IGrainWithStringKey - { - [OneWay] - Task NotifyAsync(UsageNotification notification); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotification.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/Messages.cs similarity index 58% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotification.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/Messages.cs index ad0c27fac..8055333b0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotification.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/Messages.cs @@ -7,18 +7,20 @@ using Squidex.Infrastructure; +#pragma warning disable MA0048 // File name must match type name + namespace Squidex.Domain.Apps.Entities.Apps.Plans { - public sealed class UsageNotification + public sealed record UsageTrackingCheck { - public DomainId AppId { get; set; } + public DomainId AppId { get; init; } - public string AppName { get; set; } + public string AppName { get; init; } - public long Usage { get; set; } + public long Usage { get; init; } - public long UsageLimit { get; set; } + public long UsageLimit { get; init; } - public string[] Users { get; set; } + public string[] Users { get; init; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs index bd60a7af1..ccd571397 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/NoopAppPlanBillingManager.cs @@ -16,12 +16,14 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans get => false; } - public Task ChangePlanAsync(string userId, NamedId appId, string? planId, string? referer) + public Task ChangePlanAsync(string userId, NamedId appId, string? planId, string? referer, + CancellationToken ct = default) { return Task.FromResult(new PlanChangedResult()); } - public Task GetPortalLinkAsync(string userId) + public Task GetPortalLinkAsync(string userId, + CancellationToken ct = default) { return Task.FromResult(string.Empty); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs index 0f7431b0a..05973e9f2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/RestrictAppsCommandMiddleware.cs @@ -27,17 +27,18 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans this.userResolver = userResolver; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (usageOptions.MaximumNumberOfApps <= 0 || context.Command is not CreateApp createApp || createApp.Actor.IsClient) { - await next(context); + await next(context, ct); return; } var totalApps = 0; - var user = await userResolver.FindByIdAsync(createApp.Actor.Identifier); + var user = await userResolver.FindByIdAsync(createApp.Actor.Identifier, ct); if (user != null) { @@ -49,14 +50,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans } } - await next(context); + await next(context, ct); if (context.IsCompleted && user != null) { var newApps = totalApps + 1; var newAppsValue = newApps.ToString(CultureInfo.InvariantCulture); - await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.TotalApps, newAppsValue, true); + // Always update the user and therefore do nto pass cancellation token. + await userResolver.SetClaimAsync(user.Id, SquidexClaimTypes.TotalApps, newAppsValue, true, default); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs index 0310a789a..a08eed647 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageGate.cs @@ -7,12 +7,10 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using Orleans; using Squidex.Domain.Apps.Core.Apps; using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.UsageTracking; +using Squidex.Messaging; namespace Squidex.Domain.Apps.Entities.Apps.Plans { @@ -21,63 +19,60 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans private readonly MemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); private readonly IAppPlansProvider appPlansProvider; private readonly IApiUsageTracker apiUsageTracker; - private readonly IGrainFactory grainFactory; + private readonly IMessageBus messaging; - public UsageGate(IAppPlansProvider appPlansProvider, IApiUsageTracker apiUsageTracker, IGrainFactory grainFactory) + public UsageGate(IAppPlansProvider appPlansProvider, IApiUsageTracker apiUsageTracker, IMessageBus messaging) { this.appPlansProvider = appPlansProvider; this.apiUsageTracker = apiUsageTracker; - this.grainFactory = grainFactory; + this.messaging = messaging; } - public virtual async Task IsBlockedAsync(IAppEntity app, string? clientId, DateTime today) + public virtual async Task IsBlockedAsync(IAppEntity app, string? clientId, DateTime today, + CancellationToken ct = default) { Guard.NotNull(app); - var appId = app.Id; - - var isBlocked = false; - - if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && client.ApiCallsLimit > 0) - { - var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), today, clientId); - - isBlocked = usage >= client.ApiCallsLimit; - } - var (plan, _) = appPlansProvider.GetPlanForApp(app); - var limit = plan.MaxApiCalls; + var appId = app.Id; + var blocking = false; + var blockLimit = plan.MaxApiCalls; - if (limit > 0 || plan.BlockingApiCalls > 0) + if (blockLimit > 0 || plan.BlockingApiCalls > 0) { - var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), today, null); + var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), today, null, ct); - if (IsOver10Percent(limit, usage) && IsAboutToBeLocked(today, limit, usage) && !HasNotifiedBefore(app.Id)) + if (IsOver10Percent(blockLimit, usage) && IsAboutToBeLocked(today, blockLimit, usage) && !HasNotifiedBefore(app.Id)) { - var notification = new UsageNotification + var notification = new UsageTrackingCheck { AppId = appId, AppName = app.Name, Usage = usage, - UsageLimit = limit, + UsageLimit = blockLimit, Users = GetUsers(app) }; - GetGrain().NotifyAsync(notification).Forget(); + await messaging.PublishAsync(notification, ct: ct); TrackNotified(appId); } - isBlocked = isBlocked || (plan.BlockingApiCalls > 0 && usage > plan.BlockingApiCalls); + blocking = plan.BlockingApiCalls > 0 && usage > plan.BlockingApiCalls; } - return isBlocked; - } + if (!blocking) + { + if (clientId != null && app.Clients.TryGetValue(clientId, out var client) && client.ApiCallsLimit > 0) + { + var usage = await apiUsageTracker.GetMonthCallsAsync(appId.ToString(), today, clientId, ct); - private IUsageNotifierGrain GetGrain() - { - return grainFactory.GetGrain(SingleGrain.Id); + blocking = usage >= client.ApiCallsLimit; + } + } + + return blocking; } private bool HasNotifiedBefore(DomainId appId) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs similarity index 73% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs index def700836..348f587f1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Plans/UsageNotifierWorker.cs @@ -6,23 +6,21 @@ // ========================================================================== using NodaTime; -using Orleans.Core; using Squidex.Domain.Apps.Entities.Notifications; using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; +using Squidex.Messaging; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Apps.Plans { - public sealed class UsageNotifierGrain : GrainBase, IUsageNotifierGrain + public sealed class UsageNotifierWorker : IMessageHandler { private static readonly TimeSpan TimeBetweenNotifications = TimeSpan.FromDays(3); - private readonly IGrainState grainState; + private readonly SimpleState state; private readonly INotificationSender notificationSender; private readonly IUserResolver userResolver; - private readonly IClock clock; [CollectionName("UsageNotifications")] public sealed class State @@ -30,24 +28,26 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans public Dictionary NotificationsSent { get; } = new Dictionary(); } - public UsageNotifierGrain(IGrainIdentity identity, - IGrainState grainState, INotificationSender notificationSender, IUserResolver userResolver, IClock clock) - : base(identity) + public IClock Clock { get; set; } = SystemClock.Instance; + + public UsageNotifierWorker(IPersistenceFactory persistenceFactory, + INotificationSender notificationSender, IUserResolver userResolver) { - this.grainState = grainState; this.notificationSender = notificationSender; this.userResolver = userResolver; - this.clock = clock; + + state = new SimpleState(persistenceFactory, GetType(), DomainId.Create("Default")); } - public async Task NotifyAsync(UsageNotification notification) + public async Task HandleAsync(UsageTrackingCheck notification, + CancellationToken ct) { if (!notificationSender.IsActive) { return; } - var now = clock.GetCurrentInstant().ToDateTimeUtc(); + var now = Clock.GetCurrentInstant().ToDateTimeUtc(); if (!HasBeenSentBefore(notification.AppId, now)) { @@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { foreach (var userId in notification.Users) { - var user = await userResolver.FindByIdOrEmailAsync(userId); + var user = await userResolver.FindByIdOrEmailAsync(userId, ct); if (user != null) { @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans private bool HasBeenSentBefore(DomainId appId, DateTime now) { - if (grainState.Value.NotificationsSent.TryGetValue(appId, out var lastSent)) + if (state.Value.NotificationsSent.TryGetValue(appId, out var lastSent)) { var elapsed = now - lastSent; @@ -85,9 +85,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans private Task TrackNotifiedAsync(DomainId appId, DateTime now) { - grainState.Value.NotificationsSent[appId] = now; + state.Value.NotificationsSent[appId] = now; - return grainState.WriteAsync(); + return state.WriteAsync(); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs index 8ee67036a..09a7eaad5 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Repositories/IAppRepository.cs @@ -11,10 +11,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.Repositories { public interface IAppRepository { - Task> QueryIdsAsync(string contributorId, + Task> QueryAllAsync(string contributorId, IEnumerable names, CancellationToken ct = default); - Task> QueryIdsAsync(IEnumerable names, + Task FindAsync(DomainId id, + CancellationToken ct = default); + + Task FindAsync(string name, CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs index c1cf6bf1a..d5a38a8b0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Apps/Templates/TemplateCommandMiddleware.cs @@ -44,9 +44,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Templates this.log = log; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { - await next(context); + await next(context, ct); if (context.IsCompleted && context.Command is CreateApp createApp) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs index 0e36de17a..1d896da72 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetChangedTriggerHandler.cs @@ -74,7 +74,8 @@ namespace Squidex.Domain.Apps.Entities.Assets var asset = await assetLoader.GetAsync( assetEvent.AppId.Id, assetEvent.AssetId, - @event.Headers.EventStreamNumber()); + @event.Headers.EventStreamNumber(), + ct); if (asset != null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupProcess.cs similarity index 51% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupProcess.cs index bb7d0dcf9..bedd18dd9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetCleanupProcess.cs @@ -5,36 +5,40 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; -using Orleans.Runtime; +using Squidex.Hosting; +using Squidex.Infrastructure.Timers; using tusdotnet.Interfaces; namespace Squidex.Domain.Apps.Entities.Assets { - public sealed class AssetCleanupGrain : Grain, IRemindable, IAssetCleanupGrain + public sealed class AssetCleanupProcess : IBackgroundProcess { private readonly ITusExpirationStore expirationStore; + private CompletionTimer timer; - public AssetCleanupGrain(ITusExpirationStore expirationStore) + public AssetCleanupProcess(ITusExpirationStore expirationStore) { this.expirationStore = expirationStore; } - public override Task OnActivateAsync() + public Task StartAsync( + CancellationToken ct) { - RegisterOrUpdateReminder("Cleanup", TimeSpan.Zero, TimeSpan.FromMinutes(10)); + timer = new CompletionTimer((int)TimeSpan.FromMinutes(10).TotalMilliseconds, CleanupAsync); - return base.OnActivateAsync(); + return Task.CompletedTask; } - public Task ActivateAsync() + public Task StopAsync( + CancellationToken ct) { - return Task.CompletedTask; + return timer?.StopAsync() ?? Task.CompletedTask; } - public Task ReceiveReminder(string reminderName, TickStatus status) + public Task CleanupAsync( + CancellationToken ct) { - return expirationStore.RemoveExpiredFilesAsync(default); + return expirationStore.RemoveExpiredFilesAsync(ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs index e73f59b2d..e7d1b0d63 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetTagsDeleter.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Assets public Task DeleteAppAsync(IAppEntity app, CancellationToken ct) { - return tagService.ClearAsync(app.Id, TagGroups.Assets); + return tagService.ClearAsync(app.Id, TagGroups.Assets, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs index fcea93bcb..1640494ed 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetCommandMiddleware.cs @@ -6,7 +6,6 @@ // ========================================================================== using System.Security.Cryptography; -using Orleans; using Squidex.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Infrastructure; @@ -14,7 +13,7 @@ using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { - public sealed class AssetCommandMiddleware : GrainCommandMiddleware + public sealed class AssetCommandMiddleware : CachingDomainObjectMiddleware { private readonly IAssetFileStore assetFileStore; private readonly IAssetEnricher assetEnricher; @@ -23,13 +22,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private readonly IEnumerable assetMetadataSources; public AssetCommandMiddleware( - IGrainFactory grainFactory, + IDomainObjectFactory domainObjectFactory, + IDomainObjectCache domainObjectCache, IAssetEnricher assetEnricher, IAssetFileStore assetFileStore, IAssetQueryService assetQuery, IContextProvider contextProvider, IEnumerable assetMetadataSources) - : base(grainFactory) + : base(domainObjectFactory, domainObjectCache) { this.assetEnricher = assetEnricher; this.assetFileStore = assetFileStore; @@ -38,39 +38,41 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject this.contextProvider = contextProvider; } - public override async Task HandleAsync(CommandContext context, NextDelegate next) + public override async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { switch (context.Command) { case CreateAsset create: - await UploadWithDuplicateCheckAsync(context, create, create.Duplicate, next); + await UploadWithDuplicateCheckAsync(context, create, create.Duplicate, next, ct); break; case UpsertAsset upsert: - await UploadWithDuplicateCheckAsync(context, upsert, upsert.Duplicate, next); + await UploadWithDuplicateCheckAsync(context, upsert, upsert.Duplicate, next, ct); break; case MoveAsset move: - await base.HandleAsync(context, next); + await base.HandleAsync(context, next, ct); break; case UpdateAsset upload: - await UploadAndHandleAsync(context, upload, next); + await UploadAndHandleAsync(context, upload, next, ct); break; default: - await base.HandleAsync(context, next); + await base.HandleAsync(context, next, ct); break; } } - private async Task UploadWithDuplicateCheckAsync(CommandContext context, UploadAssetCommand command, bool duplicate, NextDelegate next) + private async Task UploadWithDuplicateCheckAsync(CommandContext context, UploadAssetCommand command, bool duplicate, NextDelegate next, + CancellationToken ct) { var tempFile = context.ContextId.ToString(); try { - await EnrichWithHashAndUploadAsync(command, tempFile); + await EnrichWithHashAndUploadAsync(command, tempFile, ct); if (!duplicate) { @@ -78,51 +80,54 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject await assetQuery.FindByHashAsync(contextProvider.Context, command.FileHash, command.File.FileName, - command.File.FileSize); + command.File.FileSize, + ct); if (existing != null) { context.Complete(new AssetDuplicate(existing)); - await next(context); + await next(context, ct); return; } } - await EnrichWithMetadataAsync(command); + await EnrichWithMetadataAsync(command, ct); - await base.HandleAsync(context, next); + await base.HandleAsync(context, next, ct); } finally { - await assetFileStore.DeleteAsync(tempFile); + await assetFileStore.DeleteAsync(tempFile, ct); await command.File.DisposeAsync(); } } - private async Task UploadAndHandleAsync(CommandContext context, UploadAssetCommand command, NextDelegate next) + private async Task UploadAndHandleAsync(CommandContext context, UploadAssetCommand command, NextDelegate next, + CancellationToken ct) { var tempFile = context.ContextId.ToString(); try { - await EnrichWithHashAndUploadAsync(command, tempFile); - await EnrichWithMetadataAsync(command); + await EnrichWithHashAndUploadAsync(command, tempFile, ct); + await EnrichWithMetadataAsync(command, ct); - await base.HandleAsync(context, next); + await base.HandleAsync(context, next, ct); } finally { - await assetFileStore.DeleteAsync(tempFile); + await assetFileStore.DeleteAsync(tempFile, ct); await command.File.DisposeAsync(); } } - protected override async Task EnrichResultAsync(CommandContext context, CommandResult result) + protected override async Task EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) { - var payload = await base.EnrichResultAsync(context, result); + var payload = await base.EnrichResultAsync(context, result, ct); if (payload is IAssetEntity asset) { @@ -132,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject try { - await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion, null); + await assetFileStore.CopyAsync(tempFile, asset.AppId.Id, asset.AssetId, asset.FileVersion, null, ct); } catch (AssetAlreadyExistsException) when (context.Command is not UpsertAsset) { @@ -142,20 +147,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject if (payload is not IEnrichedAssetEntity) { - payload = await assetEnricher.EnrichAsync(asset, contextProvider.Context, default); + payload = await assetEnricher.EnrichAsync(asset, contextProvider.Context, ct); } } return payload; } - private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile) + private async Task EnrichWithHashAndUploadAsync(UploadAssetCommand command, string tempFile, + CancellationToken ct) { await using (var uploadStream = command.File.OpenRead()) { using (var hashStream = new HasherStream(uploadStream, HashAlgorithmName.SHA256)) { - await assetFileStore.UploadAsync(tempFile, hashStream); + await assetFileStore.UploadAsync(tempFile, hashStream, ct); command.FileHash = ComputeHash(command.File, hashStream); } @@ -169,11 +175,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject return $"{steamHash}{file.FileName}{file.FileSize}".ToSha256Base64(); } - private async Task EnrichWithMetadataAsync(UploadAssetCommand command) + private async Task EnrichWithMetadataAsync(UploadAssetCommand command, + CancellationToken ct) { foreach (var metadataSource in assetMetadataSources) { - await metadataSource.EnhanceAsync(command); + await metadataSource.EnhanceAsync(command, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs index b6b16c0d8..9d7424bf1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.State.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { - public sealed partial class AssetDomainObject + public partial class AssetDomainObject { public sealed class State : DomainObjectState, IAssetEntity { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs index 06813b306..908976528 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObject.cs @@ -20,7 +20,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { - public sealed partial class AssetDomainObject : DomainObject + public partial class AssetDomainObject : DomainObject { private readonly IServiceProvider serviceProvider; @@ -29,8 +29,6 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject : base(id, persistence, log) { this.serviceProvider = serviceProvider; - - Capacity = 2; } protected override bool IsDeleted(State snapshot) @@ -60,12 +58,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Equals(assetCommand.AssetId, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) { switch (command) { case UpsertAsset upsert: - return UpsertReturnAsync(upsert, async c => + return UpsertReturnAsync(upsert, async (c, ct) => { var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); @@ -84,14 +83,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject } return Snapshot; - }); + }, ct); - case CreateAsset c: - return CreateReturnAsync(c, async create => + case CreateAsset create: + return CreateReturnAsync(create, async (c, ct) => { var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); - await CreateCore(create, operation); + await CreateCore(c, operation); if (Is.Change(Snapshot.ParentId, c.ParentId)) { @@ -99,53 +98,54 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject } return Snapshot; - }); + }, ct); - case AnnotateAsset c: - return UpdateReturnAsync(c, async c => + case AnnotateAsset annotate: + return UpdateReturnAsync(annotate, async (c, ct) => { var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); await AnnotateCore(c, operation); return Snapshot; - }); + }, ct); case UpdateAsset update: - return UpdateReturnAsync(update, async c => + return UpdateReturnAsync(update, async (c, ct) => { var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); await UpdateCore(c, operation); return Snapshot; - }); + }, ct); case MoveAsset move: - return UpdateReturnAsync(move, async c => + return UpdateReturnAsync(move, async (c, ct) => { var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); await MoveCore(c, operation); return Snapshot; - }); + }, ct); case DeleteAsset delete when delete.Permanent: - return DeletePermanentAsync(delete, async c => + return DeletePermanentAsync(delete, async (c, ct) => { var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); await DeleteCore(c, operation); - }); + }, ct); case DeleteAsset delete: - return UpdateAsync(delete, async c => + return UpdateAsync(delete, async (c, ct) => { var operation = await AssetOperation.CreateAsync(serviceProvider, c, () => Snapshot); await DeleteCore(c, operation); - }); + }, ct); + default: ThrowHelper.NotSupportedException(); return default!; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObjectGrain.cs deleted file mode 100644 index 3c5db5af1..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetDomainObjectGrain.cs +++ /dev/null @@ -1,40 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject -{ - public sealed class AssetDomainObjectGrain : DomainObjectGrain, IAssetGrain - { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); - - public AssetDomainObjectGrain(IGrainIdentity identity, IDomainObjectFactory factory, - IActivationLimit limit) - : base(identity, factory) - { - limit?.SetLimit(5000, Lifetime); - } - - public override Task OnActivateAsync() - { - TryDelayDeactivation(Lifetime); - - return base.OnActivateAsync(); - } - - public async Task GetStateAsync(long version = EtagVersion.Any) - { - await DomainObject.EnsureLoadedAsync(); - - return await DomainObject.GetSnapshotAsync(version); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs index 34d82e461..e4c09a3d2 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObject.cs @@ -48,39 +48,40 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Equals(assetFolderCommand.AssetFolderId, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) { switch (command) { - case CreateAssetFolder c: - return CreateReturnAsync(c, async create => + case CreateAssetFolder create: + return CreateReturnAsync(create, async (c, ct) => { - await CreateCore(create, c); + await CreateCore(c, c); return Snapshot; - }); + }, ct); case MoveAssetFolder move: - return UpdateReturnAsync(move, async c => + return UpdateReturnAsync(move, async (c, ct) => { await MoveCore(c); return Snapshot; - }); + }, ct); case RenameAssetFolder rename: - return UpdateReturnAsync(rename, async c => + return UpdateReturnAsync(rename, async (c, ct) => { await RenameCore(c); return Snapshot; - }); + }, ct); case DeleteAssetFolder delete: return Update(delete, c => { Delete(c); - }); + }, ct); default: ThrowHelper.NotSupportedException(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObjectGrain.cs deleted file mode 100644 index f2ab521a3..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetFolderDomainObjectGrain.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject -{ - public sealed class AssetFolderDomainObjectGrain : DomainObjectGrain, IAssetFolderGrain - { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); - - public AssetFolderDomainObjectGrain(IGrainIdentity grainIdentity, IDomainObjectFactory factory, - IActivationLimit limit) - : base(grainIdentity, factory) - { - limit?.SetLimit(5000, Lifetime); - } - - public override Task OnActivateAsync() - { - TryDelayDeactivation(Lifetime); - - return base.OnActivateAsync(); - } - - public async Task GetStateAsync() - { - await DomainObject.EnsureLoadedAsync(); - - return Snapshot; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs index e3ee4d3f2..4575e6da9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/AssetsBulkUpdateCommandMiddleware.cs @@ -26,27 +26,26 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private readonly IContextProvider contextProvider; private readonly ILogger log; - private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command) - { - } + private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command, + CancellationToken CancellationToken); private sealed record BulkTask( ICommandBus Bus, int JobIndex, BulkUpdateJob CommandJob, BulkUpdateAssets Command, - ConcurrentBag Results - ) - { - } + ConcurrentBag Results, + CancellationToken CancellationToken); public AssetsBulkUpdateCommandMiddleware(IContextProvider contextProvider, ILogger log) { this.contextProvider = contextProvider; + this.log = log; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (context.Command is BulkUpdateAssets bulkUpdates) { @@ -103,7 +102,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject i, bulkUpdates.Jobs[i], bulkUpdates, - results); + results, + ct); if (!await createCommandsBlock.SendAsync(task)) { @@ -124,16 +124,16 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject } else { - await next(context); + await next(context, ct); } } private async Task ExecuteCommandAsync(BulkTaskCommand bulkCommand) { - var (task, id, command) = bulkCommand; + var (task, id, command, ct) = bulkCommand; try { - await task.Bus.PublishAsync(command); + await task.Bus.PublishAsync(command, ct); task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex)); } @@ -157,7 +157,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject command.AssetId = id; - return new BulkTaskCommand(task, id, command); + return new BulkTaskCommand(task, id, command, task.CancellationToken); } catch (Exception ex) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetGrain.cs deleted file mode 100644 index e6e17f61d..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetGrain.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject -{ - public interface IAssetGrain : IDomainObjectGrain - { - Task GetStateAsync(long version = EtagVersion.Any); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs index 9ad6fedfa..a7ad91b97 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTagAssetMetadataSource.cs @@ -47,7 +47,8 @@ namespace Squidex.Domain.Apps.Entities.Assets } } - public Task EnhanceAsync(UploadAssetCommand command) + public Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) { try { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs index 3eb8a34a5..ba96d654d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/FileTypeAssetMetadataSource.cs @@ -12,7 +12,8 @@ namespace Squidex.Domain.Apps.Entities.Assets { public sealed class FileTypeAssetMetadataSource : IAssetMetadataSource { - public Task EnhanceAsync(UploadAssetCommand command) + public Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) { if (command.Tags != null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs index 2a28f9009..0de3c1df9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetLoader.cs @@ -11,6 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { public interface IAssetLoader { - Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any); + Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs index c6e694485..94982fd2a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetMetadataSource.cs @@ -13,7 +13,8 @@ namespace Squidex.Domain.Apps.Entities.Assets { int Order => 0; - Task EnhanceAsync(UploadAssetCommand command); + Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct); IEnumerable Format(IAssetEntity asset); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs index 99b7f1b85..d1a8c06a7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/ImageAssetMetadataSource.cs @@ -20,7 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Assets this.assetThumbnailGenerator = assetThumbnailGenerator; } - public async Task EnhanceAsync(UploadAssetCommand command) + public async Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) { if (command.Type == AssetType.Unknown || command.Type == AssetType.Image) { @@ -30,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await using (var uploadStream = command.File.OpenRead()) { - imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, mimeType); + imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(uploadStream, mimeType, ct); } if (imageInfo != null) @@ -45,13 +46,13 @@ namespace Squidex.Domain.Apps.Entities.Assets { await using (var tempStream = tempFile.OpenWrite()) { - await assetThumbnailGenerator.FixOrientationAsync(uploadStream, mimeType, tempStream); + await assetThumbnailGenerator.FixOrientationAsync(uploadStream, mimeType, tempStream, ct); } } await using (var tempStream = tempFile.OpenRead()) { - imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(tempStream, mimeType) ?? imageInfo; + imageInfo = await assetThumbnailGenerator.GetImageInfoAsync(tempStream, mimeType, ct) ?? imageInfo; } await command.File.DisposeAsync(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs index ceae89429..068ec4526 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetEnricher.cs @@ -21,16 +21,16 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries private readonly IEnumerable assetMetadataSources; private readonly IRequestCache requestCache; private readonly IUrlGenerator urlGenerator; - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; public AssetEnricher(ITagService tagService, IEnumerable assetMetadataSources, IRequestCache requestCache, - IUrlGenerator urlGenerator, IJsonSerializer jsonSerializer) + IUrlGenerator urlGenerator, IJsonSerializer serializer) { this.tagService = tagService; this.assetMetadataSources = assetMetadataSources; this.requestCache = requestCache; this.urlGenerator = urlGenerator; - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; } public async Task EnrichAsync(IAssetEntity asset, Context context, @@ -88,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries u = url }; - var json = jsonSerializer.Serialize(token); + var json = serializer.Serialize(token); asset.EditToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); } @@ -136,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { ct.ThrowIfCancellationRequested(); - var tagsById = await CalculateTags(group); + var tagsById = await CalculateTagsAsync(group, ct); foreach (var asset in group) { @@ -156,11 +156,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries } } - private async Task> CalculateTags(IGrouping group) + private async Task> CalculateTagsAsync(IGrouping group, + CancellationToken ct) { var uniqueIds = group.Where(x => x.Tags != null).SelectMany(x => x.Tags).ToHashSet(); - return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds); + return await tagService.DenormalizeTagsAsync(group.Key, TagGroups.Assets, uniqueIds, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs index ddc6e45f1..1b764a1bf 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetLoader.cs @@ -5,36 +5,60 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Assets.Queries { public sealed class AssetLoader : IAssetLoader { - private readonly IGrainFactory grainFactory; + private readonly IDomainObjectFactory domainObjectFactory; + private readonly IDomainObjectCache domainObjectCache; - public AssetLoader(IGrainFactory grainFactory) + public AssetLoader(IDomainObjectFactory domainObjectFactory, IDomainObjectCache domainObjectCache) { - this.grainFactory = grainFactory; + this.domainObjectFactory = domainObjectFactory; + this.domainObjectCache = domainObjectCache; } - public async Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any) + public async Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("AssetLoader/GetAsync")) + var uniqueId = DomainId.Combine(appId, id); + + var asset = await GetCachedAsync(uniqueId, version, ct); + + if (asset == null) + { + asset = await GetAsync(uniqueId, version, ct); + } + + if (asset == null || asset.Version <= EtagVersion.Empty || (version > EtagVersion.Any && asset.Version != version)) { - var key = DomainId.Combine(appId, id); + return null; + } + + return asset; + } - var assetGrain = grainFactory.GetGrain(key.ToString()); - var assetState = await assetGrain.GetStateAsync(version); + private async Task GetCachedAsync(DomainId uniqueId, long version, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("AssetLoader/GetCachedAsync")) + { + return await domainObjectCache.GetAsync(uniqueId, version, ct); + } + } - if (assetState == null || assetState.Version <= EtagVersion.Empty || (version > EtagVersion.Any && assetState.Version != version)) - { - return null; - } + private async Task GetAsync(DomainId uniqueId, long version, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("AssetLoader/GetAsync")) + { + var contentObject = domainObjectFactory.Create(uniqueId); - return assetState; + return await contentObject.GetSnapshotAsync(version, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs index c91257fca..2d3e4ec85 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryParser.cs @@ -24,20 +24,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { private readonly QueryModel queryModel = AssetQueryModel.Build(); private readonly IEdmModel edmModel; - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; private readonly ITagService tagService; private readonly AssetOptions options; - public AssetQueryParser(IJsonSerializer jsonSerializer, ITagService tagService, IOptions options) + public AssetQueryParser(IJsonSerializer serializer, ITagService tagService, IOptions options) { - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; this.tagService = tagService; this.options = options.Value; edmModel = queryModel.ConvertToEdm("Squidex", "Asset"); } - public virtual async Task ParseAsync(Context context, Q q) + public virtual async Task ParseAsync(Context context, Q q, + CancellationToken ct = default) { Guard.NotNull(context); Guard.NotNull(q); @@ -46,7 +47,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var query = ParseClrQuery(q); - await TransformTagAsync(context, query); + await TransformTagAsync(context, query, ct); WithSorting(query); WithPaging(query, q); @@ -114,17 +115,18 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries } } - private async Task TransformTagAsync(Context context, ClrQuery query) + private async Task TransformTagAsync(Context context, ClrQuery query, + CancellationToken ct) { if (query.Filter != null) { - query.Filter = await FilterTagTransformer.TransformAsync(query.Filter, context.App.Id, tagService); + query.Filter = await FilterTagTransformer.TransformAsync(query.Filter, context.App.Id, tagService, ct); } } private ClrQuery ParseJson(string json) { - return queryModel.Parse(json, jsonSerializer); + return queryModel.Parse(json, serializer); } private ClrQuery ParseOData(string odata) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs index 74498f8af..4cea7752a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -178,7 +178,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries using (Telemetry.Activities.StartActivity("AssetQueryService/QueryAsync")) { - q = await queryParser.ParseAsync(context, q); + q = await queryParser.ParseAsync(context, q, ct); var assets = await QueryCoreAsync(context, parentId, q, ct); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs index 66f5c2c83..9319c2cdb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/FilterTagTransformer.cs @@ -17,15 +17,16 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { private static readonly FilterTagTransformer Instance = new FilterTagTransformer(); - public record struct Args(DomainId AppId, ITagService TagService); + public record struct Args(DomainId AppId, ITagService TagService, CancellationToken CancellationToken); private FilterTagTransformer() { } - public static ValueTask?> TransformAsync(FilterNode nodeIn, DomainId appId, ITagService tagService) + public static ValueTask?> TransformAsync(FilterNode nodeIn, DomainId appId, ITagService tagService, + CancellationToken ct) { - var args = new Args(appId, tagService); + var args = new Args(appId, tagService, ct); return nodeIn.Accept(Instance, args); } @@ -34,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { if (string.Equals(nodeIn.Path[0], nameof(IAssetEntity.Tags), StringComparison.OrdinalIgnoreCase) && nodeIn.Value.Value is string stringValue) { - var tagNames = await args.TagService.GetTagIdsAsync(args.AppId, TagGroups.Assets, HashSet.Of(stringValue)); + var tagNames = await args.TagService.GetTagIdsAsync(args.AppId, TagGroups.Assets, HashSet.Of(stringValue), args.CancellationToken); if (tagNames.TryGetValue(stringValue, out var normalized)) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs index 07247dec3..32f948c88 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/RecursiveDeleter.cs @@ -73,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { command.Actor = folderDeleted.Actor; - await commandBus.PublishAsync(command); + await commandBus.PublishAsync(command, default); } catch (Exception ex) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs index 03b6d788b..23cd10da8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/SvgAssetMetadataSource.cs @@ -16,7 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Assets { private const int FileSizeLimit = 2 * 1024 * 1024; // 2MB - public async Task EnhanceAsync(UploadAssetCommand command) + public async Task EnhanceAsync(UploadAssetCommand command, + CancellationToken ct) { var isSvg = command.File.MimeType == "image/svg+xml" || diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs deleted file mode 100644 index b12f8d65f..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs +++ /dev/null @@ -1,285 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; -using NodaTime; -using Orleans.Concurrency; -using Orleans.Core; -using Squidex.Domain.Apps.Entities.Backup.State; -using Squidex.Domain.Apps.Events; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.Tasks; -using Squidex.Infrastructure.Translations; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - [Reentrant] - public sealed class BackupGrain : GrainBase, IBackupGrain - { - private const int MaxBackups = 10; - private static readonly Duration UpdateDuration = Duration.FromSeconds(1); - private readonly IBackupArchiveLocation backupArchiveLocation; - private readonly IBackupArchiveStore backupArchiveStore; - private readonly IBackupHandlerFactory backupHandlers; - private readonly IClock clock; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly IGrainState state; - private readonly IUserResolver userResolver; - private readonly ILogger log; - private CancellationTokenSource? currentJobToken; - private BackupJob? currentJob; - - public BackupGrain(IGrainIdentity identity, - IBackupArchiveLocation backupArchiveLocation, - IBackupArchiveStore backupArchiveStore, - IBackupHandlerFactory backupHandlers, - IClock clock, - IEventFormatter eventFormatter, - IEventStore eventStore, - IGrainState state, - IUserResolver userResolver, - ILogger log) - : base(identity) - { - this.backupArchiveLocation = backupArchiveLocation; - this.backupArchiveStore = backupArchiveStore; - this.backupHandlers = backupHandlers; - this.clock = clock; - this.eventFormatter = eventFormatter; - this.eventStore = eventStore; - this.state = state; - this.userResolver = userResolver; - - this.log = log; - } - - public override Task OnActivateAsync() - { - RecoverAfterRestartAsync().Forget(); - - return Task.CompletedTask; - } - - private async Task RecoverAfterRestartAsync() - { - state.Value.Jobs.RemoveAll(x => x.Stopped == null); - - await state.WriteAsync(); - } - - public async Task ClearAsync() - { - foreach (var backup in state.Value.Jobs) - { - await backupArchiveStore.DeleteAsync(backup.Id, default); - } - - TryDeactivateOnIdle(); - - await state.ClearAsync(); - } - - public async Task BackupAsync(RefToken actor) - { - if (currentJobToken != null) - { - throw new DomainException(T.Get("backups.alreadyRunning")); - } - - if (state.Value.Jobs.Count >= MaxBackups) - { - throw new DomainException(T.Get("backups.maxReached", new { max = MaxBackups })); - } - - var job = new BackupJob - { - Id = DomainId.NewGuid(), - Started = clock.GetCurrentInstant(), - Status = JobStatus.Started - }; - - currentJobToken = new CancellationTokenSource(); - currentJob = job; - - state.Value.Jobs.Insert(0, job); - - await state.WriteAsync(); - -#pragma warning disable MA0042 // Do not use blocking calls in an async method - Process(job, actor, currentJobToken.Token); -#pragma warning restore MA0042 // Do not use blocking calls in an async method - } - - private void Process(BackupJob job, RefToken actor, - CancellationToken ct) - { - ProcessAsync(job, actor, ct).Forget(); - } - - private async Task ProcessAsync(BackupJob job, RefToken actor, - CancellationToken ct) - { - var handlers = backupHandlers.CreateMany(); - - var lastTimestamp = job.Started; - - try - { - await using (var stream = backupArchiveLocation.OpenStream(job.Id)) - { - using (var writer = await backupArchiveLocation.OpenWriterAsync(stream)) - { - await writer.WriteVersionAsync(); - - var userMapping = new UserMapping(actor); - - var context = new BackupContext(Key, userMapping, writer); - - await foreach (var storedEvent in eventStore.QueryAllAsync(GetFilter(), ct: ct)) - { - var @event = eventFormatter.Parse(storedEvent); - - if (@event.Payload is SquidexEvent squidexEvent && squidexEvent.Actor != null) - { - context.UserMapping.Backup(squidexEvent.Actor); - } - - foreach (var handler in handlers) - { - await handler.BackupEventAsync(@event, context, ct); - } - - writer.WriteEvent(storedEvent, ct); - - job.HandledEvents = writer.WrittenEvents; - job.HandledAssets = writer.WrittenAttachments; - - lastTimestamp = await WritePeriodically(lastTimestamp); - } - - foreach (var handler in handlers) - { - ct.ThrowIfCancellationRequested(); - - await handler.BackupAsync(context, ct); - } - - foreach (var handler in handlers) - { - ct.ThrowIfCancellationRequested(); - - await handler.CompleteBackupAsync(context); - } - - await userMapping.StoreAsync(writer, userResolver, ct); - } - - stream.Position = 0; - - ct.ThrowIfCancellationRequested(); - - await backupArchiveStore.UploadAsync(job.Id, stream, ct); - } - - job.Status = JobStatus.Completed; - } - catch (OperationCanceledException) - { - await RemoveAsync(job); - } - catch (Exception ex) - { - log.LogError(ex, "Faield to make backup with backup id '{backupId}'.", job.Id); - - job.Status = JobStatus.Failed; - } - finally - { - job.Stopped = clock.GetCurrentInstant(); - - await state.WriteAsync(); - - currentJobToken?.Dispose(); - currentJobToken = null; - currentJob = null; - } - } - - private string GetFilter() - { - return $"^[^\\-]*-{Regex.Escape(Key.ToString())}"; - } - - private async Task WritePeriodically(Instant lastTimestamp) - { - var now = clock.GetCurrentInstant(); - - if ((now - lastTimestamp) >= UpdateDuration) - { - lastTimestamp = now; - - await state.WriteAsync(); - } - - return lastTimestamp; - } - - public async Task DeleteAsync(DomainId id) - { - var job = state.Value.Jobs.Find(x => x.Id == id); - - if (job == null) - { - throw new DomainObjectNotFoundException(id.ToString()); - } - - if (currentJob == job) - { - try - { - currentJobToken?.Cancel(); - } - catch (ObjectDisposedException) - { - return; - } - } - else - { - await RemoveAsync(job); - } - } - - private async Task RemoveAsync(BackupJob job) - { - try - { -#pragma warning disable MA0040 // Flow the cancellation token - await backupArchiveStore.DeleteAsync(job.Id); -#pragma warning restore MA0040 // Flow the cancellation token - } - catch (Exception ex) - { - log.LogError(ex, "Failed to make remove with backup id '{backupId}'.", job.Id); - } - - state.Value.Jobs.Remove(job); - - await state.WriteAsync(); - } - - public Task> GetStateAsync() - { - return Task.FromResult(state.Value.Jobs.OfType().ToList()); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs new file mode 100644 index 000000000..c386a0f42 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupProcessor.cs @@ -0,0 +1,305 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using NodaTime; +using Squidex.Domain.Apps.Entities.Backup.State; +using Squidex.Domain.Apps.Events; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.Translations; +using Squidex.Shared.Users; + +#pragma warning disable MA0040 // Flow the cancellation token + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class BackupProcessor + { + private static readonly Duration UpdateDuration = Duration.FromSeconds(1); + private readonly IBackupArchiveLocation backupArchiveLocation; + private readonly IBackupArchiveStore backupArchiveStore; + private readonly IBackupHandlerFactory backupHandlerFactory; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly IUserResolver userResolver; + private readonly ILogger log; + private readonly SimpleState state; + private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); + private readonly DomainId appId; + private Run? currentRun; + + // Use a run to store all state that is necessary for a single run. + private sealed class Run : IDisposable + { + private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly CancellationTokenSource cancellationLinked; + + public IEnumerable Handlers { get; init; } + + public RefToken Actor { get; init; } + + public BackupJob Job { get; init; } + + public CancellationToken CancellationToken => cancellationLinked.Token; + + public Run(CancellationToken ct) + { + cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token); + } + + public void Dispose() + { + cancellationSource.Dispose(); + cancellationLinked.Dispose(); + } + + public void Cancel() + { + try + { + cancellationSource.Cancel(); + } + catch (ObjectDisposedException) + { + // Cancellation token might have been disposed, if the run is completed. + } + } + } + + public IClock Clock { get; set; } = SystemClock.Instance; + + public BackupProcessor( + DomainId appId, + IBackupArchiveLocation backupArchiveLocation, + IBackupArchiveStore backupArchiveStore, + IBackupHandlerFactory backupHandlerFactory, + IEventFormatter eventFormatter, + IEventStore eventStore, + IPersistenceFactory persistenceFactory, + IUserResolver userResolver, + ILogger log) + { + this.appId = appId; + this.backupArchiveLocation = backupArchiveLocation; + this.backupArchiveStore = backupArchiveStore; + this.backupHandlerFactory = backupHandlerFactory; + this.eventFormatter = eventFormatter; + this.eventStore = eventStore; + this.userResolver = userResolver; + this.log = log; + + state = new SimpleState(persistenceFactory, GetType(), appId); + } + + public async Task LoadAsync( + CancellationToken ct) + { + await state.LoadAsync(ct); + + if (state.Value.Jobs.RemoveAll(x => x.Stopped == null) > 0) + { + await state.WriteAsync(ct); + } + } + + public Task ClearAsync() + { + return scheduler.ScheduleAsync(async _ => + { + foreach (var backup in state.Value.Jobs) + { + await backupArchiveStore.DeleteAsync(backup.Id, default); + } + + await state.ClearAsync(); + }); + } + + public Task BackupAsync(RefToken actor, + CancellationToken ct) + { + return scheduler.ScheduleAsync(async _ => + { + if (currentRun != null) + { + throw new DomainException(T.Get("backups.alreadyRunning")); + } + + state.Value.EnsureCanStart(); + + // Set the current run first to indicate that we are running a rule at the moment. + var run = currentRun = new Run(ct) + { + Actor = actor, + Job = new BackupJob + { + Id = DomainId.NewGuid(), + Started = Clock.GetCurrentInstant(), + Status = JobStatus.Started + }, + Handlers = backupHandlerFactory.CreateMany() + }; + + state.Value.Jobs.Insert(0, run.Job); + try + { + await state.WriteAsync(run.CancellationToken); + + await ProcessAsync(run, run.CancellationToken); + } + finally + { + // Unset the run to indicate that we are done. + currentRun.Dispose(); + currentRun = null; + } + }); + } + + private async Task ProcessAsync(Run run, + CancellationToken ct) + { + var lastTimestamp = run.Job.Started; + try + { + await using (var stream = backupArchiveLocation.OpenStream(run.Job.Id)) + { + using (var writer = await backupArchiveLocation.OpenWriterAsync(stream, ct)) + { + await writer.WriteVersionAsync(); + + var backupUsers = new UserMapping(run.Actor); + var backupContext = new BackupContext(appId, backupUsers, writer); + + await foreach (var storedEvent in eventStore.QueryAllAsync(GetFilter(), ct: ct)) + { + var @event = eventFormatter.Parse(storedEvent); + + if (@event.Payload is SquidexEvent squidexEvent && squidexEvent.Actor != null) + { + backupUsers.Backup(squidexEvent.Actor); + } + + foreach (var handler in run.Handlers) + { + await handler.BackupEventAsync(@event, backupContext, ct); + } + + writer.WriteEvent(storedEvent, ct); + + run.Job.HandledEvents = writer.WrittenEvents; + run.Job.HandledAssets = writer.WrittenAttachments; + + lastTimestamp = await WritePeriodically(lastTimestamp); + } + + foreach (var handler in run.Handlers) + { + ct.ThrowIfCancellationRequested(); + + await handler.BackupAsync(backupContext, ct); + } + + foreach (var handler in run.Handlers) + { + ct.ThrowIfCancellationRequested(); + + await handler.CompleteBackupAsync(backupContext); + } + + await backupUsers.StoreAsync(writer, userResolver, ct); + } + + stream.Position = 0; + + ct.ThrowIfCancellationRequested(); + + await backupArchiveStore.UploadAsync(run.Job.Id, stream, ct); + } + + run.Job.Status = JobStatus.Completed; + } + catch (OperationCanceledException) + { + await RemoveAsync(run.Job); + } + catch (Exception ex) + { + log.LogError(ex, "Faield to make backup with backup id '{backupId}'.", run.Job.Id); + + run.Job.Status = JobStatus.Failed; + } + finally + { + run.Job.Stopped = Clock.GetCurrentInstant(); + + await state.WriteAsync(); + } + } + + private string GetFilter() + { + return $"^[^\\-]*-{Regex.Escape(appId.ToString())}"; + } + + private async Task WritePeriodically(Instant lastTimestamp) + { + var now = Clock.GetCurrentInstant(); + + if ((now - lastTimestamp) >= UpdateDuration) + { + lastTimestamp = now; + + await state.WriteAsync(); + } + + return lastTimestamp; + } + + public Task DeleteAsync(DomainId id) + { + return scheduler.ScheduleAsync(async _ => + { + var job = state.Value.Jobs.Find(x => x.Id == id); + + if (job == null) + { + throw new DomainObjectNotFoundException(id.ToString()); + } + + if (currentRun?.Job == job) + { + currentRun.Cancel(); + } + else + { + await RemoveAsync(job); + } + }); + } + + private async Task RemoveAsync(BackupJob job) + { + try + { + await backupArchiveStore.DeleteAsync(job.Id); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to make remove with backup id '{backupId}'.", job.Id); + } + + state.Value.Jobs.Remove(job); + + await state.WriteAsync(); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs index 0681297ef..01d2f612b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupReader.cs @@ -97,11 +97,13 @@ namespace Squidex.Domain.Apps.Entities.Backup return attachmentEntry; } - public async IAsyncEnumerable<(string Stream, Envelope Event)> ReadEventsAsync(IEventStreamNames eventStreams, IEventFormatter eventFormatter, + public async IAsyncEnumerable<(string Stream, Envelope Event)> ReadEventsAsync( + IEventStreamNames eventStreamNames, + IEventFormatter eventFormatter, [EnumeratorCancellation] CancellationToken ct = default) { Guard.NotNull(eventFormatter); - Guard.NotNull(eventStreams); + Guard.NotNull(eventStreamNames); while (!ct.IsCancellationRequested) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs index 8840fb65b..f8e201f83 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs @@ -5,72 +5,95 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Domain.Apps.Entities.Backup.State; using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; +using Squidex.Messaging; namespace Squidex.Domain.Apps.Entities.Backup { public sealed class BackupService : IBackupService, IDeleter { - private readonly IGrainFactory grainFactory; + private readonly SimpleState restoreState; + private readonly IPersistenceFactory persistenceFactoryBackup; + private readonly IMessageBus messaging; - public BackupService(IGrainFactory grainFactory) + public BackupService( + IPersistenceFactory persistenceFactoryRestore, + IPersistenceFactory persistenceFactoryBackup, + IMessageBus messaging) { - this.grainFactory = grainFactory; + this.persistenceFactoryBackup = persistenceFactoryBackup; + this.messaging = messaging; + + restoreState = new SimpleState(persistenceFactoryRestore, GetType(), "Default"); } Task IDeleter.DeleteAppAsync(IAppEntity app, CancellationToken ct) { - return BackupGrain(app.Id).ClearAsync(); + return messaging.PublishAsync(new BackupClear(app.Id), ct: ct); } - public Task StartBackupAsync(DomainId appId, RefToken actor) + public async Task StartBackupAsync(DomainId appId, RefToken actor, + CancellationToken ct = default) { - return BackupGrain(appId).BackupAsync(actor); + var state = await GetStateAsync(appId, ct); + + state.Value.EnsureCanStart(); + + await messaging.PublishAsync(new BackupStart(appId, actor), ct: ct); } - public Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName) + public async Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName, + CancellationToken ct = default) { - return RestoreGrain().RestoreAsync(url, actor, newAppName); + await restoreState.LoadAsync(ct); + + restoreState.Value.EnsureCanStart(); + + await messaging.PublishAsync(new BackupRestore(actor, url, newAppName), ct: ct); } public Task DeleteBackupAsync(DomainId appId, DomainId backupId, CancellationToken ct = default) { - return BackupGrain(appId).DeleteAsync(backupId); + return messaging.PublishAsync(new BackupDelete(appId, backupId), ct: ct); } - public Task GetRestoreAsync( + public async Task GetRestoreAsync( CancellationToken ct = default) { - return RestoreGrain().GetStateAsync(); + await restoreState.LoadAsync(ct); + + return restoreState.Value; } - public Task> GetBackupsAsync(DomainId appId, + public async Task> GetBackupsAsync(DomainId appId, CancellationToken ct = default) { - return BackupGrain(appId).GetStateAsync(); + var state = await GetStateAsync(appId, ct); + + return state.Value.Jobs.OfType().ToList(); } public async Task GetBackupAsync(DomainId appId, DomainId backupId, CancellationToken ct = default) { - var state = await BackupGrain(appId).GetStateAsync(); + var state = await GetStateAsync(appId, ct); - return state.Find(x => x.Id == backupId); + return state.Value.Jobs.Find(x => x.Id == backupId); } - private IRestoreGrain RestoreGrain() + private async Task> GetStateAsync(DomainId appId, + CancellationToken ct) { - return grainFactory.GetGrain(SingleGrain.Id); - } + var state = new SimpleState(persistenceFactoryBackup, GetType(), appId); - private IBackupGrain BackupGrain(DomainId appId) - { - return grainFactory.GetGrain(appId.ToString()); + await state.LoadAsync(ct); + + return state; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWorker.cs new file mode 100644 index 000000000..6d9d028dc --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/BackupWorker.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Hosting; +using Squidex.Infrastructure; +using Squidex.Messaging; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class BackupWorker : + IMessageHandler, + IMessageHandler, + IMessageHandler, + IMessageHandler, + IInitializable + { + private readonly Dictionary> backupProcessors = new Dictionary>(); + private readonly Func backupFactory; + private readonly RestoreProcessor restoreProcessor; + + public BackupWorker(IServiceProvider serviceProvider) + { + var objectFactory = ActivatorUtilities.CreateFactory(typeof(BackupProcessor), new[] { typeof(DomainId) }); + + backupFactory = key => + { + return (BackupProcessor)objectFactory(serviceProvider, new object[] { key }); + }; + + restoreProcessor = serviceProvider.GetRequiredService(); + } + + public Task InitializeAsync( + CancellationToken ct) + { + return restoreProcessor.LoadAsync(ct); + } + + public Task HandleAsync(BackupRestore message, + CancellationToken ct) + { + return restoreProcessor.RestoreAsync(message.Url, message.Actor, message.NewAppName, ct); + } + + public async Task HandleAsync(BackupStart message, + CancellationToken ct) + { + var processor = await GetBackupProcessorAsync(message.AppId); + + await processor.BackupAsync(message.Actor, ct); + } + + public async Task HandleAsync(BackupDelete message, + CancellationToken ct) + { + var processor = await GetBackupProcessorAsync(message.AppId); + + await processor.DeleteAsync(message.Id); + } + + public async Task HandleAsync(BackupClear message, + CancellationToken ct) + { + var processor = await GetBackupProcessorAsync(message.AppId); + + await processor.ClearAsync(); + } + + private Task GetBackupProcessorAsync(DomainId appId) + { + lock (backupProcessors) + { + return backupProcessors.GetOrAdd(appId, async key => + { + var processor = backupFactory(key); + + await processor.LoadAsync(default); + + return processor; + }); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs index 724124933..28024fb2d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupArchiveLocation.cs @@ -13,8 +13,10 @@ namespace Squidex.Domain.Apps.Entities.Backup { Stream OpenStream(DomainId backupId); - Task OpenWriterAsync(Stream stream); + Task OpenWriterAsync(Stream stream, + CancellationToken ct); - Task OpenReaderAsync(Uri url, DomainId id); + Task OpenReaderAsync(Uri url, DomainId id, + CancellationToken ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs index 8028c5a74..c0fc9e60b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupReader.cs @@ -25,7 +25,9 @@ namespace Squidex.Domain.Apps.Entities.Backup Task HasFileAsync(string name, CancellationToken ct = default); - IAsyncEnumerable<(string Stream, Envelope Event)> ReadEventsAsync(IEventStreamNames eventStreams, IEventFormatter eventFormatter, + IAsyncEnumerable<(string Stream, Envelope Event)> ReadEventsAsync( + IEventStreamNames eventStreamNames, + IEventFormatter eventFormatter, CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs index 3765fb691..27de3e77b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupService.cs @@ -11,9 +11,11 @@ namespace Squidex.Domain.Apps.Entities.Backup { public interface IBackupService { - Task StartBackupAsync(DomainId appId, RefToken actor); + Task StartBackupAsync(DomainId appId, RefToken actor, + CancellationToken ct = default); - Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName); + Task StartRestoreAsync(RefToken actor, Uri url, string? newAppName, + CancellationToken ct = default); Task GetRestoreAsync( CancellationToken ct = default); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/Messages.cs similarity index 50% rename from backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Backup/Messages.cs index fc48d4c02..12d153b0c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IRestoreGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/Messages.cs @@ -5,15 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Infrastructure; +#pragma warning disable MA0048 // File name must match type name +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + namespace Squidex.Domain.Apps.Entities.Backup { - public interface IRestoreGrain : IGrainWithStringKey - { - Task RestoreAsync(Uri url, RefToken actor, string? newAppName = null); + public sealed record BackupRestore(RefToken Actor, Uri Url, string? NewAppName = null); + + public sealed record BackupStart(DomainId AppId, RefToken Actor); + + public sealed record BackupDelete(DomainId AppId, DomainId Id); - Task GetStateAsync(); - } + public sealed record BackupClear(DomainId AppId); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs deleted file mode 100644 index 920dd3f53..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs +++ /dev/null @@ -1,435 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Threading.Tasks.Dataflow; -using Microsoft.Extensions.Logging; -using NodaTime; -using Orleans.Core; -using Squidex.Domain.Apps.Core.Apps; -using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Backup.State; -using Squidex.Domain.Apps.Events; -using Squidex.Domain.Apps.Events.Apps; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Tasks; -using Squidex.Infrastructure.Translations; -using Squidex.Shared.Users; - -namespace Squidex.Domain.Apps.Entities.Backup -{ - public sealed class RestoreGrain : GrainBase, IRestoreGrain - { - private readonly IBackupArchiveLocation backupArchiveLocation; - private readonly IBackupHandlerFactory backupHandlers; - private readonly IClock clock; - private readonly ICommandBus commandBus; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly IEventStreamNames eventStreams; - private readonly IGrainState state; - private readonly ILogger log; - private readonly IUserResolver userResolver; - private RestoreContext runningContext; - private StreamMapper runningStreamMapper; - - private RestoreJob CurrentJob - { - get => state.Value.Job; - } - - public RestoreGrain( - IBackupArchiveLocation backupArchiveLocation, - IBackupHandlerFactory backupHandlers, - IClock clock, - ICommandBus commandBus, - IEventFormatter eventFormatter, - IEventStore eventStore, - IEventStreamNames eventStreams, - IGrainIdentity identity, - IGrainState state, - IUserResolver userResolver, - ILogger log) - : base(identity) - { - this.backupArchiveLocation = backupArchiveLocation; - this.backupHandlers = backupHandlers; - this.clock = clock; - this.commandBus = commandBus; - this.eventFormatter = eventFormatter; - this.eventStore = eventStore; - this.eventStreams = eventStreams; - this.state = state; - this.userResolver = userResolver; - this.log = log; - } - - public override Task OnActivateAsync() - { - RecoverAfterRestartAsync().Forget(); - - return Task.CompletedTask; - } - - private async Task RecoverAfterRestartAsync() - { - if (CurrentJob?.Status == JobStatus.Started) - { - Log("Failed due application restart"); - - CurrentJob.Status = JobStatus.Failed; - - await state.WriteAsync(); - } - } - - public async Task RestoreAsync(Uri url, RefToken actor, string? newAppName = null) - { - Guard.NotNull(url); - Guard.NotNull(actor); - - if (!string.IsNullOrWhiteSpace(newAppName)) - { - Guard.ValidSlug(newAppName); - } - - if (CurrentJob?.Status == JobStatus.Started) - { - throw new DomainException(T.Get("backups.restoreRunning")); - } - - state.Value.Job = new RestoreJob - { - Id = DomainId.NewGuid(), - NewAppName = newAppName, - Actor = actor, - Started = clock.GetCurrentInstant(), - Status = JobStatus.Started, - Url = url - }; - - await state.WriteAsync(); - -#pragma warning disable MA0042 // Do not use blocking calls in an async method - Process(); -#pragma warning restore MA0042 // Do not use blocking calls in an async method - } - - private void Process() - { - ProcessAsync().Forget(); - } - - private async Task ProcessAsync() - { - var handlers = backupHandlers.CreateMany(); - - var ct = default(CancellationToken); - - using (Telemetry.Activities.StartActivity("RestoreBackup")) - { - try - { - Log("Started. The restore process has the following steps:"); - Log(" * Download backup"); - Log(" * Restore events and attachments."); - Log(" * Restore all objects like app, schemas and contents"); - Log(" * Complete the restore operation for all objects"); - - log.LogInformation("Backup with job id {backupId} with from URL '{url}' started.", - CurrentJob.Id, - CurrentJob.Url); - - using (var reader = await DownloadAsync()) - { - await reader.CheckCompatibilityAsync(); - - using (Telemetry.Activities.StartActivity("ReadEvents")) - { - await ReadEventsAsync(reader, handlers); - } - - foreach (var handler in handlers) - { - using (Telemetry.Activities.StartActivity($"{handler.GetType().Name}/RestoreAsync")) - { - await handler.RestoreAsync(runningContext, ct); - } - - Log($"Restored {handler.Name}"); - } - - foreach (var handler in handlers) - { - using (Telemetry.Activities.StartActivity($"{handler.GetType().Name}/CompleteRestoreAsync")) - { - await handler.CompleteRestoreAsync(runningContext, CurrentJob.NewAppName!); - } - - Log($"Completed {handler.Name}"); - } - } - - await AssignContributorAsync(); - - CurrentJob.Status = JobStatus.Completed; - - Log("Completed, Yeah!"); - - log.LogInformation("Backup with job id {backupId} from URL '{url}' completed.", - CurrentJob.Id, - CurrentJob.Url); - } - catch (Exception ex) - { - switch (ex) - { - case BackupRestoreException backupException: - Log(backupException.Message); - break; - case FileNotFoundException fileNotFoundException: - Log(fileNotFoundException.Message); - break; - default: - Log("Failed with internal error"); - break; - } - - await CleanupAsync(handlers); - - CurrentJob.Status = JobStatus.Failed; - - log.LogError(ex, "Backup with job id {backupId} from URL '{url}' failed.", - CurrentJob.Id, - CurrentJob.Url); - } - finally - { - CurrentJob.Stopped = clock.GetCurrentInstant(); - - await state.WriteAsync(); - - runningStreamMapper = null!; - runningContext = null!; - } - } - } - - private async Task AssignContributorAsync() - { - var actor = CurrentJob.Actor; - - if (actor?.IsUser == true) - { - try - { - await commandBus.PublishAsync(new AssignContributor - { - Actor = actor, - AppId = CurrentJob.AppId, - ContributorId = actor.Identifier, - Restoring = true, - Role = Role.Owner - }); - - Log("Assigned current user."); - } - catch (DomainException ex) - { - Log($"Failed to assign contributor: {ex.Message}"); - } - } - else - { - Log("Current user not assigned because restore was triggered by client."); - } - } - - private async Task CleanupAsync(IEnumerable handlers) - { - if (CurrentJob.AppId != null) - { - var appId = CurrentJob.AppId.Id; - - foreach (var handler in handlers) - { - try - { - await handler.CleanupRestoreErrorAsync(appId); - } - catch (Exception ex) - { - log.LogError(ex, "Failed to clean up restore."); - } - } - } - } - - private async Task DownloadAsync() - { - using (Telemetry.Activities.StartActivity("Download")) - { - Log("Downloading Backup"); - - var reader = await backupArchiveLocation.OpenReaderAsync(CurrentJob.Url, CurrentJob.Id); - - Log("Downloaded Backup"); - - return reader; - } - } - - private async Task ReadEventsAsync(IBackupReader reader, IEnumerable handlers) - { - const int BatchSize = 100; - - var handled = 0; - - var writeBlock = new ActionBlock<(string, Envelope)[]>(async batch => - { - try - { - var commits = new List(batch.Length); - - foreach (var (stream, @event) in batch) - { - var offset = runningStreamMapper.GetStreamOffset(stream); - - commits.Add(EventCommit.Create(stream, offset, @event, eventFormatter)); - } - - await eventStore.AppendUnsafeAsync(commits); - - handled += commits.Count; - - Log($"Reading {reader.ReadEvents}/{handled} events and {reader.ReadAttachments} attachments completed.", true); - } - catch (OperationCanceledException ex) - { - // Dataflow swallows operation cancelled exception. - throw new AggregateException(ex); - } - }, new ExecutionDataflowBlockOptions - { - MaxDegreeOfParallelism = 1, - MaxMessagesPerTask = 1, - BoundedCapacity = 2 - }); - - var batchBlock = new BatchBlock<(string, Envelope)>(BatchSize, new GroupingDataflowBlockOptions - { - BoundedCapacity = BatchSize * 2 - }); - - batchBlock.BidirectionalLinkTo(writeBlock); - - await foreach (var job in reader.ReadEventsAsync(eventStreams, eventFormatter)) - { - var newStream = await HandleEventAsync(reader, handlers, job.Stream, job.Event); - - if (newStream != null) - { - if (!await batchBlock.SendAsync((newStream, job.Event))) - { - break; - } - } - } - - batchBlock.Complete(); - - await writeBlock.Completion; - } - - private async Task HandleEventAsync(IBackupReader reader, IEnumerable handlers, string stream, Envelope @event, - CancellationToken ct = default) - { - if (@event.Payload is AppCreated appCreated) - { - var previousAppId = appCreated.AppId.Id; - - if (!string.IsNullOrWhiteSpace(CurrentJob.NewAppName)) - { - appCreated.Name = CurrentJob.NewAppName; - - CurrentJob.AppId = NamedId.Of(DomainId.NewGuid(), CurrentJob.NewAppName); - } - else - { - CurrentJob.AppId = NamedId.Of(DomainId.NewGuid(), appCreated.Name); - } - - await CreateContextAsync(reader, previousAppId); - } - - if (@event.Payload is SquidexEvent squidexEvent && squidexEvent.Actor != null) - { - if (runningContext.UserMapping.TryMap(squidexEvent.Actor, out var newUser)) - { - squidexEvent.Actor = newUser; - } - } - - if (@event.Payload is AppEvent appEvent) - { - appEvent.AppId = CurrentJob.AppId; - } - - var (newStream, id) = runningStreamMapper.Map(stream); - - @event.SetAggregateId(id); - @event.SetRestored(); - - foreach (var handler in handlers) - { - if (!await handler.RestoreEventAsync(@event, runningContext, ct)) - { - return null; - } - } - - return newStream; - } - - private async Task CreateContextAsync(IBackupReader reader, DomainId previousAppId) - { - var userMapping = new UserMapping(CurrentJob.Actor); - - using (Telemetry.Activities.StartActivity("CreateUsers")) - { - Log("Creating Users"); - - await userMapping.RestoreAsync(reader, userResolver); - - Log("Created Users"); - } - - runningContext = new RestoreContext(CurrentJob.AppId.Id, userMapping, reader, previousAppId); - runningStreamMapper = new StreamMapper(runningContext); - } - - private void Log(string message, bool replace = false) - { - if (replace && CurrentJob.Log.Count > 0) - { - CurrentJob.Log[^1] = $"{clock.GetCurrentInstant()}: {message}"; - } - else - { - CurrentJob.Log.Add($"{clock.GetCurrentInstant()}: {message}"); - } - } - - public Task GetStateAsync() - { - return Task.FromResult(CurrentJob); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs new file mode 100644 index 000000000..5420464ae --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/RestoreProcessor.cs @@ -0,0 +1,465 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using System.Threading.Tasks.Dataflow; +using Microsoft.Extensions.Logging; +using NodaTime; +using Squidex.Domain.Apps.Core.Apps; +using Squidex.Domain.Apps.Entities.Apps.Commands; +using Squidex.Domain.Apps.Entities.Backup.State; +using Squidex.Domain.Apps.Events; +using Squidex.Domain.Apps.Events.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.Translations; +using Squidex.Shared.Users; + +namespace Squidex.Domain.Apps.Entities.Backup +{ + public sealed class RestoreProcessor + { + private readonly IBackupArchiveLocation backupArchiveLocation; + private readonly IBackupHandlerFactory backupHandlerFactory; + private readonly ICommandBus commandBus; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly IEventStreamNames eventStreamNames; + private readonly IUserResolver userResolver; + private readonly ILogger log; + private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); + private readonly SimpleState state; + private Run? currentRun; + + // Use a run to store all state that is necessary for a single run. + private sealed class Run : IDisposable + { + private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly CancellationTokenSource cancellationLinked; + private readonly IClock clock; + + public IEnumerable Handlers { get; init; } + + public IBackupReader Reader { get; set; } + + public RestoreJob Job { get; init; } + + public RestoreContext Context { get; set; } + + public StreamMapper StreamMapper { get; set; } + + public CancellationToken CancellationToken => cancellationLinked.Token; + + public Run(IClock clock, CancellationToken ct) + { + cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token); + + this.clock = clock; + } + + public void Log(string message, bool replace = false) + { + if (replace && Job.Log.Count > 0) + { + Job.Log[^1] = $"{clock.GetCurrentInstant()}: {message}"; + } + else + { + Job.Log.Add($"{clock.GetCurrentInstant()}: {message}"); + } + } + + public void Dispose() + { + Reader?.Dispose(); + + cancellationSource.Dispose(); + cancellationLinked.Dispose(); + } + + public void Cancel() + { + try + { + cancellationSource.Cancel(); + } + catch (ObjectDisposedException) + { + // Cancellation token might have been disposed, if the run is completed. + } + } + } + + public IClock Clock { get; set; } = SystemClock.Instance; + + public RestoreProcessor( + IBackupArchiveLocation backupArchiveLocation, + IBackupHandlerFactory backupHandlerFactory, + ICommandBus commandBus, + IEventFormatter eventFormatter, + IEventStore eventStore, + IEventStreamNames eventStreamNames, + IPersistenceFactory persistenceFactory, + IUserResolver userResolver, + ILogger log) + { + this.backupArchiveLocation = backupArchiveLocation; + this.backupHandlerFactory = backupHandlerFactory; + this.commandBus = commandBus; + this.eventFormatter = eventFormatter; + this.eventStore = eventStore; + this.eventStreamNames = eventStreamNames; + this.userResolver = userResolver; + this.log = log; + + state = new SimpleState(persistenceFactory, GetType(), "Default"); + } + + public async Task LoadAsync( + CancellationToken ct) + { + await state.LoadAsync(ct); + + if (state.Value.Job?.Status == JobStatus.Started) + { + state.Value.Job.Status = JobStatus.Failed; + + await state.WriteAsync(ct); + } + } + + public Task RestoreAsync(Uri url, RefToken actor, string? newAppName, + CancellationToken ct) + { + Guard.NotNull(url); + Guard.NotNull(actor); + + if (!string.IsNullOrWhiteSpace(newAppName)) + { + Guard.ValidSlug(newAppName); + } + + return scheduler.ScheduleAsync(async ct => + { + if (currentRun != null) + { + throw new DomainException(T.Get("backups.restoreRunning")); + } + + state.Value.Job?.EnsureCanStart(); + + // Set the current run first to indicate that we are running a rule at the moment. + var run = currentRun = new Run(Clock, ct) + { + Job = new RestoreJob + { + Id = DomainId.NewGuid(), + NewAppName = newAppName, + Actor = actor, + Started = Clock.GetCurrentInstant(), + Status = JobStatus.Started, + Url = url + }, + Handlers = backupHandlerFactory.CreateMany() + }; + + state.Value.Job = run.Job; + try + { + await state.WriteAsync(run.CancellationToken); + + await ProcessAsync(run, run.CancellationToken); + } + finally + { + // Unset the run to indicate that we are done. + currentRun.Dispose(); + currentRun = null; + } + }, ct); + } + + private async Task ProcessAsync(Run run, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("RestoreBackup")) + { + try + { + run.Log("Started. The restore process has the following steps:"); + run.Log(" * Download backup"); + run.Log(" * Restore events and attachments."); + run.Log(" * Restore all objects like app, schemas and contents"); + run.Log(" * Complete the restore operation for all objects"); + + log.LogInformation("Backup with job id {backupId} with from URL '{url}' started.", run.Job.Id, run.Job.Url); + + run.Reader = await DownloadAsync(run, ct); + + await run.Reader.CheckCompatibilityAsync(); + + using (Telemetry.Activities.StartActivity("ReadEvents")) + { + await ReadEventsAsync(run, ct); + } + + foreach (var handler in run.Handlers) + { + using (Telemetry.Activities.StartActivity($"{handler.GetType().Name}/RestoreAsync")) + { + await handler.RestoreAsync(run.Context, ct); + } + + run.Log($"Restored {handler.Name}"); + } + + foreach (var handler in run.Handlers) + { + using (Telemetry.Activities.StartActivity($"{handler.GetType().Name}/CompleteRestoreAsync")) + { + await handler.CompleteRestoreAsync(run.Context, run.Job.NewAppName!); + } + + run.Log($"Completed {handler.Name}"); + } + + await AssignContributorAsync(run); + + run.Job.Status = JobStatus.Completed; + run.Log("Completed, Yeah!"); + + log.LogInformation("Backup with job id {backupId} from URL '{url}' completed.", run.Job.Id, run.Job.Url); + } + catch (Exception ex) + { + switch (ex) + { + case BackupRestoreException backupException: + run.Log(backupException.Message); + break; + case FileNotFoundException fileNotFoundException: + run.Log(fileNotFoundException.Message); + break; + default: + run.Log("Failed with internal error"); + break; + } + + await CleanupAsync(run); + + run.Job.Status = JobStatus.Failed; + + log.LogError(ex, "Backup with job id {backupId} from URL '{url}' failed.", run.Job.Id, run.Job.Url); + } + finally + { + run.Job.Stopped = Clock.GetCurrentInstant(); + + await state.WriteAsync(ct); + } + } + } + + private async Task AssignContributorAsync(Run run) + { + if (run.Job.Actor?.IsUser != true) + { + run.Log("Current user not assigned because restore was triggered by client."); + return; + } + + try + { + var command = new AssignContributor + { + Actor = run.Job.Actor, + AppId = run.Job.AppId, + ContributorId = run.Job.Actor.Identifier, + Restoring = true, + Role = Role.Owner + }; + + await commandBus.PublishAsync(command, default); + + run.Log("Assigned current user."); + } + catch (DomainException ex) + { + run.Log($"Failed to assign contributor: {ex.Message}"); + } + } + + private async Task CleanupAsync(Run run) + { + if (run.Job.AppId == null) + { + return; + } + + foreach (var handler in run.Handlers) + { + try + { + await handler.CleanupRestoreErrorAsync(run.Job.AppId.Id); + } + catch (Exception ex) + { + log.LogError(ex, "Failed to clean up restore."); + } + } + } + + private async Task DownloadAsync(Run run, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("Download")) + { + run.Log("Downloading Backup"); + + var reader = await backupArchiveLocation.OpenReaderAsync(run.Job.Url, run.Job.Id, ct); + + run.Log("Downloaded Backup"); + + return reader; + } + } + + private async Task ReadEventsAsync(Run run, + CancellationToken ct) + { + const int BatchSize = 100; + + var handled = 0; + + var writeBlock = new ActionBlock<(string, Envelope)[]>(async batch => + { + try + { + var commits = new List(batch.Length); + + foreach (var (stream, @event) in batch) + { + var offset = run.StreamMapper.GetStreamOffset(stream); + + commits.Add(EventCommit.Create(stream, offset, @event, eventFormatter)); + } + + await eventStore.AppendUnsafeAsync(commits, ct); + + handled += commits.Count; + + run.Log($"Reading {run.Reader.ReadEvents}/{handled} events and {run.Reader.ReadAttachments} attachments completed.", true); + } + catch (OperationCanceledException ex) + { + // Dataflow swallows operation cancelled exception. + throw new AggregateException(ex); + } + }, new ExecutionDataflowBlockOptions + { + MaxDegreeOfParallelism = 1, + MaxMessagesPerTask = 1, + BoundedCapacity = 2 + }); + + var batchBlock = new BatchBlock<(string, Envelope)>(BatchSize, new GroupingDataflowBlockOptions + { + BoundedCapacity = BatchSize * 2 + }); + + batchBlock.BidirectionalLinkTo(writeBlock); + + await foreach (var job in run.Reader.ReadEventsAsync(eventStreamNames, eventFormatter, ct)) + { + var newStream = await HandleEventAsync(run, job.Stream, job.Event, ct); + + if (newStream != null) + { + if (!await batchBlock.SendAsync((newStream, job.Event))) + { + break; + } + } + } + + batchBlock.Complete(); + + await writeBlock.Completion; + } + + private async Task HandleEventAsync(Run run, string stream, Envelope @event, + CancellationToken ct = default) + { + if (@event.Payload is AppCreated appCreated) + { + var previousAppId = appCreated.AppId.Id; + + if (!string.IsNullOrWhiteSpace(run.Job.NewAppName)) + { + appCreated.Name = run.Job.NewAppName; + + run.Job.AppId = NamedId.Of(DomainId.NewGuid(), run.Job.NewAppName); + } + else + { + run.Job.AppId = NamedId.Of(DomainId.NewGuid(), appCreated.Name); + } + + await CreateContextAsync(run, previousAppId, ct); + + run.StreamMapper = new StreamMapper(run.Context); + } + + if (@event.Payload is SquidexEvent squidexEvent && squidexEvent.Actor != null) + { + if (run.Context.UserMapping.TryMap(squidexEvent.Actor, out var newUser)) + { + squidexEvent.Actor = newUser; + } + } + + if (@event.Payload is AppEvent appEvent) + { + appEvent.AppId = run.Job.AppId; + } + + var (newStream, id) = run.StreamMapper.Map(stream); + + @event.SetAggregateId(id); + @event.SetRestored(); + + foreach (var handler in run.Handlers) + { + if (!await handler.RestoreEventAsync(@event, run.Context, ct)) + { + return null; + } + } + + return newStream; + } + + private async Task CreateContextAsync(Run run, DomainId previousAppId, + CancellationToken ct) + { + var userMapping = new UserMapping(run.Job.Actor); + + using (Telemetry.Activities.StartActivity("CreateUsers")) + { + run.Log("Creating Users"); + + await userMapping.RestoreAsync(run.Reader, userResolver, ct); + + run.Log("Created Users"); + } + + run.Context = new RestoreContext(run.Job.AppId.Id, userMapping, run.Reader, previousAppId); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs index aa3bd9743..b12dd5c3f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupRestoreState.cs @@ -9,6 +9,6 @@ namespace Squidex.Domain.Apps.Entities.Backup.State { public class BackupRestoreState { - public RestoreJob Job { get; set; } + public RestoreJob? Job { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs index b0186b02a..8da62b28b 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/BackupState.cs @@ -5,10 +5,26 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure; +using Squidex.Infrastructure.Translations; + namespace Squidex.Domain.Apps.Entities.Backup.State { public sealed class BackupState { public List Jobs { get; } = new List(); + + public void EnsureCanStart() + { + if (Jobs.Any(x => x.Status == JobStatus.Started)) + { + throw new DomainException(T.Get("backups.alreadyRunning")); + } + + if (Jobs.Count >= 10) + { + throw new DomainException(T.Get("backups.maxReached", new { max = 10 })); + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreJob.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreJob.cs index 739730f49..1e186821c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreJob.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/State/RestoreJob.cs @@ -7,6 +7,7 @@ using NodaTime; using Squidex.Infrastructure; +using Squidex.Infrastructure.Translations; namespace Squidex.Domain.Apps.Entities.Backup.State { @@ -31,5 +32,13 @@ namespace Squidex.Domain.Apps.Entities.Backup.State public JobStatus Status { get; set; } public string? NewAppName { get; set; } + + public void EnsureCanStart() + { + if (Status == JobStatus.Started) + { + throw new DomainException(T.Get("backups.restoreRunning")); + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs index d85677c28..8d31cca07 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Backup/TempFolderBackupArchiveLocation.cs @@ -14,14 +14,15 @@ namespace Squidex.Domain.Apps.Entities.Backup [ExcludeFromCodeCoverage] public sealed class TempFolderBackupArchiveLocation : IBackupArchiveLocation { - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; - public TempFolderBackupArchiveLocation(IJsonSerializer jsonSerializer) + public TempFolderBackupArchiveLocation(IJsonSerializer serializer) { - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; } - public async Task OpenReaderAsync(Uri url, DomainId id) + public async Task OpenReaderAsync(Uri url, DomainId id, + CancellationToken ct) { Stream stream; @@ -40,12 +41,12 @@ namespace Squidex.Domain.Apps.Entities.Backup { client.Timeout = TimeSpan.FromHours(1); - response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); + response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct); response.EnsureSuccessStatusCode(); - await using (var sourceStream = await response.Content.ReadAsStreamAsync()) + await using (var sourceStream = await response.Content.ReadAsStreamAsync(ct)) { - await sourceStream.CopyToAsync(stream); + await sourceStream.CopyToAsync(stream, ct); } } } @@ -63,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities.Backup try { - return new BackupReader(jsonSerializer, stream); + return new BackupReader(serializer, stream); } catch (IOException) { @@ -94,9 +95,10 @@ namespace Squidex.Domain.Apps.Entities.Backup return fileStream; } - public Task OpenWriterAsync(Stream stream) + public Task OpenWriterAsync(Stream stream, + CancellationToken ct) { - var writer = new BackupWriter(jsonSerializer, stream, true); + var writer = new BackupWriter(serializer, stream, true); return Task.FromResult(writer); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs index 6ed058323..1fc3c5639 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/Commands/CommentsCommand.cs @@ -6,15 +6,18 @@ // ========================================================================== using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Comments.Commands { - public abstract class CommentsCommand : SquidexCommand, IAppCommand + public abstract class CommentsCommand : SquidexCommand, IAppCommand, IAggregateCommand { public NamedId AppId { get; set; } public DomainId CommentsId { get; set; } public DomainId CommentId { get; set; } + + DomainId IAggregateCommand.AggregateId => DomainId.Combine(AppId.Id, CommentsId); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs index 5d387d4ba..022aa5948 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/CommentsLoader.cs @@ -5,26 +5,29 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Domain.Apps.Entities.Comments.DomainObject; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Comments { public sealed class CommentsLoader : ICommentsLoader { - private readonly IGrainFactory grainFactory; + private readonly IDomainObjectFactory domainObjectFactory; - public CommentsLoader(IGrainFactory grainFactory) + public CommentsLoader(IDomainObjectFactory domainObjectFactory) { - this.grainFactory = grainFactory; + this.domainObjectFactory = domainObjectFactory; } - public Task GetCommentsAsync(DomainId id, long version = EtagVersion.Any) + public async Task GetCommentsAsync(DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default) { - var grain = grainFactory.GetGrain(id.ToString()); + var stream = domainObjectFactory.Create(id); - return grain.GetCommentsAsync(version); + await stream.LoadAsync(ct); + + return stream.GetComments(version); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs index 048691a68..bf3b746ef 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsCommandMiddleware.cs @@ -6,27 +6,25 @@ // ========================================================================== using System.Text.RegularExpressions; -using Orleans; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Infrastructure.Commands; using Squidex.Shared.Users; namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { - public sealed class CommentsCommandMiddleware : ICommandMiddleware + public sealed class CommentsCommandMiddleware : AggregateCommandMiddleware { private static readonly Regex MentionRegex = new Regex(@"@(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+\/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*", RegexOptions.Compiled | RegexOptions.ExplicitCapture, TimeSpan.FromMilliseconds(100)); - private readonly IGrainFactory grainFactory; private readonly IUserResolver userResolver; - public CommentsCommandMiddleware(IGrainFactory grainFactory, IUserResolver userResolver) + public CommentsCommandMiddleware(IDomainObjectFactory domainObjectFactory, IUserResolver userResolver) + : base(domainObjectFactory) { - this.grainFactory = grainFactory; - this.userResolver = userResolver; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public override async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (context.Command is CommentsCommand commentsCommand) { @@ -34,21 +32,9 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { await MentionUsersAsync(createComment); } - - await ExecuteCommandAsync(commentsCommand); } - await next(context); - } - - private Task ExecuteCommandAsync(CommentsCommand commentsCommand) - { - return GetGrain(commentsCommand).ExecuteAsync(commentsCommand); - } - - private ICommentsGrain GetGrain(CommentsCommand commentsCommand) - { - return grainFactory.GetGrain(commentsCommand.CommentsId.ToString()); + await base.HandleAsync(context, next, ct); } private static bool IsMention(CreateComment createComment) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs similarity index 78% rename from backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs index e5f5c854a..dcd22e39c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/CommentsStream.cs @@ -5,24 +5,23 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans.Core; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Entities.Comments.DomainObject.Guards; using Squidex.Domain.Apps.Events.Comments; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; #pragma warning disable MA0022 // Return Task.FromResult instead of returning null namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { - public sealed class CommentsGrain : GrainBase, ICommentsGrain + public class CommentsStream : IAggregate { private readonly List> uncommittedEvents = new List>(); private readonly List> events = new List>(); + private readonly DomainId key; private readonly IEventFormatter eventFormatter; private readonly IEventStore eventStore; private long version = EtagVersion.Empty; @@ -33,20 +32,22 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject get => version; } - public CommentsGrain(IGrainIdentity identity, + public CommentsStream( + DomainId key, IEventFormatter eventFormatter, IEventStore eventStore) - : base(identity) { + this.key = key; this.eventFormatter = eventFormatter; this.eventStore = eventStore; } - public override async Task OnActivateAsync() + public virtual async Task LoadAsync( + CancellationToken ct) { - streamName = $"comments-{Key}"; + streamName = $"comments-{key}"; - var storedEvents = await eventStore.QueryReverseAsync(streamName, 100); + var storedEvents = await eventStore.QueryReverseAsync(streamName, 100, ct); foreach (var @event in storedEvents) { @@ -58,7 +59,8 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject } } - public Task ExecuteAsync(CommentsCommand command) + public virtual Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) { switch (command) { @@ -68,23 +70,23 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject GuardComments.CanCreate(c); Create(c); - }); + }, ct); case UpdateComment updateComment: return Upsert(updateComment, c => { - GuardComments.CanUpdate(c, Key.ToString(), events); + GuardComments.CanUpdate(c, key.ToString(), events); Update(c); - }); + }, ct); case DeleteComment deleteComment: return Upsert(deleteComment, c => { - GuardComments.CanDelete(c, Key.ToString(), events); + GuardComments.CanDelete(c, key.ToString(), events); Delete(c); - }); + }, ct); default: ThrowHelper.NotSupportedException(); @@ -92,14 +94,15 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject } } - private async Task Upsert(TCommand command, Action handler) where TCommand : CommentsCommand + private async Task Upsert(TCommand command, Action handler, + CancellationToken ct) where TCommand : CommentsCommand { Guard.NotNull(command); Guard.NotNull(handler); if (command.ExpectedVersion > EtagVersion.Any && command.ExpectedVersion != Version) { - throw new DomainObjectVersionException(Key.ToString(), Version, command.ExpectedVersion); + throw new DomainObjectVersionException(key.ToString(), Version, command.ExpectedVersion); } var previousVersion = version; @@ -114,12 +117,12 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var eventData = uncommittedEvents.Select(x => eventFormatter.ToEventData(x, commitId)).ToList(); - await eventStore.AppendAsync(commitId, streamName, previousVersion, eventData); + await eventStore.AppendAsync(commitId, streamName, previousVersion, eventData, ct); } events.AddRange(uncommittedEvents); - return CommandResult.Empty(Key, Version, previousVersion); + return CommandResult.Empty(key, Version, previousVersion); } catch { @@ -155,14 +158,14 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject version++; } - public List> GetUncommittedEvents() + public virtual List> GetUncommittedEvents() { return uncommittedEvents; } - public Task GetCommentsAsync(long sinceVersion = EtagVersion.Any) + public virtual CommentsResult GetComments(long sinceVersion = EtagVersion.Any) { - return Task.FromResult(CommentsResult.FromEvents(events, Version, (int)sinceVersion)); + return CommentsResult.FromEvents(events, Version, (int)sinceVersion); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/ICommentsGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/ICommentsGrain.cs deleted file mode 100644 index 978933d09..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/DomainObject/ICommentsGrain.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Domain.Apps.Entities.Comments.Commands; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Comments.DomainObject -{ - public interface ICommentsGrain : IGrainWithStringKey - { - Task ExecuteAsync(CommentsCommand command); - - Task GetCommentsAsync(long sinceVersion = EtagVersion.Any); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/GrainWatchingService.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/GrainWatchingService.cs deleted file mode 100644 index 746e0ab6c..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/GrainWatchingService.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Comments -{ - public sealed class GrainWatchingService : IWatchingService - { - private readonly IGrainFactory grainFactory; - - public GrainWatchingService(IGrainFactory grainFactory) - { - this.grainFactory = grainFactory; - } - - public Task GetWatchingUsersAsync(DomainId appId, string resource, string userId) - { - Guard.NotNullOrEmpty(resource); - Guard.NotNullOrEmpty(userId); - - return GetGrain(appId).GetWatchingUsersAsync(resource, userId); - } - - private IWatchingGrain GetGrain(DomainId appId) - { - return grainFactory.GetGrain(appId.ToString()); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs index 28a074166..572f72a28 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/ICommentsLoader.cs @@ -11,6 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Comments { public interface ICommentsLoader { - Task GetCommentsAsync(DomainId id, long version = EtagVersion.Any); + Task GetCommentsAsync(DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingGrain.cs deleted file mode 100644 index fff24d4f6..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingGrain.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; - -namespace Squidex.Domain.Apps.Entities.Comments -{ - public interface IWatchingGrain : IGrainWithStringKey - { - Task GetWatchingUsersAsync(string resource, string userId); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs index 8c6607eb4..fedebc222 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/IWatchingService.cs @@ -11,6 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Comments { public interface IWatchingService { - Task GetWatchingUsersAsync(DomainId appId, string resource, string userId); + Task GetWatchingUsersAsync(DomainId appId, string? resource, string userId, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingGrain.cs deleted file mode 100644 index c02889785..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingGrain.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using NodaTime; -using Orleans.Core; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Comments -{ - public sealed class WatchingGrain : GrainBase, IWatchingGrain - { - private static readonly Duration Timeout = Duration.FromMinutes(1); - private readonly Dictionary> users = new Dictionary>(); - private readonly IClock clock; - - public WatchingGrain(IGrainIdentity grainIdentity, IClock clock) - : base(grainIdentity) - { - this.clock = clock; - } - - public override Task OnActivateAsync() - { - var time = TimeSpan.FromSeconds(30); - - RegisterTimer(x => - { - Cleanup(); - - return Task.CompletedTask; - }, null, time, time); - - return Task.CompletedTask; - } - - public Task GetWatchingUsersAsync(string resource, string userId) - { - Guard.NotNullOrEmpty(resource); - Guard.NotNullOrEmpty(userId); - - var usersByResource = users.GetOrAddNew(resource); - - usersByResource[userId] = clock.GetCurrentInstant(); - - return Task.FromResult(usersByResource.Keys.ToArray()); - } - - public void Cleanup() - { - if (users.Count == 0) - { - return; - } - - var now = clock.GetCurrentInstant(); - - foreach (var (resource, usersByResource) in users.ToList()) - { - foreach (var (userId, lastSeen) in usersByResource.ToList()) - { - var timeSinceLastSeen = now - lastSeen; - - if (timeSinceLastSeen > Timeout) - { - usersByResource.Remove(userId); - } - } - - if (usersByResource.Count == 0) - { - users.Remove(resource); - } - } - - if (users.Count == 0) - { - TryDeactivateOnIdle(); - } - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs b/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs new file mode 100644 index 000000000..d30a43122 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Comments/WatchingService.cs @@ -0,0 +1,62 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using NodaTime; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Comments +{ + public sealed class WatchingService : IWatchingService + { + private readonly IPersistenceFactory persistenceFactory; + + [CollectionName("Watches")] + public sealed class State + { + private static readonly Duration Timeout = Duration.FromMinutes(1); + + public Dictionary Users { get; } = new Dictionary(); + + public string[] Add(string watcherId, IClock clock) + { + var now = clock.GetCurrentInstant(); + + foreach (var (userId, lastSeen) in Users.ToList()) + { + var timeSinceLastSeen = now - lastSeen; + + if (timeSinceLastSeen > Timeout) + { + Users.Remove(userId); + } + } + + Users[watcherId] = now; + + return Users.Keys.ToArray(); + } + } + + public IClock Clock { get; set; } = SystemClock.Instance; + + public WatchingService(IPersistenceFactory persistenceFactory) + { + this.persistenceFactory = persistenceFactory; + } + + public async Task GetWatchingUsersAsync(DomainId appId, string? resource, string userId, + CancellationToken ct = default) + { + var state = new SimpleState(persistenceFactory, GetType(), $"{appId}_{resource}"); + + await state.LoadAsync(ct); + + return await state.UpdateAsync(x => x.Add(userId, Clock), ct: ct); + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs index 3441912ba..fd90fe85e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentChangedTriggerHandler.cs @@ -83,7 +83,8 @@ namespace Squidex.Domain.Apps.Entities.Contents await contentLoader.GetAsync( contentEvent.AppId.Id, contentEvent.ContentId, - @event.Headers.EventStreamNumber()); + @event.Headers.EventStreamNumber(), + ct); if (content != null) { @@ -117,7 +118,8 @@ namespace Squidex.Domain.Apps.Entities.Contents await contentLoader.GetAsync( content.AppId.Id, content.Id, - content.Version - 1); + content.Version - 1, + ct); if (previousContent != null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerProcess.cs similarity index 68% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerProcess.cs index 538ffff3d..23b39a42a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentSchedulerProcess.cs @@ -7,56 +7,54 @@ using Microsoft.Extensions.Logging; using NodaTime; -using Orleans; -using Orleans.Runtime; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Repositories; +using Squidex.Hosting; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.Timers; namespace Squidex.Domain.Apps.Entities.Contents { - public sealed class ContentSchedulerGrain : Grain, IContentSchedulerGrain, IRemindable + public sealed class ContentSchedulerProcess : IBackgroundProcess { private readonly IContentRepository contentRepository; private readonly ICommandBus commandBus; - private readonly IClock clock; - private readonly ILogger log; + private readonly ILogger log; + private CompletionTimer timer; - public ContentSchedulerGrain( + public IClock Clock { get; set; } = SystemClock.Instance; + + public ContentSchedulerProcess( IContentRepository contentRepository, ICommandBus commandBus, - IClock clock, - ILogger log) + ILogger log) { - this.clock = clock; - this.commandBus = commandBus; this.contentRepository = contentRepository; - this.log = log; } - public override Task OnActivateAsync() + public Task StartAsync( + CancellationToken ct) { - DelayDeactivation(TimeSpan.FromDays(1)); + timer = new CompletionTimer((int)TimeSpan.FromSeconds(10).TotalMilliseconds, PublishAsync); - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => PublishAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); - - return Task.FromResult(true); + return Task.CompletedTask; } - public Task ActivateAsync() + public Task StopAsync( + CancellationToken ct) { - return Task.CompletedTask; + return timer?.StopAsync() ?? Task.CompletedTask; } - public async Task PublishAsync() + public async Task PublishAsync( + CancellationToken ct = default) { - var now = clock.GetCurrentInstant(); + var now = Clock.GetCurrentInstant(); - await foreach (var content in contentRepository.QueryScheduledWithoutDataAsync(now)) + await foreach (var content in contentRepository.QueryScheduledWithoutDataAsync(now, ct)) { await TryPublishAsync(content); } @@ -82,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Contents StatusJobId = job.Id }; - await commandBus.PublishAsync(command); + await commandBus.PublishAsync(command, default); } } catch (DomainObjectNotFoundException) @@ -94,10 +92,5 @@ namespace Squidex.Domain.Apps.Entities.Contents log.LogError(ex, "Failed to execute scheduled status change for content '{contentId}'.", content.Id); } } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return Task.CompletedTask; - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterDeleter.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterDeleter.cs deleted file mode 100644 index 07a9c3c7d..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterDeleter.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Domain.Apps.Entities.Apps; - -namespace Squidex.Domain.Apps.Entities.Contents.Counter -{ - public sealed class CounterDeleter : IDeleter - { - private readonly IGrainFactory grainFactory; - - public CounterDeleter(IGrainFactory grainFactory) - { - this.grainFactory = grainFactory; - } - - public Task DeleteAppAsync(IAppEntity app, - CancellationToken ct) - { - var grain = grainFactory.GetGrain(app.Id.ToString()); - - return grain.ClearAsync(); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterGrain.cs deleted file mode 100644 index 7048742ee..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterGrain.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Contents.Counter -{ - public sealed class CounterGrain : GrainBase, ICounterGrain - { - private readonly IGrainState state; - - [CollectionName("Counters")] - public sealed class State - { - public Dictionary Counters { get; set; } = new Dictionary(); - } - - public CounterGrain(IGrainIdentity identity, IGrainState state) - : base(identity) - { - this.state = state; - } - - public Task ClearAsync() - { - TryDeactivateOnIdle(); - - return state.ClearAsync(); - } - - public Task IncrementAsync(string name) - { - state.Value.Counters.TryGetValue(name, out var value); - - return ResetAsync(name, value + 1); - } - - public async Task ResetAsync(string name, long value) - { - state.Value.Counters[name] = value; - - await state.WriteAsync(); - - return value; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs index e0d6f158f..06901762a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterJintExtension.cs @@ -6,7 +6,6 @@ // ========================================================================== using Jint.Native; -using Orleans; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Domain.Apps.Entities.Properties; using Squidex.Infrastructure; @@ -18,11 +17,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter { private delegate long CounterReset(string name, long value = 0); private delegate void CounterResetV2(string name, Action? callback = null, long value = 0); - private readonly IGrainFactory grainFactory; + private readonly ICounterService counterService; - public CounterJintExtension(IGrainFactory grainFactory) + public CounterJintExtension(ICounterService counterService) { - this.grainFactory = grainFactory; + this.counterService = counterService; } public void Extend(ScriptExecutionContext context) @@ -86,18 +85,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter private long Increment(DomainId appId, string name) { - var grain = grainFactory.GetGrain(appId.ToString()); - - return AsyncHelper.Sync(() => grain.IncrementAsync(name)); + return AsyncHelper.Sync(() => counterService.IncrementAsync(appId, name)); } private void IncrementV2(ScriptExecutionContext context, DomainId appId, string name, Action callback) { context.Schedule(async (scheduler, ct) => { - var grain = grainFactory.GetGrain(appId.ToString()); - - var result = await grain.IncrementAsync(name); + var result = await counterService.IncrementAsync(appId, name); if (callback != null) { @@ -108,18 +103,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter private long Reset(DomainId appId, string name, long value) { - var grain = grainFactory.GetGrain(appId.ToString()); - - return AsyncHelper.Sync(() => grain.ResetAsync(name, value)); + return AsyncHelper.Sync(() => counterService.ResetAsync(appId, name, value)); } private void ResetV2(ScriptExecutionContext context, DomainId appId, string name, Action? callback, long value) { context.Schedule(async (scheduler, ct) => { - var grain = grainFactory.GetGrain(appId.ToString()); - - var result = await grain.ResetAsync(name, value); + var result = await counterService.ResetAsync(appId, name, value); if (callback != null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs new file mode 100644 index 000000000..16c8ddb3c --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/CounterService.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Entities.Apps; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Contents.Counter +{ + public sealed class CounterService : ICounterService, IDeleter + { + private readonly IPersistenceFactory persistenceFactory; + + [CollectionName("Counters")] + public sealed class State + { + public Dictionary Counters { get; set; } = new Dictionary(); + + public void Increment(string name) + { + Counters[name] = Counters.GetValueOrDefault(name) + 1; + } + + public void Reset(string name, long value) + { + Counters[name] = value; + } + } + + public CounterService(IPersistenceFactory persistenceFactory) + { + this.persistenceFactory = persistenceFactory; + } + + async Task IDeleter.DeleteAppAsync(IAppEntity app, + CancellationToken ct) + { + var state = await GetStateAsync(app.Id, ct); + + await state.ClearAsync(ct); + } + + public async Task IncrementAsync(DomainId appId, string name, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, ct); + + await state.UpdateAsync(x => x.Increment(name), ct: ct); + + return state.Value.Counters[name]; + } + + public async Task ResetAsync(DomainId appId, string name, long value, + CancellationToken ct = default) + { + var state = await GetStateAsync(appId, ct); + + await state.UpdateAsync(x => x.Reset(name, value), ct: ct); + + return state.Value.Counters[name]; + } + + private async Task> GetStateAsync(DomainId appId, + CancellationToken ct) + { + var state = new SimpleState(persistenceFactory, GetType(), appId); + + await state.LoadAsync(ct); + + return state; + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterService.cs similarity index 60% rename from backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterService.cs index 1318e107f..1ed274b62 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Counter/ICounterService.cs @@ -5,16 +5,16 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; +using Squidex.Infrastructure; namespace Squidex.Domain.Apps.Entities.Contents.Counter { - public interface ICounterGrain : IGrainWithStringKey + public interface ICounterService { - Task IncrementAsync(string name); + Task IncrementAsync(DomainId appId, string name, + CancellationToken ct = default); - Task ResetAsync(string name, long value); - - Task ClearAsync(); + Task ResetAsync(DomainId appId, string name, long value, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs index 35f6576e8..8b88afa98 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentCommandMiddleware.cs @@ -5,32 +5,36 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { - public sealed class ContentCommandMiddleware : GrainCommandMiddleware + public sealed class ContentCommandMiddleware : CachingDomainObjectMiddleware { private readonly IContentEnricher contentEnricher; private readonly IContextProvider contextProvider; - public ContentCommandMiddleware(IGrainFactory grainFactory, IContentEnricher contentEnricher, IContextProvider contextProvider) - : base(grainFactory) + public ContentCommandMiddleware( + IDomainObjectFactory domainObjectFactory, + IDomainObjectCache domainObjectCache, + IContentEnricher contentEnricher, + IContextProvider contextProvider) + : base(domainObjectFactory, domainObjectCache) { this.contentEnricher = contentEnricher; this.contextProvider = contextProvider; } - protected override async Task EnrichResultAsync(CommandContext context, CommandResult result) + protected override async Task EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) { - var payload = await base.EnrichResultAsync(context, result); + var payload = await base.EnrichResultAsync(context, result, ct); if (payload is IContentEntity content && payload is not IEnrichedContentEntity) { - payload = await contentEnricher.EnrichAsync(content, true, contextProvider.Context, default); + payload = await contentEnricher.EnrichAsync(content, true, contextProvider.Context, ct); } return payload; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs index 910796a45..d680138b4 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.State.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure.Reflection; namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { - public sealed partial class ContentDomainObject + public partial class ContentDomainObject { public sealed class State : DomainObjectState, IContentEntity { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs index 340d730c5..87f5c2875 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObject.cs @@ -24,7 +24,7 @@ using Squidex.Shared; namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { - public sealed partial class ContentDomainObject : DomainObject + public partial class ContentDomainObject : DomainObject { private readonly IServiceProvider serviceProvider; @@ -33,8 +33,6 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject : base(id, persistence, log) { this.serviceProvider = serviceProvider; - - Capacity = 5; } protected override bool IsDeleted(State snapshot) @@ -65,12 +63,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Equals(contentCommand.ContentId, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) { switch (command) { case UpsertContent upsertContent: - return UpsertReturnAsync(upsertContent, async c => + return UpsertReturnAsync(upsertContent, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); @@ -93,10 +92,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } return Snapshot; - }); + }, ct); case CreateContent createContent: - return CreateReturnAsync(createContent, async c => + return CreateReturnAsync(createContent, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); @@ -112,70 +111,70 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } return Snapshot; - }); + }, ct); case ValidateContent validate: - return UpdateReturnAsync(validate, async c => + return UpdateReturnAsync(validate, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); await ValidateCore(operation); return true; - }); + }, ct); case CreateContentDraft createDraft: - return UpdateReturnAsync(createDraft, async c => + return UpdateReturnAsync(createDraft, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); await CreateDraftCore(c, operation); return Snapshot; - }); + }, ct); case DeleteContentDraft deleteDraft: - return UpdateReturnAsync(deleteDraft, async c => + return UpdateReturnAsync(deleteDraft, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); DeleteDraftCore(c, operation); return Snapshot; - }); + }, ct); case PatchContent patchContent: - return UpdateReturnAsync(patchContent, async c => + return UpdateReturnAsync(patchContent, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); await PatchCore(c, operation); return Snapshot; - }); + }, ct); case UpdateContent updateContent: - return UpdateReturnAsync(updateContent, async c => + return UpdateReturnAsync(updateContent, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); await UpdateCore(c, operation); return Snapshot; - }); + }, ct); case CancelContentSchedule cancelContentSchedule: - return UpdateReturnAsync(cancelContentSchedule, async c => + return UpdateReturnAsync(cancelContentSchedule, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); CancelChangeCore(c, operation); return Snapshot; - }); + }, ct); case ChangeContentStatus changeContentStatus: - return UpdateReturnAsync(changeContentStatus, async c => + return UpdateReturnAsync(changeContentStatus, async (c, ct) => { try { @@ -203,23 +202,23 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } return Snapshot; - }); + }, ct); case DeleteContent deleteContent when deleteContent.Permanent: - return DeletePermanentAsync(deleteContent, async c => + return DeletePermanentAsync(deleteContent, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); await DeleteCore(c, operation); - }); + }, ct); case DeleteContent deleteContent: - return UpdateAsync(deleteContent, async c => + return UpdateAsync(deleteContent, async (c, ct) => { var operation = await ContentOperation.CreateAsync(serviceProvider, c, () => Snapshot); await DeleteCore(c, operation); - }); + }, ct); default: ThrowHelper.NotSupportedException(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObjectGrain.cs deleted file mode 100644 index 925beb62e..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentDomainObjectGrain.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject -{ - public sealed class ContentDomainObjectGrain : DomainObjectGrain, IContentGrain - { - private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); - - public ContentDomainObjectGrain(IGrainIdentity identity, IDomainObjectFactory factory, IActivationLimit limit) - : base(identity, factory) - { - limit?.SetLimit(5000, Lifetime); - } - - public async Task GetStateAsync(long version = -2) - { - await DomainObject.EnsureLoadedAsync(); - - return await DomainObject.GetSnapshotAsync(version); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs index bf7194713..f7db5e6e8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/ContentsBulkUpdateCommandMiddleware.cs @@ -29,9 +29,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject private readonly IContextProvider contextProvider; private readonly ILogger log; - private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command) - { - } + private sealed record BulkTaskCommand(BulkTask Task, DomainId Id, ICommand Command, + CancellationToken CancellationToken); private sealed record BulkTask( ICommandBus Bus, @@ -39,10 +38,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject int JobIndex, BulkUpdateJob CommandJob, BulkUpdateContents Command, - ConcurrentBag Results - ) - { - } + ConcurrentBag Results, + CancellationToken CancellationToken); public ContentsBulkUpdateCommandMiddleware( IContentQueryService contentQuery, @@ -55,7 +52,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject this.log = log; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (context.Command is BulkUpdateContents bulkUpdates) { @@ -112,7 +110,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject i, bulkUpdates.Jobs[i], bulkUpdates, - results); + results, + ct); if (!await createCommandsBlock.SendAsync(task)) { @@ -133,17 +132,17 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject } else { - await next(context); + await next(context, ct); } } private async Task ExecuteCommandAsync(BulkTaskCommand bulkCommand) { - var (task, id, command) = bulkCommand; + var (task, id, command, ct) = bulkCommand; try { - await task.Bus.PublishAsync(command); + await task.Bus.PublishAsync(command, ct); task.Results.Add(new BulkUpdateResultItem(id, task.JobIndex)); } @@ -178,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject command.ContentId = id; - commands.Add(new BulkTaskCommand(task, id, command)); + commands.Add(new BulkTaskCommand(task, id, command, task.CancellationToken)); } catch (Exception ex) { @@ -273,7 +272,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject if (!string.IsNullOrWhiteSpace(task.CommandJob.Schema)) { - var schema = await contentQuery.GetSchemaOrThrowAsync(contextProvider.Context, task.Schema); + var schema = await contentQuery.GetSchemaOrThrowAsync(contextProvider.Context, task.Schema, task.CancellationToken); command.SchemaId = schema.NamedId(); } @@ -299,7 +298,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { task.CommandJob.Query.Take = task.CommandJob.ExpectedCount; - var existing = await contentQuery.QueryAsync(contextProvider.Context, task.Schema, Q.Empty.WithJsonQuery(task.CommandJob.Query)); + var existing = await contentQuery.QueryAsync(contextProvider.Context, task.Schema, Q.Empty.WithJsonQuery(task.CommandJob.Query), task.CancellationToken); if (existing.Total > task.CommandJob.ExpectedCount) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/IContentGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/IContentGrain.cs deleted file mode 100644 index 4f4009f95..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/DomainObject/IContentGrain.cs +++ /dev/null @@ -1,17 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject -{ - public interface IContentGrain : IDomainObjectGrain - { - Task GetStateAsync(long version = EtagVersion.Any); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs index 59c6c263d..90efa73e7 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/Types/Contents/ContentActions.cs @@ -470,7 +470,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Contents contentCommand.SchemaId = schemaId; contentCommand.ExpectedVersion = fieldContext.GetArgument("expectedVersion", EtagVersion.Any); - var commandContext = await context.Resolve().PublishAsync(contentCommand); + var commandBus = context.Resolve(); + var commandContext = await commandBus.PublishAsync(contentCommand, fieldContext.CancellationToken); return commandContext.PlainResult!; }); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs index dee3f13a0..2667b7412 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentLoader.cs @@ -11,6 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Contents { public interface IContentLoader { - Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any); + Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default); } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs deleted file mode 100644 index 77f7fcc0e..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/IContentSchedulerGrain.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// 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/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs index 489866513..be89ea185 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentLoader.cs @@ -5,36 +5,60 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Domain.Apps.Entities.Contents.DomainObject; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Contents.Queries { public sealed class ContentLoader : IContentLoader { - private readonly IGrainFactory grainFactory; + private readonly IDomainObjectFactory domainObjectFactory; + private readonly IDomainObjectCache domainObjectCache; - public ContentLoader(IGrainFactory grainFactory) + public ContentLoader(IDomainObjectFactory domainObjectFactory, IDomainObjectCache domainObjectCache) { - this.grainFactory = grainFactory; + this.domainObjectFactory = domainObjectFactory; + this.domainObjectCache = domainObjectCache; } - public async Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any) + public async Task GetAsync(DomainId appId, DomainId id, long version = EtagVersion.Any, + CancellationToken ct = default) { - using (Telemetry.Activities.StartActivity("ContentLoader/GetAsync")) + var uniqueId = DomainId.Combine(appId, id); + + var content = await GetCachedAsync(uniqueId, version, ct); + + if (content == null) + { + content = await GetAsync(uniqueId, version, ct); + } + + if (content == null || content.Version <= EtagVersion.Empty || (version > EtagVersion.Any && content.Version != version)) { - var key = DomainId.Combine(appId, id).ToString(); + return null; + } + + return content; + } - var contentGrain = grainFactory.GetGrain(key); - var contentState = await contentGrain.GetStateAsync(version); + private async Task GetCachedAsync(DomainId uniqueId, long version, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("ContentLoader/GetCachedAsync")) + { + return await domainObjectCache.GetAsync(uniqueId, version, ct); + } + } - if (contentState == null || contentState.Version <= EtagVersion.Empty || (version > EtagVersion.Any && contentState.Version != version)) - { - return null; - } + private async Task GetAsync(DomainId uniqueId, long version, + CancellationToken ct) + { + using (Telemetry.Activities.StartActivity("ContentLoader/GetAsync")) + { + var contentObject = domainObjectFactory.Create(uniqueId); - return contentState; + return await contentObject.GetSnapshotAsync(version, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs index 8282d5329..ef3ee6567 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs @@ -29,15 +29,15 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60); private readonly IMemoryCache cache; - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; private readonly IAppProvider appprovider; private readonly ITextIndex textIndex; private readonly ContentOptions options; public ContentQueryParser(IAppProvider appprovider, ITextIndex textIndex, IOptions options, - IMemoryCache cache, IJsonSerializer jsonSerializer) + IMemoryCache cache, IJsonSerializer serializer) { - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; this.appprovider = appprovider; this.textIndex = textIndex; this.cache = cache; @@ -186,7 +186,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries { var queryModel = BuildQueryModel(context, schema, components); - return queryModel.Parse(json, jsonSerializer); + return queryModel.Parse(json, serializer); } private ClrQuery ParseOData(Context context, ISchemaEntity? schema, string odata, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs index b2e807198..edcd9a3e0 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/CalculateTokens.cs @@ -13,12 +13,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps { public sealed class CalculateTokens : IContentEnricherStep { - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; private readonly IUrlGenerator urlGenerator; - public CalculateTokens(IUrlGenerator urlGenerator, IJsonSerializer jsonSerializer) + public CalculateTokens(IUrlGenerator urlGenerator, IJsonSerializer serializer) { - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; this.urlGenerator = urlGenerator; } @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps u = url }; - var json = jsonSerializer.Serialize(token); + var json = serializer.Serialize(token); content.EditToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(json)); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs index cff201ea0..fc769d4e3 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/Steps/ConvertData.cs @@ -23,20 +23,18 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries.Steps public sealed class ConvertData : IContentEnricherStep { private readonly IUrlGenerator urlGenerator; - private readonly IJsonSerializer jsonSerializer; private readonly IAssetRepository assetRepository; private readonly IContentRepository contentRepository; private readonly ExcludeChangedTypes excludeChangedTypes; - public ConvertData(IUrlGenerator urlGenerator, IJsonSerializer jsonSerializer, + public ConvertData(IUrlGenerator urlGenerator, IJsonSerializer serializer, IAssetRepository assetRepository, IContentRepository contentRepository) { this.urlGenerator = urlGenerator; - this.jsonSerializer = jsonSerializer; this.assetRepository = assetRepository; this.contentRepository = contentRepository; - excludeChangedTypes = new ExcludeChangedTypes(jsonSerializer); + excludeChangedTypes = new ExcludeChangedTypes(serializer); } public async Task EnrichAsync(Context context, IEnumerable contents, ProvideSchema schemas, diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs index bfe193bf9..0a12fac5d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/SingletonCommandMiddleware.cs @@ -17,9 +17,10 @@ namespace Squidex.Domain.Apps.Entities.Contents { public sealed class SingletonCommandMiddleware : ICommandMiddleware { - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { - await next(context); + await next(context, ct); if (context.IsCompleted && context.Command is CreateSchema { Type: SchemaType.Singleton } createSchema) { @@ -40,7 +41,8 @@ namespace Squidex.Domain.Apps.Entities.Contents SimpleMapper.Map(createSchema, content); - await context.CommandBus.PublishAsync(content); + // Always create the corresponding content and therefore do not pass over cancellation token. + await context.CommandBus.PublishAsync(content, default); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs index 21c8783d2..9f9d4f27d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/Extensions.cs @@ -16,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { public static class Extensions { - public static Dictionary? ToGeo(this ContentData data, IJsonSerializer jsonSerializer) + public static Dictionary? ToGeo(this ContentData data, IJsonSerializer serializer) { Dictionary? result = null; @@ -26,7 +26,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { foreach (var (key, jsonValue) in value) { - GeoJsonValue.TryParse(jsonValue, jsonSerializer, out var geoJson); + GeoJsonValue.TryParse(jsonValue, serializer, out var geoJson); if (geoJson != null) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs index 6ab99b8f8..c73e68414 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Text/TextIndexingProcess.cs @@ -17,7 +17,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text public sealed class TextIndexingProcess : IEventConsumer { private const string NotFound = "<404>"; - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; private readonly ITextIndex textIndex; private readonly ITextIndexerState textIndexerState; @@ -49,15 +49,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text private sealed class Updates { private readonly Dictionary states; - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; private readonly Dictionary updates = new Dictionary(); private readonly Dictionary commands = new Dictionary(); - public Updates(Dictionary states, IJsonSerializer jsonSerializer) + public Updates(Dictionary states, IJsonSerializer serializer) { this.states = states; - - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; } public async Task WriteAsync(ITextIndex textIndex, ITextIndexerState textIndexerState) @@ -126,7 +125,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { ContentId = @event.ContentId, DocId = state.DocIdCurrent, - GeoObjects = data.ToGeo(jsonSerializer), + GeoObjects = data.ToGeo(serializer), ServeAll = true, ServePublished = false, Texts = data.ToTexts(), @@ -183,7 +182,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { ContentId = @event.ContentId, DocId = state.DocIdNew, - GeoObjects = data.ToGeo(jsonSerializer), + GeoObjects = data.ToGeo(serializer), ServeAll = true, ServePublished = false, Texts = data.ToTexts() @@ -206,7 +205,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { ContentId = @event.ContentId, DocId = state.DocIdCurrent, - GeoObjects = data.ToGeo(jsonSerializer), + GeoObjects = data.ToGeo(serializer), ServeAll = true, ServePublished = isPublished, Texts = data.ToTexts() @@ -329,11 +328,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text } public TextIndexingProcess( - IJsonSerializer jsonSerializer, + IJsonSerializer serializer, ITextIndex textIndex, ITextIndexerState textIndexerState) { - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; this.textIndex = textIndex; this.textIndexerState = textIndexerState; } @@ -348,7 +347,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text { var states = await QueryStatesAsync(events); - var updates = new Updates(states, jsonSerializer); + var updates = new Updates(states, serializer); foreach (var @event in events) { diff --git a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs index 9612bf17b..08e3b62e9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/History/NotifoService.cs @@ -31,21 +31,18 @@ namespace Squidex.Domain.Apps.Entities.History private readonly IUrlGenerator urlGenerator; private readonly IUserResolver userResolver; private readonly ILogger log; - private readonly IClock clock; private readonly INotifoClient? client; + public IClock Clock { get; set; } = SystemClock.Instance; + public NotifoService(IOptions options, IUrlGenerator urlGenerator, IUserResolver userResolver, - ILogger log, - IClock clock) + ILogger log) { this.options = options.Value; - this.urlGenerator = urlGenerator; this.userResolver = userResolver; - this.clock = clock; - this.log = log; if (options.Value.IsConfigured()) @@ -155,7 +152,7 @@ namespace Squidex.Domain.Apps.Entities.History try { - var now = clock.GetCurrentInstant(); + var now = Clock.GetCurrentInstant(); var maxAge = now - MaxAge; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/IRuleGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/IRuleGrain.cs deleted file mode 100644 index 7a6ed1870..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/IRuleGrain.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject -{ - public interface IRuleGrain : IDomainObjectGrain - { - Task GetStateAsync(); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs index 646724f6d..9daf73b3a 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.State.cs @@ -14,7 +14,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Rules.DomainObject { - public sealed partial class RuleDomainObject + public partial class RuleDomainObject { [CollectionName("Rules")] public sealed class State : DomainObjectState, IRuleEntity diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs index 676d02b27..a3ca8c9ef 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObject.cs @@ -21,7 +21,7 @@ using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Rules.DomainObject { - public sealed partial class RuleDomainObject : DomainObject + public partial class RuleDomainObject : DomainObject { private readonly IServiceProvider serviceProvider; @@ -49,29 +49,30 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject ruleCommand.RuleId.Equals(Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) { switch (command) { case CreateRule createRule: - return CreateReturnAsync(createRule, async c => + return CreateReturnAsync(createRule, async (c, ct) => { await GuardRule.CanCreate(c, AppProvider()); Create(c); return Snapshot; - }); + }, ct); case UpdateRule updateRule: - return UpdateReturnAsync(updateRule, async c => + return UpdateReturnAsync(updateRule, async (c, ct) => { await GuardRule.CanUpdate(c, Snapshot, AppProvider()); Update(c); return Snapshot; - }); + }, ct); case EnableRule enable: return UpdateReturn(enable, c => @@ -79,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject Enable(c); return Snapshot; - }); + }, ct); case DisableRule disable: return UpdateReturn(disable, c => @@ -87,21 +88,21 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject Disable(c); return Snapshot; - }); + }, ct); case DeleteRule delete: return Update(delete, c => { Delete(c); - }); + }, ct); case TriggerRule triggerRule: - return UpdateReturnAsync(triggerRule, async c => + return UpdateReturnAsync(triggerRule, async (c, ct) => { await Trigger(triggerRule); return true; - }); + }, ct); default: ThrowHelper.NotSupportedException(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObjectGrain.cs deleted file mode 100644 index c49887ae7..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/DomainObject/RuleDomainObjectGrain.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Rules.DomainObject -{ - public sealed class RuleDomainObjectGrain : DomainObjectGrain, IRuleGrain - { - public RuleDomainObjectGrain(IGrainIdentity identity, IDomainObjectFactory factory) - : base(identity, factory) - { - } - - public async Task GetStateAsync() - { - await DomainObject.EnsureLoadedAsync(); - - return Snapshot; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs deleted file mode 100644 index 793e43617..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/IRuleDequeuerGrain.cs +++ /dev/null @@ -1,15 +0,0 @@ -// ========================================================================== -// 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.Rules -{ - public interface IRuleDequeuerGrain : IBackgroundGrain - { - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs deleted file mode 100644 index 879b441a6..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesCacheGrain.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Concurrency; -using Orleans.Core; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Rules.Indexes -{ - [Reentrant] - public sealed class RulesCacheGrain : GrainBase, IRulesCacheGrain - { - private readonly IRuleRepository ruleRepository; - private List? ruleIds; - - public RulesCacheGrain(IGrainIdentity grainIdentity, IRuleRepository ruleRepository) - : base(grainIdentity) - { - this.ruleRepository = ruleRepository; - } - - public async Task> GetRuleIdsAsync() - { - var ids = ruleIds; - - if (ids == null) - { - ids = await ruleRepository.QueryIdsAsync(Key); - - ruleIds = ids; - } - - return ids; - } - - public Task AddAsync(DomainId id) - { - ruleIds?.Add(id); - - return Task.CompletedTask; - } - - public Task RemoveAsync(DomainId id) - { - ruleIds?.Remove(id); - - return Task.CompletedTask; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs index 4e73bb7de..b268526ec 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs @@ -5,21 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.Rules.DomainObject; +using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Rules.Indexes { - public sealed class RulesIndex : ICommandMiddleware, IRulesIndex + public sealed class RulesIndex : IRulesIndex { - private readonly IGrainFactory grainFactory; + private readonly IRuleRepository ruleRepository; - public RulesIndex(IGrainFactory grainFactory) + public RulesIndex(IRuleRepository ruleRepository) { - this.grainFactory = grainFactory; + this.ruleRepository = ruleRepository; } public async Task> GetRulesAsync(DomainId appId, @@ -27,67 +24,15 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { using (Telemetry.Activities.StartActivity("RulesIndex/GetRulesAsync")) { - var ids = await GetRuleIdsAsync(appId); + var rules = await ruleRepository.QueryAllAsync(appId, ct); - var rules = - await Task.WhenAll( - ids.Select(id => GetRuleCoreAsync(DomainId.Combine(appId, id)))); - - return rules.NotNull().ToList(); + return rules.Where(IsValid).ToList(); } } - private async Task> GetRuleIdsAsync(DomainId appId) + private static bool IsValid(IRuleEntity? rule) { - using (Telemetry.Activities.StartActivity("RulesIndex/GetRuleIdsAsync")) - { - return await Cache(appId).GetRuleIdsAsync(); - } - } - - public async Task HandleAsync(CommandContext context, NextDelegate next) - { - await next(context); - - if (context.IsCompleted) - { - switch (context.Command) - { - case CreateRule create: - await OnCreateAsync(create); - break; - case DeleteRule delete: - await OnDeleteAsync(delete); - break; - } - } - } - - private async Task OnCreateAsync(CreateRule create) - { - await Cache(create.AppId.Id).AddAsync(create.RuleId); - } - - private async Task OnDeleteAsync(DeleteRule delete) - { - await Cache(delete.AppId.Id).RemoveAsync(delete.RuleId); - } - - private IRulesCacheGrain Cache(DomainId appId) - { - return grainFactory.GetGrain(appId.ToString()); - } - - private async Task GetRuleCoreAsync(DomainId id) - { - var rule = await grainFactory.GetGrain(id.ToString()).GetStateAsync(); - - if (rule.Version <= EtagVersion.Empty || rule.IsDeleted) - { - return null; - } - - return rule; + return rule != null && rule.Version > EtagVersion.Empty && !rule.IsDeleted; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs index b3a303b6d..ef0e3c538 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Repositories/IRuleRepository.cs @@ -11,7 +11,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Repositories { public interface IRuleRepository { - Task> QueryIdsAsync(DomainId appId, + Task> QueryAllAsync(DomainId appId, CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs index 05da32f46..4225a15d8 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleCommandMiddleware.cs @@ -5,33 +5,33 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.Rules.DomainObject; using Squidex.Infrastructure.Commands; namespace Squidex.Domain.Apps.Entities.Rules { - public sealed class RuleCommandMiddleware : GrainCommandMiddleware + public sealed class RuleCommandMiddleware : AggregateCommandMiddleware { private readonly IRuleEnricher ruleEnricher; private readonly IContextProvider contextProvider; - public RuleCommandMiddleware(IGrainFactory grainFactory, IRuleEnricher ruleEnricher, IContextProvider contextProvider) - : base(grainFactory) + public RuleCommandMiddleware(IDomainObjectFactory domainObjectFactory, + IRuleEnricher ruleEnricher, IContextProvider contextProvider) + : base(domainObjectFactory) { this.ruleEnricher = ruleEnricher; - this.contextProvider = contextProvider; } - protected override async Task EnrichResultAsync(CommandContext context, CommandResult result) + protected override async Task EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) { - var payload = await base.EnrichResultAsync(context, result); + var payload = await base.EnrichResultAsync(context, result, ct); if (payload is IRuleEntity rule && payload is not IEnrichedRuleEntity) { - payload = await ruleEnricher.EnrichAsync(rule, contextProvider.Context, default); + payload = await ruleEnricher.EnrichAsync(rule, contextProvider.Context, ct); } return payload; diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerWorker.cs similarity index 80% rename from backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerWorker.cs index 6ce9e1bec..14a2e469e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/RuleDequeuerWorker.cs @@ -9,36 +9,34 @@ using System.Collections.Concurrent; using System.Threading.Tasks.Dataflow; using Microsoft.Extensions.Logging; 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.Hosting; using Squidex.Infrastructure; using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.Timers; namespace Squidex.Domain.Apps.Entities.Rules { - public class RuleDequeuerGrain : Grain, IRuleDequeuerGrain, IRemindable + public sealed class RuleDequeuerWorker : IBackgroundProcess { + private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); private readonly ITargetBlock requestBlock; private readonly IRuleEventRepository ruleEventRepository; private readonly IRuleService ruleService; - private readonly ConcurrentDictionary executing = new ConcurrentDictionary(); - private readonly IClock clock; - private readonly ILogger log; + private readonly ILogger log; + private CompletionTimer timer; - public RuleDequeuerGrain( + public IClock Clock { get; set; } = SystemClock.Instance; + + public RuleDequeuerWorker( IRuleService ruleService, IRuleEventRepository ruleEventRepository, - ILogger log, - IClock clock) + ILogger log) { this.ruleEventRepository = ruleEventRepository; this.ruleService = ruleService; - - this.clock = clock; - this.log = log; requestBlock = @@ -46,35 +44,32 @@ namespace Squidex.Domain.Apps.Entities.Rules new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32, BoundedCapacity = 32 }); } - public override Task OnActivateAsync() + public Task StartAsync( + CancellationToken ct) { - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => QueryAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(10)); + timer = new CompletionTimer((int)TimeSpan.FromSeconds(10).TotalMilliseconds, QueryAsync); return Task.CompletedTask; } - public override Task OnDeactivateAsync() + public async Task StopAsync( + CancellationToken ct) { - requestBlock.Complete(); + await (timer?.StopAsync() ?? Task.CompletedTask); - return requestBlock.Completion; - } + requestBlock.Complete(); - public Task ActivateAsync() - { - return Task.CompletedTask; + await requestBlock.Completion; } - public async Task QueryAsync() + public async Task QueryAsync( + CancellationToken ct = default) { try { - var now = clock.GetCurrentInstant(); + var now = Clock.GetCurrentInstant(); - await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync); + await ruleEventRepository.QueryPendingAsync(now, requestBlock.SendAsync, ct); } catch (Exception ex) { @@ -98,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Rules var jobDelay = ComputeJobDelay(response.Status, @event, job); var jobResult = ComputeJobResult(response.Status, jobDelay); - var now = clock.GetCurrentInstant(); + var now = Clock.GetCurrentInstant(); var update = new RuleJobUpdate { @@ -164,10 +159,5 @@ namespace Squidex.Domain.Apps.Entities.Rules return null; } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return Task.CompletedTask; - } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs index db0209a61..4566a2e98 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/DefaultRuleRunnerService.cs @@ -6,33 +6,38 @@ // ========================================================================== using NodaTime; -using Orleans; using Squidex.Domain.Apps.Core.HandleRules; using Squidex.Domain.Apps.Core.Rules; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Events; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; +using Squidex.Messaging; namespace Squidex.Domain.Apps.Entities.Rules.Runner { public sealed class DefaultRuleRunnerService : IRuleRunnerService { private const int MaxSimulatedEvents = 100; + private readonly IPersistenceFactory persistenceFactory; private readonly IEventFormatter eventFormatter; private readonly IEventStore eventStore; - private readonly IGrainFactory grainFactory; private readonly IRuleService ruleService; + private readonly IMessageBus messaging; - public DefaultRuleRunnerService(IGrainFactory grainFactory, + public DefaultRuleRunnerService( + IPersistenceFactory persistenceFactory, IEventFormatter eventFormatter, IEventStore eventStore, - IRuleService ruleService) + IRuleService ruleService, + IMessageBus messaging) { - this.grainFactory = grainFactory; this.eventFormatter = eventFormatter; + this.persistenceFactory = persistenceFactory; this.eventStore = eventStore; this.ruleService = ruleService; + this.messaging = messaging; } public Task> SimulateAsync(IRuleEntity rule, @@ -66,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner if (@event?.Payload is AppEvent appEvent) { // Also create jobs for rules with failing conditions because we want to show them in th table. - await foreach (var result in ruleService.CreateJobsAsync(@event, context, ct)) + await foreach (var result in ruleService.CreateJobsAsync(@event, context, ct).WithCancellation(ct)) { var eventName = result.Job?.EventName; @@ -110,25 +115,31 @@ namespace Squidex.Domain.Apps.Entities.Rules.Runner public Task CancelAsync(DomainId appId, CancellationToken ct = default) { - var grain = grainFactory.GetGrain(appId.ToString()); + return messaging.PublishAsync(new RuleRunnerCancel(appId), ct: ct); + } - return grain.CancelAsync(); + public Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false, + CancellationToken ct = default) + { + return messaging.PublishAsync(new RuleRunnerRun(appId, ruleId, fromSnapshots), ct: ct); } - public Task GetRunningRuleIdAsync(DomainId appId, + public async Task GetRunningRuleIdAsync(DomainId appId, CancellationToken ct = default) { - var grain = grainFactory.GetGrain(appId.ToString()); + var state = await GetStateAsync(appId, ct); - return grain.GetRunningRuleIdAsync(); + return state.Value.RuleId; } - public Task RunAsync(DomainId appId, DomainId ruleId, bool fromSnapshots = false, - CancellationToken ct = default) + private async Task> GetStateAsync(DomainId appId, + CancellationToken ct) { - var grain = grainFactory.GetGrain(appId.ToString()); + var state = new SimpleState(persistenceFactory, GetType(), appId); + + await state.LoadAsync(ct); - return grain.RunAsync(ruleId, fromSnapshots); + return state; } private static RuleContext GetContext(IRuleEntity rule) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesCacheGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/Messages.cs similarity index 53% rename from backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesCacheGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/Messages.cs index 2bbde1a56..c8296df12 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/IRulesCacheGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/Messages.cs @@ -5,17 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Infrastructure; -namespace Squidex.Domain.Apps.Entities.Rules.Indexes -{ - public interface IRulesCacheGrain : IGrainWithStringKey - { - Task> GetRuleIdsAsync(); +#pragma warning disable MA0048 // File name must match type name +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter - Task AddAsync(DomainId id); +namespace Squidex.Domain.Apps.Entities.Rules.Runner +{ + public sealed record RuleRunnerRun(DomainId AppId, DomainId RuleId, bool FromSnapshots); - Task RemoveAsync(DomainId id); - } + public sealed record RuleRunnerCancel(DomainId AppId); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs deleted file mode 100644 index 8df40787c..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerGrain.cs +++ /dev/null @@ -1,292 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Logging; -using Orleans; -using Orleans.Core; -using Orleans.Runtime; -using Squidex.Caching; -using Squidex.Domain.Apps.Core.HandleRules; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; -using Squidex.Infrastructure.Translations; -using TaskExtensions = Squidex.Infrastructure.Tasks.TaskExtensions; - -#pragma warning disable RECS0015 // If an extension method is called as static method convert it to method syntax - -namespace Squidex.Domain.Apps.Entities.Rules.Runner -{ - public sealed class RuleRunnerGrain : GrainBase, IRuleRunnerGrain, IRemindable - { - private const int MaxErrors = 10; - private readonly IAppProvider appProvider; - private readonly IEventFormatter eventFormatter; - private readonly IEventStore eventStore; - private readonly IGrainState state; - private readonly ILocalCache localCache; - private readonly IRuleEventRepository ruleEventRepository; - private readonly IRuleService ruleService; - private readonly ILogger log; - private CancellationTokenSource? currentJobToken; - private IGrainReminder? currentReminder; - private bool isStopping; - - [CollectionName("Rules_Runner")] - public sealed class State - { - public DomainId? RuleId { get; set; } - - public string? Position { get; set; } - - public bool RunFromSnapshots { get; set; } - } - - public RuleRunnerGrain(IGrainIdentity identity, - IAppProvider appProvider, - IEventFormatter eventFormatter, - IEventStore eventStore, - IGrainState state, - ILocalCache localCache, - IRuleEventRepository ruleEventRepository, - IRuleService ruleService, - ILogger log) - : base(identity) - { - this.state = state; - this.appProvider = appProvider; - this.localCache = localCache; - this.eventStore = eventStore; - this.eventFormatter = eventFormatter; - this.ruleEventRepository = ruleEventRepository; - this.ruleService = ruleService; - this.log = log; - } - - public override Task OnActivateAsync() - { - return EnsureIsRunningAsync(true); - } - - public override Task OnDeactivateAsync() - { - isStopping = true; - - currentJobToken?.Cancel(); - - return base.OnDeactivateAsync(); - } - - public Task CancelAsync() - { - try - { - currentJobToken?.Cancel(); - } - catch (ObjectDisposedException) - { - return Task.CompletedTask; - } - - return Task.CompletedTask; - } - - public Task GetRunningRuleIdAsync() - { - return Task.FromResult(state.Value.RuleId); - } - - public async Task RunAsync(DomainId ruleId, bool fromSnapshots) - { - if (currentJobToken != null) - { - throw new DomainException(T.Get("rules.ruleAlreadyRunning")); - } - - state.Value = new State - { - RuleId = ruleId, - RunFromSnapshots = fromSnapshots - }; - - await EnsureIsRunningAsync(false); - - await state.WriteAsync(); - } - - private async Task EnsureIsRunningAsync(bool continues) - { - var job = state.Value; - - if (job.RuleId != null && currentJobToken == null) - { - if (state.Value.RunFromSnapshots && continues) - { - state.Value = new State(); - - await state.WriteAsync(); - } - else - { - currentJobToken = new CancellationTokenSource(); - -#pragma warning disable MA0042 // Do not use blocking calls in an async method - Process(state.Value, currentJobToken.Token); -#pragma warning restore MA0042 // Do not use blocking calls in an async method - } - } - } - - private void Process(State job, - CancellationToken ct) - { - TaskExtensions.Forget(ProcessAsync(job, ct)); - } - - private async Task ProcessAsync(State currentState, - CancellationToken ct) - { - try - { - currentReminder = await RegisterOrUpdateReminder("KeepAlive", TimeSpan.Zero, TimeSpan.FromMinutes(2)); - - var rule = await appProvider.GetRuleAsync(Key, currentState.RuleId!.Value, ct); - - if (rule == null) - { - throw new DomainObjectNotFoundException(currentState.RuleId.ToString()!); - } - - using (localCache.StartContext()) - { - var context = new RuleContext - { - AppId = rule.AppId, - Rule = rule.RuleDef, - RuleId = rule.Id, - IncludeStale = true - }; - - if (currentState.RunFromSnapshots && ruleService.CanCreateSnapshotEvents(context)) - { - await EnqueueFromSnapshotsAsync(context, ct); - } - else - { - await EnqueueFromEventsAsync(currentState, context, ct); - } - } - } - catch (OperationCanceledException) - { - return; - } - catch (Exception ex) - { - log.LogError(ex, "Failed to run rule with ID {ruleId}.", currentState.RuleId); - } - finally - { - if (!isStopping) - { - currentState.RuleId = null; - currentState.Position = null; - - await state.WriteAsync(); - - if (currentReminder != null) - { - await UnregisterReminder(currentReminder); - - currentReminder = null; - } - - currentJobToken?.Dispose(); - currentJobToken = null; - } - } - } - - private async Task EnqueueFromSnapshotsAsync(RuleContext context, - CancellationToken ct) - { - var errors = 0; - - await foreach (var job in ruleService.CreateSnapshotJobsAsync(context, ct)) - { - if (job.Job != null && job.SkipReason == SkipReason.None) - { - await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); - } - else if (job.EnrichmentError != null) - { - errors++; - - if (errors >= MaxErrors) - { - throw job.EnrichmentError; - } - - log.LogWarning(job.EnrichmentError, "Failed to run rule with ID {ruleId}, continue with next job.", context.RuleId); - } - } - } - - private async Task EnqueueFromEventsAsync(State currentState, RuleContext context, - CancellationToken ct) - { - var errors = 0; - - var filter = $"^([a-z]+)\\-{Key}"; - - await foreach (var storedEvent in eventStore.QueryAllAsync(filter, currentState.Position, ct: ct)) - { - try - { - var @event = eventFormatter.ParseIfKnown(storedEvent); - - if (@event != null) - { - var jobs = ruleService.CreateJobsAsync(@event, context, ct); - - await foreach (var job in jobs.WithCancellation(ct)) - { - if (job.Job != null && job.SkipReason == SkipReason.None) - { - await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); - } - } - } - } - catch (Exception ex) - { - errors++; - - if (errors >= MaxErrors) - { - throw; - } - - log.LogWarning(ex, "Failed to run rule with ID {ruleId}, continue with next job.", context.RuleId); - } - finally - { - currentState.Position = storedEvent.EventPosition; - } - - await state.WriteAsync(); - } - } - - public Task ReceiveReminder(string reminderName, TickStatus status) - { - return EnsureIsRunningAsync(true); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerProcessor.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerProcessor.cs new file mode 100644 index 000000000..be6a064ac --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerProcessor.cs @@ -0,0 +1,288 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Logging; +using Squidex.Caching; +using Squidex.Domain.Apps.Core.HandleRules; +using Squidex.Domain.Apps.Entities.Rules.Repositories; +using Squidex.Infrastructure; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Tasks; +using Squidex.Infrastructure.Translations; +using TaskHelper = Squidex.Infrastructure.Tasks.TaskExtensions; + +namespace Squidex.Domain.Apps.Entities.Rules.Runner +{ + public sealed class RuleRunnerProcessor + { + private const int MaxErrors = 10; + private readonly IAppProvider appProvider; + private readonly IEventFormatter eventFormatter; + private readonly IEventStore eventStore; + private readonly ILocalCache localCache; + private readonly IRuleEventRepository ruleEventRepository; + private readonly IRuleService ruleService; + private readonly ILogger log; + private readonly SimpleState state; + private readonly ReentrantScheduler scheduler = new ReentrantScheduler(1); + private readonly DomainId appId; + private Run? currentRun; + + // Use a run to store all state that is necessary for a single run. + private sealed class Run : IDisposable + { + private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); + private readonly CancellationTokenSource cancellationLinked; + + public RuleRunnerState Job { get; init; } + + public RuleContext Context { get; set; } + + public CancellationToken CancellationToken => cancellationLinked.Token; + + public Run(CancellationToken ct) + { + cancellationLinked = CancellationTokenSource.CreateLinkedTokenSource(ct, cancellationSource.Token); + } + + public void Dispose() + { + cancellationSource.Dispose(); + cancellationLinked.Dispose(); + } + + public void Cancel() + { + try + { + cancellationSource.Cancel(); + } + catch (ObjectDisposedException) + { + // Cancellation token might have been disposed, if the run is completed. + } + } + } + + public RuleRunnerProcessor( + DomainId appId, + IAppProvider appProvider, + IEventFormatter eventFormatter, + IEventStore eventStore, + ILocalCache localCache, + IPersistenceFactory persistenceFactory, + IRuleEventRepository ruleEventRepository, + IRuleService ruleService, + ILogger log) + { + this.appId = appId; + this.appProvider = appProvider; + this.localCache = localCache; + this.eventStore = eventStore; + this.eventFormatter = eventFormatter; + this.ruleEventRepository = ruleEventRepository; + this.ruleService = ruleService; + this.log = log; + + state = new SimpleState(persistenceFactory, GetType(), appId); + } + + public async Task LoadAsync( + CancellationToken ct = default) + { + await state.LoadAsync(ct); + + if (!state.Value.RunFromSnapshots) + { + TaskHelper.Forget(RunAsync(state.Value.RuleId, false, default)); + } + else + { + await state.ClearAsync(ct); + } + } + + public Task CancelAsync() + { + // Ensure that only one thread is accessing the current state at a time. + return scheduler.Schedule(() => + { + currentRun?.Cancel(); + }); + } + + public Task RunAsync(DomainId ruleId, bool fromSnapshots, + CancellationToken ct) + { + return scheduler.ScheduleAsync(async ct => + { + // There is no continuation token for snapshots, therefore we cannot continue with the run. + if (currentRun?.Job.RunFromSnapshots == true) + { + throw new DomainException(T.Get("rules.ruleAlreadyRunning")); + } + + var previousJob = state.Value; + + // If we have not removed the state, we have not completed the previous run and can therefore just continue. + var position = + previousJob.RuleId == ruleId && !previousJob.RunFromSnapshots ? + previousJob.Position : + null; + + // Set the current run first to indicate that we are running a rule at the moment. + var run = currentRun = new Run(ct) + { + Job = new RuleRunnerState + { + RuleId = ruleId, + RunId = DomainId.NewGuid(), + RunFromSnapshots = fromSnapshots, + Position = position + } + }; + + state.Value = run.Job; + try + { + await state.WriteAsync(run.CancellationToken); + + await ProcessAsync(run, run.CancellationToken); + } + finally + { + // Unset the run to indicate that we are done. + currentRun.Dispose(); + currentRun = null; + } + }, ct); + } + + private async Task ProcessAsync(Run run, + CancellationToken ct) + { + try + { + var rule = await appProvider.GetRuleAsync(appId, run.Job.RuleId, ct); + + // The rule might have been deleted in the meantime. + if (rule == null) + { + throw new DomainObjectNotFoundException(run.Job.RuleId.ToString()!); + } + + using (localCache.StartContext()) + { + run.Context = new RuleContext + { + AppId = rule.AppId, + Rule = rule.RuleDef, + RuleId = rule.Id, + IncludeStale = true + }; + + if (run.Job.RunFromSnapshots && ruleService.CanCreateSnapshotEvents(run.Context)) + { + await EnqueueFromSnapshotsAsync(run, ct); + } + else + { + await EnqueueFromEventsAsync(run, ct); + } + } + } + catch (OperationCanceledException) + { + return; + } + catch (Exception ex) + { + log.LogError(ex, "Failed to run rule with ID {ruleId}.", run.Job.RuleId); + } + finally + { + // Remove the state to indicate that the run has been completed. + await state.ClearAsync(default); + } + } + + private async Task EnqueueFromSnapshotsAsync(Run run, + CancellationToken ct) + { + // We collect errors and allow a few erors before we throw an exception. + var errors = 0; + + await foreach (var job in ruleService.CreateSnapshotJobsAsync(run.Context, ct)) + { + if (job.Job != null && job.SkipReason == SkipReason.None) + { + await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); + } + else if (job.EnrichmentError != null) + { + errors++; + + if (errors >= MaxErrors) + { + throw job.EnrichmentError; + } + + log.LogWarning(job.EnrichmentError, "Failed to run rule with ID {ruleId}, continue with next job.", run.Context.RuleId); + } + } + } + + private async Task EnqueueFromEventsAsync(Run run, + CancellationToken ct) + { + // We collect errors and allow a few erors before we throw an exception. + var errors = 0; + + // Use a prefix query so that the storage can use an index for the query. + var filter = $"^([a-z]+)\\-{appId}"; + + await foreach (var storedEvent in eventStore.QueryAllAsync(filter, run.Job.Position, ct: ct)) + { + try + { + var @event = eventFormatter.ParseIfKnown(storedEvent); + + if (@event != null) + { + var jobs = ruleService.CreateJobsAsync(@event, run.Context, ct); + + await foreach (var job in jobs.WithCancellation(ct)) + { + if (job.Job != null && job.SkipReason == SkipReason.None) + { + await ruleEventRepository.EnqueueAsync(job.Job, job.EnrichmentError, ct); + } + } + } + } + catch (Exception ex) + { + errors++; + + if (errors >= MaxErrors) + { + throw; + } + + log.LogWarning(ex, "Failed to run rule with ID {ruleId}, continue with next job.", run.Context.RuleId); + } + finally + { + run.Job.Position = storedEvent.EventPosition; + } + + await state.WriteAsync(ct); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerState.cs similarity index 61% rename from backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerState.cs index 4d60c5587..2a9f1fdcd 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/IRuleRunnerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerState.cs @@ -5,17 +5,20 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Infrastructure; +using Squidex.Infrastructure.States; namespace Squidex.Domain.Apps.Entities.Rules.Runner { - public interface IRuleRunnerGrain : IGrainWithStringKey + [CollectionName("Rules_Runner")] + public sealed class RuleRunnerState { - Task RunAsync(DomainId ruleId, bool fromSnapshots); + public DomainId RuleId { get; set; } - Task CancelAsync(); + public DomainId RunId { get; set; } - Task GetRunningRuleIdAsync(); + public string? Position { get; set; } + + public bool RunFromSnapshots { get; set; } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerWorker.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerWorker.cs new file mode 100644 index 000000000..ff0196892 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/Runner/RuleRunnerWorker.cs @@ -0,0 +1,79 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.DependencyInjection; +using Squidex.Hosting; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; +using Squidex.Messaging; + +namespace Squidex.Domain.Apps.Entities.Rules.Runner +{ + public sealed class RuleRunnerWorker : + IBackgroundProcess, + IMessageHandler, + IMessageHandler + { + private readonly Dictionary> processors = new Dictionary>(); + private readonly Func processorFactory; + private readonly ISnapshotStore snapshotStore; + + public RuleRunnerWorker(IServiceProvider serviceProvider, ISnapshotStore snapshotStore) + { + var objectFactory = ActivatorUtilities.CreateFactory(typeof(RuleRunnerProcessor), new[] { typeof(DomainId) }); + + processorFactory = key => + { + return (RuleRunnerProcessor)objectFactory(serviceProvider, new object[] { key }); + }; + + this.snapshotStore = snapshotStore; + } + + public async Task StartAsync( + CancellationToken ct) + { + await foreach (var snapshot in snapshotStore.ReadAllAsync(ct)) + { + await GetProcessorAsync(snapshot.Key, ct); + } + } + + public async Task HandleAsync(RuleRunnerRun message, + CancellationToken ct) + { + var processor = await GetProcessorAsync(message.AppId, ct); + + await processor.RunAsync(message.RuleId, message.FromSnapshots, ct); + } + + public async Task HandleAsync(RuleRunnerCancel message, + CancellationToken ct) + { + var processor = await GetProcessorAsync(message.AppId, ct); + + await processor.CancelAsync(); + } + + private Task GetProcessorAsync(DomainId appId, + CancellationToken ct) + { + // Use a normal dictionary to avoid double creations. + lock (processors) + { + return processors.GetOrAdd(appId, async key => + { + var processor = processorFactory(key); + + await processor.LoadAsync(ct); + + return processor; + }); + } + } + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs deleted file mode 100644 index f2f6c4ed5..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/IUsageTrackerGrain.cs +++ /dev/null @@ -1,21 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking -{ - public interface IUsageTrackerGrain : IBackgroundGrain - { - Task AddTargetAsync(DomainId ruleId, NamedId appId, int limits, int? numDays); - - Task RemoveTargetAsync(DomainId ruleId); - - Task UpdateTargetAsync(DomainId ruleId, int limits, int? numDays); - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/Messages.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/Messages.cs new file mode 100644 index 000000000..00f5c70be --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/Messages.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure; + +#pragma warning disable MA0048 // File name must match type name +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + +namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking +{ + public sealed record UsageTrackingAdd(DomainId RuleId, NamedId AppId, int Limits, int? NumDays) + : UsageTrackingMessage(RuleId); + + public sealed record UsageTrackingRemove(DomainId RuleId) + : UsageTrackingMessage(RuleId); + + public sealed record UsageTrackingUpdate(DomainId RuleId, int Limits, int? NumDays) + : UsageTrackingMessage(RuleId); + + public abstract record UsageTrackingMessage(DomainId RuleId); +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs index 432244284..6078881fc 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerCommandMiddleware.cs @@ -5,57 +5,39 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Domain.Apps.Core.Rules.Triggers; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; +using Squidex.Messaging; namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { public sealed class UsageTrackerCommandMiddleware : ICommandMiddleware { - private readonly IGrainFactory grainFactory; + private readonly IMessageBus messaging; - public UsageTrackerCommandMiddleware(IGrainFactory grainFactory) + public UsageTrackerCommandMiddleware(IMessageBus messaging) { - this.grainFactory = grainFactory; + this.messaging = messaging; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { switch (context.Command) { case DeleteRule deleteRule: - await GetGrain().RemoveTargetAsync(deleteRule.RuleId); + await messaging.PublishAsync(new UsageTrackingRemove(deleteRule.RuleId), ct: default); + break; + case CreateRule createRule when createRule.Trigger is UsageTrigger usage: + await messaging.PublishAsync(new UsageTrackingAdd(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays), ct: default); + break; + case UpdateRule ruleUpdated when ruleUpdated.Trigger is UsageTrigger usage: + await messaging.PublishAsync(new UsageTrackingUpdate(ruleUpdated.RuleId, usage.Limit, usage.NumDays), ct: default); break; - case CreateRule createRule: - { - if (createRule.Trigger is UsageTrigger usage) - { - await GetGrain().AddTargetAsync(createRule.RuleId, createRule.AppId, usage.Limit, usage.NumDays); - } - - break; - } - - case UpdateRule ruleUpdated: - { - if (ruleUpdated.Trigger is UsageTrigger usage) - { - await GetGrain().UpdateTargetAsync(ruleUpdated.RuleId, usage.Limit, usage.NumDays); - } - - break; - } } - await next(context); - } - - private IUsageTrackerGrain GetGrain() - { - return grainFactory.GetGrain(SingleGrain.Id); + await next(context, ct); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerWorker.cs similarity index 62% rename from backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs rename to backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerWorker.cs index d02fdd96d..7e8f3dc9c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerWorker.cs @@ -5,24 +5,22 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; -using Orleans.Concurrency; -using Orleans.Core; -using Orleans.Runtime; using Squidex.Domain.Apps.Events; +using Squidex.Hosting; using Squidex.Infrastructure; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.States; +using Squidex.Infrastructure.Timers; using Squidex.Infrastructure.UsageTracking; +using Squidex.Messaging; namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking { - [Reentrant] - public sealed class UsageTrackerGrain : GrainBase, IRemindable, IUsageTrackerGrain + public sealed class UsageTrackerWorker : IMessageHandler, IBackgroundProcess { - private readonly IGrainState state; + private readonly SimpleState state; private readonly IApiUsageTracker usageTracker; + private CompletionTimer timer; public sealed class Target { @@ -62,31 +60,25 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking public Dictionary Targets { get; set; } = new Dictionary(); } - public UsageTrackerGrain(IGrainIdentity identity, IGrainState state, IApiUsageTracker usageTracker) - : base(identity) + public UsageTrackerWorker(IPersistenceFactory persistenceFactory, IApiUsageTracker usageTracker) { - this.state = state; this.usageTracker = usageTracker; - } - - public override Task OnActivateAsync() - { - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(10)); - RegisterTimer(x => CheckUsagesAsync(), null, TimeSpan.Zero, TimeSpan.FromMinutes(10)); - return Task.CompletedTask; + state = new SimpleState(persistenceFactory, GetType(), "Default"); } - public Task ActivateAsync() + public async Task StartAsync( + CancellationToken ct) { - return Task.CompletedTask; + await state.LoadAsync(ct); + + timer = new CompletionTimer((int)TimeSpan.FromMinutes(10).TotalMilliseconds, _ => CheckUsagesAsync()); } - public Task ReceiveReminder(string reminderName, TickStatus status) + public Task StopAsync( + CancellationToken ct) { - return Task.CompletedTask; + return timer?.StopAsync() ?? Task.CompletedTask; } public async Task CheckUsagesAsync() @@ -135,32 +127,44 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking } } - public Task AddTargetAsync(DomainId ruleId, NamedId appId, int limits, int? numDays) + public Task HandleAsync(UsageTrackingMessage message, + CancellationToken ct) { - UpdateTarget(ruleId, t => t.SetApp(appId).SetLimit(limits).SetNumDays(numDays)); - - return state.WriteAsync(); + switch (message) + { + case UsageTrackingAdd add: + return HandleAsync(add, ct); + case UsageTrackingRemove remove: + return HandleAsync(remove, ct); + case UsageTrackingUpdate update: + return HandleAsync(update, ct); + default: + return Task.CompletedTask; + } } - public Task UpdateTargetAsync(DomainId ruleId, int limits, int? numDays) + public Task HandleAsync(UsageTrackingAdd message, + CancellationToken ct) { - UpdateTarget(ruleId, t => t.SetLimit(limits).SetNumDays(numDays)); + UpdateTarget(message.RuleId, t => t.SetApp(message.AppId).SetLimit(message.Limits).SetNumDays(message.NumDays)); - return state.WriteAsync(); + return state.WriteAsync(ct); } - public Task AddTargetAsync(DomainId ruleId, int limits) + public Task HandleAsync(UsageTrackingUpdate message, + CancellationToken ct) { - UpdateTarget(ruleId, t => t.SetLimit(limits)); + UpdateTarget(message.RuleId, t => t.SetLimit(message.Limits).SetNumDays(message.NumDays)); - return state.WriteAsync(); + return state.WriteAsync(ct); } - public Task RemoveTargetAsync(DomainId ruleId) + public Task HandleAsync(UsageTrackingRemove message, + CancellationToken ct) { - state.Value.Targets.Remove(ruleId); + state.Value.Targets.Remove(message.RuleId); - return state.WriteAsync(); + return state.WriteAsync(ct); } private void UpdateTarget(DomainId ruleId, Action updater) diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/ISchemaGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/ISchemaGrain.cs deleted file mode 100644 index b9369c01a..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/ISchemaGrain.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject -{ - public interface ISchemaGrain : IDomainObjectGrain - { - Task GetStateAsync(); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs index a67edee0a..06b47d7e1 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObject.cs @@ -46,7 +46,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject Equals(schemaCommand.SchemaId?.Id, Snapshot.Id); } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) { switch (command) { @@ -58,7 +59,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject AddField(c); return Snapshot; - }); + }, ct); case CreateSchema createSchema: return CreateReturn(createSchema, c => @@ -68,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject Create(c); return Snapshot; - }); + }, ct); case SynchronizeSchema synchronize: return UpdateReturn(synchronize, c => @@ -78,7 +79,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject Synchronize(c); return Snapshot; - }); + }, ct); case DeleteField deleteField: return UpdateReturn(deleteField, c => @@ -88,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject DeleteField(c); return Snapshot; - }); + }, ct); case LockField lockField: return UpdateReturn(lockField, c => @@ -98,7 +99,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject LockField(c); return Snapshot; - }); + }, ct); case HideField hideField: return UpdateReturn(hideField, c => @@ -108,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject HideField(c); return Snapshot; - }); + }, ct); case ShowField showField: return UpdateReturn(showField, c => @@ -118,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject ShowField(c); return Snapshot; - }); + }, ct); case DisableField disableField: return UpdateReturn(disableField, c => @@ -128,7 +129,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject DisableField(c); return Snapshot; - }); + }, ct); case EnableField enableField: return UpdateReturn(enableField, c => @@ -138,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject EnableField(c); return Snapshot; - }); + }, ct); case UpdateField updateField: return UpdateReturn(updateField, c => @@ -148,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject UpdateField(c); return Snapshot; - }); + }, ct); case ReorderFields reorderFields: return UpdateReturn(reorderFields, c => @@ -158,7 +159,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject Reorder(c); return Snapshot; - }); + }, ct); case ConfigureFieldRules configureFieldRules: return UpdateReturn(configureFieldRules, c => @@ -168,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject ConfigureFieldRules(c); return Snapshot; - }); + }, ct); case ConfigurePreviewUrls configurePreviewUrls: return UpdateReturn(configurePreviewUrls, c => @@ -178,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject ConfigurePreviewUrls(c); return Snapshot; - }); + }, ct); case ConfigureUIFields configureUIFields: return UpdateReturn(configureUIFields, c => @@ -188,7 +189,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject ConfigureUIFields(c); return Snapshot; - }); + }, ct); case ChangeCategory changeCategory: return UpdateReturn(changeCategory, c => @@ -196,7 +197,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject ChangeCategory(c); return Snapshot; - }); + }, ct); case UpdateSchema update: return UpdateReturn(update, c => @@ -204,7 +205,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject Update(c); return Snapshot; - }); + }, ct); case PublishSchema publish: return UpdateReturn(publish, c => @@ -212,7 +213,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject Publish(c); return Snapshot; - }); + }, ct); case UnpublishSchema unpublish: return UpdateReturn(unpublish, c => @@ -220,7 +221,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject Unpublish(c); return Snapshot; - }); + }, ct); case ConfigureScripts configureScripts: return UpdateReturn(configureScripts, c => @@ -228,13 +229,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject ConfigureScripts(c); return Snapshot; - }); + }, ct); case DeleteSchema deleteSchema: return Update(deleteSchema, c => { Delete(c); - }); + }, ct); default: ThrowHelper.NotSupportedException(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObjectGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObjectGrain.cs deleted file mode 100644 index 43c4de63e..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/DomainObject/SchemaDomainObjectGrain.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject -{ - public sealed class SchemaDomainObjectGrain : DomainObjectGrain, ISchemaGrain - { - public SchemaDomainObjectGrain(IGrainIdentity identity, IDomainObjectFactory factory) - : base(identity, factory) - { - } - - public async Task GetStateAsync() - { - await DomainObject.EnsureLoadedAsync(); - - return Snapshot; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasCacheGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasCacheGrain.cs deleted file mode 100644 index ff29161c3..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasCacheGrain.cs +++ /dev/null @@ -1,23 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans.Indexes; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public interface ISchemasCacheGrain : IUniqueNameGrain - { - Task> GetSchemaIdsAsync(); - - Task GetSchemaIdAsync(string name); - - Task AddAsync(DomainId id, string name); - - Task RemoveAsync(DomainId id); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs deleted file mode 100644 index ec83d6b41..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasCacheGrain.cs +++ /dev/null @@ -1,101 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Concurrency; -using Orleans.Core; -using Squidex.Domain.Apps.Entities.Schemas.Repositories; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans.Indexes; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - [Reentrant] - public sealed class SchemasCacheGrain : UniqueNameGrain, ISchemasCacheGrain - { - private readonly ISchemaRepository schemaRepository; - private Dictionary? schemaIds; - - public SchemasCacheGrain(IGrainIdentity identity, ISchemaRepository schemaRepository) - : base(identity) - { - this.schemaRepository = schemaRepository; - } - - public override async Task ReserveAsync(DomainId id, string name) - { - var token = await base.ReserveAsync(id, name); - - if (token == null) - { - return null; - } - - var ids = await GetIdsAsync(); - - if (ids.ContainsKey(name)) - { - await RemoveReservationAsync(token); - return null; - } - - return token; - } - - public async Task> GetSchemaIdsAsync() - { - var ids = await GetIdsAsync(); - - return ids.Values; - } - - public async Task GetSchemaIdAsync(string name) - { - var ids = await GetIdsAsync(); - - return ids.GetOrDefault(name); - } - - private async Task> GetIdsAsync() - { - var ids = schemaIds; - - if (ids == null) - { - ids = await schemaRepository.QueryIdsAsync(Key); - - schemaIds = ids; - } - - return ids; - } - - public Task AddAsync(DomainId id, string name) - { - if (schemaIds != null) - { - schemaIds[name] = id; - } - - return Task.CompletedTask; - } - - public Task RemoveAsync(DomainId id) - { - if (schemaIds != null) - { - var name = schemaIds.FirstOrDefault(x => x.Value == id).Key; - - if (name != null) - { - schemaIds.Remove(name); - } - } - - return Task.CompletedTask; - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs index 85b8e9aa3..832b86764 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs @@ -5,12 +5,12 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Caching; using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.Schemas.DomainObject; +using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.States; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.Validation; @@ -19,13 +19,16 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes public sealed class SchemasIndex : ICommandMiddleware, ISchemasIndex { private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5); - private readonly IGrainFactory grainFactory; - private readonly IReplicatedCache grainCache; + private readonly ISchemaRepository schemaRepository; + private readonly IReplicatedCache schemaCache; + private readonly IPersistenceFactory persistenceFactory; - public SchemasIndex(IGrainFactory grainFactory, IReplicatedCache grainCache) + public SchemasIndex(ISchemaRepository schemaRepository, IReplicatedCache schemaCache, + IPersistenceFactory persistenceFactory) { - this.grainFactory = grainFactory; - this.grainCache = grainCache; + this.schemaRepository = schemaRepository; + this.schemaCache = schemaCache; + this.persistenceFactory = persistenceFactory; } public async Task> GetSchemasAsync(DomainId appId, @@ -33,13 +36,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemasAsync")) { - var ids = await GetSchemaIdsAsync(appId); + var schemas = await schemaRepository.QueryAllAsync(appId, ct); - var schemas = - await Task.WhenAll( - ids.Select(id => GetSchemaAsync(appId, id, false, ct))); + foreach (var schema in schemas.Where(IsValid)) + { + await InvalidateItAsync(appId, schema.Id, schema.SchemaDef.Name); + } - return schemas.NotNull().ToList(); + return schemas.Where(IsValid).ToList(); } } @@ -52,20 +56,25 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes if (canCache) { - if (grainCache.TryGetValue(cacheKey, out var value) && value is ISchemaEntity cachedSchema) + if (schemaCache.TryGetValue(cacheKey, out var value) && value is ISchemaEntity cachedSchema) { return cachedSchema; } } - var id = await GetSchemaIdAsync(appId, name); + var schema = await schemaRepository.FindAsync(appId, name, ct); - if (id == DomainId.Empty) + if (!IsValid(schema)) { - return null; + schema = null; + } + + if (schema != null) + { + await CacheItAsync(schema); } - return await GetSchemaAsync(appId, id, canCache, ct); + return schema; } } @@ -78,13 +87,18 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes if (canCache) { - if (grainCache.TryGetValue(cacheKey, out var v) && v is ISchemaEntity cachedSchema) + if (schemaCache.TryGetValue(cacheKey, out var v) && v is ISchemaEntity cachedSchema) { return cachedSchema; } } - var schema = await GetSchemaCoreAsync(DomainId.Combine(appId, id)); + var schema = await schemaRepository.FindAsync(appId, id, ct); + + if (!IsValid(schema)) + { + schema = null; + } if (schema != null) { @@ -95,43 +109,29 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes } } - private async Task GetSchemaIdAsync(DomainId appId, string name) - { - using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaIdAsync")) - { - return await Cache(appId).GetSchemaIdAsync(name); - } - } - - private async Task> GetSchemaIdsAsync(DomainId appId) - { - using (Telemetry.Activities.StartActivity("SchemasIndex/GetSchemaIdsAsync")) - { - return await Cache(appId).GetSchemaIdsAsync(); - } - } - - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { var command = context.Command; if (command is CreateSchema createSchema) { - var cache = Cache(createSchema.AppId.Id); + var names = await GetNamesAsync(createSchema.AppId.Id, ct); - var token = await CheckSchemaAsync(cache, createSchema); + var token = await CheckSchemaAsync(createSchema, names, ct); try { - await next(context); + await next(context, ct); } finally { - await cache.RemoveReservationAsync(token); + // Always remove the reservation and therefore do not pass over cancellation token. + await names.RemoveReservationAsync(token, default); } } else { - await next(context); + await next(context, ct); } if (context.IsCompleted) @@ -151,75 +151,49 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes } } - private async Task OnCreateAsync(CreateSchema create) + private Task OnCreateAsync(CreateSchema create) { - await InvalidateItAsync(create.AppId.Id, create.SchemaId, create.Name); - - await Cache(create.AppId.Id).AddAsync(create.SchemaId, create.Name); + return InvalidateItAsync(create.AppId.Id, create.SchemaId, create.Name); } - private async Task OnDeleteAsync(DeleteSchema delete) + private Task OnDeleteAsync(DeleteSchema delete) { - await InvalidateItAsync(delete.AppId.Id, delete.SchemaId.Id, delete.SchemaId.Name); - - await Cache(delete.AppId.Id).RemoveAsync(delete.SchemaId.Id); + return InvalidateItAsync(delete.AppId.Id, delete.SchemaId.Id, delete.SchemaId.Name); } - private async Task OnUpdateAsync(SchemaUpdateCommand update) + private Task OnUpdateAsync(SchemaUpdateCommand update) { - await InvalidateItAsync(update.AppId.Id, update.SchemaId.Id, update.SchemaId.Name); + return InvalidateItAsync(update.AppId.Id, update.SchemaId.Id, update.SchemaId.Name); } - private async Task CheckSchemaAsync(ISchemasCacheGrain cache, CreateSchema command) + private async Task CheckSchemaAsync(CreateSchema command, NameReservationState names, + CancellationToken ct) { - var token = await cache.ReserveAsync(command.SchemaId, command.Name); + var existing = await schemaRepository.FindAsync(command.AppId.Id, command.Name, ct); - if (token == null) + if (existing != null && !existing.IsDeleted) { throw new ValidationException(T.Get("schemas.nameAlreadyExists")); } - try - { - var existingId = await GetSchemaIdAsync(command.AppId.Id, command.Name); + var token = await names.ReserveAsync(command.SchemaId, command.Name, ct); - if (existingId != default) - { - throw new ValidationException(T.Get("schemas.nameAlreadyExists")); - } - } - catch + if (token == null) { - // Catch our own exception, just in case something went wrong before. - await cache.RemoveReservationAsync(token); - throw; + throw new ValidationException(T.Get("schemas.nameAlreadyExists")); } return token; } - private ISchemasCacheGrain Cache(DomainId appId) + private async Task GetNamesAsync(DomainId appId, + CancellationToken ct) { - return grainFactory.GetGrain(appId.ToString()); - } + var state = new NameReservationState(persistenceFactory, $"{appId}_Schemas"); - private async Task GetSchemaCoreAsync(DomainId id, bool allowDeleted = false) - { - var schema = await grainFactory.GetGrain(id.ToString()).GetStateAsync(); + await state.LoadAsync(ct); - if (schema.Version <= EtagVersion.Empty || (schema.IsDeleted && !allowDeleted)) - { - return null; - } - - return schema; - } - - private Task InvalidateItAsync(DomainId appId, DomainId id, string name) - { - return grainCache.RemoveAsync( - GetCacheKey(appId, id), - GetCacheKey(appId, name)); + return state; } private static string GetCacheKey(DomainId appId, string name) @@ -232,11 +206,23 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes return $"{typeof(SchemasIndex)}_Schemas_Id_{appId}_{id}"; } + private static bool IsValid(ISchemaEntity? schema) + { + return schema != null && schema.Version > EtagVersion.Empty && !schema.IsDeleted; + } + + private Task InvalidateItAsync(DomainId appId, DomainId id, string name) + { + return schemaCache.RemoveAsync( + GetCacheKey(appId, id), + GetCacheKey(appId, name)); + } + private Task CacheItAsync(ISchemaEntity schema) { return Task.WhenAll( - grainCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.Id), schema, CacheDuration), - grainCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema, CacheDuration)); + schemaCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.Id), schema, CacheDuration), + schemaCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema, CacheDuration)); } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs index 436a286b9..dfbab4bbb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Schemas/Repositories/ISchemaRepository.cs @@ -11,7 +11,13 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Repositories { public interface ISchemaRepository { - Task> QueryIdsAsync(DomainId appId, + Task> QueryAllAsync(DomainId appId, + CancellationToken ct = default); + + Task FindAsync(DomainId appId, DomainId id, + CancellationToken ct = default); + + Task FindAsync(DomainId appId, string name, CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj index b199757b2..120752c1e 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj +++ b/backend/src/Squidex.Domain.Apps.Entities/Squidex.Domain.Apps.Entities.csproj @@ -27,11 +27,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers - - diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs deleted file mode 100644 index 3ebb7873f..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Tags/GrainTagService.cs +++ /dev/null @@ -1,79 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public sealed class GrainTagService : ITagService - { - private readonly IGrainFactory grainFactory; - - public GrainTagService(IGrainFactory grainFactory) - { - this.grainFactory = grainFactory; - } - - public Task RenameTagAsync(DomainId appId, string group, string name, string newName) - { - Guard.NotNullOrEmpty(name); - Guard.NotNullOrEmpty(newName); - - return GetGrain(appId, group).RenameTagAsync(name, newName); - } - - public Task RebuildTagsAsync(DomainId appId, string group, TagsExport export) - { - Guard.NotNull(export); - - return GetGrain(appId, group).RebuildAsync(export); - } - - public Task> GetTagIdsAsync(DomainId appId, string group, HashSet names) - { - Guard.NotNull(names); - - return GetGrain(appId, group).GetTagIdsAsync(names); - } - - public Task> DenormalizeTagsAsync(DomainId appId, string group, HashSet ids) - { - Guard.NotNull(ids); - - return GetGrain(appId, group).DenormalizeTagsAsync(ids); - } - - public Task> NormalizeTagsAsync(DomainId appId, string group, HashSet? names, HashSet? ids) - { - return GetGrain(appId, group).NormalizeTagsAsync(names, ids); - } - - public Task GetTagsAsync(DomainId appId, string group) - { - return GetGrain(appId, group).GetTagsAsync(); - } - - public Task GetExportableTagsAsync(DomainId appId, string group) - { - return GetGrain(appId, group).GetExportableTagsAsync(); - } - - public Task ClearAsync(DomainId appId, string group) - { - return GetGrain(appId, group).ClearAsync(); - } - - private ITagGrain GetGrain(DomainId appId, string group) - { - Guard.NotNullOrEmpty(group); - - return grainFactory.GetGrain($"{appId}_{group}"); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs deleted file mode 100644 index 4c118414e..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Tags/ITagGrain.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Domain.Apps.Core.Tags; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public interface ITagGrain : IGrainWithStringKey - { - Task> NormalizeTagsAsync(HashSet? names, HashSet? ids); - - Task> GetTagIdsAsync(HashSet names); - - Task> DenormalizeTagsAsync(HashSet ids); - - Task GetTagsAsync(); - - Task GetExportableTagsAsync(); - - Task ClearAsync(); - - Task RenameTagAsync(string name, string newName); - - Task RebuildAsync(TagsExport export); - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs deleted file mode 100644 index 1457f4d8c..000000000 --- a/backend/src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs +++ /dev/null @@ -1,212 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Squidex.Infrastructure.States; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public sealed class TagGrain : GrainBase, ITagGrain - { - private readonly IGrainState grainState; - - [CollectionName("Index_Tags")] - public sealed class State : TagsExport - { - } - - private Dictionary Tags - { - get => grainState.Value.Tags ??= new Dictionary(); - } - - private Dictionary Alias - { - get => grainState.Value.Alias ??= new Dictionary(); - } - - public TagGrain(IGrainIdentity grainIdentity, IGrainState grainState) - : base(grainIdentity) - { - this.grainState = grainState; - } - - public Task ClearAsync() - { - return grainState.ClearAsync(); - } - - public Task RebuildAsync(TagsExport export) - { - grainState.Value.Tags = export.Tags; - grainState.Value.Alias = export.Alias; - - return grainState.WriteAsync(); - } - - public Task RenameTagAsync(string name, string newName) - { - Guard.NotNull(name); - Guard.NotNull(newName); - - name = NormalizeName(name); - - var (_, tag) = FindTag(name); - - if (tag == null) - { - return Task.CompletedTask; - } - - newName = NormalizeName(newName); - - tag.Name = newName; - - foreach (var alias in Alias.Where(x => x.Value == name).ToList()) - { - Alias.Remove(alias.Key); - - if (alias.Key != newName) - { - Alias[alias.Key] = newName; - } - } - - Alias[name] = newName; - - return grainState.WriteAsync(); - } - - public async Task> NormalizeTagsAsync(HashSet? names, HashSet? ids) - { - var result = new Dictionary(); - - if (names != null) - { - foreach (var tag in names) - { - var name = NormalizeName(tag); - - if (!string.IsNullOrWhiteSpace(name)) - { - result.Add(name, GetId(name, ids)); - } - } - } - - if (ids != null) - { - foreach (var id in ids) - { - if (!result.ContainsValue(id)) - { - if (Tags.TryGetValue(id, out var tagInfo)) - { - tagInfo.Count--; - - if (tagInfo.Count <= 0) - { - Tags.Remove(id); - } - } - } - } - } - - await grainState.WriteAsync(); - - return result; - } - - public Task> GetTagIdsAsync(HashSet names) - { - Guard.NotNull(names); - - var result = new Dictionary(); - - foreach (var tag in names) - { - var name = NormalizeName(tag); - - var (id, _) = FindTag(name); - - if (!string.IsNullOrWhiteSpace(id)) - { - result.Add(name, id); - } - } - - return Task.FromResult(result); - } - - public Task> DenormalizeTagsAsync(HashSet ids) - { - var result = new Dictionary(); - - foreach (var id in ids) - { - if (Tags.TryGetValue(id, out var tagInfo)) - { - result[id] = tagInfo.Name; - } - } - - return Task.FromResult(result); - } - - public Task GetTagsAsync() - { - var tags = Tags.Values.ToDictionary(x => x.Name, x => x.Count); - - return Task.FromResult(new TagsSet(tags, grainState.Version)); - } - - public Task GetExportableTagsAsync() - { - return Task.FromResult(grainState.Value.Clone()); - } - - private string GetId(string name, HashSet? ids) - { - var (id, tag) = FindTag(name); - - if (tag != null) - { - if (ids == null || !ids.Contains(id)) - { - tag.Count++; - } - } - else - { - id = DomainId.NewGuid().ToString(); - - Tags.Add(id, new Tag { Name = name }); - } - - return id; - } - - private static string NormalizeName(string name) - { - return name.Trim().ToLowerInvariant(); - } - - private KeyValuePair FindTag(string name) - { - if (Alias.TryGetValue(name, out var newName)) - { - name = newName; - } - - return Tags.FirstOrDefault(x => x.Value.Name == name); - } - } -} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs b/backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs new file mode 100644 index 000000000..f29b17219 --- /dev/null +++ b/backend/src/Squidex.Domain.Apps.Entities/Tags/TagService.cs @@ -0,0 +1,278 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.States; + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public sealed class TagService : ITagService + { + private readonly IPersistenceFactory persistenceFactory; + + [CollectionName("Index_Tags")] + public sealed class State : TagsExport + { + public void Rebuild(TagsExport export) + { + Tags = export.Tags; + + Alias = export.Alias; + } + + public void Rename(string name, string newName) + { + Guard.NotNull(name); + Guard.NotNull(newName); + + name = NormalizeName(name); + + var (_, tag) = FindTag(name); + + if (tag == null) + { + return; + } + + newName = NormalizeName(newName); + + tag.Name = newName; + + if (Alias != null) + { + foreach (var alias in Alias.Where(x => x.Value == name).ToList()) + { + Alias.Remove(alias.Key); + + if (alias.Key != newName) + { + Alias[alias.Key] = newName; + } + } + } + + Alias ??= new Dictionary(); + Alias[name] = newName; + } + + public Dictionary Normalize(HashSet? names, HashSet? ids) + { + var result = new Dictionary(); + + if (names != null) + { + foreach (var tag in names) + { + var name = NormalizeName(tag); + + if (!string.IsNullOrWhiteSpace(name)) + { + result.Add(name, GetId(name, ids)); + } + } + } + + if (ids != null) + { + foreach (var id in ids) + { + if (!result.ContainsValue(id)) + { + if (Tags != null && Tags.TryGetValue(id, out var tagInfo)) + { + tagInfo.Count--; + + if (tagInfo.Count <= 0) + { + Tags.Remove(id); + } + } + } + } + } + + return result; + } + + public Dictionary GetTagIds(HashSet names) + { + Guard.NotNull(names); + + var result = new Dictionary(); + + foreach (var tag in names) + { + var name = NormalizeName(tag); + + var (id, _) = FindTag(name); + + if (!string.IsNullOrWhiteSpace(id)) + { + result.Add(name, id); + } + } + + return result; + } + + public Dictionary Denormalize(HashSet ids) + { + var result = new Dictionary(); + + foreach (var id in ids) + { + if (Tags?.TryGetValue(id, out var tagInfo) == true) + { + result[id] = tagInfo.Name; + } + } + + return result; + } + + public TagsSet GetTags(long version) + { + var tags = Tags?.Values.ToDictionary(x => x.Name, x => x.Count) ?? new Dictionary(); + + return new TagsSet(tags, version); + } + + public TagsExport GetExportableTags() + { + var clone = Clone(); + + return clone; + } + + private string GetId(string name, HashSet? ids) + { + var (id, tag) = FindTag(name); + + if (tag != null) + { + if (ids == null || !ids.Contains(id)) + { + tag.Count++; + } + } + else + { + id = DomainId.NewGuid().ToString(); + + Tags ??= new Dictionary(); + Tags.Add(id, new Tag { Name = name }); + } + + return id; + } + + private static string NormalizeName(string name) + { + return name.Trim().ToLowerInvariant(); + } + + private KeyValuePair FindTag(string name) + { + if (Alias?.TryGetValue(name, out var newName) == true) + { + name = newName; + } + + return Tags?.FirstOrDefault(x => x.Value.Name == name) ?? default; + } + } + + public TagService(IPersistenceFactory persistenceFactory) + { + this.persistenceFactory = persistenceFactory; + } + + public async Task RenameTagAsync(DomainId id, string group, string name, string newName, + CancellationToken ct = default) + { + Guard.NotNullOrEmpty(name); + Guard.NotNullOrEmpty(newName); + + var state = await GetStateAsync(id, group, ct); + + await state.UpdateAsync(s => s.Rename(name, newName), ct: ct); + } + + public async Task RebuildTagsAsync(DomainId id, string group, TagsExport export, + CancellationToken ct = default) + { + Guard.NotNull(export); + + var state = await GetStateAsync(id, group, ct); + + await state.UpdateAsync(s => s.Rebuild(export), ct: ct); + } + + public async Task> GetTagIdsAsync(DomainId id, string group, HashSet names, + CancellationToken ct = default) + { + Guard.NotNull(names); + + var state = await GetStateAsync(id, group, ct); + + return await state.UpdateAsync(s => s.GetTagIds(names), ct: ct); + } + + public async Task> DenormalizeTagsAsync(DomainId id, string group, HashSet ids, + CancellationToken ct = default) + { + Guard.NotNull(ids); + + var state = await GetStateAsync(id, group, ct); + + return await state.UpdateAsync(s => s.Denormalize(ids), ct: ct); + } + + public async Task> NormalizeTagsAsync(DomainId id, string group, HashSet? names, HashSet? ids, + CancellationToken ct = default) + { + var state = await GetStateAsync(id, group, ct); + + return await state.UpdateAsync(s => s.Normalize(names, ids), ct: ct); + } + + public async Task GetTagsAsync(DomainId id, string group, + CancellationToken ct = default) + { + var state = await GetStateAsync(id, group, ct); + + return state.Value.GetTags(state.Version); + } + + public async Task GetExportableTagsAsync(DomainId id, string group, + CancellationToken ct = default) + { + var state = await GetStateAsync(id, group, ct); + + return state.Value.GetExportableTags(); + } + + public async Task ClearAsync(DomainId id, string group, + CancellationToken ct = default) + { + var state = await GetStateAsync(id, group, ct); + + await state.ClearAsync(ct); + } + + private async Task> GetStateAsync(DomainId id, string group, + CancellationToken ct) + { + var state = new SimpleState(persistenceFactory, GetType(), $"{id}_{group}"); + + await state.LoadAsync(ct); + + return state; + } + } +} diff --git a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs index 5487723b3..6f5be9cee 100644 --- a/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs +++ b/backend/src/Squidex.Infrastructure.GetEventStore/EventSourcing/GetEventStoreSubscription.cs @@ -70,7 +70,7 @@ namespace Squidex.Infrastructure.EventSourcing }, cts.Token); } - public void Unsubscribe() + public void Dispose() { subscription?.Dispose(); diff --git a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoCacheEntry.cs similarity index 55% rename from backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs rename to backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoCacheEntry.cs index 8c7c7c61a..7c72dbd80 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Backup/IBackupGrain.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoCacheEntry.cs @@ -5,19 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; -using Squidex.Infrastructure; +using MongoDB.Bson.Serialization.Attributes; -namespace Squidex.Domain.Apps.Entities.Backup +namespace Squidex.Infrastructure.Caching { - public interface IBackupGrain : IGrainWithStringKey + public sealed class MongoCacheEntry { - Task BackupAsync(RefToken actor); + [BsonId] + public string Key { get; set; } - Task DeleteAsync(DomainId id); + [BsonElement] + public DateTime Expires { get; set; } - Task ClearAsync(); - - Task> GetStateAsync(); + [BsonElement] + public byte[] Value { get; set; } } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs new file mode 100644 index 000000000..840cbfb35 --- /dev/null +++ b/backend/src/Squidex.Infrastructure.MongoDb/Caching/MongoDistributedCache.cs @@ -0,0 +1,109 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Caching.Distributed; +using MongoDB.Driver; +using Squidex.Infrastructure.MongoDb; + +namespace Squidex.Infrastructure.Caching +{ + public sealed class MongoDistributedCache : MongoRepositoryBase, IDistributedCache + { + public MongoDistributedCache(IMongoDatabase database, bool setup = false) + : base(database, setup) + { + } + + protected override string CollectionName() + { + return "Cache"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection, + CancellationToken ct) + { + return Collection.Indexes.CreateOneAsync( + new CreateIndexModel( + Index.Ascending(x => x.Expires), + new CreateIndexOptions + { + ExpireAfter = TimeSpan.Zero + }), + null, ct); + } + + public byte[] Get(string key) + { + throw new NotSupportedException(); + } + + public void Refresh(string key) + { + throw new NotSupportedException(); + } + + public void Remove(string key) + { + throw new NotSupportedException(); + } + + public void Set(string key, byte[] value, DistributedCacheEntryOptions options) + { + throw new NotSupportedException(); + } + + public Task RefreshAsync(string key, + CancellationToken token = default) + { + return Task.CompletedTask; + } + + public Task RemoveAsync(string key, + CancellationToken token = default) + { + return Collection.DeleteOneAsync(x => x.Key == key, token); + } + + public async Task GetAsync(string key, + CancellationToken token = default) + { + var entry = await Collection.Find(x => x.Key == key).FirstOrDefaultAsync(token); + + if (entry != null && entry.Expires > DateTime.UtcNow) + { + return entry.Value; + } + + return null; + } + + public Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, + CancellationToken token = default) + { + var expires = DateTime.UtcNow; + + if (options.AbsoluteExpiration.HasValue) + { + expires = options.AbsoluteExpiration.Value.UtcDateTime; + } + else if (options.AbsoluteExpirationRelativeToNow.HasValue) + { + expires += options.AbsoluteExpirationRelativeToNow.Value; + } + else if (options.SlidingExpiration.HasValue) + { + expires += options.SlidingExpiration.Value; + } + + return Collection.UpdateOneAsync(x => x.Key == key, + Update + .Set(x => x.Value, value) + .Set(x => x.Expires, expires), + Upsert, token); + } + } +} diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs index 8ec276bb4..1ce345beb 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStoreSubscription.cs @@ -152,7 +152,7 @@ namespace Squidex.Infrastructure.EventSourcing return result; } - public void Unsubscribe() + public void Dispose() { stopToken.Cancel(); } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs index 666436048..8bd3b1ee9 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/EventSourcing/MongoEventStore_Writer.cs @@ -64,7 +64,7 @@ namespace Squidex.Infrastructure.EventSourcing var commit = BuildCommit(commitId, streamName, expectedVersion >= -1 ? expectedVersion : currentVersion, events); - for (var attempt = 0; attempt < MaxWriteAttempts; attempt++) + for (var attempt = 1; attempt <= MaxWriteAttempts; attempt++) { try { @@ -88,11 +88,7 @@ namespace Squidex.Infrastructure.EventSourcing throw new WrongEventVersionException(currentVersion, expectedVersion); } - if (attempt < MaxWriteAttempts) - { - expectedVersion = currentVersion; - } - else + if (attempt >= MaxWriteAttempts) { throw new TimeoutException("Could not acquire a free slot for the commit within the provided time."); } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs index 032718502..6ddfcb716 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs @@ -12,8 +12,8 @@ namespace Squidex.Infrastructure.States { public sealed class MongoSnapshotStore : MongoSnapshotStoreBase> { - public MongoSnapshotStore(IMongoDatabase database, JsonSerializer jsonSerializer) - : base(database, jsonSerializer) + public MongoSnapshotStore(IMongoDatabase database, JsonSerializer serializer) + : base(database, serializer) { } } diff --git a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs index b0f50f652..cd3e0b021 100644 --- a/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs +++ b/backend/src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStoreBase.cs @@ -15,16 +15,16 @@ namespace Squidex.Infrastructure.States { public abstract class MongoSnapshotStoreBase : MongoRepositoryBase, ISnapshotStore where TState : MongoState, new() { - protected MongoSnapshotStoreBase(IMongoDatabase database, JsonSerializer jsonSerializer) - : base(database, Register(jsonSerializer)) + protected MongoSnapshotStoreBase(IMongoDatabase database, JsonSerializer serializer) + : base(database, Register(serializer)) { } - private static bool Register(JsonSerializer jsonSerializer) + private static bool Register(JsonSerializer serializer) { - Guard.NotNull(jsonSerializer); + Guard.NotNull(serializer); - BsonJsonConvention.Register(jsonSerializer); + BsonJsonConvention.Register(serializer); return true; } diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs index 6c36c1843..8cef970a4 100644 --- a/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs +++ b/backend/src/Squidex.Infrastructure.RabbitMq/CQRS/Events/RabbitMqEventConsumer.cs @@ -16,7 +16,7 @@ namespace Squidex.Infrastructure.CQRS.Events { public sealed class RabbitMqEventConsumer : DisposableObjectBase, IInitializable, IEventConsumer { - private readonly IJsonSerializer jsonSerializer; + private readonly IJsonSerializer serializer; private readonly string eventPublisherName; private readonly string exchange; private readonly string eventsFilter; @@ -34,7 +34,7 @@ namespace Squidex.Infrastructure.CQRS.Events get => eventsFilter; } - public RabbitMqEventConsumer(IJsonSerializer jsonSerializer, string eventPublisherName, string uri, string exchange, string eventsFilter) + public RabbitMqEventConsumer(IJsonSerializer serializer, string eventPublisherName, string uri, string exchange, string eventsFilter) { connectionFactory = new ConnectionFactory { Uri = new Uri(uri, UriKind.Absolute) }; connection = new Lazy(connectionFactory.CreateConnection); @@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.CQRS.Events this.exchange = exchange; this.eventsFilter = eventsFilter; this.eventPublisherName = eventPublisherName; - this.jsonSerializer = jsonSerializer; + this.serializer = serializer; } protected override void DisposeObject(bool disposing) @@ -86,7 +86,7 @@ namespace Squidex.Infrastructure.CQRS.Events return Task.CompletedTask; } - var jsonString = jsonSerializer.Serialize(@event); + var jsonString = serializer.Serialize(@event); var jsonBytes = Encoding.UTF8.GetBytes(jsonString); channel.Value.BasicPublish(exchange, string.Empty, null, jsonBytes); diff --git a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj index d128b4572..4d5f5fb22 100644 --- a/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj +++ b/backend/src/Squidex.Infrastructure.RabbitMq/Squidex.Infrastructure.RabbitMq.csproj @@ -15,7 +15,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/backend/src/Squidex.Infrastructure/Commands/AggregateCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/AggregateCommandMiddleware.cs new file mode 100644 index 000000000..66cf2a923 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/AggregateCommandMiddleware.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Commands +{ + public class AggregateCommandMiddleware : ICommandMiddleware + where TCommand : IAggregateCommand where T1 : IAggregate + { + private readonly IDomainObjectFactory domainObjectFactory; + + public AggregateCommandMiddleware(IDomainObjectFactory domainObjectFactory) + { + this.domainObjectFactory = domainObjectFactory; + } + + public virtual async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) + { + await ExecuteCommandAsync(context, ct); + + await next(context, ct); + } + + protected async Task ExecuteCommandAsync(CommandContext context, + CancellationToken ct) + { + if (context.Command is TCommand typedCommand) + { + var commandResult = await ExecuteCommandAsync(typedCommand, ct); + var commandPayload = await EnrichResultAsync(context, commandResult, ct); + + context.Complete(commandPayload); + } + } + + protected virtual Task EnrichResultAsync(CommandContext context, CommandResult result, + CancellationToken ct) + { + return Task.FromResult(result.Payload is None ? result : result.Payload); + } + + protected virtual Task ExecuteCommandAsync(TCommand command, + CancellationToken ct) + { + var executable = domainObjectFactory.Create(command.AggregateId); + + return ExecuteCommandAsync(executable, command, ct); + } + + protected virtual Task ExecuteCommandAsync(T1 executable, TCommand command, + CancellationToken ct) + { + return executable.ExecuteAsync(command, ct); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/CachingDomainObjectMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/CachingDomainObjectMiddleware.cs new file mode 100644 index 000000000..b1dbe841e --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/CachingDomainObjectMiddleware.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Commands +{ + public class CachingDomainObjectMiddleware : AggregateCommandMiddleware + where TCommand : IAggregateCommand where T : DomainObject where TState : class, IDomainState, new() + { + private readonly IDomainObjectCache domainObjectCache; + + public CachingDomainObjectMiddleware(IDomainObjectFactory domainObjectFactory, IDomainObjectCache domainObjectCache) + : base(domainObjectFactory) + { + this.domainObjectCache = domainObjectCache; + } + + protected override async Task ExecuteCommandAsync(T executable, TCommand command, + CancellationToken ct) + { + var oldSnapshot = executable.Snapshot; + + var result = await base.ExecuteCommandAsync(executable, command, ct); + + var newSnapshot = executable.Snapshot; + + if (newSnapshot.Version != oldSnapshot.Version) + { + // If we are so far it is not worth to cancel the flow anymore. + await domainObjectCache.SetAsync(executable.UniqueId, oldSnapshot.Version, oldSnapshot, default); + await domainObjectCache.SetAsync(executable.UniqueId, newSnapshot.Version, newSnapshot, default); + } + + return result; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs b/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs index 5d650fa79..7af5b4762 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CommandExtensions.cs @@ -11,9 +11,12 @@ namespace Squidex.Infrastructure.Commands { public static class CommandExtensions { - public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context) + private static readonly NextDelegate End = (c, ct) => Task.CompletedTask; + + public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context, + CancellationToken ct) { - return commandMiddleware.HandleAsync(context, x => Task.CompletedTask); + return commandMiddleware.HandleAsync(context, End, ct); } public static Envelope Migrate(this Envelope @event, T snapshot) diff --git a/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs b/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs index 55960870d..3d3422a2f 100644 --- a/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs +++ b/backend/src/Squidex.Infrastructure/Commands/CustomCommandMiddlewareRunner.cs @@ -16,19 +16,20 @@ namespace Squidex.Infrastructure.Commands this.extensions = extensions.Reverse().ToList(); } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { foreach (var handler in extensions) { next = Join(handler, next); } - await next(context); + await next(context, ct); } private static NextDelegate Join(ICommandMiddleware handler, NextDelegate next) { - return context => handler.HandleAsync(context, next); + return (context, ct) => handler.HandleAsync(context, next, ct); } } } diff --git a/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs b/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs new file mode 100644 index 000000000..d5b900ad1 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Commands/DefaultDomainObjectCache.cs @@ -0,0 +1,92 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Infrastructure.Json; +using Squidex.Infrastructure.ObjectPool; + +namespace Squidex.Infrastructure.Commands +{ + public sealed class DefaultDomainObjectCache : IDomainObjectCache + { + private static readonly DistributedCacheEntryOptions CacheOptions = new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromMinutes(10) + }; + + private readonly IMemoryCache cache; + private readonly IJsonSerializer serializer; + private readonly IDistributedCache distributedCache; + + public DefaultDomainObjectCache(IMemoryCache cache, IJsonSerializer serializer, IDistributedCache distributedCache) + { + this.cache = cache; + this.serializer = serializer; + this.distributedCache = distributedCache; + } + + public async Task GetAsync(DomainId id, long version, + CancellationToken ct = default) + { + var cacheKey = CacheKey(id, version); + + if (cache.TryGetValue(cacheKey, out var found) && found is T typed) + { + return typed; + } + + var buffer = await distributedCache.GetAsync(cacheKey, ct); + + if (buffer == null) + { + return default!; + } + + try + { + using (var stream = new MemoryStream(buffer)) + { + var result = serializer.Deserialize(stream); + + return result; + } + } + catch + { + return default!; + } + } + + public async Task SetAsync(DomainId id, long version, T snapshot, + CancellationToken ct = default) + { + var cacheKey = CacheKey(id, version); + + cache.Set(cacheKey, snapshot, CacheOptions.SlidingExpiration!.Value); + + try + { + using (var stream = DefaultPools.MemoryStream.GetStream()) + { + serializer.Serialize(snapshot, stream, true); + + await distributedCache.SetAsync(cacheKey, stream.ToArray(), CacheOptions, ct); + } + } + catch + { + return; + } + } + + private static string CacheKey(DomainId key, long version) + { + return $"{key}_{version}"; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs index e664c4e46..40ff87ad6 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObject.Execute.cs @@ -9,149 +9,163 @@ namespace Squidex.Infrastructure.Commands { public partial class DomainObject { - protected Task CreateReturnAsync(TCommand command, Func> handler) where TCommand : ICommand + protected Task CreateReturnAsync(TCommand command, Func> handler, + CancellationToken ct = default) where TCommand : ICommand { EnsureCanCreate(command); - return UpsertCoreAsync(command, handler, true); + return UpsertCoreAsync(command, handler, true, ct); } - protected Task CreateReturn(TCommand command, Func handler) where TCommand : ICommand + protected Task CreateReturn(TCommand command, Func handler, + CancellationToken ct = default) where TCommand : ICommand { - return CreateReturnAsync(command, x => + return CreateReturnAsync(command, (c, _) => { - var result = handler(x); + var result = handler(c); return Task.FromResult(result); - }); + }, ct); } - protected Task CreateAsync(TCommand command, Func handler) where TCommand : ICommand + protected Task CreateAsync(TCommand command, Func handler, + CancellationToken ct = default) where TCommand : ICommand { EnsureCanCreate(command); - return UpsertCoreAsync(command, async x => + return UpsertCoreAsync(command, async (c, ct) => { - await handler(x); + await handler(c, ct); return None.Value; - }, true); + }, true, ct); } - protected Task Create(TCommand command, Action handler) where TCommand : ICommand + protected Task Create(TCommand command, Action handler, + CancellationToken ct = default) where TCommand : ICommand { - return CreateAsync(command, x => + return CreateAsync(command, (c, ct) => { - handler(x); + handler(c); return Task.FromResult(None.Value); - }); + }, ct); } - protected async Task UpdateReturnAsync(TCommand command, Func> handler) where TCommand : ICommand + protected async Task UpdateReturnAsync(TCommand command, Func> handler, + CancellationToken ct = default) where TCommand : ICommand { - await EnsureCanUpdateAsync(command); + await EnsureCanUpdateAsync(command, ct); - return await UpsertCoreAsync(command, handler); + return await UpsertCoreAsync(command, handler, false, ct); } - protected Task UpdateReturn(TCommand command, Func handler) where TCommand : ICommand + protected Task UpdateReturn(TCommand command, Func handler, + CancellationToken ct = default) where TCommand : ICommand { - return UpdateReturnAsync(command, x => + return UpdateReturnAsync(command, (c, ct) => { - var result = handler(x); + var result = handler(c); return Task.FromResult(result); - }); + }, ct); } - protected async Task UpdateAsync(TCommand command, Func handler) where TCommand : ICommand + protected async Task UpdateAsync(TCommand command, Func handler, + CancellationToken ct = default) where TCommand : ICommand { - await EnsureCanUpdateAsync(command); + await EnsureCanUpdateAsync(command, ct); - return await UpsertCoreAsync(command, async x => + return await UpsertCoreAsync(command, async (c, ct) => { - await handler(x); + await handler(c, ct); return None.Value; - }, true); + }, false, ct); } - protected async Task Update(TCommand command, Action handler) where TCommand : ICommand + protected async Task Update(TCommand command, Action handler, + CancellationToken ct = default) where TCommand : ICommand { - return await UpdateAsync(command, x => + return await UpdateAsync(command, (c, _) => { - handler(x); + handler(c); return Task.FromResult(None.Value); - }); + }, ct); } - protected async Task UpsertReturnAsync(TCommand command, Func> handler) where TCommand : ICommand + protected async Task UpsertReturnAsync(TCommand command, Func> handler, + CancellationToken ct = default) where TCommand : ICommand { - await EnsureCanUpsertAsync(command); + await EnsureCanUpsertAsync(command, ct); - return await UpsertCoreAsync(command, handler, true); + return await UpsertCoreAsync(command, handler, true, ct); } - protected async Task UpsertReturn(TCommand command, Func handler) where TCommand : ICommand + protected async Task UpsertReturn(TCommand command, Func handler, + CancellationToken ct = default) where TCommand : ICommand { - return await UpsertReturnAsync(command, x => + return await UpsertReturnAsync(command, (c, _) => { - var result = handler(x); + var result = handler(c); return Task.FromResult(result); - }); + }, ct); } - protected async Task UpsertAsync(TCommand command, Func handler) where TCommand : ICommand + protected async Task UpsertAsync(TCommand command, Func handler, + CancellationToken ct = default) where TCommand : ICommand { - await EnsureCanUpsertAsync(command); + await EnsureCanUpsertAsync(command, ct); - return await UpsertCoreAsync(command, async x => + return await UpsertCoreAsync(command, async (c, ct) => { - await handler(x); + await handler(c, ct); return None.Value; - }, true); + }, true, ct); } - protected async Task Upsert(TCommand command, Action handler) where TCommand : ICommand + protected async Task Upsert(TCommand command, Action handler, + CancellationToken ct = default) where TCommand : ICommand { Guard.NotNull(handler); - return await UpsertAsync(command, x => + return await UpsertAsync(command, (c, _) => { - handler(x); + handler(c); return Task.FromResult(None.Value); - }); + }, ct); } - protected async Task DeletePermanentAsync(TCommand command, Func handler) where TCommand : ICommand + protected async Task DeletePermanentAsync(TCommand command, Func handler, + CancellationToken ct = default) where TCommand : ICommand { Guard.NotNull(handler); - await EnsureCanDeleteAsync(command); + await EnsureCanDeleteAsync(command, ct); - return await DeleteCoreAsync(command, async x => + return await DeleteCoreAsync(command, async (c, ct) => { - await handler(x); + await handler(c, ct); return None.Value; - }); + }, ct); } - protected async Task DeletePermanent(TCommand command, Action handler) where TCommand : ICommand + protected async Task DeletePermanent(TCommand command, Action handler, + CancellationToken ct = default) where TCommand : ICommand { Guard.NotNull(handler); - return await DeletePermanentAsync(command, x => + return await DeletePermanentAsync(command, (c, _) => { - handler(x); + handler(c); return Task.FromResult(None.Value); - }); + }, ct); } private void EnsureCanCreate(TCommand command) where TCommand : ICommand @@ -167,11 +181,12 @@ namespace Squidex.Infrastructure.Commands MatchingCreateCommand(command); } - private async Task EnsureCanUpdateAsync(TCommand command) where TCommand : ICommand + private async Task EnsureCanUpdateAsync(TCommand command, + CancellationToken ct) where TCommand : ICommand { Guard.NotNull(command); - await EnsureLoadedAsync(); + await EnsureLoadedAsync(ct); NotDeleted(); NotEmpty(); @@ -180,11 +195,12 @@ namespace Squidex.Infrastructure.Commands MatchingCommand(command); } - private async Task EnsureCanUpsertAsync(TCommand command) where TCommand : ICommand + private async Task EnsureCanUpsertAsync(TCommand command, + CancellationToken ct) where TCommand : ICommand { Guard.NotNull(command); - await EnsureLoadedAsync(); + await EnsureLoadedAsync(ct); if (IsDeleted(Snapshot) && !CanRecreate()) { @@ -203,11 +219,12 @@ namespace Squidex.Infrastructure.Commands } } - private async Task EnsureCanDeleteAsync(TCommand command) where TCommand : ICommand + private async Task EnsureCanDeleteAsync(TCommand command, + CancellationToken ct) where TCommand : ICommand { Guard.NotNull(command); - await EnsureLoadedAsync(); + await EnsureLoadedAsync(ct); NotEmpty(); diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs index c09af3fd5..33d67830e 100644 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs +++ b/backend/src/Squidex.Infrastructure/Commands/DomainObject.cs @@ -9,16 +9,19 @@ using Microsoft.Extensions.Logging; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.States; +#pragma warning disable MA0056 // Do not call overridable members in constructor +#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it + namespace Squidex.Infrastructure.Commands { - public abstract partial class DomainObject where T : class, IDomainState, new() + public abstract partial class DomainObject : IAggregate where T : class, IDomainState, new() { private readonly List> uncomittedEvents = new List>(); - private readonly SnapshotList snapshots = new SnapshotList(); private readonly ILogger log; - private readonly IPersistenceFactory factory; + private readonly IPersistenceFactory persistenceFactory; private readonly IPersistence persistence; private readonly DomainId uniqueId; + private T snapshot = new T { Version = EtagVersion.Empty }; private bool isLoaded; public DomainId UniqueId @@ -26,100 +29,107 @@ namespace Squidex.Infrastructure.Commands get => uniqueId; } - public T Snapshot - { - get => snapshots.Current; - } - - public long Version + public virtual T Snapshot { - get => snapshots.Version; + get => snapshot; } - protected int Capacity + public virtual long Version { - get => snapshots.Capacity; - set => snapshots.Capacity = value; + get => snapshot.Version; } - protected DomainObject(DomainId uniqueId, IPersistenceFactory factory, + protected DomainObject(DomainId uniqueId, IPersistenceFactory persistenceFactory, ILogger log) { - Guard.NotNull(factory); + Guard.NotNull(persistenceFactory); Guard.NotNull(log); this.uniqueId = uniqueId; - this.factory = factory; - persistence = factory.WithSnapshotsAndEventSourcing(GetType(), UniqueId, - new HandleSnapshot((snapshot, version) => + this.persistenceFactory = persistenceFactory; + + persistence = persistenceFactory.WithSnapshotsAndEventSourcing(GetType(), UniqueId, + new HandleSnapshot((newSnapshot, version) => { - snapshot.Version = version; - snapshots.Add(snapshot, version, true); + newSnapshot.Version = version; + + snapshot = newSnapshot; }), @event => { - // Some migrations needs the current state. - @event = @event.Migrate(Snapshot); + var (newSnapshot, changed) = ApplyEvent(@event, Snapshot, Version, false, true); + + if (changed && newSnapshot != null) + { + snapshot = newSnapshot; + } - return ApplyEvent(@event, true, Snapshot, Version, true).Success; + return true; }); this.log = log; } - public async Task GetSnapshotAsync(long version) + public virtual async Task GetSnapshotAsync(long version, + CancellationToken ct = default) { - var (result, valid) = snapshots.Get(version); - - if (result == null && valid) + if (version <= EtagVersion.Any) { - var snapshot = new T - { - Version = EtagVersion.Empty - }; + await EnsureLoadedAsync(ct); + return Snapshot; + } - snapshots.Add(snapshot, snapshot.Version, false); + var result = new T + { + Version = EtagVersion.Empty + }; - var allEvents = factory.WithEventSourcing(GetType(), UniqueId, @event => - { - // Some migrations needs the current state. - @event = @event.Migrate(snapshot); + if (version == result.Version) + { + return result; + } - var (newSnapshot, isChanged) = ApplyEvent(@event, true, snapshot, snapshot.Version, false); + var allEvents = persistenceFactory.WithEventSourcing(GetType(), UniqueId, @event => + { + var (newSnapshot, _) = ApplyEvent(@event, result, result.Version, false, false); - // Can only be null in case of errors or inconsistent streams. - if (newSnapshot != null) - { - snapshot = newSnapshot; - } + // Can only be null in case of errors or inconsistent streams. + if (newSnapshot != null && newSnapshot.Version <= version) + { + result = newSnapshot; + } - // If all snapshorts from this one here are valid we can stop. - return newSnapshot != null && !snapshots.ContainsThisAndNewer(newSnapshot.Version); - }); + return result.Version <= version; + }); - await allEvents.ReadAsync(); + await allEvents.ReadAsync(ct: ct); - (result, valid) = snapshots.Get(version); + if (result.Version != version) + { + result = new T { Version = EtagVersion.Empty }; } - return result ?? new T { Version = EtagVersion.Empty }; + return result; } - public virtual async Task EnsureLoadedAsync() + public virtual async Task EnsureLoadedAsync( + CancellationToken ct = default) { if (isLoaded) { return; } - await persistence.ReadAsync(); + await persistence.ReadAsync(ct: ct); if (persistence.IsSnapshotStale) { try { +#pragma warning disable MA0040 // Flow the cancellation token await persistence.WriteSnapshotAsync(Snapshot); +#pragma warning restore MA0040 // Flow the cancellation token } catch (Exception ex) { @@ -141,7 +151,7 @@ namespace Squidex.Infrastructure.Commands @event.SetAggregateId(uniqueId); - if (ApplyEvent(@event, false, Snapshot, Version, true).Success) + if (ApplyEvent(@event, Snapshot, Version, false, true).Success) { uncomittedEvents.Add(@event); } @@ -157,7 +167,8 @@ namespace Squidex.Infrastructure.Commands uncomittedEvents.Clear(); } - private async Task DeleteCoreAsync(TCommand command, Func> handler) where TCommand : ICommand + private async Task DeleteCoreAsync(TCommand command, Func> handler, + CancellationToken ct = default) where TCommand : ICommand { Guard.NotNull(handler); @@ -165,30 +176,30 @@ namespace Squidex.Infrastructure.Commands var previousVersion = Version; try { - var result = (await handler(command)) ?? None.Value; + var result = (await handler(command, ct)) ?? None.Value; - var events = uncomittedEvents.ToArray(); - - if (events != null) + if (uncomittedEvents.Count > 0) { var deletedId = DomainId.Combine(UniqueId, DomainId.Create("deleted")); - var deletedStream = factory.WithEventSourcing(GetType(), deletedId, null); + var deletedStream = persistenceFactory.WithEventSourcing(GetType(), deletedId, null); // Write to the deleted stream first so we never loose this information. - await deletedStream.WriteEventsAsync(events); + await deletedStream.WriteEventsAsync(uncomittedEvents, ct); + } - // Cleanup the secondary stream first. - await persistence.DeleteAsync(); + // Cleanup the primary stream second. + await persistence.DeleteAsync(ct); - snapshots.Clear(); - } + snapshot = new T + { + Version = EtagVersion.Empty + }; return new CommandResult(UniqueId, Version, previousVersion, result); } catch { - RestorePreviousSnapshot(previousSnapshot, previousVersion); - + snapshot = previousSnapshot; throw; } finally @@ -197,52 +208,53 @@ namespace Squidex.Infrastructure.Commands } } - private async Task UpsertCoreAsync(TCommand command, Func> handler, bool isCreation = false) where TCommand : ICommand + private async Task UpsertCoreAsync(TCommand command, Func> handler, bool isCreation, + CancellationToken ct) where TCommand : ICommand { Guard.NotNull(handler); - var wasDeleted = IsDeleted(Snapshot); - var previousSnapshot = Snapshot; var previousVersion = Version; try { - var result = (await handler(command)) ?? None.Value; + var result = (await handler(command, ct)) ?? None.Value; var events = uncomittedEvents.ToArray(); - try { - await WriteAsync(events); + await WriteAsync(events, ct); } - catch (InconsistentStateException) + catch (InconsistentStateException ex) { - await EnsureLoadedAsync(); + // Start from the previous, unchanged snapshot. + snapshot = previousSnapshot; - if (wasDeleted) - { - if (CanRecreate() && isCreation) - { - snapshots.ResetTo(new T(), previousVersion); + // Create commands do not load the domain object for performance reasons, therefore we ensure it is loaded. + await EnsureLoadedAsync(ct); - foreach (var @event in uncomittedEvents) - { - ApplyEvent(@event, false, Snapshot, Version, true); - } + var isDeleted = IsDeleted(Snapshot); - await WriteAsync(events); - } - else + if (isDeleted && isCreation && CanRecreate()) + { + foreach (var @event in uncomittedEvents) { - throw new DomainObjectDeletedException(uniqueId.ToString()); + ApplyEvent(@event, Snapshot, Version, false, true); } + + await WriteAsync(events, ct); } - else + else if (isDeleted) + { + throw new DomainObjectDeletedException(uniqueId.ToString()); + } + else if (isCreation) { - RestorePreviousSnapshot(previousSnapshot, previousVersion); - throw new DomainObjectConflictException(uniqueId.ToString()); } + else + { + throw new DomainObjectVersionException(uniqueId.ToString(), ex.VersionCurrent, ex.VersionExpected, ex); + } } isLoaded = true; @@ -251,8 +263,7 @@ namespace Squidex.Infrastructure.Commands } catch { - RestorePreviousSnapshot(previousSnapshot, previousVersion); - + snapshot = previousSnapshot; throw; } finally @@ -286,12 +297,7 @@ namespace Squidex.Infrastructure.Commands return false; } - private void RestorePreviousSnapshot(T previousSnapshot, long previousVersion) - { - snapshots.ResetTo(previousSnapshot, previousVersion); - } - - private (T?, bool Success) ApplyEvent(Envelope @event, bool isLoading, T snapshot, long version, bool clean) + private (T?, bool Success) ApplyEvent(Envelope @event, T snapshot, long version, bool loading, bool update) { if (IsDeleted(snapshot)) { @@ -306,47 +312,50 @@ namespace Squidex.Infrastructure.Commands }; } + @event = @event.Migrate(snapshot); + var newVersion = version + 1; var newSnapshot = Apply(snapshot, @event); - if (ReferenceEquals(snapshot, newSnapshot) && isLoading) - { - newSnapshot = snapshot.Copy(); - } - - var isChanged = !ReferenceEquals(snapshot, newSnapshot); + var isChanged = loading || !ReferenceEquals(snapshot, newSnapshot); + // If we are loading events at the moment, we will always update the version. if (isChanged) { newSnapshot.Version = newVersion; + } - snapshots.Add(newSnapshot, newVersion, clean); + if (update) + { + this.snapshot = newSnapshot; } return (newSnapshot, isChanged); } - private async Task WriteAsync(Envelope[] newEvents) + private async Task WriteAsync(Envelope[] newEvents, + CancellationToken ct) { - if (newEvents.Length == 0) + if (newEvents.Length > 0) { - return; + // Writing the events is the first step, so we can cancel it if requested by the user, but if events are written, + // we should also write the snapshots to keep both collection / database as consitent as possible. + await persistence.WriteEventsAsync(newEvents, ct); + await persistence.WriteSnapshotAsync(Snapshot, default); } - - await persistence.WriteEventsAsync(newEvents); - await persistence.WriteSnapshotAsync(Snapshot); } - public async Task RebuildStateAsync() + public async Task RebuildStateAsync( + CancellationToken ct = default) { - await EnsureLoadedAsync(); + await EnsureLoadedAsync(ct); if (Version <= EtagVersion.Empty) { throw new DomainObjectNotFoundException(UniqueId.ToString()); } - await persistence.WriteSnapshotAsync(Snapshot); + await persistence.WriteSnapshotAsync(Snapshot, ct); } protected virtual T Apply(T snapshot, Envelope @event) @@ -354,6 +363,7 @@ namespace Squidex.Infrastructure.Commands return snapshot.Apply(@event); } - public abstract Task ExecuteAsync(IAggregateCommand command); + public abstract Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs deleted file mode 100644 index 89bf52707..000000000 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs +++ /dev/null @@ -1,38 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.Commands -{ - public abstract class DomainObjectGrain : GrainBase where T : DomainObject where TState : class, IDomainState, new() - { - private readonly T domainObject; - - public TState Snapshot - { - get => domainObject.Snapshot; - } - - protected T DomainObject - { - get => domainObject; - } - - protected DomainObjectGrain(IGrainIdentity identity, IDomainObjectFactory factory) - : base(identity) - { - domainObject = factory.Create(DomainId.Create(identity.PrimaryKeyString)); - } - - public Task ExecuteAsync(IAggregateCommand command) - { - return domainObject.ExecuteAsync(command); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs b/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs deleted file mode 100644 index 239890a0b..000000000 --- a/backend/src/Squidex.Infrastructure/Commands/DomainObjectGrainFormatter.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; - -namespace Squidex.Infrastructure.Commands -{ - public static class DomainObjectGrainFormatter - { - public static readonly Func Format = context => - { - if (context.InterfaceMethod == null) - { - return "Unknown"; - } - - if (string.Equals(context.InterfaceMethod.Name, nameof(IDomainObjectGrain.ExecuteAsync), StringComparison.CurrentCultureIgnoreCase) && - context.Arguments?.Length > 0 && - context.Arguments[0] != null) - { - var argumentFullName = context.Arguments[0].ToString(); - - if (argumentFullName != null) - { - var argumentParts = argumentFullName.Split('.'); - var argumentName = argumentParts[^1]; - - return $"{nameof(IDomainObjectGrain.ExecuteAsync)}({argumentName})"; - } - } - - return context.InterfaceMethod.Name; - }; - } -} diff --git a/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs index cdb8f9dbb..93065d954 100644 --- a/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/EnrichWithTimestampCommandMiddleware.cs @@ -11,21 +11,17 @@ namespace Squidex.Infrastructure.Commands { public sealed class EnrichWithTimestampCommandMiddleware : ICommandMiddleware { - private readonly IClock clock; + public IClock Clock { get; set; } = SystemClock.Instance; - public EnrichWithTimestampCommandMiddleware(IClock clock) - { - this.clock = clock; - } - - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (context.Command is ITimestampCommand timestampCommand) { - timestampCommand.Timestamp = clock.GetCurrentInstant(); + timestampCommand.Timestamp = Clock.GetCurrentInstant(); } - return next(context); + return next(context, ct); } } } diff --git a/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs deleted file mode 100644 index 3a12ac617..000000000 --- a/backend/src/Squidex.Infrastructure/Commands/GrainCommandMiddleware.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; - -namespace Squidex.Infrastructure.Commands -{ - public class GrainCommandMiddleware : ICommandMiddleware where TCommand : IAggregateCommand where TGrain : IDomainObjectGrain - { - private readonly IGrainFactory grainFactory; - - public GrainCommandMiddleware(IGrainFactory grainFactory) - { - this.grainFactory = grainFactory; - } - - public virtual async Task HandleAsync(CommandContext context, NextDelegate next) - { - await ExecuteCommandAsync(context); - - await next(context); - } - - protected async Task ExecuteCommandAsync(CommandContext context) - { - if (context.Command is TCommand typedCommand) - { - var result = await ExecuteCommandAsync(typedCommand); - - var payload = await EnrichResultAsync(context, result); - - context.Complete(payload); - } - } - - protected virtual Task EnrichResultAsync(CommandContext context, CommandResult result) - { - return Task.FromResult(result.Payload is None ? result : result.Payload); - } - - private Task ExecuteCommandAsync(TCommand typedCommand) - { - var grain = grainFactory.GetGrain(typedCommand.AggregateId.ToString()); - - return grain.ExecuteAsync(typedCommand); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs b/backend/src/Squidex.Infrastructure/Commands/IAggregate.cs similarity index 85% rename from backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs rename to backend/src/Squidex.Infrastructure/Commands/IAggregate.cs index 8a921cd4f..d15f4683f 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IAggregate.cs @@ -5,12 +5,11 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; - namespace Squidex.Infrastructure.Commands { - public interface IDomainObjectGrain : IGrainWithStringKey + public interface IAggregate { - Task ExecuteAsync(IAggregateCommand command); + Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs b/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs index 2b5a091cc..fd1ccd7c8 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IAggregateCommand.cs @@ -11,4 +11,4 @@ namespace Squidex.Infrastructure.Commands { DomainId AggregateId { get; } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs b/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs index f82fa62fd..3443b52a5 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ICommandBus.cs @@ -9,6 +9,7 @@ namespace Squidex.Infrastructure.Commands { public interface ICommandBus { - Task PublishAsync(ICommand command); + Task PublishAsync(ICommand command, + CancellationToken ct); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs index 5bdb61ad7..5924dc5f8 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ICommandMiddleware.cs @@ -9,10 +9,11 @@ namespace Squidex.Infrastructure.Commands { - public delegate Task NextDelegate(CommandContext context); + public delegate Task NextDelegate(CommandContext context, CancellationToken ct); public interface ICommandMiddleware { - Task HandleAsync(CommandContext context, NextDelegate next); + Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetFolderGrain.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectCache.cs similarity index 57% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetFolderGrain.cs rename to backend/src/Squidex.Infrastructure/Commands/IDomainObjectCache.cs index 0dd310ab6..2fe65998f 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/DomainObject/IAssetFolderGrain.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IDomainObjectCache.cs @@ -5,11 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Infrastructure.Commands; - -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject +namespace Squidex.Infrastructure.Commands { - public interface IAssetFolderGrain : IDomainObjectGrain + public interface IDomainObjectCache { + Task GetAsync(DomainId id, long version, + CancellationToken ct = default); + + Task SetAsync(DomainId id, long version, T snapshot, + CancellationToken ct = default); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs b/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs index a99bc11ef..50d43920e 100644 --- a/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs +++ b/backend/src/Squidex.Infrastructure/Commands/IDomainState.cs @@ -13,8 +13,6 @@ namespace Squidex.Infrastructure.Commands { long Version { get; set; } - T Copy(); - T Apply(Envelope @event); } } diff --git a/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs index 266ec7d0d..9ec067f91 100644 --- a/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs +++ b/backend/src/Squidex.Infrastructure/Commands/InMemoryCommandBus.cs @@ -9,60 +9,35 @@ namespace Squidex.Infrastructure.Commands { public sealed class InMemoryCommandBus : ICommandBus { - private readonly IStep pipeline; - - private interface IStep - { - Task InvokeAsync(CommandContext context); - } - - private sealed class NoopStep : IStep - { - public Task InvokeAsync(CommandContext context) - { - return Task.CompletedTask; - } - } - - private sealed class DefaultStep : IStep - { - private readonly IStep next; - private readonly ICommandMiddleware middleware; - - public DefaultStep(ICommandMiddleware middleware, IStep next) - { - this.middleware = middleware; - - this.next = next; - } - - public Task InvokeAsync(CommandContext context) - { - return middleware.HandleAsync(context, next.InvokeAsync); - } - } + private readonly NextDelegate pipeline; public InMemoryCommandBus(IEnumerable middlewares) { var reverseMiddlewares = middlewares.Reverse().ToList(); - IStep next = new NoopStep(); + NextDelegate next = (c, ct) => Task.CompletedTask; - foreach (var middleware in reverseMiddlewares) + foreach (var middleware in middlewares.Reverse()) { - next = new DefaultStep(middleware, next); + next = Create(next, middleware); } pipeline = next; } - public async Task PublishAsync(ICommand command) + private static NextDelegate Create(NextDelegate next, ICommandMiddleware middleware) + { + return (c, ct) => middleware.HandleAsync(c, next, ct); + } + + public async Task PublishAsync(ICommand command, + CancellationToken ct) { Guard.NotNull(command); var context = new CommandContext(command, this); - await pipeline.InvokeAsync(context); + await pipeline(context, ct); return context; } diff --git a/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs index 35ee4efe1..4b503d3ea 100644 --- a/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/LogCommandMiddleware.cs @@ -18,7 +18,8 @@ namespace Squidex.Infrastructure.Commands this.log = log; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { var type = context.Command.GetType(); @@ -32,7 +33,7 @@ namespace Squidex.Infrastructure.Commands var watch = ValueStopwatch.StartNew(); try { - await next(context); + await next(context, ct); log.LogInformation("Command {command} with ID {id} succeeded.", type, context.ContextId); } diff --git a/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs b/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs index 3d9775a6c..5e7458e40 100644 --- a/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs +++ b/backend/src/Squidex.Infrastructure/Commands/ReadonlyCommandMiddleware.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -19,14 +19,15 @@ namespace Squidex.Infrastructure.Commands this.options = options.Value; } - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (options.IsReadonly) { throw new DomainException(T.Get("common.readonlyMode")); } - return next(context); + return next(context, ct); } } } diff --git a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs index e4aefdf22..280622297 100644 --- a/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs +++ b/backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs @@ -100,7 +100,7 @@ namespace Squidex.Infrastructure.Commands { var domainObject = domainObjectFactory.Create(id, context); - await domainObject.RebuildStateAsync(); + await domainObject.RebuildStateAsync(ct); } catch (DomainObjectNotFoundException) { diff --git a/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs b/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs deleted file mode 100644 index 090e7549e..000000000 --- a/backend/src/Squidex.Infrastructure/Commands/SnapshotList.cs +++ /dev/null @@ -1,123 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Commands -{ - public sealed class SnapshotList where T : class, IDomainState, new() - { - private readonly List items = new List(2); - private int capacity = 2; - - public int Capacity - { - get => capacity; - set - { - value = Math.Max(1, value); - - if (capacity != value) - { - capacity = value; - - Clean(); - } - } - } - - public long Version - { - get => items.Count - 2; - } - - public T Current - { - get => items[^1]!; - } - - public SnapshotList() - { - Clear(); - } - - public void Clear() - { - items.Clear(); - items.Add(new T - { - Version = EtagVersion.Empty - }); - } - - public (T?, bool Valid) Get(long version) - { - if (version == EtagVersion.Any || version == EtagVersion.Auto) - { - return (Current, true); - } - - var index = GetIndex(version); - - if (index >= 0 && index < items.Count) - { - return (items[index], true); - } - - return (null, false); - } - - public bool ContainsThisAndNewer(long version) - { - var index = GetIndex(version); - - return items.Skip(index).All(x => x != null); - } - - public void Add(T snapshot, long version, bool clean = false) - { - var index = GetIndex(version); - - while (items.Count <= index) - { - items.Add(null); - } - - items[index] = snapshot; - - if (clean) - { - Clean(); - } - } - - public void ResetTo(T snapshot, long version) - { - var index = GetIndex(version); - - while (items.Count > index + 1) - { - items.RemoveAt(items.Count - 1); - } - - items[index] = snapshot; - } - - private void Clean() - { - var lastIndex = items.Count - 1; - - for (var i = lastIndex - capacity; i > 0; i--) - { - items[i] = null; - } - } - - private static int GetIndex(long version) - { - return (int)(version + 1); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs b/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs deleted file mode 100644 index 79c8a6027..000000000 --- a/backend/src/Squidex.Infrastructure/Diagnostics/OrleansHealthCheck.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Orleans; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Diagnostics -{ - public sealed class OrleansHealthCheck : IHealthCheck - { - private readonly IManagementGrain managementGrain; - - public OrleansHealthCheck(IGrainFactory grainFactory) - { - managementGrain = grainFactory.GetGrain(0); - } - - public async Task CheckHealthAsync(HealthCheckContext context, - CancellationToken cancellationToken = default) - { - var activationCount = await managementGrain.GetTotalActivationCount(); - - var status = activationCount > 0 ? - HealthStatus.Healthy : - HealthStatus.Unhealthy; - - return new HealthCheckResult(status, "Orleans must have at least one activation."); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs b/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs index 2bf6a2b69..05bd658d1 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectConflictException.cs @@ -13,10 +13,10 @@ namespace Squidex.Infrastructure [Serializable] public class DomainObjectConflictException : DomainObjectException { - private const string ValidationError = "OBJECT_CONFLICT"; + private const string ExposedErrorCode = "OBJECT_CONFLICT"; public DomainObjectConflictException(string id, Exception? inner = null) - : base(FormatMessage(id), id, ValidationError, inner) + : base(FormatMessage(id), id, ExposedErrorCode, inner) { } diff --git a/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs b/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs index 15eb90c00..37e94c82f 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectDeletedException.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -13,10 +13,10 @@ namespace Squidex.Infrastructure [Serializable] public class DomainObjectDeletedException : DomainObjectException { - private const string ValidationError = "OBJECT_DELETED"; + private const string ExposedErrorCode = "OBJECT_DELETED"; public DomainObjectDeletedException(string id, Exception? inner = null) - : base(FormatMessage(id), id, ValidationError, inner) + : base(FormatMessage(id), id, ExposedErrorCode, inner) { } diff --git a/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs b/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs index db3ef36bb..140d389d4 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectNotFoundException.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -13,10 +13,10 @@ namespace Squidex.Infrastructure [Serializable] public class DomainObjectNotFoundException : DomainObjectException { - private const string ValidationError = "OBJECT_NOTFOUND"; + private const string ExposedErrorCode = "OBJECT_NOTFOUND"; public DomainObjectNotFoundException(string id, Exception? inner = null) - : base(FormatMessage(id), id, ValidationError, inner) + : base(FormatMessage(id), id, ExposedErrorCode, inner) { } diff --git a/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs b/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs index 960839cc0..3dd4ae7ec 100644 --- a/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs +++ b/backend/src/Squidex.Infrastructure/DomainObjectVersionException.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -13,14 +13,14 @@ namespace Squidex.Infrastructure [Serializable] public class DomainObjectVersionException : DomainObjectException { - private const string ValidationError = "OBJECT_VERSION_CONFLICT"; + private const string ExposedErrorCode = "OBJECT_VERSION_CONFLICT"; public long CurrentVersion { get; } public long ExpectedVersion { get; } public DomainObjectVersionException(string id, long currentVersion, long expectedVersion, Exception? inner = null) - : base(FormatMessage(id, currentVersion, expectedVersion), id, ValidationError, inner) + : base(FormatMessage(id, currentVersion, expectedVersion), id, ExposedErrorCode, inner) { CurrentVersion = currentVersion; diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/BatchSubscriber.cs similarity index 60% rename from backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Consume/BatchSubscriber.cs index 45a0897ea..f38eff342 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/BatchSubscriber.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/BatchSubscriber.cs @@ -11,7 +11,7 @@ using Squidex.Infrastructure.Tasks; #pragma warning disable RECS0082 // Parameter has the same name as a member and hides it #pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Infrastructure.EventSourcing.Grains +namespace Squidex.Infrastructure.EventSourcing.Consume { internal sealed class BatchSubscriber : IEventSubscriber, IEventSubscription { @@ -21,19 +21,14 @@ namespace Squidex.Infrastructure.EventSourcing.Grains private readonly Task handleTask; private readonly CancellationTokenSource completed = new CancellationTokenSource(); - public object? Sender - { - get => eventSubscription.Sender!; - } - - private sealed record EventSource(StoredEvent StoredEvent, object Sender); - private sealed record BatchItem(Envelope? Event, string Position, object Sender); + private sealed record EventSource(StoredEvent StoredEvent); + private sealed record BatchItem(Envelope? Event, string Position); private sealed record BatchJob(BatchItem[] Items); - private sealed record ErrorJob(Exception Exception, object? Sender); + private sealed record ErrorJob(Exception Exception); public BatchSubscriber( - EventConsumerGrain grain, - IEventFormatter eventDataFormatter, + EventConsumerProcessor processor, + IEventFormatter eventFormatter, IEventConsumer eventConsumer, Func factory) { @@ -67,67 +62,51 @@ namespace Squidex.Infrastructure.EventSourcing.Grains Task.Run(async () => { - await foreach (var (storedEvent, sender) in parseQueue.Reader.ReadAllAsync(completed.Token)) + await foreach (var source in parseQueue.Reader.ReadAllAsync(completed.Token)) { + var storedEvent = source.StoredEvent; try { Envelope? @event = null; if (eventConsumer.Handles(storedEvent)) { - @event = eventDataFormatter.ParseIfKnown(storedEvent); + @event = eventFormatter.ParseIfKnown(storedEvent); } - await batchQueue.Writer.WriteAsync(new BatchItem(@event, storedEvent.EventPosition, sender), completed.Token); + await batchQueue.Writer.WriteAsync(new BatchItem(@event, storedEvent.EventPosition), completed.Token); } catch (Exception ex) { - await taskQueue.Writer.WriteAsync(new ErrorJob(ex, sender), completed.Token); + await taskQueue.Writer.WriteAsync(new ErrorJob(ex), completed.Token); } } }).ContinueWith(x => batchQueue.Writer.TryComplete(x.Exception)); #pragma warning restore MA0040 // Flow the cancellation token - handleTask = Run(grain); + handleTask = Run(processor); } - private async Task Run(EventConsumerGrain grain) + private async Task Run(EventConsumerProcessor processor) { try { await foreach (var task in taskQueue.Reader.ReadAllAsync(completed.Token)) { - var sender = eventSubscription?.Sender; - - if (sender == null) - { - continue; - } - switch (task) { case ErrorJob error when error.Exception is not OperationCanceledException: { - if (ReferenceEquals(error.Sender, sender)) - { - await grain.OnErrorAsync(sender, error.Exception); - } - + await processor.OnErrorAsync(this, error.Exception); break; } case BatchJob batch: { - foreach (var itemsBySender in batch.Items.GroupBy(x => x.Sender)) - { - if (ReferenceEquals(itemsBySender.Key, sender)) - { - var position = itemsBySender.Last().Position; - - await grain.OnEventsAsync(sender, itemsBySender.Select(x => x.Event).NotNull().ToList(), position); - } - } + var eventsPosition = batch.Items[^1].Position; + var eventsCollection = batch.Items.Select(x => x.Event).NotNull().ToList(); + await processor.OnEventsAsync(this, eventsCollection, eventsPosition); break; } } @@ -139,14 +118,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } catch (Exception ex) { - var sender = eventSubscription?.Sender; - - if (sender != null) - { - await grain.OnErrorAsync(sender, ex); - } - - throw; + await processor.OnErrorAsync(this, ex); } } @@ -157,32 +129,26 @@ namespace Squidex.Infrastructure.EventSourcing.Grains return handleTask; } - void IEventSubscription.Unsubscribe() + public void Dispose() { completed.Cancel(); - eventSubscription.Unsubscribe(); + eventSubscription.Dispose(); } - void IEventSubscription.WakeUp() + public void WakeUp() { eventSubscription.WakeUp(); } - async Task IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + ValueTask IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) { - if (subscription.Sender != null) - { - await parseQueue.Writer.WriteAsync(new EventSource(storedEvent, subscription.Sender), completed.Token); - } + return parseQueue.Writer.WriteAsync(new EventSource(storedEvent), completed.Token); } - async Task IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) + ValueTask IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) { - if (subscription.Sender != null && exception is not OperationCanceledException) - { - await taskQueue.Writer.WriteAsync(new ErrorJob(exception, subscription.Sender), completed.Token); - } + return taskQueue.Writer.WriteAsync(new ErrorJob(exception), completed.Token); } } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerManager.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerManager.cs new file mode 100644 index 000000000..0e613e3f9 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerManager.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.States; +using Squidex.Messaging; + +namespace Squidex.Infrastructure.EventSourcing.Consume +{ + public sealed class EventConsumerManager : IEventConsumerManager + { + private readonly IPersistenceFactory persistence; + private readonly IMessageBus messaging; + + public EventConsumerManager(IPersistenceFactory persistence, + IMessageBus messaging) + { + this.persistence = persistence; + this.messaging = messaging; + } + + public async Task> GetConsumersAsync( + CancellationToken ct = default) + { + var snapshots = await persistence.Snapshots.ReadAllAsync(ct).ToListAsync(ct); + + return snapshots.Select(x => x.Value.ToInfo(x.Key.ToString())).ToList(); + } + + public async Task ResetAsync(string consumerName, + CancellationToken ct = default) + { + var state = await GetStateAsync(consumerName, ct); + + await messaging.PublishAsync(new EventConsumerReset(consumerName), ct: ct); + + return state.Value.ToInfo(consumerName); + } + + public async Task StartAsync(string consumerName, + CancellationToken ct = default) + { + var state = await GetStateAsync(consumerName, ct); + + await messaging.PublishAsync(new EventConsumerStart(consumerName), ct: ct); + + return state.Value.ToInfo(consumerName); + } + + public async Task StopAsync(string consumerName, + CancellationToken ct = default) + { + var state = await GetStateAsync(consumerName, ct); + + await messaging.PublishAsync(new EventConsumerStop(consumerName), ct: ct); + + return state.Value.ToInfo(consumerName); + } + + private async Task> GetStateAsync(string consumerName, + CancellationToken ct) + { + var state = new SimpleState(persistence, GetType(), DomainId.Create(consumerName)); + + await state.LoadAsync(ct); + + if (state.Version <= EtagVersion.Empty) + { + throw new DomainObjectNotFoundException(consumerName); + } + + return state; + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs similarity index 61% rename from backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs index ef1a08e6c..876b49a38 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs @@ -7,52 +7,49 @@ using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; -using Orleans.Core; -using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; using Squidex.Infrastructure.Tasks; -namespace Squidex.Infrastructure.EventSourcing.Grains +namespace Squidex.Infrastructure.EventSourcing.Consume { - public class EventConsumerGrain : GrainBase, IEventConsumerGrain + public class EventConsumerProcessor { - private readonly IGrainState state; - private readonly IEventConsumer eventConsumer; + private readonly SimpleState state; private readonly IEventFormatter eventFormatter; + private readonly IEventConsumer? eventConsumer; private readonly IEventStore eventStore; - private readonly ILogger log; - private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); + private readonly ILogger log; + private readonly AsyncLock asyncLock = new AsyncLock(); private IEventSubscription? currentSubscription; - private EventConsumerState State + public EventConsumerState State { get => state.Value; set => state.Value = value; } - public EventConsumerGrain( - IGrainIdentity identity, - IGrainState state, - IEventConsumerFactory eventConsumerFactory, + public EventConsumerProcessor( + IPersistenceFactory persistenceFactory, + IEventConsumer eventConsumer, IEventFormatter eventFormatter, IEventStore eventStore, - ILogger log) - : base(identity) + ILogger log) { - this.eventConsumer = eventConsumerFactory.Create(identity.PrimaryKeyString); - this.eventFormatter = eventFormatter; this.eventStore = eventStore; - this.state = state; + this.eventFormatter = eventFormatter; + this.eventConsumer = eventConsumer; this.log = log; + + state = new SimpleState(persistenceFactory, GetType(), eventConsumer.Name); } - public override Task OnDeactivateAsync() + public virtual Task InitializeAsync( + CancellationToken ct) { - CompleteAsync().Forget(); - - return Task.CompletedTask; + return state.LoadAsync(ct); } - public async Task CompleteAsync() + public virtual async Task CompleteAsync() { if (currentSubscription is BatchSubscriber batchSubscriber) { @@ -67,100 +64,86 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } } - public Task GetStateAsync() - { - return Task.FromResult(CreateInfo()); - } - - private EventConsumerInfo CreateInfo() - { - return State.ToInfo(eventConsumer!.Name); - } - - public Task OnEventsAsync(object sender, IReadOnlyList> events, string position) + public virtual Task OnEventsAsync(IEventSubscription subscription, IReadOnlyList> events, string position) { - if (!ReferenceEquals(sender, currentSubscription?.Sender)) + return UpdateAsync(async () => { - return Task.CompletedTask; - } + if (!ReferenceEquals(subscription, currentSubscription)) + { + return; + } - return DoAndUpdateStateAsync(async () => - { await DispatchAsync(events); State = State.Handled(position, events.Count); }, State.Position); } - public Task OnErrorAsync(object sender, Exception exception) + public virtual Task OnErrorAsync(IEventSubscription subscription, Exception exception) { - if (!ReferenceEquals(sender, currentSubscription?.Sender)) + return UpdateAsync(() => { - return Task.CompletedTask; - } + if (!ReferenceEquals(subscription, currentSubscription)) + { + return; + } - return DoAndUpdateStateAsync(() => - { Unsubscribe(); State = State.Stopped(exception); }, State.Position); } - public async Task ActivateAsync() + public virtual Task ActivateAsync() { - if (State.IsFailed) + return UpdateAsync(() => { - await DoAndUpdateStateAsync(() => + if (State.IsFailed) { Subscribe(); State = State.Started(); - }, State.Position); - } - else if (!State.IsStopped) - { - Subscribe(); - } + } + else if (!State.IsStopped) + { + Subscribe(); + } + }, State.Position); } - public async Task StartAsync() + public virtual Task StartAsync() { - if (!State.IsStopped) + return UpdateAsync(() => { - return CreateInfo(); - } + if (!State.IsStopped) + { + return; + } - await DoAndUpdateStateAsync(() => - { Subscribe(); State = State.Started(); }, State.Position); - - return CreateInfo(); } - public async Task StopAsync() + public virtual Task StopAsync() { - if (State.IsStopped) + return UpdateAsync(() => { - return CreateInfo(); - } + if (State.IsStopped) + { + return; + } - await DoAndUpdateStateAsync(() => - { Unsubscribe(); State = State.Stopped(); }, State.Position); - - return CreateInfo(); } - public async Task ResetAsync() + public virtual async Task ResetAsync() { - await DoAndUpdateStateAsync(async () => + await UpdateAsync(async () => { Unsubscribe(); @@ -170,8 +153,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains Subscribe(); }, State.Position); - - return CreateInfo(); } private async Task DispatchAsync(IReadOnlyList> events) @@ -182,9 +163,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } } - private Task DoAndUpdateStateAsync(Action action, string? position, [CallerMemberName] string? caller = null) + private Task UpdateAsync(Action action, string? position, [CallerMemberName] string? caller = null) { - return DoAndUpdateStateAsync(() => + return UpdateAsync(() => { action(); @@ -192,10 +173,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains }, position, caller); } - private async Task DoAndUpdateStateAsync(Func action, string? position, [CallerMemberName] string? caller = null) + private async Task UpdateAsync(Func action, string? position, [CallerMemberName] string? caller = null) { - await semaphore.WaitAsync(); - try + using (await asyncLock.EnterAsync()) { var previousState = State; @@ -225,10 +205,6 @@ namespace Squidex.Infrastructure.EventSourcing.Grains await state.WriteAsync(); } } - finally - { - semaphore.Release(); - } } private async Task ClearAsync() @@ -251,9 +227,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains private void Unsubscribe() { - var subscription = Interlocked.Exchange(ref currentSubscription, null); - - subscription?.Unsubscribe(); + if (currentSubscription != null) + { + currentSubscription.Dispose(); + currentSubscription = null; + } } private void Subscribe() @@ -268,7 +246,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains } } - private BatchSubscriber CreateSubscription() + private IEventSubscription CreateSubscription() { return new BatchSubscriber(this, eventFormatter, eventConsumer!, CreateRetrySubscription); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerState.cs similarity index 94% rename from backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerState.cs index 28c521cfb..f11cce66c 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerState.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerState.cs @@ -7,7 +7,7 @@ using Squidex.Infrastructure.Reflection; -namespace Squidex.Infrastructure.EventSourcing.Grains +namespace Squidex.Infrastructure.EventSourcing.Consume { public sealed class EventConsumerState { @@ -49,7 +49,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains public EventConsumerState Stopped(Exception? ex = null) { - return new EventConsumerState(Position, Count) { IsStopped = true, Error = ex?.ToString() }; + return new EventConsumerState(Position, Count) { IsStopped = true, Error = ex?.Message }; } public EventConsumerState Started() diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs new file mode 100644 index 000000000..c5b2fe1a6 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs @@ -0,0 +1,83 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Hosting; +using Squidex.Infrastructure.Timers; +using Squidex.Messaging; + +namespace Squidex.Infrastructure.EventSourcing.Consume +{ + public sealed class EventConsumerWorker : + IMessageHandler, + IMessageHandler, + IMessageHandler, + IInitializable + { + private readonly Dictionary processors = new Dictionary(); + private CompletionTimer? timer; + + public EventConsumerWorker(IEnumerable eventConsumers, + Func factory) + { + foreach (var consumer in eventConsumers) + { + processors[consumer.Name] = factory(consumer); + } + } + + public async Task InitializeAsync( + CancellationToken ct) + { + foreach (var (_, processor) in processors) + { + await processor.InitializeAsync(ct); + await processor.ActivateAsync(); + } + + timer = new CompletionTimer(TimeSpan.FromSeconds(30), async ct => + { + foreach (var (_, processor) in processors) + { + await processor.ActivateAsync(); + } + }); + } + + public Task ReleaseAsync( + CancellationToken ct) + { + return timer?.StopAsync() ?? Task.CompletedTask; + } + + public async Task HandleAsync(EventConsumerStart message, + CancellationToken ct) + { + if (processors.TryGetValue(message.Name, out var processor)) + { + await processor.StartAsync(); + } + } + + public async Task HandleAsync(EventConsumerStop message, + CancellationToken ct) + { + if (processors.TryGetValue(message.Name, out var processor)) + { + await processor.StopAsync(); + } + } + + public async Task HandleAsync(EventConsumerReset message, + CancellationToken ct) + { + if (processors.TryGetValue(message.Name, out var processor)) + { + await processor.ResetAsync(); + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Consume/IEventConsumerManager.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/IEventConsumerManager.cs new file mode 100644 index 000000000..025628ad3 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/IEventConsumerManager.cs @@ -0,0 +1,24 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.EventSourcing.Consume +{ + public interface IEventConsumerManager + { + Task> GetConsumersAsync( + CancellationToken ct = default); + + Task ResetAsync(string consumerName, + CancellationToken ct = default); + + Task StopAsync(string consumerName, + CancellationToken ct = default); + + Task StartAsync(string consumerName, + CancellationToken ct = default); + } +} diff --git a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/Messages.cs similarity index 50% rename from backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs rename to backend/src/Squidex.Infrastructure/EventSourcing/Consume/Messages.cs index 690cefd69..511bfd34d 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Apps/IAppUISettingsGrain.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/Consume/Messages.cs @@ -5,21 +5,14 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; -using Squidex.Infrastructure.Json.Objects; +#pragma warning disable MA0048 // File name must match type name +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Apps +namespace Squidex.Infrastructure.EventSourcing.Consume { - public interface IAppUISettingsGrain : IGrainWithStringKey - { - Task GetAsync(); + public sealed record EventConsumerReset(string Name); - Task SetAsync(string path, JsonValue value); + public sealed record EventConsumerStart(string Name); - Task SetAsync(JsonObject settings); - - Task RemoveAsync(string path); - - Task ClearAsync(); - } + public sealed record EventConsumerStop(string Name); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventConsumerFactory.cs b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventConsumerFactory.cs deleted file mode 100644 index 2c07eec35..000000000 --- a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventConsumerFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class DefaultEventConsumerFactory : IEventConsumerFactory - { - private readonly Dictionary eventConsumers; - - public DefaultEventConsumerFactory(IEnumerable eventConsumers) - { - this.eventConsumers = eventConsumers.ToDictionary(x => x.Name); - } - - public IEventConsumer Create(string name) - { - Guard.NotNullOrEmpty(name); - - if (!eventConsumers.TryGetValue(name, out var eventConsumer)) - { - throw new ArgumentException($"Cannot find event consuemr with name '{name}'", nameof(name)); - } - - return eventConsumer; - } - } -} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs index 746d70258..74d0f5221 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/DefaultEventFormatter.cs @@ -19,7 +19,6 @@ namespace Squidex.Infrastructure.EventSourcing public DefaultEventFormatter(TypeNameRegistry typeNameRegistry, IJsonSerializer serializer) { this.typeNameRegistry = typeNameRegistry; - this.serializer = serializer; } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs index 2a3c37871..1f06c4896 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventCommit.cs @@ -16,11 +16,11 @@ namespace Squidex.Infrastructure.EventSourcing return new EventCommit(id, streamName, offset, new List { @event }); } - public static EventCommit Create(string streamName, long offset, Envelope envelope, IEventFormatter eventDataFormatter) + public static EventCommit Create(string streamName, long offset, Envelope envelope, IEventFormatter eventFormatter) { var id = Guid.NewGuid(); - var eventData = eventDataFormatter.ToEventData(envelope, id); + var eventData = eventFormatter.ToEventData(envelope, id); return new EventCommit(id, streamName, offset, new List { eventData }); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs index da2454de6..2241fef5e 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumerInfo.cs @@ -5,21 +5,18 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans.Concurrency; - namespace Squidex.Infrastructure.EventSourcing { - [Immutable] - public sealed class EventConsumerInfo + public sealed record EventConsumerInfo { - public bool IsStopped { get; set; } + public bool IsStopped { get; init; } - public int Count { get; set; } + public int Count { get; init; } - public string Name { get; set; } + public string Name { get; init; } - public string Error { get; set; } + public string Error { get; init; } - public string Position { get; set; } + public string Position { get; init; } } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumersHealthCheck.cs b/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumersHealthCheck.cs deleted file mode 100644 index be346ba9b..000000000 --- a/backend/src/Squidex.Infrastructure/EventSourcing/EventConsumersHealthCheck.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Orleans; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.EventSourcing -{ - public sealed class EventConsumersHealthCheck : IHealthCheck - { - private readonly IGrainFactory grainFactory; - - public EventConsumersHealthCheck(IGrainFactory grainFactory) - { - this.grainFactory = grainFactory; - } - - public async Task CheckHealthAsync(HealthCheckContext context, - CancellationToken cancellationToken = default) - { - var eventConsumers = await GetGrain().GetConsumersAsync(); - - var data = new Dictionary(); - - var numTotal = 0; - var numFailed = 0; - - foreach (var eventConsumer in eventConsumers.Value) - { - var status = "Running"; - - if (eventConsumer.Error != null) - { - status = "Failed"; - - numFailed++; - } - else if (eventConsumer.IsStopped) - { - status = "Stopped"; - } - - data[eventConsumer.Name] = status; - - numTotal++; - } - - if (numTotal > 0 && numFailed == numTotal) - { - return HealthCheckResult.Unhealthy("All event consumers failed", null, data); - } - else if (numFailed > 0) - { - return HealthCheckResult.Degraded("One or more event consumers failed", null, data); - } - else - { - return HealthCheckResult.Healthy(data: data); - } - } - - private IEventConsumerManagerGrain GetGrain() - { - return grainFactory.GetGrain(SingleGrain.Id); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs deleted file mode 100644 index 95c1b9715..000000000 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerManagerGrain.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Text.RegularExpressions; -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) - { - this.eventConsumers = eventConsumers; - } - - public override Task OnActivateAsync() - { - DelayDeactivation(TimeSpan.FromDays(1)); - - RegisterOrUpdateReminder("Default", TimeSpan.Zero, TimeSpan.FromMinutes(5)); - 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 consumerInfos.ToList().AsImmutable(); - } - - public Task StartAllAsync() - { - return Task.WhenAll( - eventConsumers - .Select(c => StartAsync(c.Name))); - } - - public Task StopAllAsync() - { - return Task.WhenAll( - eventConsumers - .Select(c => StopAsync(c.Name))); - } - - 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/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs deleted file mode 100644 index ca737c93b..000000000 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerGrain.cs +++ /dev/null @@ -1,22 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public interface IEventConsumerGrain : IBackgroundGrain - { - Task GetStateAsync(); - - Task StopAsync(); - - Task StartAsync(); - - Task ResetAsync(); - } -} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs deleted file mode 100644 index c5096ed52..000000000 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/IEventConsumerManagerGrain.cs +++ /dev/null @@ -1,29 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Concurrency; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public interface IEventConsumerManagerGrain : IBackgroundGrain - { - Task ActivateAsync(string streamName); - - Task StartAllAsync(); - - Task StopAllAsync(); - - Task StopAsync(string consumerName); - - Task StartAsync(string consumerName); - - Task ResetAsync(string consumerName); - - Task>> GetConsumersAsync(); - } -} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs b/backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs deleted file mode 100644 index 59b8cb0f1..000000000 --- a/backend/src/Squidex.Infrastructure/EventSourcing/Grains/OrleansEventNotifier.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Infrastructure.Orleans; - -namespace Squidex.Infrastructure.EventSourcing.Grains -{ - public sealed class OrleansEventNotifier : IEventNotifier - { - private readonly Lazy eventConsumerManagerGrain; - - public OrleansEventNotifier(IGrainFactory factory) - { - eventConsumerManagerGrain = new Lazy(() => - { - return factory.GetGrain(SingleGrain.Id); - }); - } - - public void NotifyEventsStored(string streamName) - { - eventConsumerManagerGrain.Value.ActivateAsync(streamName); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumerFactory.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumerFactory.cs deleted file mode 100644 index 1d89f2612..000000000 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventConsumerFactory.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.EventSourcing -{ - public interface IEventConsumerFactory - { - IEventConsumer Create(string name); - } -} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs index 672733b10..8c5f13cbe 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventNotifier.cs @@ -5,10 +5,19 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +#pragma warning disable MA0048 // File name must match type name + namespace Squidex.Infrastructure.EventSourcing { public interface IEventNotifier { void NotifyEventsStored(string streamName); } + + public sealed class NoopEventNotifier : IEventNotifier + { + public void NotifyEventsStored(string streamName) + { + } + } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs index ee7bfdc37..c4c25abdf 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscriber.cs @@ -9,8 +9,8 @@ namespace Squidex.Infrastructure.EventSourcing { public interface IEventSubscriber { - Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent); + ValueTask OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent); - Task OnErrorAsync(IEventSubscription subscription, Exception exception); + ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception); } } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs index 40833a84d..1402f8fae 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/IEventSubscription.cs @@ -7,16 +7,10 @@ namespace Squidex.Infrastructure.EventSourcing { - public interface IEventSubscription + public interface IEventSubscription : IDisposable { - object? Sender => this; - - void Unsubscribe() - { - } - void WakeUp() { } } -} \ No newline at end of file +} diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs index e0adecb02..c8e14eb62 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/PollingSubscription.cs @@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.EventSourcing timer.SkipCurrentDelay(); } - public void Unsubscribe() + public void Dispose() { timer.StopAsync().Forget(); } diff --git a/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs index 7e6d2e325..75653bbe4 100644 --- a/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs +++ b/backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs @@ -5,21 +5,21 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using Squidex.Infrastructure.Tasks; + namespace Squidex.Infrastructure.EventSourcing { public sealed class RetrySubscription : IEventSubscription, IEventSubscriber { private readonly RetryWindow retryWindow = new RetryWindow(TimeSpan.FromMinutes(5), 5); + private readonly AsyncLock lockObject = new AsyncLock(); private readonly IEventSubscriber eventSubscriber; private readonly Func eventSubscriptionFactory; - private readonly object lockObject = new object(); private CancellationTokenSource timerCancellation = new CancellationTokenSource(); private IEventSubscription? currentSubscription; public int ReconnectWaitMs { get; set; } = 5000; - public object? Sender => currentSubscription?.Sender; - public RetrySubscription(IEventSubscriber eventSubscriber, Func eventSubscriptionFactory) { Guard.NotNull(eventSubscriber); @@ -31,38 +31,40 @@ namespace Squidex.Infrastructure.EventSourcing Subscribe(); } - private void Subscribe() + public void Dispose() { - if (currentSubscription == null) + using (lockObject.Enter()) { - lock (lockObject) - { - if (currentSubscription == null) - { - currentSubscription = eventSubscriptionFactory(this); - } - } + Unsubscribe(); } + + lockObject.Dispose(); } - public void Unsubscribe() + private void Subscribe() { if (currentSubscription != null) { - lock (lockObject) - { - if (currentSubscription != null) - { - timerCancellation.Cancel(); - timerCancellation.Dispose(); + return; + } - currentSubscription.Unsubscribe(); - currentSubscription = null; + currentSubscription = eventSubscriptionFactory(this); + } - timerCancellation = new CancellationTokenSource(); - } - } + private void Unsubscribe() + { + if (currentSubscription == null) + { + return; } + + timerCancellation.Cancel(); + timerCancellation.Dispose(); + + currentSubscription.Dispose(); + currentSubscription = null; + + timerCancellation = new CancellationTokenSource(); } public void WakeUp() @@ -70,29 +72,54 @@ namespace Squidex.Infrastructure.EventSourcing currentSubscription?.WakeUp(); } - public async Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + async ValueTask IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) { - await eventSubscriber.OnEventAsync(subscription, storedEvent); + using (await lockObject.EnterAsync(default)) + { + if (!ReferenceEquals(subscription, currentSubscription)) + { + return; + } + + await eventSubscriber.OnEventAsync(this, storedEvent); + } } - public async Task OnErrorAsync(IEventSubscription subscription, Exception exception) + async ValueTask IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) { if (exception is OperationCanceledException) { return; } - if (retryWindow.CanRetryAfterFailure()) + using (await lockObject.EnterAsync(default)) { + if (!ReferenceEquals(subscription, currentSubscription)) + { + return; + } + Unsubscribe(); - await Task.Delay(ReconnectWaitMs, timerCancellation.Token); + if (!retryWindow.CanRetryAfterFailure()) + { + await eventSubscriber.OnErrorAsync(this, exception); + return; + } + } - Subscribe(); + try + { + await Task.Delay(ReconnectWaitMs, timerCancellation.Token); } - else + catch (OperationCanceledException) { - await eventSubscriber.OnErrorAsync(subscription, exception); + return; + } + + using (await lockObject.EnterAsync(default)) + { + Subscribe(); } } } diff --git a/backend/src/Squidex.Infrastructure/FileExtensions.cs b/backend/src/Squidex.Infrastructure/FileExtensions.cs index f48527c59..fdc7b95e5 100644 --- a/backend/src/Squidex.Infrastructure/FileExtensions.cs +++ b/backend/src/Squidex.Infrastructure/FileExtensions.cs @@ -27,6 +27,11 @@ namespace Squidex.Infrastructure public static string FileType(this string fileName) { + if (string.IsNullOrWhiteSpace(fileName)) + { + return "blob"; + } + try { var fileInfo = new FileInfo(fileName); diff --git a/backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs deleted file mode 100644 index f614b4cb2..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/ActivationLimit.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class ActivationLimit : IActivationLimit, IDeactivater - { - private readonly IGrainActivationContext context; - private readonly IActivationLimiter limiter; - private int maxActivations; - - public ActivationLimit(IGrainActivationContext context, IActivationLimiter limiter) - { - this.context = context; - this.limiter = limiter; - } - - public void ReportIAmAlive() - { - if (maxActivations > 0) - { - limiter.Register(context.GrainType, this, maxActivations); - } - } - - public void ReportIAmDead() - { - if (maxActivations > 0) - { - limiter.Unregister(context.GrainType, this); - } - } - - public void SetLimit(int activations, TimeSpan lifetime) - { - maxActivations = activations; - - context.ObservableLifecycle?.Subscribe("Limiter", GrainLifecycleStage.Activate, - ct => - { - var runtime = context.ActivationServices.GetRequiredService(); - - runtime.DelayDeactivation(context.GrainInstance, lifetime); - - ReportIAmAlive(); - - return Task.CompletedTask; - }, - ct => - { - ReportIAmDead(); - - return Task.CompletedTask; - }); - } - - void IDeactivater.Deactivate() - { - var runtime = context.ActivationServices.GetRequiredService(); - - runtime.DeactivateOnIdle(context.GrainInstance); - } - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs deleted file mode 100644 index bb05a8edb..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs +++ /dev/null @@ -1,70 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Collections.Concurrent; -using Squidex.Caching; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class ActivationLimiter : IActivationLimiter - { - private readonly ConcurrentDictionary instances = new ConcurrentDictionary(); - - private sealed class LastUsedInstances - { - private readonly LRUCache recentUsedGrains; - private readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim(); - - public LastUsedInstances(int limit) - { - recentUsedGrains = new LRUCache(limit, (key, _) => key.Deactivate()); - } - - public void Register(IDeactivater instance) - { - try - { - lockSlim.EnterWriteLock(); - - recentUsedGrains.Set(instance, instance); - } - finally - { - lockSlim.ExitWriteLock(); - } - } - - public void Unregister(IDeactivater instance) - { - try - { - lockSlim.EnterWriteLock(); - - recentUsedGrains.Remove(instance); - } - finally - { - lockSlim.ExitWriteLock(); - } - } - } - - public void Register(Type grainType, IDeactivater deactivater, int maxActivations) - { - var byType = instances.GetOrAdd(grainType, (t, a) => new LastUsedInstances(a), maxActivations); - - byType.Register(deactivater); - } - - public void Unregister(Type grainType, IDeactivater deactivater) - { - instances.TryGetValue(grainType, out var byType); - - byType?.Unregister(deactivater); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs deleted file mode 100644 index 5e712e9ee..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class ActivationLimiterFilter : IIncomingGrainCallFilter - { - public Task Invoke(IIncomingGrainCallContext context) - { - if (context.Grain is GrainBase grainBase) - { - grainBase.ReportIAmAlive(); - } - - return context.Invoke(); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/ActivityPropagationFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/ActivityPropagationFilter.cs deleted file mode 100644 index fc9915548..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/ActivityPropagationFilter.cs +++ /dev/null @@ -1,110 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Diagnostics; -using Orleans; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class ActivityPropagationFilter : IOutgoingGrainCallFilter, IIncomingGrainCallFilter - { - private const string ActivityNameIn = "Orleans.Runtime.GrainCall.In"; - private const string ActivityNameOut = "Orleans.Runtime.GrainCall.Out"; - private const string TraceParentHeaderName = "traceparent"; - private const string TraceStateHeaderName = "tracestate"; - - public Task Invoke(IOutgoingGrainCallContext context) - { - if (Activity.Current != null) - { - return ProcessCurrentActivity(context); - } - - return ProcessNewActivity(context, ActivityNameOut, ActivityKind.Client, default); - } - - public Task Invoke(IIncomingGrainCallContext context) - { - var traceParent = RequestContext.Get(TraceParentHeaderName) as string; - var traceState = RequestContext.Get(TraceStateHeaderName) as string; - var parentContext = default(ActivityContext); - - if (traceParent is not null) - { - parentContext = ActivityContext.Parse(traceParent, traceState); - } - - return ProcessNewActivity(context, ActivityNameIn, ActivityKind.Server, parentContext); - } - - private static Task ProcessCurrentActivity(IOutgoingGrainCallContext context) - { - var currentActivity = Activity.Current; - - if (currentActivity != null && currentActivity.IdFormat == ActivityIdFormat.W3C) - { - RequestContext.Set(TraceParentHeaderName, currentActivity.Id); - - if (currentActivity.TraceStateString is not null) - { - RequestContext.Set(TraceStateHeaderName, currentActivity.TraceStateString); - } - } - - return context.Invoke(); - } - - private static async Task ProcessNewActivity(IGrainCallContext context, string activityName, ActivityKind activityKind, ActivityContext activityContext) - { - ActivityTagsCollection? tags = null; - - if (Telemetry.Activities.HasListeners()) - { - tags = new ActivityTagsCollection - { - { "net.peer.name", context.Grain?.ToString() }, - { "rpc.method", context.InterfaceMethod?.Name }, - { "rpc.service", context.InterfaceMethod?.DeclaringType?.FullName }, - { "rpc.system", "orleans" } - }; - } - - using (var activity = Telemetry.Activities.StartActivity(activityName, activityKind, activityContext, tags)) - { - if (activity != null) - { - RequestContext.Set(TraceParentHeaderName, activity.Id); - } - - try - { - await context.Invoke(); - - if (activity != null && activity.IsAllDataRequested) - { - activity.SetTag("status", "Ok"); - } - } - catch (Exception e) - { - if (activity is not null && activity.IsAllDataRequested) - { - // Exception attributes from https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md - activity.SetTag("exception.type", e.GetType().FullName); - activity.SetTag("exception.message", e.Message); - activity.SetTag("exception.stacktrace", e.StackTrace); - activity.SetTag("exception.escaped", true); - activity.SetTag("status", "Error"); - } - - throw; - } - } - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/CultureFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/CultureFilter.cs deleted file mode 100644 index 13c52ec34..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/CultureFilter.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Globalization; -using Orleans; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class CultureFilter : IIncomingGrainCallFilter, IOutgoingGrainCallFilter - { - public Task Invoke(IOutgoingGrainCallContext context) - { - RequestContext.Set("Culture", CultureInfo.CurrentCulture.Name); - RequestContext.Set("CultureUI", CultureInfo.CurrentUICulture.Name); - - return context.Invoke(); - } - - public async Task Invoke(IIncomingGrainCallContext context) - { - if (RequestContext.Get("Culture") is string culture) - { - CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture); - } - - if (RequestContext.Get("CultureUI") is string cultureUI) - { - CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(cultureUI); - } - - await context.Invoke(); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/ExceptionWrapperFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/ExceptionWrapperFilter.cs deleted file mode 100644 index 5f93daa58..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/ExceptionWrapperFilter.cs +++ /dev/null @@ -1,35 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class ExceptionWrapperFilter : IIncomingGrainCallFilter - { - public async Task Invoke(IIncomingGrainCallContext context) - { - try - { - await context.Invoke(); - } - catch (Exception ex) - { - var type = ex.GetType(); - - if (!type.IsSerializable) - { - throw new OrleansWrapperException(ex, type); - } - else - { - throw; - } - } - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs deleted file mode 100644 index f24a82849..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/GrainBase.cs +++ /dev/null @@ -1,63 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Core; -using Orleans.Runtime; - -namespace Squidex.Infrastructure.Orleans -{ - public abstract class GrainBase : Grain - { - public DomainId Key { get; private set; } - - protected GrainBase(IGrainIdentity identity, IGrainRuntime? runtime = null) - : base(identity, runtime) - { - Guard.NotNull(identity); - - Key = DomainId.Create(identity.PrimaryKeyString); - } - - public void ReportIAmAlive() - { - var limit = ServiceProvider.GetService(); - - limit?.ReportIAmAlive(); - } - - public void ReportIAmDead() - { - var limit = ServiceProvider.GetService(); - - limit?.ReportIAmDead(); - } - - protected void TryDelayDeactivation(TimeSpan timeSpan) - { - try - { - DelayDeactivation(timeSpan); - } - catch (InvalidOperationException) - { - } - } - - protected void TryDeactivateOnIdle() - { - try - { - DeactivateOnIdle(); - } - catch (InvalidOperationException) - { - } - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs deleted file mode 100644 index e7d8a949e..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/GrainBootstrap.cs +++ /dev/null @@ -1,52 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Orleans.Runtime; -using Squidex.Hosting; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class GrainBootstrap : IBackgroundProcess where T : IBackgroundGrain - { - private const int NumTries = 10; - private readonly IGrainFactory grainFactory; - - public string Name => typeof(T).Name; - - public GrainBootstrap(IGrainFactory grainFactory) - { - Guard.NotNull(grainFactory); - - this.grainFactory = grainFactory; - } - - public async Task StartAsync( - CancellationToken ct) - { - for (var i = 1; i <= NumTries; i++) - { - ct.ThrowIfCancellationRequested(); - try - { - var grain = grainFactory.GetGrain(SingleGrain.Id); - - await grain.ActivateAsync(); - - return; - } - catch (OrleansException) - { - if (i == NumTries) - { - throw; - } - } - } - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs b/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs deleted file mode 100644 index db356451a..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/GrainState.cs +++ /dev/null @@ -1,85 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.DependencyInjection; -using Orleans; -using Orleans.Runtime; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class GrainState : IGrainState where T : class, new() - { - private readonly IGrainActivationContext context; - private IPersistence persistence; - - public T Value { get; set; } = new T(); - - public long Version - { - get => persistence.Version; - } - - public GrainState(IGrainActivationContext context) - { - Guard.NotNull(context); - - this.context = context; - - context.ObservableLifecycle.Subscribe("Persistence", GrainLifecycleStage.SetupState, SetupAsync); - } - - public Task SetupAsync( - CancellationToken ct = default) - { - if (ct.IsCancellationRequested) - { - return Task.CompletedTask; - } - - DomainId key; - - if (context.GrainIdentity.PrimaryKeyString != null) - { - key = DomainId.Create(context.GrainIdentity.PrimaryKeyString); - } - else - { - key = DomainId.Create(context.GrainIdentity.PrimaryKey); - } - - var factory = context.ActivationServices.GetRequiredService>(); - - persistence = factory.WithSnapshots(GetType(), key, ApplyState); - - return persistence.ReadAsync(ct: ct); - } - - private void ApplyState(T value, long version) - { - Value = value; - } - - public Task ClearAsync() - { - Value = new T(); - - return persistence.DeleteAsync(); - } - - public Task WriteAsync() - { - return persistence.WriteSnapshotAsync(Value); - } - - public Task WriteEventAsync(Envelope envelope) - { - return persistence.WriteEventAsync(envelope); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs b/backend/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs deleted file mode 100644 index f5e0a3071..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/IActivationLimit.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Orleans -{ - public interface IActivationLimit - { - void SetLimit(int activations, TimeSpan lifetime); - - void ReportIAmAlive(); - - void ReportIAmDead(); - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs b/backend/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs deleted file mode 100644 index 1ba19ceb2..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Orleans -{ - public interface IActivationLimiter - { - void Register(Type grainType, IDeactivater deactivater, int maxActivations); - - void Unregister(Type grainType, IDeactivater deactivater); - } -} \ No newline at end of file diff --git a/backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs deleted file mode 100644 index 8b3d2fc13..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/IBackgroundGrain.cs +++ /dev/null @@ -1,16 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; - -namespace Squidex.Infrastructure.Orleans -{ - public interface IBackgroundGrain : IGrainWithStringKey - { - Task ActivateAsync(); - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/IDeactivater.cs b/backend/src/Squidex.Infrastructure/Orleans/IDeactivater.cs deleted file mode 100644 index 76d0729fc..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/IDeactivater.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Orleans -{ - public interface IDeactivater - { - void Deactivate(); - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/IGrainState.cs b/backend/src/Squidex.Infrastructure/Orleans/IGrainState.cs deleted file mode 100644 index 19933e853..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/IGrainState.cs +++ /dev/null @@ -1,24 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Squidex.Infrastructure.EventSourcing; - -namespace Squidex.Infrastructure.Orleans -{ - public interface IGrainState where T : class, new() - { - long Version { get; } - - T Value { get; set; } - - Task ClearAsync(); - - Task WriteAsync(); - - Task WriteEventAsync(Envelope envelope); - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameGrain.cs deleted file mode 100644 index 7c9b03a37..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/Indexes/IUniqueNameGrain.cs +++ /dev/null @@ -1,18 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public interface IUniqueNameGrain : IGrainWithStringKey - { - Task ReserveAsync(T id, string name); - - Task RemoveReservationAsync(string? token); - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameGrain.cs deleted file mode 100644 index 2b7a6864d..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/Indexes/UniqueNameGrain.cs +++ /dev/null @@ -1,48 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Core; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class UniqueNameGrain : GrainBase, IUniqueNameGrain - { - private readonly Dictionary reservations = new Dictionary(); - - public UniqueNameGrain(IGrainIdentity identity) - : base(identity) - { - } - - public virtual Task ReserveAsync(T id, string name) - { - string? token = null; - - var reservation = reservations.FirstOrDefault(x => x.Value.Name == name); - - if (Equals(reservation.Value.Id, id)) - { - token = reservation.Key; - } - else if (reservation.Key == null) - { - token = RandomHash.Simple(); - - reservations.Add(token, (name, id)); - } - - return Task.FromResult(token); - } - - public Task RemoveReservationAsync(string? token) - { - reservations.Remove(token ?? string.Empty); - - return Task.CompletedTask; - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/JsonSerializer.cs b/backend/src/Squidex.Infrastructure/Orleans/JsonSerializer.cs deleted file mode 100644 index 5937b00ff..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/JsonSerializer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Serialization; -using Squidex.Infrastructure.Json; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class JsonSerializer : IExternalSerializer - { - private readonly IJsonSerializer jsonSerializer; - - public JsonSerializer(IJsonSerializer jsonSerializer) - { - this.jsonSerializer = jsonSerializer; - } - - public bool IsSupportedType(Type itemType) - { - return itemType.Namespace?.StartsWith("Squidex", StringComparison.OrdinalIgnoreCase) == true; - } - - public object DeepCopy(object source, ICopyContext context) - { - return source; - } - - public object Deserialize(Type expectedType, IDeserializationContext context) - { - var stream = new StreamReaderWrapper(context.StreamReader); - - return jsonSerializer.Deserialize(stream, expectedType); - } - - public void Serialize(object item, ISerializationContext context, Type expectedType) - { - var stream = new StreamWriterWrapper(context.StreamWriter); - - jsonSerializer.Serialize(item, stream); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs deleted file mode 100644 index 428d7bc87..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs +++ /dev/null @@ -1,37 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Squidex.Caching; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class LocalCacheFilter : IIncomingGrainCallFilter - { - private readonly ILocalCache localCache; - - public LocalCacheFilter(ILocalCache localCache) - { - this.localCache = localCache; - } - - public async Task Invoke(IIncomingGrainCallContext context) - { - if (!context.Grain.GetType().Namespace!.StartsWith("Orleans", StringComparison.OrdinalIgnoreCase)) - { - using (localCache.StartContext()) - { - await context.Invoke(); - } - } - else - { - await context.Invoke(); - } - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs deleted file mode 100644 index 304e2b0f8..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/LoggingFilter.cs +++ /dev/null @@ -1,56 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.Extensions.Logging; -using Orleans; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class LoggingFilter : IIncomingGrainCallFilter - { - private readonly ILoggerFactory logFactory; - - public LoggingFilter(ILoggerFactory logFactory) - { - this.logFactory = logFactory; - } - - public async Task Invoke(IIncomingGrainCallContext context) - { - var name = $"Grain/{context.ImplementationMethod?.DeclaringType?.FullName}/{context.ImplementationMethod?.Name}"; - - using (Telemetry.Activities.StartActivity(name)) - { - try - { - await context.Invoke(); - } - catch (DomainException ex) - { - if (ex.InnerException != null) - { - Log(context, ex.InnerException); - } - - throw; - } - catch (Exception ex) - { - Log(context, ex); - throw; - } - } - } - - private void Log(IIncomingGrainCallContext context, Exception ex) - { - var log = logFactory.CreateLogger(context.Grain.GetType()); - - log.LogError(ex, "Failed to execute method of grain.", context.ImplementationMethod); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/OrleansWrapperException.cs b/backend/src/Squidex.Infrastructure/Orleans/OrleansWrapperException.cs deleted file mode 100644 index 5b7228fdc..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/OrleansWrapperException.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Globalization; -using System.Runtime.Serialization; -using System.Text; - -namespace Squidex.Infrastructure.Orleans -{ - [Serializable] - public class OrleansWrapperException : Exception - { - public Type ExceptionType { get; } - - public OrleansWrapperException(Exception wrapped, Type exceptionType) - : base(FormatMessage(wrapped, exceptionType)) - { - ExceptionType = exceptionType; - } - - protected OrleansWrapperException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - ExceptionType = Type.GetType(info.GetString(nameof(ExceptionType))!)!; - } - - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue(nameof(ExceptionType), ExceptionType.AssemblyQualifiedName); - - base.GetObjectData(info, context); - } - - private static string FormatMessage(Exception wrapped, Type exceptionType) - { - var sb = new StringBuilder(); - - sb.AppendLine(CultureInfo.InvariantCulture, $"Wrapping exception of type {exceptionType}, because original exception is not serialized."); - sb.AppendLine(); - sb.AppendLine("Original exception:"); - sb.AppendLine(wrapped.ToString()); - - return sb.ToString(); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/SingleGrain.cs b/backend/src/Squidex.Infrastructure/Orleans/SingleGrain.cs deleted file mode 100644 index 66a9fb356..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/SingleGrain.cs +++ /dev/null @@ -1,14 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -namespace Squidex.Infrastructure.Orleans -{ - public static class SingleGrain - { - public const string Id = "Default"; - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/StateFilter.cs b/backend/src/Squidex.Infrastructure/Orleans/StateFilter.cs deleted file mode 100644 index bfba64dcb..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/StateFilter.cs +++ /dev/null @@ -1,58 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Orleans.Runtime; -using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.States; - -namespace Squidex.Infrastructure.Orleans -{ - public sealed class StateFilter : IIncomingGrainCallFilter - { - private readonly IGrainRuntime runtime; - - public StateFilter(IGrainRuntime runtime) - { - this.runtime = runtime; - } - - public async Task Invoke(IIncomingGrainCallContext context) - { - try - { - await context.Invoke(); - } - catch (DomainObjectNotFoundException) - { - TryDeactivate(context); - - throw; - } - catch (WrongEventVersionException) - { - TryDeactivate(context); - - throw; - } - catch (InconsistentStateException) - { - TryDeactivate(context); - - throw; - } - } - - private void TryDeactivate(IIncomingGrainCallContext context) - { - if (context.Grain is Grain grain) - { - runtime.DeactivateOnIdle(grain); - } - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs b/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs deleted file mode 100644 index 2806bf269..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/StreamReaderWrapper.cs +++ /dev/null @@ -1,86 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Serialization; - -namespace Squidex.Infrastructure.Orleans -{ - internal sealed class StreamReaderWrapper : Stream - { - private readonly IBinaryTokenStreamReader reader; - - public override bool CanRead - { - get => true; - } - - public override bool CanSeek - { - get => false; - } - - public override bool CanWrite - { - get => false; - } - - public override long Length - { - get => throw new NotSupportedException(); - } - - public override long Position - { - get - { - return reader.CurrentPosition; - } - set - { - throw new NotSupportedException(); - } - } - - public StreamReaderWrapper(IBinaryTokenStreamReader reader) - { - this.reader = reader; - } - - public override void Flush() - { - } - - public override int Read(byte[] buffer, int offset, int count) - { - var bytesLeft = (int)(reader.Length - reader.CurrentPosition); - - if (bytesLeft < count) - { - count = bytesLeft; - } - - reader.ReadByteArray(buffer, offset, count); - - return count; - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs b/backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs deleted file mode 100644 index 6a2385d5c..000000000 --- a/backend/src/Squidex.Infrastructure/Orleans/StreamWriterWrapper.cs +++ /dev/null @@ -1,77 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans.Serialization; - -namespace Squidex.Infrastructure.Orleans -{ - internal sealed class StreamWriterWrapper : Stream - { - private readonly IBinaryTokenStreamWriter writer; - - public override bool CanRead - { - get => false; - } - - public override bool CanSeek - { - get => false; - } - - public override bool CanWrite - { - get => true; - } - - public override long Length - { - get => writer.CurrentOffset; - } - - public override long Position - { - get - { - return writer.CurrentOffset; - } - set - { - throw new NotSupportedException(); - } - } - - public StreamWriterWrapper(IBinaryTokenStreamWriter writer) - { - this.writer = writer; - } - - public override void Flush() - { - } - - public override void Write(byte[] buffer, int offset, int count) - { - writer.Write(buffer, offset, count); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - } -} diff --git a/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs index e5f28b1ce..d2242f807 100644 --- a/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs +++ b/backend/src/Squidex.Infrastructure/Queries/Json/QueryParser.cs @@ -14,14 +14,14 @@ namespace Squidex.Infrastructure.Queries.Json { public static class QueryParser { - public static ClrQuery Parse(this QueryModel model, string json, IJsonSerializer jsonSerializer) + public static ClrQuery Parse(this QueryModel model, string json, IJsonSerializer serializer) { if (string.IsNullOrWhiteSpace(json)) { return new ClrQuery(); } - var query = ParseFromJson(json, jsonSerializer); + var query = ParseFromJson(json, serializer); return Convert(model, query); } @@ -76,11 +76,11 @@ namespace Squidex.Infrastructure.Queries.Json } } - public static Query ParseFromJson(string json, IJsonSerializer jsonSerializer) + public static Query ParseFromJson(string json, IJsonSerializer serializer) { try { - return jsonSerializer.Deserialize>(json); + return serializer.Deserialize>(json); } catch (JsonException ex) { diff --git a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj index fa3d03e75..a2775d664 100644 --- a/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj +++ b/backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj @@ -22,12 +22,6 @@ - - all - runtime; build; native; contentfiles; analyzers - - - @@ -35,6 +29,7 @@ + diff --git a/backend/src/Squidex.Infrastructure/States/BatchContext.cs b/backend/src/Squidex.Infrastructure/States/BatchContext.cs index d64a5c827..e720ef3dd 100644 --- a/backend/src/Squidex.Infrastructure/States/BatchContext.cs +++ b/backend/src/Squidex.Infrastructure/States/BatchContext.cs @@ -15,25 +15,27 @@ namespace Squidex.Infrastructure.States { private static readonly List> EmptyStream = new List>(); private readonly Type owner; - private readonly ISnapshotStore snapshotStore; - private readonly IEventFormatter eventDataFormatter; + private readonly IEventFormatter eventFormatter; private readonly IEventStore eventStore; - private readonly IEventStreamNames eventStreams; + private readonly IEventStreamNames eventStreamNames; + private readonly ISnapshotStore snapshotStore; private readonly Dictionary>)> @events = new Dictionary>)>(); private Dictionary>? snapshots; + public ISnapshotStore Snapshots => snapshotStore; + internal BatchContext( Type owner, - IEventFormatter eventDataFormatter, + IEventFormatter eventFormatter, IEventStore eventStore, - IEventStreamNames streamNameResolver, + IEventStreamNames eventStreamNames, ISnapshotStore snapshotStore) { this.owner = owner; - this.snapshotStore = snapshotStore; + this.eventFormatter = eventFormatter; this.eventStore = eventStore; - this.eventDataFormatter = eventDataFormatter; - this.eventStreams = streamNameResolver; + this.eventStreamNames = eventStreamNames; + this.snapshotStore = snapshotStore; } internal void Add(DomainId key, T snapshot, long version) @@ -48,7 +50,7 @@ namespace Squidex.Infrastructure.States public async Task LoadAsync(IEnumerable ids) { - var streamNames = ids.ToDictionary(x => x, x => eventStreams.GetStreamName(owner, x.ToString())); + var streamNames = ids.ToDictionary(x => x, x => eventStreamNames.GetStreamName(owner, x.ToString())); if (streamNames.Count == 0) { @@ -61,7 +63,7 @@ namespace Squidex.Infrastructure.States { if (streams.TryGetValue(streamName, out var data)) { - var stream = data.Select(eventDataFormatter.ParseIfKnown).NotNull().ToList(); + var stream = data.Select(eventFormatter.ParseIfKnown).NotNull().ToList(); events[id] = (data.Count - 1, stream); } diff --git a/backend/src/Squidex.Infrastructure/States/DefaultEventStreamNames.cs b/backend/src/Squidex.Infrastructure/States/DefaultEventStreamNames.cs index b2a252571..bb73ca273 100644 --- a/backend/src/Squidex.Infrastructure/States/DefaultEventStreamNames.cs +++ b/backend/src/Squidex.Infrastructure/States/DefaultEventStreamNames.cs @@ -11,7 +11,7 @@ namespace Squidex.Infrastructure.States { public sealed class DefaultEventStreamNames : IEventStreamNames { - private static readonly string[] Suffixes = { "Grain", "DomainObject", "State" }; + private static readonly string[] Suffixes = { "Processor", "DomainObject", "State" }; public string GetStreamName(Type aggregateType, string id) { diff --git a/backend/src/Squidex.Infrastructure/States/IPersistenceFactory.cs b/backend/src/Squidex.Infrastructure/States/IPersistenceFactory.cs index 508bfe826..31bc9943b 100644 --- a/backend/src/Squidex.Infrastructure/States/IPersistenceFactory.cs +++ b/backend/src/Squidex.Infrastructure/States/IPersistenceFactory.cs @@ -17,6 +17,8 @@ namespace Squidex.Infrastructure.States public interface IPersistenceFactory { + ISnapshotStore Snapshots { get; } + IPersistence WithEventSourcing(Type owner, DomainId id, HandleEvent? applyEvent); IPersistence WithSnapshots(Type owner, DomainId id, HandleSnapshot? applySnapshot); diff --git a/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs b/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs index 6e6462292..4bb0d24ad 100644 --- a/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs +++ b/backend/src/Squidex.Infrastructure/States/ISnapshotStore.cs @@ -31,8 +31,7 @@ namespace Squidex.Infrastructure.States CancellationToken ct = default); } - public record struct SnapshotResult(DomainId Key, T Value, long Version, - bool IsValid = true); + public record struct SnapshotResult(DomainId Key, T Value, long Version, bool IsValid = true); public record struct SnapshotWriteJob(DomainId Key, T Value, long NewVersion, long OldVersion = EtagVersion.Any); } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCleanupGrain.cs b/backend/src/Squidex.Infrastructure/States/NameReservation.cs similarity index 64% rename from backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCleanupGrain.cs rename to backend/src/Squidex.Infrastructure/States/NameReservation.cs index e525b8890..c32ccb661 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/IAssetCleanupGrain.cs +++ b/backend/src/Squidex.Infrastructure/States/NameReservation.cs @@ -5,11 +5,9 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Infrastructure.Orleans; +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter -namespace Squidex.Domain.Apps.Entities.Assets +namespace Squidex.Infrastructure.States { - public interface IAssetCleanupGrain : IBackgroundGrain - { - } + public sealed record NameReservation(string Token, string Name, DomainId Id); } diff --git a/backend/src/Squidex.Infrastructure/States/NameReservationState.cs b/backend/src/Squidex.Infrastructure/States/NameReservationState.cs new file mode 100644 index 000000000..70383985a --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/NameReservationState.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.States +{ + public sealed class NameReservationState : SimpleState + { + public sealed class State + { + public List Reservations { get; set; } = new List(); + + public string? Reserve(DomainId id, string name) + { + string? token = null; + + var reservation = Reservations.Find(x => x.Name == name); + + if (reservation?.Id == id) + { + token = reservation.Token; + } + else if (reservation == null) + { + token = RandomHash.Simple(); + + Reservations.Add(new NameReservation(token, name, id)); + } + + return token; + } + + public void Remove(string? token) + { + Reservations.RemoveAll(x => x.Token == token); + } + } + + public NameReservationState(IPersistenceFactory persistenceFactory, string id) + : base(persistenceFactory, typeof(NameReservationState), id) + { + } + + public NameReservationState(IPersistenceFactory persistenceFactory, DomainId id) + : base(persistenceFactory, typeof(NameReservationState), id) + { + } + + public async Task ReserveAsync(DomainId id, string name, + CancellationToken ct = default) + { + try + { + return await UpdateAsync(s => s.Reserve(id, name), ct: ct); + } + catch (InconsistentStateException) + { + return null; + } + } + + public async Task RemoveReservationAsync(string? token, + CancellationToken ct = default) + { + try + { + await UpdateAsync(s => s.Remove(token), ct: ct); + } + catch (InconsistentStateException) + { + return; + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/States/Persistence.cs b/backend/src/Squidex.Infrastructure/States/Persistence.cs index 72eb21db9..c8f13bd25 100644 --- a/backend/src/Squidex.Infrastructure/States/Persistence.cs +++ b/backend/src/Squidex.Infrastructure/States/Persistence.cs @@ -49,7 +49,7 @@ namespace Squidex.Infrastructure.States PersistenceMode persistenceMode, IEventFormatter eventFormatter, IEventStore eventStore, - IEventStreamNames eventStreams, + IEventStreamNames eventStreamNames, ISnapshotStore snapshotStore, HandleSnapshot? applyState, HandleEvent? applyEvent) @@ -62,7 +62,7 @@ namespace Squidex.Infrastructure.States this.persistenceMode = persistenceMode; this.snapshotStore = snapshotStore; - streamName = new Lazy(() => eventStreams.GetStreamName(ownerType, ownerKey.ToString()!)); + streamName = new Lazy(() => eventStreamNames.GetStreamName(ownerType, ownerKey.ToString()!)); } public async Task DeleteAsync( diff --git a/backend/src/Squidex.Infrastructure/States/SimpleState.cs b/backend/src/Squidex.Infrastructure/States/SimpleState.cs new file mode 100644 index 000000000..09c633b2d --- /dev/null +++ b/backend/src/Squidex.Infrastructure/States/SimpleState.cs @@ -0,0 +1,146 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Squidex.Infrastructure.EventSourcing; + +namespace Squidex.Infrastructure.States +{ + public class SimpleState where T : class, new() + { + private readonly IPersistence persistence; + private bool isLoaded; + + public T Value { get; set; } = new T(); + + public long Version + { + get => persistence.Version; + } + + public SimpleState(IPersistenceFactory persistenceFactory, Type ownerType, string id) + : this(persistenceFactory, ownerType, DomainId.Create(id)) + { + } + + public SimpleState(IPersistenceFactory persistenceFactory, Type ownerType, DomainId id) + { + Guard.NotNull(persistenceFactory); + + persistence = persistenceFactory.WithSnapshots(ownerType, id, (state, version) => + { + Value = state; + }); + } + + public async Task LoadAsync( + CancellationToken ct = default) + { + await persistence.ReadAsync(ct: ct); + + isLoaded = true; + } + + public Task ClearAsync( + CancellationToken ct = default) + { + Value = new T(); + + return persistence.DeleteAsync(ct); + } + + public Task WriteAsync( + CancellationToken ct = default) + { + return persistence.WriteSnapshotAsync(Value, ct); + } + + public Task WriteEventAsync(Envelope envelope, + CancellationToken ct = default) + { + return persistence.WriteEventAsync(envelope, ct); + } + + public async Task UpdateIfAsync(Func updater, int retries = 20, + CancellationToken ct = default) + { + await EnsureLoadedAsync(ct); + + for (var i = 0; i < retries; i++) + { + try + { + if (!updater(Value)) + { + return; + } + + await WriteAsync(ct); + return; + } + catch (InconsistentStateException) when (i < retries) + { + await LoadAsync(ct); + } + } + } + + public async Task UpdateAsync(Action updater, int retries = 20, + CancellationToken ct = default) + { + await EnsureLoadedAsync(ct); + + for (var i = 0; i < retries; i++) + { + try + { + updater(Value); + + await WriteAsync(ct); + return; + } + catch (InconsistentStateException) when (i < retries) + { + await LoadAsync(ct); + } + } + } + + public async Task UpdateAsync(Func updater, int retries = 5, + CancellationToken ct = default) + { + await EnsureLoadedAsync(ct); + + for (var i = 0; i < retries; i++) + { + try + { + var result = updater(Value); + + await WriteAsync(ct); + return result; + } + catch (InconsistentStateException) when (i < retries) + { + await LoadAsync(ct); + } + } + + return default!; + } + + private async Task EnsureLoadedAsync( + CancellationToken ct) + { + if (isLoaded) + { + return; + } + + await LoadAsync(ct); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/States/Store.cs b/backend/src/Squidex.Infrastructure/States/Store.cs index 650f65850..d14c7ca8d 100644 --- a/backend/src/Squidex.Infrastructure/States/Store.cs +++ b/backend/src/Squidex.Infrastructure/States/Store.cs @@ -16,6 +16,8 @@ namespace Squidex.Infrastructure.States private readonly IEventStreamNames eventStreamNames; private readonly ISnapshotStore snapshotStore; + public ISnapshotStore Snapshots => snapshotStore; + public Store( IEventFormatter eventFormatter, IEventStore eventStore, diff --git a/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs b/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs index 2daff1c9f..cd9efa5f8 100644 --- a/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs +++ b/backend/src/Squidex.Infrastructure/States/StoreExtensions.cs @@ -11,9 +11,10 @@ namespace Squidex.Infrastructure.States { public static class StoreExtensions { - public static Task WriteEventAsync(this IPersistence persistence, Envelope @event) + public static Task WriteEventAsync(this IPersistence persistence, Envelope @event, + CancellationToken ct = default) { - return persistence.WriteEventsAsync(new[] { @event }); + return persistence.WriteEventsAsync(new[] { @event }, ct); } } } diff --git a/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs b/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs new file mode 100644 index 000000000..46a314901 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs @@ -0,0 +1,55 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Tasks +{ + public sealed class AsyncLock : IDisposable + { + private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); + private readonly Disposable disposable; + + private sealed class Disposable : IDisposable + { + private readonly SemaphoreSlim semaphore; + + public Disposable(SemaphoreSlim semaphore) + { + this.semaphore = semaphore; + } + + public void Dispose() + { + semaphore.Release(); + } + } + + public AsyncLock() + { + disposable = new Disposable(semaphore); + } + + public async Task EnterAsync( + CancellationToken ct = default) + { + await semaphore.WaitAsync(ct); + + return disposable; + } + + public IDisposable Enter() + { + semaphore.Wait(); + + return disposable; + } + + public void Dispose() + { + semaphore.Dispose(); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/LimitedConcurrencyLevelTaskScheduler.cs b/backend/src/Squidex.Infrastructure/Tasks/LimitedConcurrencyLevelTaskScheduler.cs new file mode 100644 index 000000000..2fe606892 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/LimitedConcurrencyLevelTaskScheduler.cs @@ -0,0 +1,132 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Tasks +{ + internal sealed class LimitedConcurrencyLevelTaskScheduler : TaskScheduler + { + [ThreadStatic] + private static bool currentThreadIsProcessingItems; + private readonly LinkedList tasks = new LinkedList(); + private readonly int maxDegreeOfParallelism; + private int delegatesQueuedOrRunning; + + public override int MaximumConcurrencyLevel => maxDegreeOfParallelism; + + public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) + { + Guard.GreaterEquals(maxDegreeOfParallelism, 1, nameof(maxDegreeOfParallelism)); + + this.maxDegreeOfParallelism = maxDegreeOfParallelism; + } + + protected override void QueueTask(Task task) + { + lock (tasks) + { + tasks.AddLast(task); + + if (delegatesQueuedOrRunning < maxDegreeOfParallelism) + { + ++delegatesQueuedOrRunning; + + NotifyThreadPoolOfPendingWork(); + } + } + } + + private void NotifyThreadPoolOfPendingWork() + { + ThreadPool.UnsafeQueueUserWorkItem(_ => + { + currentThreadIsProcessingItems = true; + try + { + while (true) + { + Task task; + + lock (tasks) + { + // When there are no more items to be processed, + // note that we're done processing, and get out. + if (tasks.Count == 0) + { + --delegatesQueuedOrRunning; + break; + } + + // Cannot be null because of previous check. + task = tasks.First!.Value; + tasks.RemoveFirst(); + } + + TryExecuteTask(task); + } + } + finally + { + currentThreadIsProcessingItems = false; + } + }, null); + } + + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + // If this thread isn't already processing a task, we don't support inlining + if (!currentThreadIsProcessingItems) + { + return false; + } + + // If the task was previously queued, remove it from the queue + if (taskWasPreviouslyQueued) + { + // Try to run the next task from the tasks. + if (TryDequeue(task)) + { + return TryExecuteTask(task); + } + + return false; + } + + return TryExecuteTask(task); + } + + protected override bool TryDequeue(Task task) + { + lock (tasks) + { + return tasks.Remove(task); + } + } + + protected override IEnumerable GetScheduledTasks() + { + var lockTaken = false; + try + { + Monitor.TryEnter(tasks, ref lockTaken); + + if (lockTaken) + { + return tasks; + } + + throw new NotSupportedException(); + } + finally + { + if (lockTaken) + { + Monitor.Exit(tasks); + } + } + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/ReentrantScheduler.cs b/backend/src/Squidex.Infrastructure/Tasks/ReentrantScheduler.cs new file mode 100644 index 000000000..1476bb4b7 --- /dev/null +++ b/backend/src/Squidex.Infrastructure/Tasks/ReentrantScheduler.cs @@ -0,0 +1,41 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +namespace Squidex.Infrastructure.Tasks +{ +#pragma warning disable MA0048 // File name must match type name + public delegate Task ReentrantSchedulerTask(CancellationToken ct); +#pragma warning restore MA0048 // File name must match type name + + public sealed class ReentrantScheduler + { + private readonly TaskScheduler taskScheduler; + + public ReentrantScheduler(int maxDegreeOfParallelism) + { + taskScheduler = new LimitedConcurrencyLevelTaskScheduler(maxDegreeOfParallelism); + } + + public Task ScheduleAsync(ReentrantSchedulerTask action, + CancellationToken ct = default) + { + var inner = Task.Factory.StartNew(() => action(ct), ct, TaskCreationOptions.DenyChildAttach, taskScheduler); + + return inner.Unwrap(); + } + + public Task Schedule(Action action) + { + return Task.Factory.StartNew(() => + { + action(); + + return false; + }, default, TaskCreationOptions.DenyChildAttach, taskScheduler); + } + } +} diff --git a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs index 80cefb7e8..ddb8a2470 100644 --- a/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs +++ b/backend/src/Squidex.Infrastructure/Tasks/TaskExtensions.cs @@ -66,11 +66,10 @@ namespace Squidex.Infrastructure.Tasks { await target.Completion.ConfigureAwait(false); } -#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body catch -#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body { - // we do not want to change the stacktrace of the exception. + // We do not want to change the stacktrace of the exception. + return; } if (target.Completion.IsFaulted && target.Completion.Exception != null) diff --git a/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs b/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs index f4240ed89..13e4dc859 100644 --- a/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs +++ b/backend/src/Squidex.Infrastructure/Timers/CompletionTimer.cs @@ -17,6 +17,11 @@ namespace Squidex.Infrastructure.Timers private int oneCallState; private CancellationTokenSource? wakeupToken; + public CompletionTimer(TimeSpan delay, Func callback, TimeSpan initialDelay = default) + : this((int)delay.TotalMilliseconds, callback, (int)initialDelay.TotalMilliseconds) + { + } + public CompletionTimer(int delayInMs, Func callback, int initialDelay = 0) { Guard.NotNull(callback); diff --git a/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index 5f5d45541..c3560bd8d 100644 --- a/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/backend/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -120,13 +120,12 @@ namespace Squidex.Infrastructure.UsageTracking ThrowIfDisposed(); - var usages = await usageRepository.QueryAsync(key, fromDate, toDate, ct); - var result = new Dictionary>(); - var categories = usages.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); + var usageData = await usageRepository.QueryAsync(key, fromDate, toDate, ct); + var usageGroups = usageData.GroupBy(x => GetCategory(x.Category)).ToDictionary(x => x.Key, x => x.ToList()); - if (categories.Keys.Count == 0) + if (usageGroups.Keys.Count == 0) { var enriched = new List<(DateTime Date, Counters Counters)>(); @@ -138,7 +137,7 @@ namespace Squidex.Infrastructure.UsageTracking result[FallbackCategory] = enriched; } - foreach (var (category, value) in categories) + foreach (var (category, value) in usageGroups) { var enriched = new List<(DateTime Date, Counters Counters)>(); diff --git a/backend/src/Squidex.Shared/Permissions.cs b/backend/src/Squidex.Shared/Permissions.cs index 5af03382f..d867d9b0d 100644 --- a/backend/src/Squidex.Shared/Permissions.cs +++ b/backend/src/Squidex.Shared/Permissions.cs @@ -17,9 +17,6 @@ namespace Squidex.Shared public const string Admin = "squidex.admin.*"; - // Orleans - public const string AdminOrleans = "squidex.admin.orleans"; - // Admin App Creation public const string AdminAppCreate = "squidex.admin.apps.create"; diff --git a/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs index aa353eba1..844947f06 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/ETagCommandMiddleware.cs @@ -23,14 +23,14 @@ namespace Squidex.Web.CommandMiddlewares this.httpContextAccessor = httpContextAccessor; } - public async Task HandleAsync(CommandContext context, NextDelegate next) + public async Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { var httpContext = httpContextAccessor.HttpContext; if (httpContext == null) { - await next(context); - + await next(context, ct); return; } @@ -48,7 +48,7 @@ namespace Squidex.Web.CommandMiddlewares } } - await next(context); + await next(context, ct); if (context.PlainResult is CommandResult result) { diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs index 909f0b7aa..8fc686897 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithActorCommandMiddleware.cs @@ -22,11 +22,12 @@ namespace Squidex.Web.CommandMiddlewares this.httpContextAccessor = httpContextAccessor; } - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (httpContextAccessor.HttpContext == null) { - return next(context); + return next(context, ct); } if (context.Command is SquidexCommand squidexCommand) @@ -43,7 +44,7 @@ namespace Squidex.Web.CommandMiddlewares squidexCommand.User ??= httpContextAccessor.HttpContext.User; } - return next(context); + return next(context, ct); } } } diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs index 17ae3cb58..9c9b36144 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithAppIdCommandMiddleware.cs @@ -21,7 +21,8 @@ namespace Squidex.Web.CommandMiddlewares this.contextProvider = contextProvider; } - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (context.Command is IAppCommand appCommand && appCommand.AppId == null) { @@ -30,7 +31,7 @@ namespace Squidex.Web.CommandMiddlewares appCommand.AppId = appId; } - return next(context); + return next(context, ct); } private NamedId GetAppId() diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs index f1c946cd4..3e9637fb6 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithContentIdCommandMiddleware.cs @@ -14,7 +14,8 @@ namespace Squidex.Web.CommandMiddlewares { private const string SingletonId = "_schemaId_"; - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (context.Command is ContentCommand contentCommand && contentCommand is not CreateContent) { @@ -24,7 +25,7 @@ namespace Squidex.Web.CommandMiddlewares } } - return next(context); + return next(context, ct); } } } diff --git a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs index 4a4cfcf9f..0248be838 100644 --- a/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs +++ b/backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs @@ -22,11 +22,12 @@ namespace Squidex.Web.CommandMiddlewares this.httpContextAccessor = httpContextAccessor; } - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (httpContextAccessor.HttpContext == null) { - return next(context); + return next(context, ct); } if (context.Command is ISchemaCommand schemaCommand && schemaCommand.SchemaId == null) @@ -36,7 +37,7 @@ namespace Squidex.Web.CommandMiddlewares schemaCommand.SchemaId = schemaId; } - return next(context); + return next(context, ct); } private NamedId GetSchemaId() diff --git a/backend/src/Squidex.Web/Constants.cs b/backend/src/Squidex.Web/Constants.cs index 12c5a6081..228576d1f 100644 --- a/backend/src/Squidex.Web/Constants.cs +++ b/backend/src/Squidex.Web/Constants.cs @@ -14,14 +14,10 @@ namespace Squidex.Web { public const string SecurityDefinition = "squidex-oauth-auth"; - public const string OrleansClusterId = "squidex-v2"; - public const string ApiSecurityScheme = "API"; public const string PrefixApi = "/api"; - public const string PrefixOrleans = "/orleans"; - public const string PrefixPortal = "/portal"; public const string PrefixIdentityServer = "/identity-server"; diff --git a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs index 98a2a1094..dc48b80d8 100644 --- a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs @@ -50,7 +50,7 @@ namespace Squidex.Web.Pipeline { var (_, clientId) = context.HttpContext.User.GetClient(); - var isBlocked = await usageGate.IsBlockedAsync(app, clientId, DateTime.Today); + var isBlocked = await usageGate.IsBlockedAsync(app, clientId, DateTime.Today, context.HttpContext.RequestAborted); if (isBlocked) { diff --git a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs index 1a4cf1896..0a2260df2 100644 --- a/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs +++ b/backend/src/Squidex.Web/Pipeline/UsageMiddleware.cs @@ -19,14 +19,13 @@ namespace Squidex.Web.Pipeline { private readonly IAppLogStore usageLog; private readonly IApiUsageTracker usageTracker; - private readonly IClock clock; - public UsageMiddleware(IAppLogStore usageLog, IApiUsageTracker usageTracker, IClock clock) + public IClock Clock { get; set; } = SystemClock.Instance; + + public UsageMiddleware(IAppLogStore usageLog, IApiUsageTracker usageTracker ) { this.usageLog = usageLog; this.usageTracker = usageTracker; - - this.clock = clock; } public async Task InvokeAsync(HttpContext context, RequestDelegate next) @@ -64,7 +63,7 @@ namespace Squidex.Web.Pipeline request.ElapsedMs = watch.Stop(); request.RequestMethod = context.Request.Method; request.RequestPath = context.Request.Path; - request.Timestamp = clock.GetCurrentInstant(); + request.Timestamp = Clock.GetCurrentInstant(); request.StatusCode = context.Response.StatusCode; request.UserId = context.User.OpenIdSubject(); request.UserClientId = clientId; diff --git a/backend/src/Squidex.Web/Resources.cs b/backend/src/Squidex.Web/Resources.cs index 644782d7c..2b1567a33 100644 --- a/backend/src/Squidex.Web/Resources.cs +++ b/backend/src/Squidex.Web/Resources.cs @@ -164,10 +164,6 @@ namespace Squidex.Web [Lazy] public bool CanManageEvents => IsAllowed(Permissions.AdminEventsManage); - // Orleans - [Lazy] - public bool CanReadOrleans => IsAllowed(Permissions.AdminOrleans); - // Backups [Lazy] public bool CanRestoreBackup => IsAllowed(Permissions.AdminRestore); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs index 70b67f225..8775acdb0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppAssetsController.cs @@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Apps private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = GetResponse(result); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs index 5d8a800b4..a208bdb4a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppClientsController.cs @@ -139,7 +139,7 @@ namespace Squidex.Areas.Api.Controllers.Apps private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = GetResponse(result); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs index de8e782f6..2dcf3667c 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppContributorsController.cs @@ -135,7 +135,7 @@ namespace Squidex.Areas.Api.Controllers.Apps private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); if (context.PlainResult is InvitedResult invited) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs index 41e50c533..62b19688d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppLanguagesController.cs @@ -128,7 +128,7 @@ namespace Squidex.Areas.Api.Controllers.Apps private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = GetResponse(result); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs index 46d9b30f0..6b3fa000b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppRolesController.cs @@ -158,7 +158,7 @@ namespace Squidex.Areas.Api.Controllers.Apps private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = GetResponse(result); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs index e587e4d7f..fa4442f52 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppSettingsController.cs @@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Apps private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = GetResponse(result); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs index 332741a8e..c9c793ef2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppWorkflowsController.cs @@ -131,7 +131,7 @@ namespace Squidex.Areas.Api.Controllers.Apps private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = await GetResponse(result); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs index cca7e643e..60c4b0be2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Apps/AppsController.cs @@ -200,7 +200,9 @@ namespace Squidex.Areas.Api.Controllers.Apps [ApiCosts(0)] public async Task DeleteApp(string app) { - await CommandBus.PublishAsync(new DeleteApp()); + var command = new DeleteApp(); + + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } @@ -219,7 +221,7 @@ namespace Squidex.Areas.Api.Controllers.Apps private async Task InvokeCommandAsync(ICommand command, Func converter) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = converter(result); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs index 6b652b3d0..35cdc13f6 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetContentController.cs @@ -126,7 +126,7 @@ namespace Squidex.Areas.Api.Controllers.Assets else { // Fallback for old endpoint. Does not set the surrogate key. - asset = await assetLoader.GetAsync(asset.AppId.Id, asset.Id, request.Version); + asset = await assetLoader.GetAsync(asset.AppId.Id, asset.Id, request.Version, HttpContext.RequestAborted); } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs index c3c6bdbfa..f5b51f6b8 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetFoldersController.cs @@ -159,14 +159,16 @@ namespace Squidex.Areas.Api.Controllers.Assets [ApiCosts(1)] public async Task DeleteAssetFolder(string app, DomainId id) { - await CommandBus.PublishAsync(new DeleteAssetFolder { AssetFolderId = id }); + var command = new DeleteAssetFolder { AssetFolderId = id }; + + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return AssetFolderDto.FromDomain(context.Result(), Resources); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs index 830eaa3c8..01c6d4ae7 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Assets/AssetsController.cs @@ -72,7 +72,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [ApiCosts(1)] public async Task GetTags(string app) { - var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets); + var tags = await tagService.GetTagsAsync(AppId, TagGroups.Assets, HttpContext.RequestAborted); Response.Headers[HeaderNames.ETag] = tags.Version.ToString(CultureInfo.InvariantCulture); @@ -96,7 +96,7 @@ namespace Squidex.Areas.Api.Controllers.Assets [ApiCosts(1)] public async Task PutTag(string app, string name, [FromBody] RenameTagDto request) { - await tagService.RenameTagAsync(AppId, TagGroups.Assets, Uri.UnescapeDataString(name), request.TagName); + await tagService.RenameTagAsync(AppId, TagGroups.Assets, Uri.UnescapeDataString(name), request.TagName, HttpContext.RequestAborted); return await GetTags(app); } @@ -277,7 +277,7 @@ namespace Squidex.Areas.Api.Controllers.Assets { var command = request.ToCommand(); - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); @@ -415,7 +415,7 @@ namespace Squidex.Areas.Api.Controllers.Assets { var command = request.ToCommand(id); - await CommandBus.PublishAsync(command); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } @@ -448,7 +448,7 @@ namespace Squidex.Areas.Api.Controllers.Assets private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); if (context.PlainResult is AssetDuplicate created) { diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs index 20dfb5532..37472810e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupContentController.cs @@ -49,7 +49,7 @@ namespace Squidex.Areas.Api.Controllers.Backups [AllowAnonymous] public Task GetBackupContent(string app, DomainId id) { - return BackBackupAsync(AppId, app, id); + return GetBackupAsync(AppId, app, id); } /// @@ -70,10 +70,10 @@ namespace Squidex.Areas.Api.Controllers.Backups [AllowAnonymous] public Task GetBackupContentV2(DomainId id, [FromQuery] DomainId appId = default, [FromQuery] string app = "") { - return BackBackupAsync(appId, app, id); + return GetBackupAsync(appId, app, id); } - private async Task BackBackupAsync(DomainId appId, string app, DomainId id) + private async Task GetBackupAsync(DomainId appId, string app, DomainId id) { var backup = await backupservice.GetBackupAsync(appId, id, HttpContext.RequestAborted); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs index 611bb939d..33810a14f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/BackupsController.cs @@ -11,7 +11,6 @@ using Squidex.Domain.Apps.Entities.Backup; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Security; -using Squidex.Infrastructure.Tasks; using Squidex.Shared; using Squidex.Web; @@ -67,9 +66,9 @@ namespace Squidex.Areas.Api.Controllers.Backups [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppBackupsCreate)] [ApiCosts(0)] - public IActionResult PostBackup(string app) + public async Task PostBackup(string app) { - backupService.StartBackupAsync(App.Id, User.Token()!).Forget(); + await backupService.StartBackupAsync(App.Id, User.Token()!, HttpContext.RequestAborted); return NoContent(); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs index 726e754ca..2cf73ffb3 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Backups/Models/BackupJobDto.cs @@ -68,7 +68,6 @@ namespace Squidex.Areas.Api.Controllers.Backups.Models AddGetLink("download", resources.Url(x => nameof(x.GetBackupContentV2), values)); } - return this; } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs index 6ed316ef0..2d7dcd5e2 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/CommentsController.cs @@ -54,7 +54,7 @@ namespace Squidex.Areas.Api.Controllers.Comments [ApiCosts(0)] public async Task GetWatchingUsers(string app, string? resource = null) { - var result = await watchingService.GetWatchingUsersAsync(App.Id, resource ?? "all", UserId()); + var result = await watchingService.GetWatchingUsersAsync(App.Id, resource, UserId(), HttpContext.RequestAborted); return Ok(result); } @@ -79,7 +79,7 @@ namespace Squidex.Areas.Api.Controllers.Comments [ApiCosts(0)] public async Task GetComments(string app, DomainId commentsId, [FromQuery] long version = EtagVersion.Any) { - var result = await commentsLoader.GetCommentsAsync(commentsId, version); + var result = await commentsLoader.GetCommentsAsync(commentsId, version, HttpContext.RequestAborted); var response = Deferred.Response(() => { @@ -111,7 +111,7 @@ namespace Squidex.Areas.Api.Controllers.Comments { var command = request.ToCreateCommand(commentsId); - await CommandBus.PublishAsync(command); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var response = CommentDto.FromDomain(command); @@ -138,7 +138,7 @@ namespace Squidex.Areas.Api.Controllers.Comments { var command = request.ToUpdateComment(commentsId, commentId); - await CommandBus.PublishAsync(command); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } @@ -161,7 +161,7 @@ namespace Squidex.Areas.Api.Controllers.Comments { var command = new DeleteComment { CommentsId = commentsId, CommentId = commentId }; - await CommandBus.PublishAsync(command); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs index b9d6ec28a..acbf37e9e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Comments/Notifications/UserNotificationsController.cs @@ -1,4 +1,4 @@ -// ========================================================================== +// ========================================================================== // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex UG (haftungsbeschraenkt) @@ -53,7 +53,7 @@ namespace Squidex.Areas.Api.Controllers.Comments.Notifications { CheckPermissions(userId); - var result = await commentsLoader.GetCommentsAsync(userId, version); + var result = await commentsLoader.GetCommentsAsync(userId, version, HttpContext.RequestAborted); var response = Deferred.Response(() => { @@ -88,7 +88,7 @@ namespace Squidex.Areas.Api.Controllers.Comments.Notifications CommentId = commentId }; - await CommandBus.PublishAsync(commmand); + await CommandBus.PublishAsync(commmand, HttpContext.RequestAborted); return NoContent(); } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs index 92f35a87a..bcc98c80a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Contents/ContentsController.cs @@ -236,7 +236,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { var command = new ValidateContent { ContentId = id }; - await CommandBus.PublishAsync(command); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } @@ -387,7 +387,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { var command = request.ToCommand(); - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); @@ -418,7 +418,7 @@ namespace Squidex.Areas.Api.Controllers.Contents { var command = request.ToCommand(); - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = result.Select(x => BulkResultDto.FromDomain(x, HttpContext)).ToArray(); @@ -647,14 +647,14 @@ namespace Squidex.Areas.Api.Controllers.Contents { var command = request.ToCommand(id); - await CommandBus.PublishAsync(command); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = ContentDto.FromDomain(result, Resources); diff --git a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs index 8bf65088f..6d759932d 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/EventConsumers/EventConsumersController.cs @@ -6,11 +6,9 @@ // ========================================================================== using Microsoft.AspNetCore.Mvc; -using Orleans; using Squidex.Areas.Api.Controllers.EventConsumers.Models; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.EventSourcing.Consume; using Squidex.Shared; using Squidex.Web; @@ -18,12 +16,12 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers { public sealed class EventConsumersController : ApiController { - private readonly IGrainFactory grainFactory; + private readonly IEventConsumerManager eventConsumerManager; - public EventConsumersController(ICommandBus commandBus, IGrainFactory grainFactory) + public EventConsumersController(ICommandBus commandBus, IEventConsumerManager eventConsumerManager) : base(commandBus) { - this.grainFactory = grainFactory; + this.eventConsumerManager = eventConsumerManager; } [HttpGet] @@ -32,9 +30,9 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [ApiPermission(Permissions.AdminEventsRead)] public async Task GetEventConsumers() { - var eventConsumers = await GetGrain().GetConsumersAsync(); + var eventConsumers = await eventConsumerManager.GetConsumersAsync(HttpContext.RequestAborted); - var response = EventConsumersDto.FromDomain(eventConsumers.Value, Resources); + var response = EventConsumersDto.FromDomain(eventConsumers, Resources); return Ok(response); } @@ -45,7 +43,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [ApiPermission(Permissions.AdminEventsManage)] public async Task StartEventConsumer(string consumerName) { - var eventConsumer = await GetGrain().StartAsync(consumerName); + var eventConsumer = await eventConsumerManager.StartAsync(consumerName, HttpContext.RequestAborted); var response = EventConsumerDto.FromDomain(eventConsumer, Resources); @@ -58,7 +56,7 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [ApiPermission(Permissions.AdminEventsManage)] public async Task StopEventConsumer(string consumerName) { - var eventConsumer = await GetGrain().StopAsync(consumerName); + var eventConsumer = await eventConsumerManager.StopAsync(consumerName, HttpContext.RequestAborted); var response = EventConsumerDto.FromDomain(eventConsumer, Resources); @@ -71,16 +69,11 @@ namespace Squidex.Areas.Api.Controllers.EventConsumers [ApiPermission(Permissions.AdminEventsManage)] public async Task ResetEventConsumer(string consumerName) { - var eventConsumer = await GetGrain().ResetAsync(consumerName); + var eventConsumer = await eventConsumerManager.ResetAsync(consumerName, HttpContext.RequestAborted); var response = EventConsumerDto.FromDomain(eventConsumer, Resources); return Ok(response); } - - private IEventConsumerManagerGrain GetGrain() - { - return grainFactory.GetGrain(SingleGrain.Id); - } } } diff --git a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs index 95df58994..c26f3271b 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Plans/AppPlansController.cs @@ -77,7 +77,7 @@ namespace Squidex.Areas.Api.Controllers.Plans [ApiCosts(0)] public async Task PutPlan(string app, [FromBody] ChangePlanDto request) { - var context = await CommandBus.PublishAsync(request.ToCommand(HttpContext)); + var context = await CommandBus.PublishAsync(request.ToCommand(HttpContext), HttpContext.RequestAborted); string? redirectUri = null; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs index 13045d861..0dbfd7f6f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/Models/RulesDto.cs @@ -29,10 +29,11 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models public static async Task FromRulesAsync(IEnumerable items, IRuleRunnerService ruleRunnerService, Resources resources) { var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(resources.Context.App.Id); + var runningAvailable = runningRuleId != default; var result = new RulesDto { - Items = items.Select(x => RuleDto.FromDomain(x, runningRuleId == null, ruleRunnerService, resources)).ToArray() + Items = items.Select(x => RuleDto.FromDomain(x, runningAvailable, ruleRunnerService, resources)).ToArray() }; result.RunningRuleId = runningRuleId; diff --git a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs index 2e80450e8..81612514a 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Rules/RulesController.cs @@ -237,7 +237,7 @@ namespace Squidex.Areas.Api.Controllers.Rules { var command = new TriggerRule { RuleId = id }; - await CommandBus.PublishAsync(command); + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } @@ -353,7 +353,9 @@ namespace Squidex.Areas.Api.Controllers.Rules [ApiCosts(1)] public async Task DeleteRule(string app, DomainId id) { - await CommandBus.PublishAsync(new DeleteRule { RuleId = id }); + var command = new DeleteRule { RuleId = id }; + + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } @@ -512,7 +514,7 @@ namespace Squidex.Areas.Api.Controllers.Rules private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var runningRuleId = await ruleRunnerService.GetRunningRuleIdAsync(Context.App.Id, HttpContext.RequestAborted); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs index 9fc4dadd8..e7e060fbd 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemaFieldsController.cs @@ -546,7 +546,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = SchemaDto.FromDomain(result, Resources); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs index 0d98d1d0c..1150f91c0 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs @@ -325,7 +325,9 @@ namespace Squidex.Areas.Api.Controllers.Schemas [ApiCosts(1)] public async Task DeleteSchema(string app, string schema) { - await CommandBus.PublishAsync(new DeleteSchema()); + var command = new DeleteSchema(); + + await CommandBus.PublishAsync(command, HttpContext.RequestAborted); return NoContent(); } @@ -393,7 +395,7 @@ namespace Squidex.Areas.Api.Controllers.Schemas private async Task InvokeCommandAsync(ICommand command) { - var context = await CommandBus.PublishAsync(command); + var context = await CommandBus.PublishAsync(command, HttpContext.RequestAborted); var result = context.Result(); var response = SchemaDto.FromDomain(result, Resources); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs index 8cf17ca59..37f6a6f1e 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Statistics/UsagesController.cs @@ -81,8 +81,8 @@ namespace Squidex.Areas.Api.Controllers.Statistics /// Get api calls in date range. /// /// The name of the app. - /// The from date. - /// The to date. + /// The from date. + /// The to date. /// /// 200 => API call returned. /// 404 => App not found. @@ -93,14 +93,14 @@ namespace Squidex.Areas.Api.Controllers.Statistics [ProducesResponseType(typeof(CallsUsageDtoDto), StatusCodes.Status200OK)] [ApiPermissionOrAnonymous(Permissions.AppUsage)] [ApiCosts(0)] - public async Task GetUsages(string app, DateTime fromDate, DateTime toDate) + public async Task GetUsages(string app, DateTime dateFrom, DateTime dateTo) { - if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) + if (dateFrom > dateTo && (dateTo - dateFrom).TotalDays > 100) { return BadRequest(); } - var (summary, details) = await usageTracker.QueryAsync(AppId.ToString(), fromDate.Date, toDate.Date, HttpContext.RequestAborted); + var (summary, details) = await usageTracker.QueryAsync(AppId.ToString(), dateFrom.Date, dateTo.Date, HttpContext.RequestAborted); var (plan, _) = appPlansProvider.GetPlanForApp(App); diff --git a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs index a4830efa7..59603a82f 100644 --- a/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs +++ b/backend/src/Squidex/Areas/Api/Controllers/Users/Models/ResourcesDto.cs @@ -36,11 +36,6 @@ namespace Squidex.Areas.Api.Controllers.Users.Models result.AddGetLink("admin/users", resources.Url(x => nameof(x.GetUsers))); } - if (resources.CanReadOrleans) - { - result.AddGetLink("admin/orleans", "/orleans"); - } - result.AddGetLink("languages", resources.Url(x => nameof(x.GetLanguages))); return result; diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs b/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs index cc3571b6b..6b0d4fb58 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/TokenStoreInitializer.cs @@ -15,7 +15,7 @@ using Squidex.Infrastructure.Timers; namespace Squidex.Areas.IdentityServer.Config { - public sealed class TokenStoreInitializer : IInitializable + public sealed class TokenStoreInitializer : IInitializable, IBackgroundProcess { private readonly OpenIddictMongoDbOptions options; private readonly IServiceProvider serviceProvider; @@ -31,13 +31,20 @@ namespace Squidex.Areas.IdentityServer.Config CancellationToken ct) { await SetupIndexAsync(ct); + } + + public async Task StartAsync( + CancellationToken ct) + { + timer = new CompletionTimer((int)TimeSpan.FromHours(6).TotalMilliseconds, PruneAsync); await PruneAsync(ct); + } - timer = new CompletionTimer((int)TimeSpan.FromHours(6).TotalMilliseconds, async ct => - { - await PruneAsync(ct); - }); + public Task StopAsync( + CancellationToken ct) + { + return timer?.StopAsync() ?? Task.CompletedTask; } private async Task PruneAsync( @@ -67,14 +74,5 @@ namespace Squidex.Areas.IdentityServer.Config cancellationToken: ct); } } - - public async Task ReleaseAsync( - CancellationToken ct) - { - if (timer != null) - { - await timer.StopAsync(); - } - } } } diff --git a/backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs b/backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs deleted file mode 100644 index 8db67d5c1..000000000 --- a/backend/src/Squidex/Areas/OrleansDashboard/Middlewares/OrleansDashboardAuthenticationMiddleware.cs +++ /dev/null @@ -1,51 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Squidex.Shared; -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 InvokeAsync(HttpContext context) - { - var authentication = await context.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); - - if (authentication.Succeeded) - { - var permissions = authentication.Principal?.Claims.Permissions(); - - if (permissions?.Allows(Permissions.AdminOrleans) == true) - { - await next(context); - } - else - { - context.Response.StatusCode = 403; - } - } - else - { - await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties - { - RedirectUri = context.Request.PathBase + context.Request.Path - }); - } - } - } -} diff --git a/backend/src/Squidex/Config/Domain/AssetServices.cs b/backend/src/Squidex/Config/Domain/AssetServices.cs index 3b3a8eb3b..29ac5b763 100644 --- a/backend/src/Squidex/Config/Domain/AssetServices.cs +++ b/backend/src/Squidex/Config/Domain/AssetServices.cs @@ -15,7 +15,6 @@ using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Search; using Squidex.Hosting; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; using tusdotnet.Interfaces; namespace Squidex.Config.Domain @@ -89,9 +88,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - - services.AddSingletonAs>() - .AsSelf(); } public static void AddSquidexAssetInfrastructure(this IServiceCollection services, IConfiguration config) diff --git a/backend/src/Squidex/Config/Domain/BackupsServices.cs b/backend/src/Squidex/Config/Domain/BackupsServices.cs index 038eccd76..d0494568d 100644 --- a/backend/src/Squidex/Config/Domain/BackupsServices.cs +++ b/backend/src/Squidex/Config/Domain/BackupsServices.cs @@ -42,6 +42,12 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); + + services.AddTransientAs() + .As(); + + services.AddTransientAs() + .AsSelf(); } } } diff --git a/backend/src/Squidex/Config/Domain/CommandsServices.cs b/backend/src/Squidex/Config/Domain/CommandsServices.cs index dc650b5b3..07ca70c27 100644 --- a/backend/src/Squidex/Config/Domain/CommandsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommandsServices.cs @@ -80,7 +80,7 @@ namespace Squidex.Config.Domain .As().As(); services.AddSingletonAs() - .As().As(); + .As(); services.AddSingletonAs() .As().As(); @@ -106,10 +106,10 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingletonAs>() + services.AddSingletonAs>() .As(); - services.AddSingletonAs>() + services.AddSingletonAs>() .As(); services.AddSingletonAs() @@ -120,6 +120,9 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); + + services.AddSingletonAs() + .As(); } } } diff --git a/backend/src/Squidex/Config/Domain/CommentsServices.cs b/backend/src/Squidex/Config/Domain/CommentsServices.cs index 080c318a2..a32cee984 100644 --- a/backend/src/Squidex/Config/Domain/CommentsServices.cs +++ b/backend/src/Squidex/Config/Domain/CommentsServices.cs @@ -13,9 +13,6 @@ namespace Squidex.Config.Domain { public static void AddSquidexComments(this IServiceCollection services) { - services.AddSingletonAs() - .As(); - services.AddSingletonAs() .As(); } diff --git a/backend/src/Squidex/Config/Domain/ContentsServices.cs b/backend/src/Squidex/Config/Domain/ContentsServices.cs index 941efd7d4..6ffd6342e 100644 --- a/backend/src/Squidex/Config/Domain/ContentsServices.cs +++ b/backend/src/Squidex/Config/Domain/ContentsServices.cs @@ -17,7 +17,6 @@ using Squidex.Domain.Apps.Entities.Contents.Validation; using Squidex.Domain.Apps.Entities.History; using Squidex.Domain.Apps.Entities.Search; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; namespace Squidex.Config.Domain { @@ -37,8 +36,8 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - services.AddTransientAs() - .As(); + services.AddTransientAs() + .As().As(); services.AddTransientAs() .As(); @@ -99,9 +98,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - - services.AddSingletonAs>() - .AsSelf(); } } } diff --git a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs index 436dc1e2a..7d4c007aa 100644 --- a/backend/src/Squidex/Config/Domain/EventSourcingServices.cs +++ b/backend/src/Squidex/Config/Domain/EventSourcingServices.cs @@ -9,7 +9,7 @@ using EventStore.Client; using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.EventSourcing.Grains; +using Squidex.Infrastructure.EventSourcing.Consume; using Squidex.Infrastructure.States; namespace Squidex.Config.Domain @@ -49,20 +49,23 @@ namespace Squidex.Config.Domain } }); - services.AddSingletonAs() - .As(); - services.AddTransientAs() .AsSelf(); + services.AddSingletonAs() + .As(); + services.AddSingletonAs() .As(); services.AddSingletonAs() .As(); - services.AddSingletonAs() - .As(); + services.AddSingletonAs() + .As(); + + services.AddSingleton>( + sb => c => ActivatorUtilities.CreateInstance(sb, c)); } } } diff --git a/backend/src/Squidex/Config/Domain/HealthCheckServices.cs b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs index 1797d90b4..a0d8ad959 100644 --- a/backend/src/Squidex/Config/Domain/HealthCheckServices.cs +++ b/backend/src/Squidex/Config/Domain/HealthCheckServices.cs @@ -5,9 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Squidex.Domain.Apps.Entities.Apps.Diagnostics; using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.EventSourcing; namespace Squidex.Config.Domain { @@ -19,10 +17,7 @@ namespace Squidex.Config.Domain "diagnostics:gc"); services.AddHealthChecks() - .AddCheck("GC", tags: new[] { "node" }) - .AddCheck("Orleans", tags: new[] { "cluster" }) - .AddCheck("OrleansApp", tags: new[] { "cluster" }) - .AddCheck("EventConsumers", tags: new[] { "background" }); + .AddCheck("GC", tags: new[] { "node" }); } } } diff --git a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs index 7ee257b19..024570a68 100644 --- a/backend/src/Squidex/Config/Domain/InfrastructureServices.cs +++ b/backend/src/Squidex/Config/Domain/InfrastructureServices.cs @@ -19,14 +19,10 @@ using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Templates; using Squidex.Domain.Apps.Core.Templates.Extensions; using Squidex.Domain.Apps.Entities.Contents.Counter; -using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Tags; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Diagnostics; -using Squidex.Infrastructure.EventSourcing.Grains; using Squidex.Infrastructure.Log; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Translations; using Squidex.Infrastructure.UsageTracking; using Squidex.Pipeline.Robots; @@ -61,9 +57,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs(_ => SystemClock.Instance) .As(); - services.AddSingletonAs>() - .AsSelf(); - services.AddSingletonAs() .AsOptional(); @@ -76,7 +69,7 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As().As(); - services.AddSingletonAs() + services.AddSingletonAs() .As(); services.AddSingletonAs() @@ -112,7 +105,8 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .As(); - services.AddSingleton(DomainObjectGrainFormatter.Format); + services.AddSingletonAs() + .As(); } public static void AddSquidexUsageTracking(this IServiceCollection services, IConfiguration config) @@ -130,9 +124,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - - services.AddSingletonAs>() - .AsSelf(); } public static void AddSquidexTranslation(this IServiceCollection services, IConfiguration config) diff --git a/backend/src/Squidex/Config/Domain/LoggingServices.cs b/backend/src/Squidex/Config/Domain/LoggingServices.cs index f03497357..72ddd7e6a 100644 --- a/backend/src/Squidex/Config/Domain/LoggingServices.cs +++ b/backend/src/Squidex/Config/Domain/LoggingServices.cs @@ -68,26 +68,6 @@ namespace Squidex.Config.Domain return level >= LogLevel.Warning; } - if (category.StartsWith("Orleans.Runtime.NoOpHostEnvironmentStatistics", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Error; - } - - if (category.StartsWith("Orleans.Runtime.SafeTimer", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Error; - } - - if (category.StartsWith("Orleans.Runtime.Scheduler", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Warning; - } - - if (category.StartsWith("Orleans.", StringComparison.OrdinalIgnoreCase)) - { - return level >= LogLevel.Warning; - } - if (category.StartsWith("Runtime.", StringComparison.OrdinalIgnoreCase)) { return level >= LogLevel.Warning; diff --git a/backend/src/Squidex/Config/Domain/MigrationServices.cs b/backend/src/Squidex/Config/Domain/MigrationServices.cs index 7ddaebad6..cd4bd1ef5 100644 --- a/backend/src/Squidex/Config/Domain/MigrationServices.cs +++ b/backend/src/Squidex/Config/Domain/MigrationServices.cs @@ -62,12 +62,6 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); - - services.AddTransientAs() - .As(); - - services.AddTransientAs() - .As(); } } } diff --git a/backend/src/Squidex/Config/Domain/RuleServices.cs b/backend/src/Squidex/Config/Domain/RuleServices.cs index b99979298..b3a332c70 100644 --- a/backend/src/Squidex/Config/Domain/RuleServices.cs +++ b/backend/src/Squidex/Config/Domain/RuleServices.cs @@ -19,7 +19,6 @@ using Squidex.Domain.Apps.Entities.Rules.Runner; using Squidex.Domain.Apps.Entities.Rules.UsageTracking; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure.EventSourcing; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Reflection; namespace Squidex.Config.Domain @@ -97,9 +96,6 @@ namespace Squidex.Config.Domain services.AddSingletonAs() .AsSelf(); - services.AddSingletonAs>() - .AsSelf(); - services.AddInitializer("Serializer (Rules)", registry => { RuleActionConverter.Mapping = registry.Actions.ToDictionary(x => x.Key, x => x.Value.Type); diff --git a/backend/src/Squidex/Config/Domain/StoreServices.cs b/backend/src/Squidex/Config/Domain/StoreServices.cs index 484578ad0..3786f7916 100644 --- a/backend/src/Squidex/Config/Domain/StoreServices.cs +++ b/backend/src/Squidex/Config/Domain/StoreServices.cs @@ -6,6 +6,7 @@ // ========================================================================== using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Caching.Distributed; using Migrations.Migrations.MongoDb; using MongoDB.Driver; using MongoDB.Driver.Core.Extensions.DiagnosticSources; @@ -38,6 +39,7 @@ using Squidex.Domain.Users.InMemory; using Squidex.Domain.Users.MongoDb; using Squidex.Hosting; using Squidex.Infrastructure; +using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Diagnostics; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Log; @@ -98,6 +100,9 @@ namespace Squidex.Config.Domain services.AddTransientAs() .As(); + services.AddSingletonAs() + .As(); + services.AddHealthChecks() .AddCheck("MongoDB", tags: new[] { "node" }); diff --git a/backend/src/Squidex/Config/Messaging/MessagingServices.cs b/backend/src/Squidex/Config/Messaging/MessagingServices.cs new file mode 100644 index 000000000..d5b618e9e --- /dev/null +++ b/backend/src/Squidex/Config/Messaging/MessagingServices.cs @@ -0,0 +1,89 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using Newtonsoft.Json; +using Squidex.Domain.Apps.Entities.Apps.Plans; +using Squidex.Domain.Apps.Entities.Assets; +using Squidex.Domain.Apps.Entities.Backup; +using Squidex.Domain.Apps.Entities.Contents; +using Squidex.Domain.Apps.Entities.Rules; +using Squidex.Domain.Apps.Entities.Rules.Runner; +using Squidex.Domain.Apps.Entities.Rules.UsageTracking; +using Squidex.Infrastructure.EventSourcing.Consume; +using Squidex.Messaging; +using Squidex.Messaging.Implementation; +using Squidex.Messaging.Implementation.Scheduler; + +namespace Squidex.Config.Messaging +{ + public static class MessagingServices + { + public static void AddSquidexMessaging(this IServiceCollection services, IConfiguration config) + { + var worker = config.GetValue("clustering:worker"); + + if (worker) + { + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf(); + + services.AddSingletonAs() + .AsSelf().As(); + + services.AddSingletonAs() + .AsSelf().As(); + + services.AddSingletonAs() + .AsSelf().As(); + + services.AddSingletonAs() + .AsSelf().As(); + + services.AddSingletonAs() + .AsSelf().As(); + } + + services.AddSingleton(c => + new NewtonsoftJsonTransportSerializer(c.GetRequiredService())); + + services.AddMessagingTransport(config); + services.AddMessaging(options => + { + options.Routing.Add(m => m is RuleRunnerRun, "rules.run"); + options.Routing.Add(m => m is BackupStart, "backup.start"); + options.Routing.Add(m => m is BackupRestore, "backup.restore"); + options.Routing.Add(_ => true, "default"); + }); + + services.AddMessaging("default", worker, options => + { + options.Scheduler = InlineScheduler.Instance; + }); + + services.AddMessaging("backup.start", worker, options => + { + options.Scheduler = new ParallelScheduler(4); + }); + + services.AddMessaging("backup.restore", worker, options => + { + options.Scheduler = InlineScheduler.Instance; + }); + + services.AddMessaging("rules.run", worker, options => + { + options.Scheduler = new ParallelScheduler(4); + }); + } + } +} diff --git a/backend/src/Squidex/Config/Orleans/Helper.cs b/backend/src/Squidex/Config/Orleans/Helper.cs deleted file mode 100644 index 5368498b4..000000000 --- a/backend/src/Squidex/Config/Orleans/Helper.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Net; -using System.Net.Sockets; -using Squidex.Hosting.Configuration; -using Squidex.Infrastructure.Net; - -namespace Squidex.Config.Orleans -{ - public static class Helper - { - internal static async Task ResolveIPAddressAsync(string addressOrHost, AddressFamily family) - { - var loopback = family == AddressFamily.InterNetwork ? IPAddress.Loopback : IPAddress.IPv6Loopback; - - if (addressOrHost.Equals("loopback", StringComparison.OrdinalIgnoreCase)) - { - return loopback; - } - - if (IPAddress.TryParse(addressOrHost, out var address)) - { - return address; - } - - var candidates = await Dns.GetHostAddressesAsync(addressOrHost); - - var chosen = candidates.OrderBy(x => x, IPAddressComparer.Instance).FirstOrDefault(); - - if (chosen == null) - { - var error = new ConfigurationError($"Hostname {addressOrHost} with family {family} is not a valid IP address or DNS name"); - - throw new ConfigurationException(error); - } - - return chosen; - } - } -} diff --git a/backend/src/Squidex/Config/Orleans/OrleansServices.cs b/backend/src/Squidex/Config/Orleans/OrleansServices.cs deleted file mode 100644 index 99dd26f1b..000000000 --- a/backend/src/Squidex/Config/Orleans/OrleansServices.cs +++ /dev/null @@ -1,195 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Globalization; -using System.Net; -using System.Net.Sockets; -using Orleans; -using Orleans.Configuration; -using Orleans.Hosting; -using Orleans.Providers.MongoDB.Configuration; -using Orleans.Providers.MongoDB.Utils; -using Orleans.Runtime; -using OrleansDashboard; -using Squidex.Domain.Apps.Entities; -using Squidex.Hosting.Configuration; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Squidex.Web; - -namespace Squidex.Config.Orleans -{ - public static class OrleansServices - { - public static void ConfigureForSquidex(this ISiloBuilder builder, IConfiguration config) - { - builder.AddOrleansPubSub(); - - builder.ConfigureServices(services => - { - services.AddScoped(typeof(IGrainState<>), typeof(Infrastructure.Orleans.GrainState<>)); - - services.AddSingletonAs() - .As(); - - services.AddSingletonAs() - .As(); - - services.AddScopedAs() - .As(); - - services.AddScoped(x => x.GetRequiredService().GrainIdentity); - }); - - builder.ConfigureApplicationParts(parts => - { - parts.AddApplicationPart(SquidexEntities.Assembly); - parts.AddApplicationPart(SquidexInfrastructure.Assembly); - }); - - builder.Configure(options => - { - options.SerializationProviders.Add(typeof(JsonSerializer)); - }); - - builder.Configure(options => - { - options.TurnWarningLengthThreshold = TimeSpan.FromSeconds(5); - }); - - builder.Configure(options => - { - options.Configure(); - }); - - builder.Configure(options => - { - options.HideTrace = true; - }); - - builder.UseDashboardEmbeddedFiles(); - builder.UseDashboard(options => - { - options.HostSelf = false; - }); - - builder.AddOutgoingGrainCallFilter(); - builder.AddOutgoingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - builder.AddIncomingGrainCallFilter(); - - var (siloPort, gatewayPort) = GetPorts(config); - - config.ConfigureByOption("orleans:clustering", new Alternatives - { - ["MongoDB"] = () => - { - var address = GetIPAddress(config); - - builder.ConfigureEndpoints( - address, - siloPort, - gatewayPort, - true); - - builder.UseMongoDBClustering(options => - { - options.Strategy = MongoDBMembershipStrategy.SingleDocument; - - options.Configure(config); - }); - - if (config.GetValue("orleans:kubernetes")) - { - builder.UseKubernetesHosting(); - } - }, - ["Development"] = () => - { - builder.UseLocalhostClustering(siloPort, gatewayPort); - } - }); - - config.ConfigureByOption("store:type", new Alternatives - { - ["MongoDB"] = () => - { - builder.UseMongoDBReminders(options => - { - options.Configure(config); - }); - } - }); - } - - private static (int, int) GetPorts(IConfiguration config) - { - var orleansPortSilo = config.GetOptionalValue("orleans:siloPort", 11111); - var orleansPortGateway = config.GetOptionalValue("orleans:gatewayPort", 40000); - - var privatePorts = config.GetOptionalValue("WEBSITE_PRIVATE_PORTS", string.Empty); - - if (!string.IsNullOrWhiteSpace(privatePorts) && config.GetValue("orleans:useAzureNetwork")) - { - var ports = privatePorts.Split(','); - - if (ports.Length < 1 || !int.TryParse(ports[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out orleansPortSilo)) - { - var error = new ConfigurationError("Insufficient private ports configured.", "WEBSITE_PRIVATE_PORTS"); - - throw new ConfigurationException(error); - } - } - - return (orleansPortSilo, orleansPortGateway); - } - - private static IPAddress GetIPAddress(IConfiguration config) - { - IPAddress? address = null; - - var configuredAddress = config.GetOptionalValue("orleans:ipAddress", string.Empty); - - if (!string.IsNullOrWhiteSpace(configuredAddress)) - { - address = IPAddress.Parse(configuredAddress); - } - - if (address == null && config.GetValue("orleans:useAzureNetwork")) - { - var privateIP = config.GetOptionalValue("WEBSITE_PRIVATE_IP", string.Empty); - - if (!string.IsNullOrWhiteSpace(privateIP)) - { - address = IPAddress.Parse(privateIP); - } - } - - address ??= Helper.ResolveIPAddressAsync(Dns.GetHostName(), AddressFamily.InterNetwork).Result; - - return address; - } - - private static void Configure(this MongoDBOptions options, IConfiguration config) - { - options.CollectionPrefix = "Orleans_"; - - options.DatabaseName = config.GetRequiredValue("store:mongoDb:database"); - } - - private static void Configure(this ClusterOptions options) - { - options.ClusterId = Constants.OrleansClusterId; - options.ServiceId = Constants.OrleansClusterId; - } - } -} diff --git a/backend/src/Squidex/Program.cs b/backend/src/Squidex/Program.cs index e133025e7..a65f91220 100644 --- a/backend/src/Squidex/Program.cs +++ b/backend/src/Squidex/Program.cs @@ -6,7 +6,6 @@ // ========================================================================== using Squidex.Config.Domain; -using Squidex.Config.Orleans; using Squidex.Config.Startup; namespace Squidex @@ -28,28 +27,21 @@ namespace Squidex { builder.ConfigureForSquidex(); }) - .ConfigureServices(services => + .ConfigureServices((context, services) => { // Step 0: Log all configuration. services.AddHostedService(); // Step 1: Initialize all services. services.AddInitializer(); - }) - .UseOrleans((context, builder) => - { - // Step 3: Start Orleans. - builder.ConfigureForSquidex(context.Configuration); - }) - .ConfigureServices(services => - { - // Step 4: Run migration. + + // Step 2: Run migration. services.AddHostedService(); - // Step 5: Run rebuild processes. + // Step 3: Run rebuild processes. services.AddHostedService(); - // Step 6: Start background processes. + // Step 4: Start background processes. services.AddBackgroundProcesses(); }) .ConfigureWebHostDefaults(builder => diff --git a/backend/src/Squidex/Squidex.csproj b/backend/src/Squidex/Squidex.csproj index d598f3088..2dd7d757c 100644 --- a/backend/src/Squidex/Squidex.csproj +++ b/backend/src/Squidex/Squidex.csproj @@ -37,6 +37,9 @@ + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -55,10 +58,6 @@ - - - - @@ -70,9 +69,6 @@ - - - diff --git a/backend/src/Squidex/Startup.cs b/backend/src/Squidex/Startup.cs index 857e1e15b..c24527b3c 100644 --- a/backend/src/Squidex/Startup.cs +++ b/backend/src/Squidex/Startup.cs @@ -5,14 +5,13 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== -using Orleans; using Squidex.Areas.Api.Config.OpenApi; using Squidex.Areas.Frontend; using Squidex.Areas.IdentityServer.Config; -using Squidex.Areas.OrleansDashboard.Middlewares; using Squidex.Areas.Portal.Middlewares; using Squidex.Config.Authentication; using Squidex.Config.Domain; +using Squidex.Config.Messaging; using Squidex.Config.Web; using Squidex.Pipeline.Plugins; using Squidex.Web; @@ -59,6 +58,7 @@ namespace Squidex services.AddSquidexImageResizing(config); services.AddSquidexInfrastructure(config); services.AddSquidexLocalization(); + services.AddSquidexMessaging(config); services.AddSquidexMigration(config); services.AddSquidexNotifications(config); services.AddSquidexOpenApiSettings(); @@ -116,12 +116,6 @@ namespace Squidex builder.UseMiddleware(); }); - app.Map(Constants.PrefixOrleans, builder => - { - builder.UseMiddleware(); - builder.UseOrleansDashboard(); - }); - app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 4e8667169..d7f40f3c3 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -75,6 +75,10 @@ } }, + "messaging": { + "type": "MongoDb" + }, + "scripting": { // The timeout for the whole script execution. "timeoutExecution": "00:00:04", @@ -408,32 +412,13 @@ // The relative or absolute path to the folder to store the assets. "path": "Assets" }, - // Allow to expose the url in graph ql url. + // Allow to expose the url in GraphQL URL. "exposeSourceUrl": false }, - "orleans": { - // Define the clustering type. - // - // SUPPORTED: MongoDB, Development - "clustering": "Development", - - // Tell Orleans it is running in kubernetes. - // - // Read more: https://dotnet.github.io/orleans/docs/deployment/kubernetes.html?q=kubernetes - "kubernetes": false, - - // The port is used to share messages between all cluster members. Must be accessible within your cluster or network. - "siloPort": "11111", - - // The ports used by Orleans to connect to external clients. Not used. - "gatewayPort": "40000", - - // The advertised IP address. Usually not needed. - "ipAddress": "", - - // Bind Orleans to a private network when running as a app service instance. - "useAzureNetwork": true + "clustering": { + // Defines whether the current instance is a worker. You should have only one worker in your deployment. + "worker": true }, "eventStore": { diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs index a4c480a4c..1a12740b6 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/EventEnricherTests.cs @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules Assert.Null(enrichedEvent.User); - A.CallTo(() => userResolver.FindByIdAsync(A._, default)) + A.CallTo(() => userResolver.FindByIdAsync(A._, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs index c16ead5b8..4c4c53393 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/HandleRules/RuleServiceTests.cs @@ -85,7 +85,10 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules sut = new RuleService(Options.Create(new RuleOptions()), new[] { ruleTriggerHandler }, new[] { ruleActionHandler }, - eventEnricher, TestUtils.DefaultSerializer, clock, log, typeNameRegistry); + eventEnricher, TestUtils.DefaultSerializer, log, typeNameRegistry) + { + Clock = clock + }; } [Fact] @@ -448,7 +451,7 @@ namespace Squidex.Domain.Apps.Core.Operations.HandleRules A.CallTo(() => ruleTriggerHandler.Trigger(A>._, A._)) .MustNotHaveHappened(); - A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A>._, A._, default)) + A.CallTo(() => ruleTriggerHandler.CreateEnrichedEventsAsync(A>._, A._, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs index 22727a0c0..bcd325b42 100644 --- a/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Core.Tests/Operations/Tags/TagNormalizerTests.cs @@ -44,7 +44,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), A>.That.Is("n_raw2_1", "n_raw2_2", "n_raw4"), - A>.That.Is("o_raw2_1", "o_raw2_2", "o_raw4"))) + A>.That.Is("o_raw2_1", "o_raw2_2", "o_raw4"), + default)) .Returns(new Dictionary { ["n_raw2_2"] = "id2_2", @@ -65,7 +66,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), A>.That.Is("name2_1", "name2_2", "name4"), - A>.That.IsEmpty())) + A>.That.IsEmpty(), + default)) .Returns(new Dictionary { ["name2_2"] = "id2_2", @@ -86,7 +88,8 @@ namespace Squidex.Domain.Apps.Core.Operations.Tags A.CallTo(() => tagService.NormalizeTagsAsync(appId, TagGroups.Schemas(schemaId), A>.That.Is("id2_1", "id2_2", "id4"), - A>.That.IsEmpty())) + A>.That.IsEmpty(), + default)) .Returns(new Dictionary { ["id2_2"] = "name2_2", diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs index de6006bc8..fc168fbd4 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/AppProviderExtensionsTests.cs @@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities Assert.Single(components); Assert.Same(schema.SchemaDef, components[schemaId.Id]); - A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, false, default)) + A.CallTo(() => appProvider.GetSchemaAsync(A._, A._, false, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AlwaysCreateClientCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AlwaysCreateClientCommandMiddlewareTests.cs index 2ceacc745..3e1999ea6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AlwaysCreateClientCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AlwaysCreateClientCommandMiddlewareTests.cs @@ -28,9 +28,9 @@ namespace Squidex.Domain.Apps.Entities.Apps var sut = new AlwaysCreateClientCommandMiddleware(); - await sut.HandleAsync(context); + await sut.HandleAsync(context, default); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.Id == "default"))) + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.Id == "default"), default)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleterTests.cs index de5d059bd..f9fffee6a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppEventDeleterTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Apps await sut.DeleteAppAsync(app, ct); - A.CallTo(() => eventStore.DeleteAsync($"^[a-zA-Z0-9]-{app.Id}", ct)) + A.CallTo(() => eventStore.DeleteAsync($"^[a-zA-Z0-9]-{app.Id}", A._)) .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs index 1d0fc3ed4..5da66ca15 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppPermanentDeleterTests.cs @@ -6,11 +6,11 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Domain.Apps.Events.Apps; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.Reflection; using Xunit; @@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.Apps { public class AppPermanentDeleterTests { - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); private readonly IDeleter deleter1 = A.Fake(); private readonly IDeleter deleter2 = A.Fake(); private readonly TypeNameRegistry typeNameRegistry; @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Apps .Map(typeof(AppContributorRemoved)) .Map(typeof(AppDeleted)); - sut = new AppPermanentDeleter(new[] { deleter1, deleter2 }, grainFactory, typeNameRegistry); + sut = new AppPermanentDeleter(new[] { deleter1, deleter2 }, domainObjectFactory, typeNameRegistry); } [Fact] @@ -111,15 +111,15 @@ namespace Squidex.Domain.Apps.Entities.Apps [Fact] public async Task Should_call_deleters_when_app_deleted() { - var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + var app = new AppDomainObject.State { Id = DomainId.NewGuid(), Name = "my-app" }; - var grain = A.Fake(); + var domainObject = A.Fake(); - A.CallTo(() => grain.GetStateAsync()) + A.CallTo(() => domainObject.Snapshot) .Returns(app); - A.CallTo(() => grainFactory.GetGrain(app.Id.ToString(), null)) - .Returns(grain); + A.CallTo(() => domainObjectFactory.Create(app.Id)) + .Returns(domainObject); await sut.On(Envelope.Create(new AppDeleted { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs deleted file mode 100644 index cfc6effb6..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs +++ /dev/null @@ -1,138 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Core; -using Squidex.Infrastructure.Json.Objects; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps -{ - public sealed class AppUISettingsGrainTests - { - private readonly IGrainIdentity identity = A.Fake(); - private readonly IGrainState state = A.Fake>(); - private readonly AppUISettingsGrain sut; - - public AppUISettingsGrainTests() - { - sut = new AppUISettingsGrain(identity, state); - } - - [Fact] - public async Task Should_set_setting() - { - await sut.SetAsync(new JsonObject().Add("key", 15)); - - var actual = await sut.GetAsync(); - - var expected = - new JsonObject().Add("key", 15); - - Assert.Equal(expected.ToString(), actual.ToString()); - - A.CallTo(() => state.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_set_root_value() - { - await sut.SetAsync("key", JsonValue.Create(123)); - - var actual = await sut.GetAsync(); - - var expected = - new JsonObject().Add("key", 123); - - Assert.Equal(expected.ToString(), actual.ToString()); - - A.CallTo(() => state.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_root_value() - { - await sut.SetAsync("key", JsonValue.Create(123)); - - await sut.RemoveAsync("key"); - - var actual = await sut.GetAsync(); - - var expected = new JsonObject(); - - Assert.Equal(expected.ToString(), actual.ToString()); - - A.CallTo(() => state.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_set_nested_value() - { - await sut.SetAsync("root.nested", JsonValue.Create(123)); - - var actual = await sut.GetAsync(); - - var expected = - new JsonObject().Add("root", - new JsonObject().Add("nested", 123)); - - Assert.Equal(expected.ToString(), actual.ToString()); - - A.CallTo(() => state.WriteAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_nested_value() - { - await sut.SetAsync("root.nested", JsonValue.Create(123)); - - await sut.RemoveAsync("root.nested"); - - var actual = await sut.GetAsync(); - - var expected = - new JsonObject().Add("root", - new JsonObject()); - - Assert.Equal(expected.ToString(), actual.ToString()); - - A.CallTo(() => state.WriteAsync()) - .MustHaveHappenedTwiceExactly(); - } - - [Fact] - public async Task Should_throw_exception_if_nested_not_an_object() - { - await sut.SetAsync("root.nested", JsonValue.Create(123)); - - await Assert.ThrowsAsync(() => sut.SetAsync("root.nested.value", JsonValue.Create(123))); - } - - [Fact] - public async Task Should_do_nothing_if_deleting_and_nested_not_found() - { - await sut.RemoveAsync("root.nested"); - - A.CallTo(() => state.WriteAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_do_nothing_if_deleting_and_key_not_found() - { - await sut.RemoveAsync("root"); - - A.CallTo(() => state.WriteAsync()) - .MustNotHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs index 6b94aa74b..9859f5ba6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsTests.cs @@ -6,92 +6,200 @@ // ========================================================================== using FakeItEasy; -using Orleans; +using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; using Squidex.Infrastructure.Json.Objects; +using Squidex.Infrastructure.TestHelpers; using Xunit; namespace Squidex.Domain.Apps.Entities.Apps { - public class AppUISettingsTests + public sealed class AppUISettingsTests { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IAppUISettingsGrain grain = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState state; + private readonly DomainId appId = DomainId.NewGuid(); + private readonly string userId = Guid.NewGuid().ToString(); + private readonly string stateId; private readonly AppUISettings sut; public AppUISettingsTests() { - A.CallTo(() => grainFactory.GetGrain(A._, null)) - .Returns(grain); + ct = cts.Token; - sut = new AppUISettings(grainFactory); + stateId = $"{appId}_{userId}"; + state = new TestState(stateId); + + sut = new AppUISettings(state.PersistenceFactory); } [Fact] - public async Task Should_call_grain_if_retrieving_settings() + public void Should_run_with_default_order() { - var settings = new JsonObject(); + var order = ((IDeleter)sut).Order; - A.CallTo(() => grain.GetAsync()) - .Returns(settings); + Assert.Equal(0, order); + } - var result = await sut.GetAsync(DomainId.NewGuid(), "user"); + [Fact] + public async Task Should_delete_contributor_state() + { + await ((IDeleter)sut).DeleteContributorAsync(appId, userId, ct); - Assert.Same(settings, result); + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); } [Fact] - public async Task Should_call_grain_if_setting_value() + public async Task Should_delete_app_and_contributors() { - var value = new JsonObject(); + var app = Mocks.App(NamedId.Of(appId, "my-app")); + + A.CallTo(() => app.Contributors) + .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); + + var rootState = new TestState(appId, state.PersistenceFactory); - await sut.SetAsync(DomainId.NewGuid(), "user", "the.path", value); + await ((IDeleter)sut).DeleteAppAsync(app, ct); - A.CallTo(() => grain.SetAsync("the.path", value)) + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); + + A.CallTo(() => rootState.Persistence.DeleteAsync(ct)) .MustHaveHappened(); } [Fact] - public async Task Should_call_grain_if_replacing_settings() + public async Task Should_set_setting() { - var value = new JsonObject(); + await sut.SetAsync(appId, userId, new JsonObject().Add("key", 42), ct); + + var actual = await sut.GetAsync(appId, userId, ct); - await sut.SetAsync(DomainId.NewGuid(), "user", value); + var expected = + new JsonObject().Add("key", 42); - A.CallTo(() => grain.SetAsync(value)) + Assert.Equal(expected.ToString(), actual.ToString()); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, ct)) .MustHaveHappened(); } [Fact] - public async Task Should_call_grain_if_removing_value() + public async Task Should_set_root_value() { - await sut.RemoveAsync(DomainId.NewGuid(), "user", "the.path"); + await sut.SetAsync(appId, userId, "key", 42, ct); + + var actual = await sut.GetAsync(appId, userId, ct); + + var expected = + new JsonObject().Add("key", 42); - A.CallTo(() => grain.RemoveAsync("the.path")) + Assert.Equal(expected.ToString(), actual.ToString()); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, ct)) .MustHaveHappened(); } [Fact] - public async Task Should_clear_grain_when_app_deleted() + public async Task Should_remove_root_value() { - var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + await sut.SetAsync(appId, userId, "key", 42, ct); - await ((IDeleter)sut).DeleteAppAsync(app, default); + await sut.RemoveAsync(appId, userId, "key", ct); + await sut.RemoveAsync(appId, userId, "key", ct); - A.CallTo(() => grain.ClearAsync()) - .MustHaveHappened(); + var actual = await sut.GetAsync(appId, userId, ct); + + var expected = new JsonObject(); + + Assert.Equal(expected.ToString(), actual.ToString()); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, ct)) + .MustHaveHappenedTwiceExactly(); } [Fact] - public async Task Should_clear_grain_when_contributor_removed() + public async Task Should_set_nested_value() { - var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); + await sut.SetAsync(appId, userId, "root.nested", 42, ct); + + var actual = await sut.GetAsync(appId, userId, ct); + + var expected = + new JsonObject().Add("root", + new JsonObject().Add("nested", 42)); - await ((IDeleter)sut).DeleteContributorAsync(app.Id, "user1", default); + Assert.Equal(expected.ToString(), actual.ToString()); - A.CallTo(() => grain.ClearAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, ct)) .MustHaveHappened(); } + + [Fact] + public async Task Should_not_write_state_if_value_not_changed() + { + await sut.SetAsync(appId, userId, "root.nested", 42, ct); + await sut.SetAsync(appId, userId, "root.nested", 42, ct); + + var actual = await sut.GetAsync(appId, userId, ct); + + var expected = + new JsonObject().Add("root", + new JsonObject().Add("nested", 42)); + + Assert.Equal(expected.ToString(), actual.ToString()); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, ct)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_remove_nested_value() + { + await sut.SetAsync(appId, userId, "root.nested", 42, ct); + + await sut.RemoveAsync(appId, userId, "root.nested", ct); + await sut.RemoveAsync(appId, userId, "key", ct); + + var actual = await sut.GetAsync(appId, userId, ct); + + var expected = + new JsonObject().Add("root", + new JsonObject()); + + Assert.Equal(expected.ToString(), actual.ToString()); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, ct)) + .MustHaveHappenedTwiceExactly(); + } + + [Fact] + public async Task Should_throw_exception_if_nested_not_an_object() + { + await sut.SetAsync(appId, userId, "root.nested", 42, ct); + + await Assert.ThrowsAsync(() => sut.SetAsync(appId, userId, "root.nested.value", 42, ct)); + } + + [Fact] + public async Task Should_do_nothing_if_deleting_and_nested_not_found() + { + await sut.RemoveAsync(appId, userId, "root.nested", ct); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Should_do_nothing_if_deleting_and_key_not_found() + { + await sut.RemoveAsync(appId, userId, "root", ct); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) + .MustNotHaveHappened(); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs index fe6a823ec..94ebef50c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/BackupAppsTests.cs @@ -84,9 +84,6 @@ namespace Squidex.Domain.Apps.Entities.Apps A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", default)) .MustHaveHappened(); - A.CallTo(() => appsIndex.RegisterAsync(appId, appName, default)) - .MustHaveHappened(); - A.CallTo(() => rebuilder.InsertManyAsync(A>.That.Is(appId), 1, default)) .MustHaveHappened(); } @@ -135,18 +132,18 @@ namespace Squidex.Domain.Apps.Entities.Apps { await sut.CleanupRestoreErrorAsync(appId); - A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", ct)) + A.CallTo(() => appsIndex.RemoveReservationAsync("Reservation", A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_writer_user_settings() + public async Task Should_write_user_settings() { var settings = new JsonObject(); var context = CreateBackupContext(); - A.CallTo(() => appUISettings.GetAsync(appId, null)) + A.CallTo(() => appUISettings.GetAsync(appId, null, ct)) .Returns(settings); await sut.BackupAsync(context, ct); @@ -167,7 +164,7 @@ namespace Squidex.Domain.Apps.Entities.Apps await sut.RestoreAsync(context, ct); - A.CallTo(() => appUISettings.SetAsync(appId, null, settings)) + A.CallTo(() => appUISettings.SetAsync(appId, null, settings, ct)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs index 3bd94c847..eb8d13288 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DefaultAppLogStoreTests.cs @@ -44,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Apps await ((IDeleter)sut).DeleteAppAsync(app, ct); - A.CallTo(() => requestLogStore.DeleteAsync($"^[a-z]-{app.Id}", ct)) + A.CallTo(() => requestLogStore.DeleteAsync($"^[a-z]-{app.Id}", A._)) .MustNotHaveHappened(); } @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Apps await sut.LogAsync(appId, default, ct); - A.CallTo(() => requestLogStore.LogAsync(A._, ct)) + A.CallTo(() => requestLogStore.LogAsync(A._, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs index 0c5f39f75..4c07081fb 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppCommandMiddlewareTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Assets; using Squidex.Domain.Apps.Entities.Apps.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -19,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { public class AppCommandMiddlewareTests : HandlerTestBase { - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly IAppImageStore appImageStore = A.Fake(); private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); @@ -43,11 +42,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject A.CallTo(() => contextProvider.Context) .Returns(requestContext); - sut = new AppCommandMiddleware(grainFactory, appImageStore, assetThumbnailGenerator, contextProvider); + sut = new AppCommandMiddleware(domainObjectFactory, appImageStore, assetThumbnailGenerator, contextProvider); } [Fact] - public async Task Should_replace_context_app_with_grain_result() + public async Task Should_replace_context_app_with_domain_object_result() { var result = A.Fake(); @@ -87,13 +86,13 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { command.AppId = appId; - var grain = A.Fake(); + var domainObject = A.Fake(); - A.CallTo(() => grain.ExecuteAsync(A._)) + A.CallTo(() => domainObject.ExecuteAsync(A._, A._)) .Returns(new CommandResult(command.AggregateId, 1, 0, result)); - A.CallTo(() => grainFactory.GetGrain(command.AggregateId.ToString(), null)) - .Returns(grain); + A.CallTo(() => domainObjectFactory.Create(command.AggregateId)) + .Returns(domainObject); return HandleAsync(sut, command); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs index f77a260a4..c6c72c14b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/AppDomainObjectTests.cs @@ -217,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var command = new ChangePlan { PlanId = planIdPaid }; - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, command.Referer)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, command.Referer, default)) .Returns(new PlanChangedResult()); await ExecuteCreateAsync(); @@ -252,7 +252,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject CreateEvent(new AppPlanChanged { PlanId = planIdPaid }) ); - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(A._, A>._, A._, A._)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(A._, A>._, A._, A._, A._)) .MustNotHaveHappened(); } @@ -261,7 +261,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var command = new ChangePlan { PlanId = planIdFree, FromCallback = true }; - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, command.Referer)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, command.Referer, default)) .Returns(new PlanChangedResult()); await ExecuteCreateAsync(); @@ -278,7 +278,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject CreateEvent(new AppPlanReset()) ); - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(A._, A>._, planIdFree, A._)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(A._, A>._, planIdFree, A._, A._)) .MustNotHaveHappened(); } @@ -287,10 +287,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var command = new ChangePlan { PlanId = planIdFree }; - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, command.Referer)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, command.Referer, default)) .Returns(new PlanChangedResult()); - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdFree, command.Referer)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdFree, command.Referer, default)) .Returns(new PlanChangedResult()); await ExecuteCreateAsync(); @@ -313,7 +313,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject { var command = new ChangePlan { PlanId = planIdPaid }; - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, command.Referer)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, command.Referer, default)) .Returns(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); await ExecuteCreateAsync(); @@ -336,7 +336,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject result.ShouldBeEquivalent(None.Value); - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, A._)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(Actor.Identifier, AppNamedId, planIdPaid, A._, default)) .MustNotHaveHappened(); } @@ -651,7 +651,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject CreateEvent(new AppDeleted()) ); - A.CallTo(() => appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, AppNamedId, null, A._)) + A.CallTo(() => appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, AppNamedId, null, A._, default)) .MustHaveHappened(); } @@ -707,7 +707,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject private async Task PublishAsync(AppCommand command) { - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await sut.ExecuteAsync(CreateCommand(command), default); return result.Payload; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs index 0bf87787d..52ac79a7d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppClientsTests.cs @@ -164,10 +164,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var app = A.Fake(); - A.CallTo(() => app.Clients) - .Returns(clients); - A.CallTo(() => app.Roles) - .Returns(roles); + A.CallTo(() => app.Clients).Returns(clients); + A.CallTo(() => app.Roles).Returns(roles); return app; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs index 0cdea58e4..b98b954cd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppContributorsTests.cs @@ -219,10 +219,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var app = A.Fake(); - A.CallTo(() => app.Contributors) - .Returns(contributors); - A.CallTo(() => app.Roles) - .Returns(roles); + A.CallTo(() => app.Contributors).Returns(contributors); + A.CallTo(() => app.Roles).Returns(roles); return app; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppLanguagesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppLanguagesTests.cs index d66e9b749..e879cafcc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppLanguagesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppLanguagesTests.cs @@ -136,8 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var app = A.Fake(); - A.CallTo(() => app.Languages) - .Returns(languages); + A.CallTo(() => app.Languages).Returns(languages); return app; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs index 7148a4b0e..800add091 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppRolesTests.cs @@ -178,12 +178,9 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var app = A.Fake(); - A.CallTo(() => app.Contributors) - .Returns(contributors); - A.CallTo(() => app.Clients) - .Returns(clients); - A.CallTo(() => app.Roles) - .Returns(roles); + A.CallTo(() => app.Contributors).Returns(contributors); + A.CallTo(() => app.Clients).Returns(clients); + A.CallTo(() => app.Roles).Returns(roles); return app; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs index 842f7b618..c9cb43d17 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppTests.cs @@ -252,8 +252,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var app = A.Fake(); - A.CallTo(() => app.Plan) - .Returns(plan); + A.CallTo(() => app.Plan).Returns(plan); return app; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppWorkflowTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppWorkflowTests.cs index e53a73d02..8c10a06dc 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppWorkflowTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/DomainObject/Guards/GuardAppWorkflowTests.cs @@ -215,8 +215,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.DomainObject.Guards { var app = A.Fake(); - A.CallTo(() => app.Workflows) - .Returns(workflows); + A.CallTo(() => app.Workflows).Returns(workflows); return app; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsCacheGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsCacheGrainTests.cs deleted file mode 100644 index 0bd4b57ed..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsCacheGrainTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Core; -using Squidex.Domain.Apps.Core.TestHelpers; -using Squidex.Domain.Apps.Entities.Apps.Repositories; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Apps.Indexes -{ - public class AppsCacheGrainTests - { - private readonly IGrainIdentity identity = A.Fake(); - private readonly IAppRepository appRepository = A.Fake(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly AppsCacheGrain sut; - - public AppsCacheGrainTests() - { - A.CallTo(() => identity.PrimaryKeyString) - .Returns(appId.ToString()); - - sut = new AppsCacheGrain(identity, appRepository); - } - - [Fact] - public async Task Should_not_reserve_name_if_already_used() - { - var ids1 = new Dictionary - { - ["name1"] = DomainId.NewGuid() - }; - - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name1"), default)) - .Returns(ids1); - - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name2"), default)) - .Returns(new Dictionary()); - - var token1 = await sut.ReserveAsync(DomainId.NewGuid(), "name1"); - var token2 = await sut.ReserveAsync(DomainId.NewGuid(), "name2"); - - Assert.Null(token1); - Assert.NotNull(token2); - } - - [Fact] - public async Task Should_provide_app_ids_from_repository_once() - { - var ids = new Dictionary - { - ["name1"] = DomainId.NewGuid(), - ["name2"] = DomainId.NewGuid() - }; - - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name1", "name2"), default)) - .Returns(ids); - - var result1 = await sut.GetAppIdsAsync(new[] { "name1", "name2" }); - var result2 = await sut.GetAppIdsAsync(new[] { "name1", "name2" }); - - Assert.Equal(ids.Values, result1); - Assert.Equal(ids.Values, result2); - - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name1", "name2"), default)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_load_pending_names() - { - var ids1 = new Dictionary - { - ["name1"] = DomainId.NewGuid(), - ["name2"] = DomainId.NewGuid() - }; - - var ids2 = new Dictionary - { - ["name4"] = DomainId.NewGuid() - }; - - // Name3 has not been found yet, but will not loaded again. - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name1", "name2", "name3"), default)) - .Returns(ids1); - - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name4"), default)) - .Returns(ids2); - - var result1 = await sut.GetAppIdsAsync(new[] { "name1", "name2", "name3" }); - var result2 = await sut.GetAppIdsAsync(new[] { "name3", "name4" }); - - Assert.Equal(ids1.Values, result1); - Assert.Equal(ids2.Values, result2); - } - - [Fact] - public async Task Should_add_id_to_loaded_result() - { - var ids = new Dictionary - { - ["name1"] = DomainId.NewGuid(), - ["name2"] = DomainId.NewGuid() - }; - - var newId = DomainId.NewGuid(); - - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name1", "name2"), default)) - .Returns(ids); - - await sut.GetAppIdsAsync(new[] { "name1", "name2" }); - await sut.AddAsync(newId, "new-name"); - - var result = await sut.GetAppIdsAsync(new[] { "new-name" }); - - Assert.Equal(Enumerable.Repeat(newId, 1), result); - - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name1", "name2"), default)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_remove_id_from_loaded_result() - { - var ids = new Dictionary - { - ["name1"] = DomainId.NewGuid(), - ["name2"] = DomainId.NewGuid() - }; - - var newId = DomainId.NewGuid(); - - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name1", "name2"), default)) - .Returns(ids); - - await sut.GetAppIdsAsync(new[] { "name1", "name2" }); - await sut.RemoveAsync(ids.Values.ElementAt(1)); - - var result = await sut.GetAppIdsAsync(new[] { "name1", "name2" }); - - Assert.Equal(ids.Values.Take(1), result); - - A.CallTo(() => appRepository.QueryIdsAsync(A>.That.Is("name1", "name2"), default)) - .MustHaveHappenedOnceExactly(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs index d2fa84491..c46c0983e 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs @@ -9,244 +9,177 @@ using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Orleans; using Squidex.Caching; -using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Apps.Commands; -using Squidex.Domain.Apps.Entities.Apps.DomainObject; using Squidex.Domain.Apps.Entities.Apps.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Security; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; using Squidex.Infrastructure.Validation; using Xunit; namespace Squidex.Domain.Apps.Entities.Apps.Indexes { - public sealed class AppsIndexTests + public class AppsIndexTests { - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState state; private readonly IAppRepository appRepository = A.Fake(); - private readonly IAppsCacheGrain cache = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly string userId = "user1"; - private readonly string clientId = "client1"; private readonly AppsIndex sut; public AppsIndexTests() { - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(cache); + state = new TestState("Apps"); + + ct = cts.Token; var replicatedCache = new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(A.Fake>()), Options.Create(new ReplicatedCacheOptions { Enable = true })); - sut = new AppsIndex(appRepository, grainFactory, replicatedCache); - } - - [Fact] - public async Task Should_resolve_all_apps_from_user_permissions() - { - var (expected, _) = CreateApp(); - - A.CallTo(() => cache.GetAppIdsAsync(A.That.Is(appId.Name))) - .Returns(new List { appId.Id }); - - var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}")); - - Assert.Same(expected, actual[0]); - } - - [Fact] - public async Task Should_resolve_all_apps_from_user() - { - var (expected, _) = CreateApp(); - - A.CallTo(() => appRepository.QueryIdsAsync(userId, default)) - .Returns(new Dictionary { [appId.Name] = appId.Id }); - - var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty); - - Assert.Same(expected, actual[0]); - } - - [Fact] - public async Task Should_resolve_combined_apps() - { - var (expected, _) = CreateApp(); - - A.CallTo(() => cache.GetAppIdsAsync(A.That.Is(appId.Name))) - .Returns(new List { appId.Id }); - - A.CallTo(() => appRepository.QueryIdsAsync(userId, default)) - .Returns(new Dictionary { [appId.Name] = appId.Id }); - - var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}")); - - Assert.Single(actual); - Assert.Same(expected, actual[0]); + sut = new AppsIndex(appRepository, replicatedCache, state.PersistenceFactory); } [Fact] public async Task Should_resolve_app_by_name() { - var (expected, _) = CreateApp(); + var expected = CreateApp(); - A.CallTo(() => cache.GetAppIdsAsync(A.That.Is(appId.Name))) - .Returns(new List { appId.Id }); + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(expected); - var actual1 = await sut.GetAppAsync(appId.Name, false); - var actual2 = await sut.GetAppAsync(appId.Name, false); + var actual1 = await sut.GetAppAsync(appId.Name, false, ct); + var actual2 = await sut.GetAppAsync(appId.Name, false, ct); Assert.Same(expected, actual1); Assert.Same(expected, actual2); - A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) - .MustHaveHappenedTwiceExactly(); - - A.CallTo(() => cache.GetAppIdsAsync(A._)) + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) .MustHaveHappenedTwiceExactly(); } [Fact] public async Task Should_resolve_app_by_name_and_id_if_cached_before() { - var (expected, _) = CreateApp(); + var expected = CreateApp(); - A.CallTo(() => cache.GetAppIdsAsync(A.That.Is(appId.Name))) - .Returns(new List { appId.Id }); + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(expected); - var actual1 = await sut.GetAppAsync(appId.Name, true); - var actual2 = await sut.GetAppAsync(appId.Name, true); - var actual3 = await sut.GetAppAsync(appId.Id, true); + var actual1 = await sut.GetAppAsync(appId.Name, true, ct); + var actual2 = await sut.GetAppAsync(appId.Name, true, ct); + var actual3 = await sut.GetAppAsync(appId.Id, true, ct); Assert.Same(expected, actual1); Assert.Same(expected, actual2); Assert.Same(expected, actual3); - A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) - .MustHaveHappenedOnceExactly(); - - A.CallTo(() => cache.GetAppIdsAsync(A._)) + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) .MustHaveHappenedOnceExactly(); } [Fact] public async Task Should_resolve_app_by_id() { - var (expected, _) = CreateApp(); + var expected = CreateApp(); + + A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) + .Returns(expected); - var actual1 = await sut.GetAppAsync(appId.Id, false); - var actual2 = await sut.GetAppAsync(appId.Id, false); + var actual1 = await sut.GetAppAsync(appId.Id, false, ct); + var actual2 = await sut.GetAppAsync(appId.Id, false, ct); Assert.Same(expected, actual1); Assert.Same(expected, actual2); - A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) + A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) .MustHaveHappenedTwiceExactly(); - - A.CallTo(() => cache.GetAppIdsAsync(A._)) - .MustNotHaveHappened(); } [Fact] public async Task Should_resolve_app_by_id_and_name_if_cached_before() { - var (expected, _) = CreateApp(); + var expected = CreateApp(); + + A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) + .Returns(expected); - var actual1 = await sut.GetAppAsync(appId.Id, true); - var actual2 = await sut.GetAppAsync(appId.Id, true); - var actual3 = await sut.GetAppAsync(appId.Name, true); + var actual1 = await sut.GetAppAsync(appId.Id, true, ct); + var actual2 = await sut.GetAppAsync(appId.Id, true, ct); + var actual3 = await sut.GetAppAsync(appId.Name, true, ct); Assert.Same(expected, actual1); Assert.Same(expected, actual2); Assert.Same(expected, actual3); - A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) + A.CallTo(() => appRepository.FindAsync(appId.Id, ct)) .MustHaveHappenedOnceExactly(); - - A.CallTo(() => cache.GetAppIdsAsync(A._)) - .MustNotHaveHappened(); } [Fact] - public async Task Should_return_null_if_app_deleted() + public async Task Should_resolve_all_apps_from_user_permissions() { - CreateApp(isArchived: true); + var expected = CreateApp(); - var actual1 = await sut.GetAppAsync(appId.Id, true); - var actual2 = await sut.GetAppAsync(appId.Id, true); + A.CallTo(() => appRepository.QueryAllAsync(userId, A>.That.Is(appId.Name), ct)) + .Returns(new List { expected }); - Assert.Null(actual1); - Assert.Null(actual2); + var actual = await sut.GetAppsForUserAsync(userId, new PermissionSet($"squidex.apps.{appId.Name}"), ct); + + Assert.Same(expected, actual[0]); } [Fact] - public async Task Should_return_null_if_app_not_created() + public async Task Should_resolve_all_apps_from_user() { - CreateApp(EtagVersion.Empty); + var expected = CreateApp(); - var actual1 = await sut.GetAppAsync(appId.Id, true); - var actual2 = await sut.GetAppAsync(appId.Id, true); + A.CallTo(() => appRepository.QueryAllAsync(userId, A>.That.IsEmpty(), ct)) + .Returns(new List { expected }); - Assert.Null(actual1); - Assert.Null(actual2); + var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty, ct); + + Assert.Same(expected, actual[0]); } [Fact] - public async Task Should_add_app_to_indexes_if_creating() + public async Task Should_return_empty_apps_if_app_not_created() { - var token = RandomHash.Simple(); - - A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) - .Returns(token); - - var command = Create(appId.Name); - - var context = - new CommandContext(command, commandBus) - .Complete(); + var expected = CreateApp(EtagVersion.Empty); - await sut.HandleAsync(context); + A.CallTo(() => appRepository.QueryAllAsync(userId, A>.That.IsEmpty(), ct)) + .Returns(new List { expected }); - A.CallTo(() => cache.AddAsync(appId.Id, appId.Name)) - .MustHaveHappened(); + var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty, ct); - A.CallTo(() => cache.RemoveReservationAsync(token)) - .MustHaveHappened(); + Assert.Empty(actual); } [Fact] - public async Task Should_clear_reservation_if_app_creation_failed() + public async Task Should_return_empty_apps_if_app_deleted() { - var token = RandomHash.Simple(); - - A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) - .Returns(token); + var expected = CreateApp(0, true); - var command = CreateFromClient(appId.Name); - - var context = - new CommandContext(command, commandBus); + A.CallTo(() => appRepository.QueryAllAsync(userId, A>.That.IsEmpty(), ct)) + .Returns(new List { expected }); - await sut.HandleAsync(context); + var actual = await sut.GetAppsForUserAsync(userId, PermissionSet.Empty, ct); - A.CallTo(() => cache.AddAsync(A._, A._)) - .MustNotHaveHappened(); - - A.CallTo(() => cache.RemoveReservationAsync(token)) - .MustHaveHappened(); + Assert.Empty(actual); } [Fact] - public async Task Should_not_add_to_indexes_if_name_is_reserved() + public async Task Should_take_and_remove_reservation_if_created() { - A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) - .Returns(Task.FromResult(null)); + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(Task.FromResult(null)); var command = Create(appId.Name); @@ -254,136 +187,115 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes new CommandContext(command, commandBus) .Complete(); - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + NameReservation? madeReservation = null; - A.CallTo(() => cache.AddAsync(A._, A._)) - .MustNotHaveHappened(); + await sut.HandleAsync(context, (c, ct) => + { + madeReservation = state.Snapshot.Reservations.FirstOrDefault(); - A.CallTo(() => cache.RemoveReservationAsync(A._)) - .MustNotHaveHappened(); + return Task.CompletedTask; + }, ct); + + Assert.Empty(state.Snapshot.Reservations); + + Assert.Equal(appId.Id, madeReservation?.Id); + Assert.Equal(appId.Name, madeReservation?.Name); } [Fact] - public async Task Should_update_index_with_result_if_app_is_updated() + public async Task Should_clear_reservation_if_app_creation_failed() { - var (app, appGrain) = CreateApp(); + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(Task.FromResult(null)); - var command = new UpdateApp { AppId = appId }; + var command = Create(appId.Name); var context = new CommandContext(command, commandBus) - .Complete(app); + .Complete(); - await sut.HandleAsync(context); + NameReservation? madeReservation = null; - A.CallTo(() => appGrain.GetStateAsync()) - .MustNotHaveHappened(); + await Assert.ThrowsAnyAsync(() => sut.HandleAsync(context, (c, ct) => + { + madeReservation = state.Snapshot.Reservations.FirstOrDefault(); + + throw new InvalidOperationException(); + }, ct)); + + Assert.Empty(state.Snapshot.Reservations); + + Assert.Equal(appId.Id, madeReservation?.Id); + Assert.Equal(appId.Name, madeReservation?.Name); } [Fact] - public async Task Should_remove_app_from_indexes_if_app_gets_deleted() + public async Task Should_not_create_app_if_name_is_reserved() { - CreateApp(isArchived: true); + state.Snapshot.Reservations.Add(new NameReservation(RandomHash.Simple(), appId.Name, DomainId.NewGuid())); + + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(Task.FromResult(null)); - var command = new DeleteApp { AppId = appId }; + var command = Create(appId.Name); var context = new CommandContext(command, commandBus) .Complete(); - await sut.HandleAsync(context); - - A.CallTo(() => cache.RemoveAsync(appId.Id)) - .MustHaveHappened(); + await Assert.ThrowsAsync(() => sut.HandleAsync(context, ct)); } [Fact] - public async Task Should_forward_reserveration() + public async Task Should_not_create_app_if_name_is_taken() { - A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) - .Returns("token"); - - var token = await sut.ReserveAsync(appId.Id, appId.Name); + A.CallTo(() => appRepository.FindAsync(appId.Name, ct)) + .Returns(CreateApp()); - Assert.Equal("token", token); + var command = Create(appId.Name); - A.CallTo(() => cache.ReserveAsync(appId.Id, appId.Name)) - .MustHaveHappened(); - } + var context = + new CommandContext(command, commandBus) + .Complete(); - [Fact] - public async Task Should_forward_remove_reservation() - { - await sut.RemoveReservationAsync("token"); + await Assert.ThrowsAsync(() => sut.HandleAsync(context, ct)); - A.CallTo(() => cache.RemoveReservationAsync("token")) - .MustHaveHappened(); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) + .MustNotHaveHappened(); } [Fact] - public async Task Should_forward_registation() + public async Task Should_not_make_an_update_for_other_command() { - await sut.RegisterAsync(appId.Id, appId.Name); + var app = CreateApp(); - A.CallTo(() => cache.AddAsync(appId.Id, appId.Name)) - .MustHaveHappened(); - } - - private (IAppEntity, IAppGrain) CreateApp(long version = 0, bool fromClient = false, bool isArchived = false) - { - var app = A.Fake(); - - A.CallTo(() => app.Id) - .Returns(appId.Id); - A.CallTo(() => app.Name) - .Returns(appId.Name); - A.CallTo(() => app.Version) - .Returns(version); - A.CallTo(() => app.IsDeleted) - .Returns(isArchived); - A.CallTo(() => app.Contributors) - .Returns(AppContributors.Empty.Assign(userId, Role.Owner)); - - if (fromClient) - { - A.CallTo(() => app.CreatedBy) - .Returns(ClientActor()); - } - else - { - A.CallTo(() => app.CreatedBy) - .Returns(UserActor()); - } - - var appGrain = A.Fake(); + var command = new UpdateApp { AppId = appId }; - A.CallTo(() => appGrain.GetStateAsync()) - .Returns(app); + var context = + new CommandContext(command, commandBus) + .Complete(app); - A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) - .Returns(appGrain); + await sut.HandleAsync(context, ct); - return (app, appGrain); + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) + .MustNotHaveHappened(); } private CreateApp Create(string name) { - return new CreateApp { AppId = appId.Id, Name = name, Actor = UserActor() }; + return new CreateApp { AppId = appId.Id, Name = name }; } - private CreateApp CreateFromClient(string name) + private IAppEntity CreateApp(long version = 0, bool isDeleted = false) { - return new CreateApp { AppId = appId.Id, Name = name, Actor = ClientActor() }; - } + var app = A.Fake(); - private RefToken UserActor() - { - return RefToken.User(userId); - } + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + A.CallTo(() => app.Version).Returns(version); + A.CallTo(() => app.IsDeleted).Returns(isDeleted); - private RefToken ClientActor() - { - return RefToken.Client(clientId); + return app; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEventConsumerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEventConsumerTests.cs index f2d001e91..44b4798ca 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEventConsumerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InvitationEventConsumerTests.cs @@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation private void MustNotResolveUser() { - A.CallTo(() => userResolver.FindByIdAsync(A._, default)) + A.CallTo(() => userResolver.FindByIdAsync(A._, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs index dee12ed2a..e42e6958b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Invitation/InviteUserCommandMiddlewareTests.cs @@ -18,6 +18,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation { public class InviteUserCommandMiddlewareTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IUserResolver userResolver = A.Fake(); private readonly IAppEntity app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); private readonly ICommandBus commandBus = A.Fake(); @@ -25,6 +27,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation public InviteUserCommandMiddlewareTests() { + ct = cts.Token; + sut = new InviteUserCommandMiddleware(userResolver); } @@ -39,15 +43,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation var user = UserMocks.User("123", command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, default)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) .Returns((user, true)); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); Assert.Same(context.Result().App, app); Assert.Equal(user.Id, command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, default)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) .MustHaveHappened(); } @@ -62,15 +66,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation var user = UserMocks.User("123", command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, default)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) .Returns((user, false)); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); Assert.Same(context.Result(), app); Assert.Equal(user.Id, command.ContributorId); - A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, default)) + A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(user.Email, true, ct)) .MustHaveHappened(); } @@ -83,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation new CommandContext(command, commandBus) .Complete(app); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(A._, A._, A._)) .MustNotHaveHappened(); @@ -98,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Invitation new CommandContext(command, commandBus) .Complete(app); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); A.CallTo(() => userResolver.CreateUserIfNotExistsAsync(A._, A._, A._)) .MustNotHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/RestrictAppsCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/RestrictAppsCommandMiddlewareTests.cs index 5cc50ab9c..a34b50bc6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/RestrictAppsCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/RestrictAppsCommandMiddlewareTests.cs @@ -20,6 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { public sealed class RestrictAppsCommandMiddlewareTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IUserResolver userResolver = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); private readonly RestrictAppsOptions options = new RestrictAppsOptions(); @@ -27,6 +29,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans public RestrictAppsCommandMiddlewareTests() { + ct = cts.Token; + sut = new RestrictAppsCommandMiddleware(Options.Create(options), userResolver); } @@ -52,17 +56,17 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans A.CallTo(() => user.Claims) .Returns(Enumerable.Repeat(new Claim(SquidexClaimTypes.TotalApps, "5"), 1).ToList()); - A.CallTo(() => userResolver.FindByIdAsync(userId, default)) + A.CallTo(() => userResolver.FindByIdAsync(userId, ct)) .Returns(user); var isNextCalled = false; - await Assert.ThrowsAsync(() => sut.HandleAsync(commandContext, x => + await Assert.ThrowsAsync(() => sut.HandleAsync(commandContext, (c, ct) => { isNextCalled = true; return Task.CompletedTask; - })); + }, ct)); Assert.False(isNextCalled); } @@ -89,15 +93,15 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans A.CallTo(() => user.Claims) .Returns(Enumerable.Repeat(new Claim(SquidexClaimTypes.TotalApps, "5"), 1).ToList()); - A.CallTo(() => userResolver.FindByIdAsync(userId, default)) + A.CallTo(() => userResolver.FindByIdAsync(userId, ct)) .Returns(user); - await sut.HandleAsync(commandContext, x => + await sut.HandleAsync(commandContext, (c, ct) => { - x.Complete(true); + c.Complete(true); return Task.CompletedTask; - }); + }, ct); A.CallTo(() => userResolver.SetClaimAsync(userId, SquidexClaimTypes.TotalApps, "6", true, default)) .MustHaveHappened(); @@ -115,12 +119,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans options.MaximumNumberOfApps = 10; - await sut.HandleAsync(commandContext, x => + await sut.HandleAsync(commandContext, (c, ct) => { - x.Complete(true); + c.Complete(true); return Task.CompletedTask; - }); + }, ct); A.CallTo(() => userResolver.FindByIdAsync(A._, A._)) .MustNotHaveHappened(); @@ -138,12 +142,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans options.MaximumNumberOfApps = 0; - await sut.HandleAsync(commandContext, x => + await sut.HandleAsync(commandContext, (c, ct) => { - x.Complete(true); + c.Complete(true); return Task.CompletedTask; - }); + }, ct); A.CallTo(() => userResolver.FindByIdAsync(A._, A._)) .MustNotHaveHappened(); @@ -161,12 +165,12 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans options.MaximumNumberOfApps = 10; - await sut.HandleAsync(commandContext, x => + await sut.HandleAsync(commandContext, (c, ct) => { - x.Complete(true); + c.Complete(true); return Task.CompletedTask; - }); + }, ct); A.CallTo(() => userResolver.FindByIdAsync(A._, A._)) .MustNotHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs index 83190e4e9..4da7957fd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageGateTests.cs @@ -6,24 +6,24 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Core.Apps; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.UsageTracking; +using Squidex.Messaging; using Xunit; namespace Squidex.Domain.Apps.Entities.Apps.Plans { public class UsageGateTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IAppEntity appEntity; private readonly IAppLimitsPlan appPlan = A.Fake(); private readonly IAppPlansProvider appPlansProvider = A.Fake(); private readonly IApiUsageTracker usageTracker = A.Fake(); - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IUsageNotifierGrain usageNotifierGrain = A.Fake(); + private readonly IMessageBus messaging = A.Fake(); private readonly string clientId = Guid.NewGuid().ToString(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly UsageGate sut; @@ -36,8 +36,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { appEntity = Mocks.App(appId); - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(usageNotifierGrain); + ct = cts.Token; A.CallTo(() => appPlansProvider.GetPlan(null)) .Returns(appPlan); @@ -51,10 +50,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans A.CallTo(() => appPlan.BlockingApiCalls) .ReturnsLazily(x => apiCallsBlocking); - A.CallTo(() => usageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, default)) + A.CallTo(() => usageTracker.GetMonthCallsAsync(appId.Id.ToString(), today, A._, ct)) .ReturnsLazily(x => Task.FromResult(apiCallsCurrent)); - sut = new UsageGate(appPlansProvider, usageTracker, grainFactory); + sut = new UsageGate(appPlansProvider, usageTracker, messaging); } [Fact] @@ -67,11 +66,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans apiCallsBlocking = 1600; apiCallsMax = 1600; - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today); + var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); Assert.True(isBlocked); - A.CallTo(() => usageNotifierGrain.NotifyAsync(A.Ignored)) + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) .MustHaveHappened(); } @@ -82,11 +81,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans apiCallsBlocking = 600; apiCallsMax = 600; - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today); + var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); Assert.True(isBlocked); - A.CallTo(() => usageNotifierGrain.NotifyAsync(A.Ignored)) + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) .MustHaveHappened(); } @@ -97,11 +96,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans apiCallsBlocking = 1600; apiCallsMax = 1600; - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today); + var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); Assert.False(isBlocked); - A.CallTo(() => usageNotifierGrain.NotifyAsync(A.Ignored)) + A.CallTo(() => messaging.PublishAsync(A._, null, A._)) .MustNotHaveHappened(); } @@ -112,11 +111,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans apiCallsBlocking = 5000; apiCallsMax = 3000; - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today); + var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); Assert.False(isBlocked); - A.CallTo(() => usageNotifierGrain.NotifyAsync(A.Ignored)) + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) .MustHaveHappened(); } @@ -127,11 +126,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans apiCallsBlocking = 5000; apiCallsMax = 0; - var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today); + var isBlocked = await sut.IsBlockedAsync(appEntity, clientId, today, ct); Assert.False(isBlocked); - A.CallTo(() => usageNotifierGrain.NotifyAsync(A.Ignored)) + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) .MustHaveHappened(); } @@ -142,10 +141,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans apiCallsBlocking = 5000; apiCallsMax = 3000; - await sut.IsBlockedAsync(appEntity, clientId, today); - await sut.IsBlockedAsync(appEntity, clientId, today); + await sut.IsBlockedAsync(appEntity, clientId, today, ct); + await sut.IsBlockedAsync(appEntity, clientId, today, ct); - A.CallTo(() => usageNotifierGrain.NotifyAsync(A.Ignored)) + A.CallTo(() => messaging.PublishAsync(A._, null, ct)) .MustHaveHappenedOnceExactly(); } @@ -158,10 +157,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans apiCallsBlocking = 5000; apiCallsMax = 3000; - await sut.IsBlockedAsync(appEntity, clientId, today); - await sut.IsBlockedAsync(appEntity, clientId, today); + await sut.IsBlockedAsync(appEntity, clientId, today, ct); + await sut.IsBlockedAsync(appEntity, clientId, today, ct); - A.CallTo(() => usageNotifierGrain.NotifyAsync(A.Ignored)) + A.CallTo(() => messaging.PublishAsync(A._, null, A._)) .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierWorkerTest.cs similarity index 80% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierWorkerTest.cs index abe7a7828..0ff28d58a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Plans/UsageNotifierWorkerTest.cs @@ -7,26 +7,24 @@ using FakeItEasy; using NodaTime; -using Orleans.Core; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Notifications; -using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.TestHelpers; using Squidex.Shared.Users; using Xunit; namespace Squidex.Domain.Apps.Entities.Apps.Plans { - public class UsageNotifierGrainTests + public class UsageNotifierWorkerTest { + private readonly TestState state = new TestState("Default"); private readonly IClock clock = A.Fake(); - private readonly IGrainIdentity identity = A.Fake(); - private readonly IGrainState state = A.Fake>(); private readonly INotificationSender notificationSender = A.Fake(); private readonly IUserResolver userResolver = A.Fake(); - private readonly UsageNotifierGrain sut; + private readonly UsageNotifierWorker sut; private Instant time = SystemClock.Instance.GetCurrentInstant(); - public UsageNotifierGrainTests() + public UsageNotifierWorkerTest() { A.CallTo(() => clock.GetCurrentInstant()) .ReturnsLazily(() => time); @@ -34,7 +32,10 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans A.CallTo(() => notificationSender.IsActive) .Returns(true); - sut = new UsageNotifierGrain(identity, state, notificationSender, userResolver, clock); + sut = new UsageNotifierWorker(state.PersistenceFactory, notificationSender, userResolver) + { + Clock = clock + }; } [Fact] @@ -43,7 +44,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans SetupUser("1", null); SetupUser("2", null); - var notification = new UsageNotification + var message = new UsageTrackingCheck { AppName = "my-app", Usage = 1000, @@ -51,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans Users = new[] { "1", "2" } }; - await sut.NotifyAsync(notification); + await sut.HandleAsync(message, default); A.CallTo(() => notificationSender.SendUsageAsync(A._, "my-app", 1000, 3000)) .MustNotHaveHappened(); @@ -64,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans var user2 = SetupUser("2", "user2@email.com"); var user3 = SetupUser("3", null); - var notification = new UsageNotification + var message = new UsageTrackingCheck { AppName = "my-app", Usage = 1000, @@ -72,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans Users = new[] { "1", "2", "3" } }; - await sut.NotifyAsync(notification); + await sut.HandleAsync(message, default); A.CallTo(() => notificationSender.SendUsageAsync(user1!, "my-app", 1000, 3000)) .MustHaveHappened(); @@ -89,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { var user = SetupUser("1", "user1@email.com"); - var notification = new UsageNotification + var message = new UsageTrackingCheck { AppName = "my-app", Usage = 1000, @@ -97,8 +98,8 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans Users = new[] { "1" } }; - await sut.NotifyAsync(notification); - await sut.NotifyAsync(notification); + await sut.HandleAsync(message, default); + await sut.HandleAsync(message, default); A.CallTo(() => notificationSender.SendUsageAsync(user!, "my-app", 1000, 3000)) .MustHaveHappenedOnceExactly(); @@ -109,7 +110,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { var user = SetupUser("1", "user1@email.com"); - var notification = new UsageNotification + var message = new UsageTrackingCheck { AppName = "my-app", Usage = 1000, @@ -117,11 +118,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans Users = new[] { "1" } }; - await sut.NotifyAsync(notification); + await sut.HandleAsync(message, default); time = time.Plus(Duration.FromDays(3)); - await sut.NotifyAsync(notification); + await sut.HandleAsync(message, default); A.CallTo(() => notificationSender.SendUsageAsync(user!, "my-app", 1000, 3000)) .MustHaveHappenedTwiceExactly(); @@ -137,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans { var user = SetupUser("1", "user1@email.com"); - var notification = new UsageNotification + var message = new UsageTrackingCheck { AppName = "my-app", Usage = 1000, @@ -145,11 +146,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Plans Users = new[] { "1" } }; - await sut.NotifyAsync(notification); + await sut.HandleAsync(message, default); time = time.Plus(Duration.FromHours(hours)); - await sut.NotifyAsync(notification); + await sut.HandleAsync(message, default); A.CallTo(() => notificationSender.SendUsageAsync(user!, "my-app", 1000, 3000)) .MustHaveHappenedOnceExactly(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs index 03f28743b..742d15d95 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetChangedTriggerHandlerTests.cs @@ -23,6 +23,8 @@ namespace Squidex.Domain.Apps.Entities.Assets { public class AssetChangedTriggerHandlerTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IScriptEngine scriptEngine = A.Fake(); private readonly IAssetLoader assetLoader = A.Fake(); private readonly IAssetRepository assetRepository = A.Fake(); @@ -30,6 +32,8 @@ namespace Squidex.Domain.Apps.Entities.Assets public AssetChangedTriggerHandlerTests() { + ct = cts.Token; + A.CallTo(() => scriptEngine.Evaluate(A._, "true", default)) .Returns(true); @@ -76,14 +80,14 @@ namespace Squidex.Domain.Apps.Entities.Assets { var ctx = Context(); - A.CallTo(() => assetRepository.StreamAll(ctx.AppId.Id, default)) + A.CallTo(() => assetRepository.StreamAll(ctx.AppId.Id, ct)) .Returns(new List { new AssetEntity(), new AssetEntity() }.ToAsyncEnumerable()); - var result = await sut.CreateSnapshotEventsAsync(ctx, default).ToListAsync(); + var result = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); var typed = result.OfType().ToList(); @@ -101,10 +105,10 @@ namespace Squidex.Domain.Apps.Entities.Assets var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - A.CallTo(() => assetLoader.GetAsync(ctx.AppId.Id, @event.AssetId, 12)) + A.CallTo(() => assetLoader.GetAsync(ctx.AppId.Id, @event.AssetId, 12, ct)) .Returns(new AssetEntity()); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); var enrichedEvent = result.Single() as EnrichedAssetEvent; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupProcessTests.cs similarity index 71% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupProcessTests.cs index 2820db8e9..e44492fb3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCleanupProcessTests.cs @@ -11,26 +11,26 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Assets { - public class AssetCleanupGrainTests + public class AssetCleanupProcessTests { private readonly ITusExpirationStore expirationStore = A.Fake(); - private readonly AssetCleanupGrain sut; + private readonly AssetCleanupProcess sut; - public AssetCleanupGrainTests() + public AssetCleanupProcessTests() { - sut = new AssetCleanupGrain(expirationStore); + sut = new AssetCleanupProcess(expirationStore); } [Fact] - public async Task Should_do_nothing_on_activate() + public async Task Should_stop_when_start_not_called() { - await sut.ActivateAsync(); + await sut.StopAsync(default); } [Fact] public async Task Should_call_expiration_store_when_reminder_invoked() { - await sut.ReceiveReminder("Reminder", default); + await sut.CleanupAsync(default); A.CallTo(() => expirationStore.RemoveExpiredFilesAsync(default)) .MustHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs index 1e5ac45a5..a751ec484 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetPermanentDeleterTests.cs @@ -83,7 +83,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.On(Envelope.Create(@event).SetRestored()); - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A._)) .MustNotHaveHappened(); } @@ -94,7 +94,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.On(Envelope.Create(@event)); - A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, default)) + A.CallTo(() => assetFiletore.DeleteAsync(appId.Id, @event.AssetId, A._)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs index ea1ec01f2..e6d329ea6 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetTagsDeleterTests.cs @@ -42,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.DeleteAppAsync(app, ct); - A.CallTo(() => tagService.ClearAsync(app.Id, TagGroups.Assets)) + A.CallTo(() => tagService.ClearAsync(app.Id, TagGroups.Assets, ct)) .MustHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs index d04ffa620..c1835002d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/BackupAssetsTests.cs @@ -52,7 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var context = CreateBackupContext(); - A.CallTo(() => tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets)) + A.CallTo(() => tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets, default)) .Returns(tags); await sut.BackupAsync(context, ct); @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Assets A.CallTo(() => context.Writer.WriteJsonAsync(A._, tags.Tags, ct)) .MustHaveHappened(); - A.CallTo(() => context.Writer.WriteJsonAsync(A._, tags.Alias!, ct)) + A.CallTo(() => context.Writer.WriteJsonAsync(A._, tags.Alias!, A._)) .MustNotHaveHappened(); } @@ -78,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var context = CreateBackupContext(); - A.CallTo(() => tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets)) + A.CallTo(() => tagService.GetExportableTagsAsync(context.AppId, TagGroups.Assets, default)) .Returns(tags); await sut.BackupAsync(context, ct); @@ -105,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RestoreAsync(context, ct); - A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A.That.Matches(x => x.Tags == tags))) + A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A.That.Matches(x => x.Tags == tags), default)) .MustHaveHappened(); } @@ -124,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RestoreAsync(context, ct); - A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A.That.Matches(x => x.Alias == alias))) + A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A.That.Matches(x => x.Alias == alias), default)) .MustHaveHappened(); } @@ -143,13 +143,13 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RestoreAsync(context, ct); - A.CallTo(() => context.Reader.ReadJsonAsync>(A._, ct)) + A.CallTo(() => context.Reader.ReadJsonAsync>(A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => context.Reader.ReadJsonAsync>(A._, ct)) + A.CallTo(() => context.Reader.ReadJsonAsync>(A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A.That.Matches(x => x.Alias == alias))) + A.CallTo(() => tagService.RebuildTagsAsync(appId.Id, TagGroups.Assets, A.That.Matches(x => x.Alias == alias), A._)) .MustNotHaveHappened(); } @@ -277,7 +277,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RestoreEventAsync(AppEvent(@event), context, ct); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, ct)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, assetId, version, null, assetStream, true, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs index 9a920429b..b63473bf5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetCommandMiddlewareTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Assets; using Squidex.Domain.Apps.Entities.Assets.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -18,12 +17,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { public class AssetCommandMiddlewareTests : HandlerTestBase { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectCache domainObjectCache = A.Fake(); + private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); private readonly IAssetEnricher assetEnricher = A.Fake(); private readonly IAssetFileStore assetFileStore = A.Fake(); private readonly IAssetMetadataSource assetMetadataSource = A.Fake(); private readonly IAssetQueryService assetQuery = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); - private readonly IGrainFactory grainFactory = A.Fake(); private readonly DomainId assetId = DomainId.NewGuid(); private readonly AssetFile file = new NoopAssetFile(); private readonly Context requestContext; @@ -40,6 +42,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject public AssetCommandMiddlewareTests() { + ct = cts.Token; + file = new NoopAssetFile(); requestContext = Context.Anonymous(Mocks.App(AppNamedId)); @@ -47,10 +51,12 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject A.CallTo(() => contextProvider.Context) .Returns(requestContext); - A.CallTo(() => assetQuery.FindByHashAsync(A._, A._, A._, A._, A._)) + A.CallTo(() => assetQuery.FindByHashAsync(A._, A._, A._, A._, ct)) .Returns(Task.FromResult(null)); - sut = new AssetCommandMiddleware(grainFactory, + sut = new AssetCommandMiddleware( + domainObjectFactory, + domainObjectCache, assetEnricher, assetFileStore, assetQuery, @@ -86,7 +92,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject var enriched = new AssetEntity(); - A.CallTo(() => assetEnricher.EnrichAsync(result, requestContext, A._)) + A.CallTo(() => assetEnricher.EnrichAsync(result, requestContext, ct)) .Returns(enriched); var context = @@ -213,11 +219,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private void AssertAssetHasBeenUploaded(long fileVersion) { - A.CallTo(() => assetFileStore.UploadAsync(A._, A._, default)) + A.CallTo(() => assetFileStore.UploadAsync(A._, A._, ct)) .MustHaveHappened(); - A.CallTo(() => assetFileStore.CopyAsync(A._, AppId, assetId, fileVersion, null, default)) + + A.CallTo(() => assetFileStore.CopyAsync(A._, AppId, assetId, fileVersion, null, ct)) .MustHaveHappened(); - A.CallTo(() => assetFileStore.DeleteAsync(A._, default)) + + A.CallTo(() => assetFileStore.DeleteAsync(A._, ct)) .MustHaveHappened(); } @@ -229,13 +237,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject FileSize = fileSize }; - A.CallTo(() => assetQuery.FindByHashAsync(requestContext, A._, fileName, fileSize, A._)) + A.CallTo(() => assetQuery.FindByHashAsync(requestContext, A._, fileName, fileSize, ct)) .Returns(duplicate); } private void AssertMetadataEnriched() { - A.CallTo(() => assetMetadataSource.EnhanceAsync(A._)) + A.CallTo(() => assetMetadataSource.EnhanceAsync(A._, ct)) .MustHaveHappened(); } @@ -245,15 +253,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject CreateCommand(command); - var grain = A.Fake(); + var domainObject = A.Fake(); - A.CallTo(() => grain.ExecuteAsync(A._)) + A.CallTo(() => domainObject.ExecuteAsync(A._, ct)) .Returns(new CommandResult(command.AggregateId, 1, 0, result)); - A.CallTo(() => grainFactory.GetGrain(command.AggregateId.ToString(), null)) - .Returns(grain); + A.CallTo(() => domainObjectFactory.Create(command.AggregateId)) + .Returns(domainObject); - return HandleAsync(sut, command); + return HandleAsync(sut, command, ct); } private IAssetEntity CreateAsset(long fileVersion = 0) diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectGrainTests.cs deleted file mode 100644 index ccbce1bd9..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectGrainTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Core; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Xunit; - -#pragma warning disable CA1806 // Do not ignore method results - -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject -{ - public class AssetDomainObjectGrainTests - { - private readonly IActivationLimit limit = A.Fake(); - - [Fact] - public void Should_set_limit() - { - var id = DomainId.NewGuid(); - - var identity = A.Fake(); - - A.CallTo(() => identity.PrimaryKeyString) - .Returns(id.ToString()); - - var factory = A.Fake(); - - A.CallTo(() => factory.Create(id)) - .Returns(A.Dummy()); - - new AssetDomainObjectGrain(identity, factory, limit); - - A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs index 35e7e29b8..29e3e3c45 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetDomainObjectTests.cs @@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject A.CallTo(() => assetQuery.FindAssetFolderAsync(AppId, parentId, A._)) .Returns(new List { A.Fake() }); - A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>._, A>._)) + A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A>._, A>._, default)) .ReturnsLazily(x => Task.FromResult(x.GetArgument>(2)?.ToDictionary(x => x) ?? new Dictionary())); var log = A.Fake>(); @@ -483,7 +483,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private async Task PublishAsync(AssetCommand command) { - var result = await sut.ExecuteAsync(CreateAssetCommand(command)); + var result = await sut.ExecuteAsync(CreateAssetCommand(command), default); return result.Payload; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectGrainTests.cs deleted file mode 100644 index e0ac9e6af..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectGrainTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Core; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Xunit; - -#pragma warning disable CA1806 // Do not ignore method results - -namespace Squidex.Domain.Apps.Entities.Assets.DomainObject -{ - public class AssetFolderDomainObjectGrainTests - { - private readonly IActivationLimit limit = A.Fake(); - - [Fact] - public void Should_set_limit() - { - var id = DomainId.NewGuid(); - - var identity = A.Fake(); - - A.CallTo(() => identity.PrimaryKeyString) - .Returns(id.ToString()); - - var factory = A.Fake(); - - A.CallTo(() => factory.Create(id)) - .Returns(A.Dummy()); - - new AssetFolderDomainObjectGrain(identity, factory, limit); - - A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs index a1f851aaa..75bf6640c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetFolderDomainObjectTests.cs @@ -177,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject private async Task PublishAsync(AssetFolderCommand command) { - var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command)); + var result = await sut.ExecuteAsync(CreateAssetFolderCommand(command), default); return result.Payload; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs index 87d60a2af..51d56c97a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/AssetsBulkUpdateCommandMiddlewareTests.cs @@ -20,6 +20,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { public class AssetsBulkUpdateCommandMiddlewareTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IContextProvider contextProvider = A.Fake(); private readonly ICommandBus commandBus = A.Dummy(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); @@ -27,6 +29,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject public AssetsBulkUpdateCommandMiddlewareTests() { + ct = cts.Token; + var log = A.Fake>(); sut = new AssetsBulkUpdateCommandMiddleware(contextProvider, log); @@ -66,7 +70,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == id && x.FileName == "file"))) + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == id && x.FileName == "file"), ct)) .MustHaveHappened(); } @@ -84,7 +88,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -102,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == id))) + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == id), ct)) .MustHaveHappened(); } @@ -120,7 +124,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -139,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.AssetId == id))) + A.That.Matches(x => x.AssetId == id), ct)) .MustHaveHappened(); } @@ -157,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -165,7 +169,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject { var context = new CommandContext(command, commandBus); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); return (context.PlainResult as BulkUpdateResult)!; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs index d22c03423..82c3553bf 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetFolderTests.cs @@ -76,7 +76,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards await operation.MustMoveToValidFolder(parentId); - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default)) + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs index a5e6e3b80..f0105f124 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/DomainObject/Guards/GuardAssetTests.cs @@ -62,7 +62,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards await operation.MustMoveToValidFolder(parentId); - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default)) + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A._)) .MustNotHaveHappened(); } @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.DomainObject.Guards await operation.MustMoveToValidFolder(parentId); - A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, default)) + A.CallTo(() => assetQuery.FindAssetFolderAsync(appId.Id, parentId, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs index 1a20e5ac0..b4232cbd2 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTagAssetMetadataSourceTests.cs @@ -22,7 +22,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = FakeCommand("NoExtension"); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Equal(AssetType.Unknown, command.Type); } @@ -32,7 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = Command("SamplePNGImage_100kbmb.png"); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Equal(AssetType.Image, command.Type); @@ -45,7 +45,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = Command("SampleAudio_0.4mb.mp3"); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Null(command.Metadata.GetPixelWidth()); Assert.Null(command.Metadata.GetPixelHeight()); @@ -56,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = Command("SampleAudio_0.4mb.mp3"); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Equal(AssetType.Audio, command.Type); @@ -71,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = Command("SampleVideo_1280x720_1mb.mp4"); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Equal(AssetType.Video, command.Type); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs index 0c4177974..3dd380d9f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/FileTypeAssetMetadataSourceTests.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset(); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Empty(command.Tags); } @@ -33,7 +33,7 @@ namespace Squidex.Domain.Apps.Entities.Assets File = new NoopAssetFile("File.DOCX") }; - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Contains("type/docx", command.Tags); } @@ -46,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Assets File = new NoopAssetFile("File") }; - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Contains("type/blob", command.Tags); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs index 0942d8555..a4221b22f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/ImageAssetMetadataSourceTests.cs @@ -16,6 +16,8 @@ namespace Squidex.Domain.Apps.Entities.Assets { public class ImageAssetMetadataSourceTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IAssetThumbnailGenerator assetThumbnailGenerator = A.Fake(); private readonly MemoryStream stream = new MemoryStream(); private readonly AssetFile file; @@ -23,6 +25,8 @@ namespace Squidex.Domain.Apps.Entities.Assets public ImageAssetMetadataSourceTests() { + ct = cts.Token; + file = new DelegateAssetFile("MyImage.png", "image/png", 1024, () => stream); sut = new ImageAssetMetadataSource(assetThumbnailGenerator); @@ -33,9 +37,9 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset { File = file, Type = AssetType.Image }; - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, ct); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, default)) + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, ct)) .MustHaveHappened(); } @@ -44,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset { File = file }; - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, ct); Assert.Empty(command.Tags); } @@ -54,16 +58,16 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, default)) + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, ct)) .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, ct); Assert.Equal(800, command.Metadata.GetPixelWidth()); Assert.Equal(600, command.Metadata.GetPixelHeight()); Assert.Equal(AssetType.Image, command.Type); - A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A._, default)) + A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A._, A._)) .MustNotHaveHappened(); } @@ -72,19 +76,19 @@ namespace Squidex.Domain.Apps.Entities.Assets { var command = new CreateAsset { File = file }; - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, default)) + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(A._, file.MimeType, ct)) .Returns(new ImageInfo(800, 600, ImageOrientation.None, ImageFormat.PNG)); - A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, default)) + A.CallTo(() => assetThumbnailGenerator.GetImageInfoAsync(stream, file.MimeType, ct)) .Returns(new ImageInfo(600, 800, ImageOrientation.BottomRight, ImageFormat.PNG)).Once(); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, ct); Assert.Equal(800, command.Metadata.GetPixelWidth()); Assert.Equal(600, command.Metadata.GetPixelHeight()); Assert.Equal(AssetType.Image, command.Type); - A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A._, default)) + A.CallTo(() => assetThumbnailGenerator.FixOrientationAsync(stream, file.MimeType, A._, ct)) .MustHaveHappened(); } @@ -96,7 +100,7 @@ namespace Squidex.Domain.Apps.Entities.Assets command.Metadata.SetPixelWidth(100); command.Metadata.SetPixelWidth(100); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, ct); Assert.Contains("image", command.Tags); Assert.Contains("image/small", command.Tags); @@ -110,7 +114,7 @@ namespace Squidex.Domain.Apps.Entities.Assets command.Metadata.SetPixelWidth(800); command.Metadata.SetPixelWidth(600); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, ct); Assert.Contains("image", command.Tags); Assert.Contains("image/medium", command.Tags); @@ -124,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Assets command.Metadata.SetPixelWidth(1200); command.Metadata.SetPixelWidth(1400); - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, ct); Assert.Contains("image", command.Tags); Assert.Contains("image/large", command.Tags); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs index c564d4925..0110c5c0d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs @@ -117,9 +117,9 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb private static void SetupJson() { - var jsonSerializer = JsonSerializer.Create(TestUtils.DefaultSerializerSettings); + var serializer = JsonSerializer.Create(TestUtils.DefaultSerializerSettings); - BsonJsonConvention.Register(jsonSerializer); + BsonJsonConvention.Register(serializer); } public DomainId RandomAppId() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs index 35c519284..1948119ab 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetEnricherTests.cs @@ -19,18 +19,22 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { public class AssetEnricherTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly ITagService tagService = A.Fake(); private readonly IRequestCache requestCache = A.Fake(); private readonly IUrlGenerator urlGenerator = A.Fake(); private readonly IAssetMetadataSource assetMetadataSource1 = A.Fake(); private readonly IAssetMetadataSource assetMetadataSource2 = A.Fake(); - private readonly IJsonSerializer jsonSerializer = A.Fake(); + private readonly IJsonSerializer serializer = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly Context requestContext; private readonly AssetEnricher sut; public AssetEnricherTests() { + ct = cts.Token; + var assetMetadataSources = new[] { assetMetadataSource1, @@ -39,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries requestContext = Context.Anonymous(Mocks.App(appId)); - sut = new AssetEnricher(tagService, assetMetadataSources, requestCache, urlGenerator, jsonSerializer); + sut = new AssetEnricher(tagService, assetMetadataSources, requestCache, urlGenerator, serializer); } [Fact] @@ -47,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var source = new AssetEntity { AppId = appId }; - var result = await sut.EnrichAsync(source, requestContext, default); + var result = await sut.EnrichAsync(source, requestContext, ct); Assert.Empty(result.TagNames); } @@ -57,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var source = new AssetEntity { AppId = appId, Id = DomainId.NewGuid(), Version = 13 }; - var result = await sut.EnrichAsync(source, requestContext, default); + var result = await sut.EnrichAsync(source, requestContext, ct); A.CallTo(() => requestCache.AddDependency(result.UniqueId, result.Version)) .MustHaveHappened(); @@ -76,14 +80,14 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A>.That.Is("id1", "id2"))) + A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A>.That.Is("id1", "id2"), ct)) .Returns(new Dictionary { ["id1"] = "name1", ["id2"] = "name2" }); - var result = await sut.EnrichAsync(source, requestContext, default); + var result = await sut.EnrichAsync(source, requestContext, ct); Assert.Equal(new HashSet { "name1", "name2" }, result.TagNames); } @@ -101,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - var result = await sut.EnrichAsync(source, requestContext.Clone(b => b.WithoutAssetEnrichment()), default); + var result = await sut.EnrichAsync(source, requestContext.Clone(b => b.WithoutAssetEnrichment()), ct); Assert.Null(result.TagNames); } @@ -126,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetMetadataSource2.Format(A._)) .Returns(new[] { "metadata2", "metadata3" }); - var result = await sut.EnrichAsync(source, requestContext, default); + var result = await sut.EnrichAsync(source, requestContext, ct); Assert.Equal("metadata1, metadata2, metadata3, 2 kB", result.MetadataText); } @@ -154,7 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A>.That.Is("id1", "id2", "id3"))) + A.CallTo(() => tagService.DenormalizeTagsAsync(appId.Id, TagGroups.Assets, A>.That.Is("id1", "id2", "id3"), ct)) .Returns(new Dictionary { ["id1"] = "name1", @@ -162,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries ["id3"] = "name3" }); - var result = await sut.EnrichAsync(new[] { source1, source2 }, requestContext, default); + var result = await sut.EnrichAsync(new[] { source1, source2 }, requestContext, ct); Assert.Equal(new HashSet { "name1", "name2" }, result[0].TagNames); Assert.Equal(new HashSet { "name2", "name3" }, result[1].TagNames); @@ -176,7 +180,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - var result = await sut.EnrichAsync(new[] { source }, new Context(Mocks.FrontendUser(), Mocks.App(appId)), default); + var result = await sut.EnrichAsync(new[] { source }, new Context(Mocks.FrontendUser(), Mocks.App(appId)), ct); Assert.Null(result[0].EditToken); @@ -192,7 +196,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries AppId = appId }; - var result = await sut.EnrichAsync(new[] { source }, requestContext, default); + var result = await sut.EnrichAsync(new[] { source }, requestContext, ct); Assert.NotNull(result[0].EditToken); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs index 2f0892824..173de1609 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetLoaderTests.cs @@ -6,73 +6,113 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Entities.Assets.DomainObject; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Xunit; namespace Squidex.Domain.Apps.Entities.Assets.Queries { public class AssetLoaderTests { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IAssetGrain grain = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); + private readonly IDomainObjectCache domainObjectCache = A.Fake(); + private readonly AssetDomainObject domainObject = A.Fake(); private readonly DomainId appId = DomainId.NewGuid(); private readonly DomainId id = DomainId.NewGuid(); + private readonly DomainId uniqueId; private readonly AssetLoader sut; public AssetLoaderTests() { - var key = DomainId.Combine(appId, id).ToString(); + ct = cts.Token; - A.CallTo(() => grainFactory.GetGrain(key, null)) - .Returns(grain); + uniqueId = DomainId.Combine(appId, id); - sut = new AssetLoader(grainFactory); + A.CallTo(() => domainObjectCache.GetAsync(A._, A._, ct)) + .Returns(Task.FromResult(null!)); + + A.CallTo(() => domainObjectFactory.Create(uniqueId)) + .Returns(domainObject); + + sut = new AssetLoader(domainObjectFactory, domainObjectCache); } [Fact] public async Task Should_return_null_if_no_state_returned() { - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(Task.FromResult(null!)); + var asset = (AssetDomainObject.State)null!; + + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(asset); - Assert.Null(await sut.GetAsync(appId, id, 10)); + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); } [Fact] public async Task Should_return_null_if_state_empty() { - var asset = new AssetEntity { Version = EtagVersion.Empty }; + var asset = new AssetDomainObject.State { Version = EtagVersion.Empty }; - A.CallTo(() => grain.GetStateAsync(10)) + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) .Returns(asset); - Assert.Null(await sut.GetAsync(appId, id, 10)); + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); } [Fact] public async Task Should_return_null_if_state_has_other_version() { - var asset = new AssetEntity { Version = 5 }; + var asset = new AssetDomainObject.State { Version = 5 }; - A.CallTo(() => grain.GetStateAsync(10)) + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) .Returns(asset); - Assert.Null(await sut.GetAsync(appId, id, 10)); + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); } [Fact] - public async Task Should_return_content_from_state() + public async Task Should_not_return_null_if_state_has_other_version_than_any() { - var asset = new AssetEntity { Version = 10 }; + var asset = new AssetDomainObject.State { Version = 5 }; - A.CallTo(() => grain.GetStateAsync(10)) + A.CallTo(() => domainObject.GetSnapshotAsync(EtagVersion.Any, ct)) .Returns(asset); - var result = await sut.GetAsync(appId, id, 10); + var result = await sut.GetAsync(appId, id, EtagVersion.Any, ct); Assert.Same(asset, result); } + + [Fact] + public async Task Should_return_asset_from_state() + { + var asset = new AssetDomainObject.State { Version = 10 }; + + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(asset); + + var result = await sut.GetAsync(appId, id, 10, ct); + + Assert.Same(asset, result); + } + + [Fact] + public async Task Should_return_content_from_cache() + { + var content = new AssetDomainObject.State { Version = 10 }; + + A.CallTo(() => domainObjectCache.GetAsync(DomainId.Combine(appId, id), 10, ct)) + .Returns(content); + + var result = await sut.GetAsync(appId, id, 10, ct); + + Assert.Same(content, result); + + A.CallTo(() => domainObjectFactory.Create(uniqueId)) + .MustNotHaveHappened(); + } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs index 67b188cb7..800769804 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryParserTests.cs @@ -154,7 +154,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries [Fact] public async Task Should_denormalize_tags() { - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A>.That.Contains("name1"))) + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A>.That.Contains("name1"), default)) .Returns(new Dictionary { ["name1"] = "id1" }); var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); @@ -167,7 +167,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries [Fact] public async Task Should_not_fail_if_tags_not_found() { - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A>.That.Contains("name1"))) + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, TagGroups.Assets, A>.That.Contains("name1"), default)) .Returns(new Dictionary()); var query = Q.Empty.WithODataQuery("$filter=tags eq 'name1'"); @@ -186,7 +186,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries Assert.Equal("Filter: fileSize == 123; Take: 30; Sort: lastModified Descending, id Ascending", q.Query.ToString()); - A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A._, A>._)) + A.CallTo(() => tagService.GetTagIdsAsync(appId.Id, A._, A>._, A._)) .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs index d4299b1fc..88cddbb5a 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs @@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { public class AssetQueryServiceTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IAssetEnricher assetEnricher = A.Fake(); private readonly IAssetRepository assetRepository = A.Fake(); private readonly IAssetLoader assetLoader = A.Fake(); @@ -28,11 +30,13 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries public AssetQueryServiceTests() { + ct = cts.Token; + requestContext = new Context(Mocks.FrontendUser(), Mocks.App(appId)); SetupEnricher(); - A.CallTo(() => queryParser.ParseAsync(requestContext, A._)) + A.CallTo(() => queryParser.ParseAsync(requestContext, A._, ct)) .ReturnsLazily(c => Task.FromResult(c.GetArgument(1)!)); var options = Options.Create(new AssetOptions()); @@ -54,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug", A._)) .Returns(asset); - var result = await sut.FindBySlugAsync(requestContext, "slug"); + var result = await sut.FindBySlugAsync(requestContext, "slug", ct); AssertAsset(asset, result); } @@ -67,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetBySlugAsync(appId.Id, "slug", A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindBySlugAsync(requestContext, "slug"); + var result = await sut.FindBySlugAsync(requestContext, "slug", ct); Assert.Null(result); } @@ -80,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id, A._)) .Returns(asset); - var result = await sut.FindAsync(requestContext, asset.Id); + var result = await sut.FindAsync(requestContext, asset.Id, ct: ct); AssertAsset(asset, result); } @@ -93,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetAsync(appId.Id, asset.Id, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindAsync(requestContext, asset.Id); + var result = await sut.FindAsync(requestContext, asset.Id, ct: ct); Assert.Null(result); } @@ -103,10 +107,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2)) + A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2, A._)) .Returns(asset); - var result = await sut.FindAsync(requestContext, asset.Id, 2); + var result = await sut.FindAsync(requestContext, asset.Id, 2, ct); AssertAsset(asset, result); } @@ -116,10 +120,10 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { var asset = CreateAsset(DomainId.NewGuid()); - A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2)) + A.CallTo(() => assetLoader.GetAsync(appId.Id, asset.Id, 2, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindAsync(requestContext, asset.Id, 2); + var result = await sut.FindAsync(requestContext, asset.Id, 2, ct); Assert.Null(result); } @@ -132,7 +136,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetAsync(asset.Id, A._)) .Returns(asset); - var result = await sut.FindGlobalAsync(requestContext, asset.Id); + var result = await sut.FindGlobalAsync(requestContext, asset.Id, ct); AssertAsset(asset, result); } @@ -145,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetAsync(asset.Id, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindGlobalAsync(requestContext, asset.Id); + var result = await sut.FindGlobalAsync(requestContext, asset.Id, ct); Assert.Null(result); } @@ -158,7 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123, A._)) .Returns(asset); - var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123); + var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); AssertAsset(asset, result); } @@ -171,7 +175,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.FindAssetByHashAsync(appId.Id, "hash", "name", 123, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123); + var result = await sut.FindByHashAsync(requestContext, "hash", "name", 123, ct); Assert.Null(result); } @@ -189,7 +193,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetRepository.QueryAsync(appId.Id, parentId, q, A._)) .Returns(ResultList.CreateFrom(8, asset1, asset2)); - var result = await sut.QueryAsync(requestContext, parentId, q); + var result = await sut.QueryAsync(requestContext, parentId, q, ct); Assert.Equal(8, result.Total); @@ -207,7 +211,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId, A._)) .Returns(assetFolders); - var result = await sut.QueryAssetFoldersAsync(requestContext, parentId); + var result = await sut.QueryAssetFoldersAsync(requestContext, parentId, ct); Assert.Same(assetFolders, result); } @@ -222,7 +226,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.QueryAsync(appId.Id, parentId, A._)) .Returns(assetFolders); - var result = await sut.QueryAssetFoldersAsync(appId.Id, parentId); + var result = await sut.QueryAssetFoldersAsync(appId.Id, parentId, ct); Assert.Same(assetFolders, result); } @@ -236,7 +240,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A._)) .Returns(folder1); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId1); + var result = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); Assert.Equal(result, new[] { folder1 }); } @@ -261,7 +265,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId3, A._)) .Returns(folder3); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId3); + var result = await sut.FindAssetFolderAsync(appId.Id, folderId3, ct); Assert.Equal(result, new[] { folder1, folder2, folder3 }); } @@ -274,7 +278,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId1, A._)) .Returns(Task.FromResult(null)); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId1); + var result = await sut.FindAssetFolderAsync(appId.Id, folderId1, ct); Assert.Empty(result); } @@ -294,7 +298,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A._)) .Returns(folder2); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId2); + var result = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); Assert.Empty(result); } @@ -314,7 +318,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => assetFolderRepository.FindAssetFolderAsync(appId.Id, folderId2, A._)) .Returns(folder2); - var result = await sut.FindAssetFolderAsync(appId.Id, folderId2); + var result = await sut.FindAssetFolderAsync(appId.Id, folderId2, ct); Assert.Empty(result); } @@ -343,7 +347,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries private void SetupEnricher() { - A.CallTo(() => assetEnricher.EnrichAsync(A>._, A._, A._)) + A.CallTo(() => assetEnricher.EnrichAsync(A>._, A._, ct)) .ReturnsLazily(x => { var input = x.GetArgument>(0)!; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs index 477660e90..f56e4d01d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RecursiveDeleterTests.cs @@ -65,7 +65,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.On(Envelope.Create(@event).SetRestored()); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -77,15 +77,15 @@ namespace Squidex.Domain.Apps.Entities.Assets var childFolderId1 = DomainId.NewGuid(); var childFolderId2 = DomainId.NewGuid(); - A.CallTo(() => assetFolderRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, A._)) + A.CallTo(() => assetFolderRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, default)) .Returns(new List { childFolderId1, childFolderId2 }); await sut.On(Envelope.Create(@event)); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetFolderId == childFolderId1))) + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetFolderId == childFolderId1), default)) .MustHaveHappened(); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetFolderId == childFolderId2))) + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetFolderId == childFolderId2), default)) .MustHaveHappened(); } @@ -97,15 +97,15 @@ namespace Squidex.Domain.Apps.Entities.Assets var childId1 = DomainId.NewGuid(); var childId2 = DomainId.NewGuid(); - A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, A._)) + A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, default)) .Returns(new List { childId1, childId2 }); await sut.On(Envelope.Create(@event)); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == childId1))) + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == childId1), default)) .MustHaveHappened(); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == childId2))) + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == childId2), default)) .MustHaveHappened(); } @@ -117,15 +117,15 @@ namespace Squidex.Domain.Apps.Entities.Assets var childId1 = DomainId.NewGuid(); var childId2 = DomainId.NewGuid(); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == childId1))) + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == childId1), default)) .Throws(new InvalidOperationException()); - A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, A._)) + A.CallTo(() => assetRepository.QueryChildIdsAsync(appId.Id, @event.AssetFolderId, default)) .Returns(new List { childId1, childId2 }); await sut.On(Envelope.Create(@event)); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == childId2))) + A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.AssetId == childId2), default)) .MustHaveHappened(); A.CallTo(log).Where(x => x.Method.Name == "Log") diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs index 047cdcd85..4329bf6dd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/RepairFilesTests.cs @@ -55,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RepairAsync(); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A._, true, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 0, null, A._, true, A._)) .MustNotHaveHappened(); } @@ -66,7 +66,7 @@ namespace Squidex.Domain.Apps.Entities.Assets SetupEvent(@event); - A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, default)) + A.CallTo(() => assetFileStore.GetFileSizeAsync(appId.Id, @event.AssetId, 3, null, A._)) .Throws(new AssetNotFoundException("file")); await sut.RepairAsync(); @@ -87,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RepairAsync(); - A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A._, true, default)) + A.CallTo(() => assetFileStore.UploadAsync(appId.Id, @event.AssetId, 3, null, A._, true, A._)) .MustNotHaveHappened(); } @@ -98,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Assets await sut.RepairAsync(); - A.CallTo(() => assetFileStore.GetFileSizeAsync(A._, A._, A._, null, default)) + A.CallTo(() => assetFileStore.GetFileSizeAsync(A._, A._, A._, null, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/SvgAssetMetadataSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/SvgAssetMetadataSourceTests.cs index aac7315a9..c7db852db 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/SvgAssetMetadataSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/SvgAssetMetadataSourceTests.cs @@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var command = new CreateAsset { File = svg }; - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Contains("image", command.Tags); } @@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var command = new CreateAsset { File = svg }; - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); Assert.Contains("image", command.Tags); } @@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var command = new CreateAsset { File = svg }; - await Assert.ThrowsAsync(() => sut.EnhanceAsync(command)); + await Assert.ThrowsAsync(() => sut.EnhanceAsync(command, default)); } [Fact] @@ -75,7 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Assets var command = new CreateAsset { File = svg }; - await sut.EnhanceAsync(command); + await sut.EnhanceAsync(command, default); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs index 757ce0e8c..90c8d8d77 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs @@ -6,18 +6,22 @@ // ========================================================================== using FakeItEasy; -using Orleans; +using FluentAssertions; +using NodaTime; using Squidex.Domain.Apps.Entities.Backup.State; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.TestHelpers; +using Squidex.Messaging; using Xunit; namespace Squidex.Domain.Apps.Entities.Backup { public class BackupServiceTests { - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly TestState stateBackup; + private readonly TestState stateRestore; + private readonly IMessageBus messaging = A.Fake(); private readonly DomainId appId = DomainId.NewGuid(); private readonly DomainId backupId = DomainId.NewGuid(); private readonly RefToken actor = RefToken.User("me"); @@ -25,130 +29,133 @@ namespace Squidex.Domain.Apps.Entities.Backup public BackupServiceTests() { - sut = new BackupService(grainFactory); + stateRestore = new TestState("Default"); + stateBackup = new TestState(appId); + + sut = new BackupService( + stateRestore.PersistenceFactory, + stateBackup.PersistenceFactory, messaging); } [Fact] - public async Task Should_call_grain_if_restoring_backup() + public async Task Should_send_message_to_restore_backup() { - var grain = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(grain); - - var initiator = RefToken.User("me"); - var restoreUrl = new Uri("http://squidex.io"); var restoreAppName = "New App"; - await sut.StartRestoreAsync(initiator, restoreUrl, restoreAppName); + await sut.StartRestoreAsync(actor, restoreUrl, restoreAppName); - A.CallTo(() => grain.RestoreAsync(restoreUrl, initiator, restoreAppName)) + A.CallTo(() => messaging.PublishAsync(new BackupRestore(actor, restoreUrl, restoreAppName), null, default)) .MustHaveHappened(); } [Fact] - public async Task Should_call_grain_to_get_restore_status() + public async Task Should_send_message_to_start_backup() { - IRestoreJob state = new RestoreJob(); - - var grain = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(grain); - - A.CallTo(() => grain.GetStateAsync()) - .Returns(state); - - var result = await sut.GetRestoreAsync(); + await sut.StartBackupAsync(appId, actor); - Assert.Same(state, result); + A.CallTo(() => messaging.PublishAsync(new BackupStart(appId, actor), null, default)) + .MustHaveHappened(); } [Fact] - public async Task Should_call_grain_to_start_backup() + public async Task Should_send_message_to_delete_backup() { - var grain = A.Fake(); + await sut.DeleteBackupAsync(appId, backupId); - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(grain); + A.CallTo(() => messaging.PublishAsync(new BackupDelete(appId, backupId), null, default)) + .MustHaveHappened(); + } - await sut.StartBackupAsync(appId, actor); + [Fact] + public async Task Should_send_message_to_clear_backups() + { + await ((IDeleter)sut).DeleteAppAsync(Mocks.App(NamedId.Of(appId, "my-app")), default); - A.CallTo(() => grain.BackupAsync(actor)) + A.CallTo(() => messaging.PublishAsync(new BackupClear(appId), null, default)) .MustHaveHappened(); } [Fact] - public async Task Should_call_grain_to_get_backups() + public async Task Should_throw_exception_when_restore_already_running() { - var state = new List + stateRestore.Snapshot = new RestoreJob { - new BackupJob { Id = backupId } + Status = JobStatus.Started }; - var grain = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(grain); + var restoreUrl = new Uri("http://squidex.io"); - A.CallTo(() => grain.GetStateAsync()) - .Returns(state); + await Assert.ThrowsAnyAsync(() => sut.StartRestoreAsync(actor, restoreUrl, null)); + } - var result = await sut.GetBackupsAsync(appId); + [Fact] + public async Task Should_throw_exception_when_backup_has_too_many_jobs() + { + for (var i = 0; i < 10; i++) + { + stateBackup.Snapshot.Jobs.Add(new BackupJob()); + } - Assert.Same(state, result); + await Assert.ThrowsAnyAsync(() => sut.StartBackupAsync(appId, actor)); } [Fact] - public async Task Should_call_grain_to_get_backup() + public async Task Should_throw_exception_when_backup_has_one_running_job() { - var state = new List + for (var i = 0; i < 2; i++) { - new BackupJob { Id = backupId } - }; - - var grain = A.Fake(); + stateBackup.Snapshot.Jobs.Add(new BackupJob { Status = JobStatus.Started }); + } - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(grain); + await Assert.ThrowsAnyAsync(() => sut.StartBackupAsync(appId, actor)); + } - A.CallTo(() => grain.GetStateAsync()) - .Returns(state); + [Fact] + public async Task Should_get_restore_state_from_store() + { + stateRestore.Snapshot = new RestoreJob + { + Stopped = SystemClock.Instance.GetCurrentInstant() + }; - var result1 = await sut.GetBackupAsync(appId, backupId); - var result2 = await sut.GetBackupAsync(appId, DomainId.NewGuid()); + var result = await sut.GetRestoreAsync(); - Assert.Same(state[0], result1); - Assert.Null(result2); + result.Should().BeEquivalentTo(stateRestore.Snapshot); } [Fact] - public async Task Should_call_grain_to_delete_backup() + public async Task Should_get_backups_state_from_store() { - var grain = A.Fake(); + var job = new BackupJob + { + Id = backupId, + Started = SystemClock.Instance.GetCurrentInstant(), + Stopped = SystemClock.Instance.GetCurrentInstant() + }; - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(grain); + stateBackup.Snapshot.Jobs.Add(job); - await sut.DeleteBackupAsync(appId, backupId); + var result = await sut.GetBackupsAsync(appId); - A.CallTo(() => grain.DeleteAsync(backupId)) - .MustHaveHappened(); + result.Should().BeEquivalentTo(stateBackup.Snapshot.Jobs); } [Fact] - public async Task Should_call_grain_to_clear_backups() + public async Task Should_get_backup_state_from_store() { - var grain = A.Fake(); + var job = new BackupJob + { + Id = backupId, + Started = SystemClock.Instance.GetCurrentInstant(), + Stopped = SystemClock.Instance.GetCurrentInstant() + }; - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(grain); + stateBackup.Snapshot.Jobs.Add(job); - await ((IDeleter)sut).DeleteAppAsync(Mocks.App(NamedId.Of(appId, "my-app")), default); + var result = await sut.GetBackupAsync(appId, backupId); - A.CallTo(() => grain.ClearAsync()) - .MustHaveHappened(); + result.Should().BeEquivalentTo(job); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs index 6b3f75b77..1ec792833 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/CommentsLoaderTests.cs @@ -6,40 +6,47 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Entities.Comments.DomainObject; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Xunit; namespace Squidex.Domain.Apps.Entities.Comments { public sealed class CommentsLoaderTests { - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); private readonly CommentsLoader sut; public CommentsLoaderTests() { - sut = new CommentsLoader(grainFactory); + ct = cts.Token; + + sut = new CommentsLoader(domainObjectFactory); } [Fact] - public async Task Should_get_comments_from_grain() + public async Task Should_get_comments_from_domain_object() { var commentsId = DomainId.NewGuid(); var comments = new CommentsResult(); - var grain = A.Fake(); + var domainObject = A.Fake(); - A.CallTo(() => grain.GetCommentsAsync(11)) - .Returns(comments); + A.CallTo(() => domainObjectFactory.Create(commentsId)) + .Returns(domainObject); - A.CallTo(() => grainFactory.GetGrain(commentsId.ToString(), null)) - .Returns(grain); + A.CallTo(() => domainObject.GetComments(11)) + .Returns(comments); - var result = await sut.GetCommentsAsync(commentsId, 11); + var result = await sut.GetCommentsAsync(commentsId, 11, ct); Assert.Same(comments, result); + + A.CallTo(() => domainObject.LoadAsync(ct)) + .MustHaveHappened(); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs index 95caf94a1..1baa7c741 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsCommandMiddlewareTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Core.TestHelpers; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Infrastructure; @@ -18,7 +17,9 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { public class CommentsCommandMiddlewareTests { - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); private readonly IUserResolver userResolver = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); private readonly RefToken actor = RefToken.User("me"); @@ -29,34 +30,36 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject public CommentsCommandMiddlewareTests() { + ct = cts.Token; + A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, default)) .Returns(Task.FromResult(null)); - sut = new CommentsCommandMiddleware(grainFactory, userResolver); + sut = new CommentsCommandMiddleware(domainObjectFactory, userResolver); } [Fact] - public async Task Should_invoke_grain_for_comments_command() + public async Task Should_invoke_domain_object_for_comments_command() { var command = CreateCommentsCommand(new CreateComment()); var context = CrateCommandContext(command); - var grain = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain(commentsId.ToString(), null)) - .Returns(grain); + var domainObject = A.Fake(); - A.CallTo(() => grain.ExecuteAsync(command)) + A.CallTo(() => domainObject.ExecuteAsync(command, ct)) .Returns(CommandResult.Empty(commentsId, 0, 0)); + A.CallTo(() => domainObjectFactory.Create(commentsId)) + .Returns(domainObject); + var isNextCalled = false; - await sut.HandleAsync(context, c => + await sut.HandleAsync(context, (c, ct) => { isNextCalled = true; return Task.CompletedTask; - }); + }, ct); Assert.True(isNextCalled); } @@ -69,12 +72,13 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var command = CreateCommentsCommand(new CreateComment { - Text = "Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io" + Text = "Hi @mail1@squidex.io, @mail2@squidex.io and @notfound@squidex.io", + IsMention = false }); var context = CrateCommandContext(command); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); Assert.Equal(command.Mentions, new[] { "id1", "id2" }); } @@ -87,14 +91,15 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var command = CreateCommentsCommand(new CreateComment { - Text = "Hi @mail1@squidex.io and @mail2@squidex.io" + Text = "Hi @mail1@squidex.io and @mail2@squidex.io", + IsMention = false }); var context = CrateCommandContext(command); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -103,12 +108,13 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { var command = CreateCommentsCommand(new CreateComment { - Text = "Hi invalid@squidex.io" + Text = "Hi invalid@squidex.io", + IsMention = false }); var context = CrateCommandContext(command); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, A._)) .MustNotHaveHappened(); @@ -117,15 +123,15 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject [Fact] public async Task Should_not_enrich_with_mentioned_user_ids_for_notification() { - var command = new CreateComment + var command = CreateCommentsCommand(new CreateComment { Text = "Hi @invalid@squidex.io", IsMention = true - }; + }); var context = CrateCommandContext(command); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); A.CallTo(() => userResolver.FindByIdOrEmailAsync(A._, A._)) .MustNotHaveHappened(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs similarity index 81% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs index f14f31897..53a9fc77b 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/DomainObject/CommentsStreamTests.cs @@ -8,7 +8,6 @@ using FakeItEasy; using FluentAssertions; using NodaTime; -using Orleans.Core; using Squidex.Domain.Apps.Core.Comments; using Squidex.Domain.Apps.Entities.Comments.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -20,32 +19,23 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { - public class CommentsGrainTests + public class CommentsStreamTests { - private readonly IGrainIdentity identity = A.Fake(); private readonly IEventFormatter eventFormatter = A.Fake(); private readonly IEventStore eventStore = A.Fake(); private readonly DomainId commentsId = DomainId.NewGuid(); private readonly DomainId commentId = DomainId.NewGuid(); private readonly RefToken actor = RefToken.User("me"); - private readonly CommentsGrain sut; - - private string Id - { - get => commentsId.ToString(); - } + private readonly CommentsStream sut; public IEnumerable> LastEvents { get; private set; } = Enumerable.Empty>(); - public CommentsGrainTests() + public CommentsStreamTests() { - A.CallTo(() => identity.PrimaryKeyString) - .Returns(Id); - A.CallTo(() => eventStore.AppendAsync(A._, A._, A._, A>._, default)) .Invokes(x => LastEvents = sut!.GetUncommittedEvents().Select(x => x.To()).ToList()); - sut = new CommentsGrain(identity, eventFormatter, eventStore); + sut = new CommentsStream(commentsId, eventFormatter, eventStore); } [Fact] @@ -53,16 +43,16 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject { var command = new CreateComment { Text = "text1", Url = new Uri("http://uri") }; - var result = await sut.ExecuteAsync(CreateCommentsCommand(command)); + var result = await sut.ExecuteAsync(CreateCommentsCommand(command), default); result.ShouldBeEquivalent(CommandResult.Empty(commentsId, 0, EtagVersion.Empty)); - (await sut.GetCommentsAsync(0)).Should().BeEquivalentTo(new CommentsResult + sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult { Version = 0 }); - (await sut.GetCommentsAsync(-1)).Should().BeEquivalentTo(new CommentsResult + sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult { CreatedComments = new List { @@ -84,11 +74,11 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var updateCommand = new UpdateComment { Text = "text2" }; - var result = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand)); + var result = await sut.ExecuteAsync(CreateCommentsCommand(updateCommand), default); result.ShouldBeEquivalent(CommandResult.Empty(commentsId, 1, 0)); - (await sut.GetCommentsAsync(-1)).Should().BeEquivalentTo(new CommentsResult + sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult { CreatedComments = new List { @@ -97,7 +87,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject Version = 1 }); - (await sut.GetCommentsAsync(0)).Should().BeEquivalentTo(new CommentsResult + sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult { UpdatedComments = new List { @@ -120,16 +110,16 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject var deleteCommand = new DeleteComment(); - var result = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand)); + var result = await sut.ExecuteAsync(CreateCommentsCommand(deleteCommand), default); result.ShouldBeEquivalent(CommandResult.Empty(commentsId, 2, 1)); - (await sut.GetCommentsAsync(-1)).Should().BeEquivalentTo(new CommentsResult + sut.GetComments(-1).Should().BeEquivalentTo(new CommentsResult { Version = 2 }); - (await sut.GetCommentsAsync(0)).Should().BeEquivalentTo(new CommentsResult + sut.GetComments(0).Should().BeEquivalentTo(new CommentsResult { DeletedComments = new List { @@ -138,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject Version = 2 }); - (await sut.GetCommentsAsync(1)).Should().BeEquivalentTo(new CommentsResult + sut.GetComments(1).Should().BeEquivalentTo(new CommentsResult { DeletedComments = new List { @@ -155,12 +145,12 @@ namespace Squidex.Domain.Apps.Entities.Comments.DomainObject private Task ExecuteCreateAsync() { - return sut.ExecuteAsync(CreateCommentsCommand(new CreateComment { Text = "text1" })); + return sut.ExecuteAsync(CreateCommentsCommand(new CreateComment { Text = "text1" }), default); } private Task ExecuteUpdateAsync() { - return sut.ExecuteAsync(CreateCommentsCommand(new UpdateComment { Text = "text2" })); + return sut.ExecuteAsync(CreateCommentsCommand(new UpdateComment { Text = "text2" }), default); } private T CreateCommentsEvent(T @event) where T : CommentsEvent diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/GrainWatchingServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/GrainWatchingServiceTests.cs deleted file mode 100644 index 12f5b3ea9..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/GrainWatchingServiceTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Comments -{ - public class GrainWatchingServiceTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly GrainWatchingService sut; - - public GrainWatchingServiceTests() - { - sut = new GrainWatchingService(grainFactory); - } - - [Fact] - public async Task Should_call_grain_if_retrieving_watching_users() - { - var appId = DomainId.NewGuid(); - var userResource = "resource1"; - var userIdentity = "user1"; - - var grain = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(grain); - - A.CallTo(() => grain.GetWatchingUsersAsync(userResource, userIdentity)) - .Returns(Task.FromResult(new[] { "user1", "user2" })); - - var result = await sut.GetWatchingUsersAsync(appId, userResource, userIdentity); - - Assert.Equal(new[] { "user1", "user2" }, result); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingGrainTests.cs deleted file mode 100644 index e83f907e0..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingGrainTests.cs +++ /dev/null @@ -1,73 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using NodaTime; -using Orleans.Core; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Comments -{ - public class WatchingGrainTests - { - private readonly IGrainIdentity identity = A.Fake(); - private readonly IClock clock = A.Fake(); - private readonly WatchingGrain sut; - private Instant now = SystemClock.Instance.GetCurrentInstant(); - - public WatchingGrainTests() - { - A.CallTo(() => clock.GetCurrentInstant()) - .ReturnsLazily(() => now); - - sut = new WatchingGrain(identity, clock); - } - - [Fact] - public async Task Should_only_return_self_if_no_one_watching() - { - var watching = await sut.GetWatchingUsersAsync("resource1", "user1"); - - Assert.Equal(new[] { "user1" }, watching); - } - - [Fact] - public async Task Should_return_users_watching_on_same_resource() - { - await sut.GetWatchingUsersAsync("resource1", "user1"); - await sut.GetWatchingUsersAsync("resource2", "user2"); - - var watching1 = await sut.GetWatchingUsersAsync("resource1", "user3"); - var watching2 = await sut.GetWatchingUsersAsync("resource2", "user4"); - - Assert.Equal(new[] { "user1", "user3" }, watching1); - Assert.Equal(new[] { "user2", "user4" }, watching2); - } - - [Fact] - public async Task Should_cleanup_old_users() - { - await sut.GetWatchingUsersAsync("resource1", "user1"); - await sut.GetWatchingUsersAsync("resource2", "user2"); - - now = now.Plus(Duration.FromMinutes(2)); - - sut.Cleanup(); - - await sut.GetWatchingUsersAsync("resource1", "user3"); - await sut.GetWatchingUsersAsync("resource2", "user4"); - - sut.Cleanup(); - - var watching1 = await sut.GetWatchingUsersAsync("resource1", "user5"); - var watching2 = await sut.GetWatchingUsersAsync("resource2", "user6"); - - Assert.Equal(new[] { "user3", "user5" }, watching1); - Assert.Equal(new[] { "user4", "user6" }, watching2); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs new file mode 100644 index 000000000..06580f7e7 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Comments/WatchingServiceTests.cs @@ -0,0 +1,90 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using NodaTime; +using Squidex.Infrastructure; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Comments +{ + public class WatchingServiceTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState state1; + private readonly TestState state2; + private readonly IClock clock = A.Fake(); + private readonly DomainId appId = DomainId.NewGuid(); + private readonly string resource1 = "resource1"; + private readonly string resource2 = "resource2"; + private readonly WatchingService sut; + private Instant now = SystemClock.Instance.GetCurrentInstant(); + + public WatchingServiceTests() + { + ct = cts.Token; + + A.CallTo(() => clock.GetCurrentInstant()) + .ReturnsLazily(() => now); + + state1 = new TestState($"{appId}_{resource1}"); + state2 = new TestState($"{appId}_{resource2}", state1.PersistenceFactory); + + sut = new WatchingService(state1.PersistenceFactory) + { + Clock = clock + }; + } + + [Fact] + public async Task Should_only_return_self_if_no_one_watching() + { + var watching = await sut.GetWatchingUsersAsync(appId, resource1, "user1", ct); + + Assert.Equal(new[] { "user1" }, watching); + } + + [Fact] + public async Task Should_return_users_watching_on_same_resource() + { + await sut.GetWatchingUsersAsync(appId, resource1, "user1", ct); + await sut.GetWatchingUsersAsync(appId, resource2, "user2", ct); + + var watching1 = await sut.GetWatchingUsersAsync(appId, resource1, "user3", ct); + var watching2 = await sut.GetWatchingUsersAsync(appId, resource2, "user4", ct); + + Assert.Equal(new[] { "user1", "user3" }, watching1); + Assert.Equal(new[] { "user2", "user4" }, watching2); + } + + [Fact] + public async Task Should_cleanup_old_users() + { + await sut.GetWatchingUsersAsync(appId, resource1, "user1", ct); + await sut.GetWatchingUsersAsync(appId, resource2, "user2", ct); + + now = now.Plus(Duration.FromMinutes(2)); + + await sut.GetWatchingUsersAsync(appId, resource1, "user3", ct); + await sut.GetWatchingUsersAsync(appId, resource2, "user4", ct); + + var watching1 = await sut.GetWatchingUsersAsync(appId, resource1, "user5", ct); + var watching2 = await sut.GetWatchingUsersAsync(appId, resource2, "user6", ct); + + Assert.Equal(new[] { "user3", "user5" }, watching1); + Assert.Equal(new[] { "user4", "user6" }, watching2); + + A.CallTo(() => state1.Persistence.WriteSnapshotAsync(A._, ct)) + .MustHaveHappened(); + + A.CallTo(() => state2.Persistence.WriteSnapshotAsync(A._, ct)) + .MustHaveHappened(); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs index 74fce6725..6a434b2c1 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs @@ -26,6 +26,8 @@ namespace Squidex.Domain.Apps.Entities.Contents { public class ContentChangedTriggerHandlerTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IScriptEngine scriptEngine = A.Fake(); private readonly IContentLoader contentLoader = A.Fake(); private readonly IContentRepository contentRepository = A.Fake(); @@ -35,6 +37,8 @@ namespace Squidex.Domain.Apps.Entities.Contents public ContentChangedTriggerHandlerTests() { + ct = cts.Token; + A.CallTo(() => scriptEngine.Evaluate(A._, "true", default)) .Returns(true); @@ -125,14 +129,14 @@ namespace Squidex.Domain.Apps.Entities.Contents { var ctx = Context(); - A.CallTo(() => contentRepository.StreamAll(ctx.AppId.Id, null, default)) + A.CallTo(() => contentRepository.StreamAll(ctx.AppId.Id, null, ct)) .Returns(new List { new ContentEntity { SchemaId = schemaMatch }, new ContentEntity { SchemaId = schemaNonMatch } }.ToAsyncEnumerable()); - var result = await sut.CreateSnapshotEventsAsync(ctx, default).ToListAsync(); + var result = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); var typed = result.OfType().ToList(); @@ -157,14 +161,14 @@ namespace Squidex.Domain.Apps.Entities.Contents var ctx = Context(trigger); - A.CallTo(() => contentRepository.StreamAll(ctx.AppId.Id, A>.That.Is(schemaMatch.Id), default)) + A.CallTo(() => contentRepository.StreamAll(ctx.AppId.Id, A>.That.Is(schemaMatch.Id), ct)) .Returns(new List { new ContentEntity { SchemaId = schemaMatch }, new ContentEntity { SchemaId = schemaMatch } }.ToAsyncEnumerable()); - var result = await sut.CreateSnapshotEventsAsync(ctx, default).ToListAsync(); + var result = await sut.CreateSnapshotEventsAsync(ctx, ct).ToListAsync(ct); var typed = result.OfType().ToList(); @@ -183,10 +187,10 @@ namespace Squidex.Domain.Apps.Entities.Contents var envelope = Envelope.Create(@event).SetEventStreamNumber(12); - A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12)) + A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct)) .Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch }); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); var enrichedEvent = result.Single() as EnrichedContentEvent; @@ -205,13 +209,13 @@ namespace Squidex.Domain.Apps.Entities.Contents var dataNow = new ContentData(); var dataOld = new ContentData(); - A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12)) + A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 12, ct)) .Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch, Version = 12, Data = dataNow, Id = @event.ContentId }); - A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 11)) + A.CallTo(() => contentLoader.GetAsync(ctx.AppId.Id, @event.ContentId, 11, ct)) .Returns(new ContentEntity { AppId = ctx.AppId, SchemaId = schemaMatch, Version = 11, Data = dataOld }); - var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, default).ToListAsync(); + var result = await sut.CreateEnrichedEventsAsync(envelope, ctx, ct).ToListAsync(ct); var enrichedEvent = result.Single() as EnrichedContentEvent; diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerProcessTests.cs similarity index 88% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerProcessTests.cs index 20d96c9e7..65c6045a7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentSchedulerProcessTests.cs @@ -17,19 +17,20 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Contents { - public class ContentSchedulerGrainTests + public class ContentSchedulerProcessTests { private readonly IContentRepository contentRepository = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); private readonly IClock clock = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); - private readonly ContentSchedulerGrain sut; + private readonly ContentSchedulerProcess sut; - public ContentSchedulerGrainTests() + public ContentSchedulerProcessTests() { - var log = A.Fake>(); - - sut = new ContentSchedulerGrain(contentRepository, commandBus, clock, log); + sut = new ContentSchedulerProcess(contentRepository, commandBus, A.Fake>()) + { + Clock = clock + }; } [Fact] @@ -60,17 +61,19 @@ namespace Squidex.Domain.Apps.Entities.Contents await sut.PublishAsync(); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => + A.That.Matches(x => x.ContentId == content1.Id && x.Status == content1.ScheduleJob.Status && - x.StatusJobId == content1.ScheduleJob.Id))) + x.StatusJobId == content1.ScheduleJob.Id), + default)) .MustHaveHappened(); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => + A.That.Matches(x => x.ContentId == content2.Id && x.Status == content2.ScheduleJob.Status && - x.StatusJobId == content2.ScheduleJob.Id))) + x.StatusJobId == content2.ScheduleJob.Id), + default)) .MustHaveHappened(); } @@ -94,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Contents await sut.PublishAsync(); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -116,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents A.CallTo(() => contentRepository.QueryScheduledWithoutDataAsync(now, default)) .Returns(new[] { content1 }.ToAsyncEnumerable()); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, default)) .Throws(new DomainObjectNotFoundException(content1.Id.ToString())); await sut.PublishAsync(); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs index 85a578dd7..d328ed250 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentsSearchSourceTests.cs @@ -161,7 +161,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Assert.Empty(result); - A.CallTo(() => contentIndex.SearchAsync(ctx.App, A._, A._, ct)) + A.CallTo(() => contentIndex.SearchAsync(ctx.App, A._, A._, A._)) .MustNotHaveHappened(); } @@ -177,7 +177,7 @@ namespace Squidex.Domain.Apps.Entities.Contents Assert.Empty(result); - A.CallTo(() => contentQuery.QueryAsync(ctx, A._, ct)) + A.CallTo(() => contentQuery.QueryAsync(ctx, A._, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterDeleterTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterDeleterTests.cs deleted file mode 100644 index 57db3d681..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterDeleterTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Entities.TestHelpers; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Counter -{ - public class CounterDeleterTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly CounterDeleter sut; - - public CounterDeleterTests() - { - sut = new CounterDeleter(grainFactory); - } - - [Fact] - public void Should_run_with_default_order() - { - var order = ((IDeleter)sut).Order; - - Assert.Equal(0, order); - } - - [Fact] - public async Task Should_remove_events_from_streams() - { - var app = Mocks.App(NamedId.Of(DomainId.NewGuid(), "my-app")); - - var grain = A.Fake(); - - A.CallTo(() => grainFactory.GetGrain(app.Id.ToString(), null)) - .Returns(grain); - - await sut.DeleteAppAsync(app, default); - - A.CallTo(() => grain.ClearAsync()) - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterGrainTests.cs deleted file mode 100644 index 7db4d3d09..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterGrainTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Core; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Contents.Counter -{ - public class CounterGrainTests - { - private readonly IGrainIdentity identity = A.Fake(); - private readonly IGrainState state = A.Fake>(); - private readonly CounterGrain sut; - - public CounterGrainTests() - { - sut = new CounterGrain(identity, state); - } - - [Fact] - public async Task Should_increment_counters() - { - Assert.Equal(1, await sut.IncrementAsync("Counter1")); - Assert.Equal(2, await sut.IncrementAsync("Counter1")); - - Assert.Equal(1, await sut.IncrementAsync("Counter2")); - Assert.Equal(2, await sut.IncrementAsync("Counter2")); - - A.CallTo(() => state.WriteAsync()) - .MustHaveHappened(4, Times.Exactly); - } - - [Fact] - public async Task Should_reset_counter() - { - Assert.Equal(1, await sut.IncrementAsync("Counter1")); - Assert.Equal(2, await sut.IncrementAsync("Counter1")); - - Assert.Equal(1, await sut.ResetAsync("Counter1", 1)); - - Assert.Equal(2, await sut.IncrementAsync("Counter1")); - - A.CallTo(() => state.WriteAsync()) - .MustHaveHappened(4, Times.Exactly); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs index 5cc226e00..91e52d76d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterJintExtensionTests.cs @@ -8,7 +8,6 @@ using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; -using Orleans; using Squidex.Domain.Apps.Core.Scripting; using Squidex.Infrastructure; using Xunit; @@ -17,15 +16,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter { public class CounterJintExtensionTests { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ICounterGrain counter = A.Fake(); + private readonly ICounterService counterService = A.Fake(); private readonly JintScriptEngine sut; public CounterJintExtensionTests() { var extensions = new IJintExtension[] { - new CounterJintExtension(grainFactory) + new CounterJintExtension(counterService) }; sut = new JintScriptEngine(new MemoryCache(Options.Create(new MemoryCacheOptions())), @@ -41,10 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter { var appId = DomainId.NewGuid(); - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(counter); - - A.CallTo(() => counter.ResetAsync("my", 4)) + A.CallTo(() => counterService.ResetAsync(appId, "my", 4, default)) .Returns(3); const string script = @" @@ -66,10 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter { var appId = DomainId.NewGuid(); - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(counter); - - A.CallTo(() => counter.ResetAsync("my", 4)) + A.CallTo(() => counterService.ResetAsync(appId, "my", 4, default)) .Returns(3); const string script = @" @@ -93,10 +85,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter { var appId = DomainId.NewGuid(); - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(counter); - - A.CallTo(() => counter.IncrementAsync("my")) + A.CallTo(() => counterService.IncrementAsync(appId, "my", default)) .Returns(3); const string script = @" @@ -118,10 +107,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Counter { var appId = DomainId.NewGuid(); - A.CallTo(() => grainFactory.GetGrain(appId.ToString(), null)) - .Returns(counter); - - A.CallTo(() => counter.IncrementAsync("my")) + A.CallTo(() => counterService.IncrementAsync(appId, "my", default)) .Returns(3); const string script = @" diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterServiceTests.cs new file mode 100644 index 000000000..af3b55bed --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Counter/CounterServiceTests.cs @@ -0,0 +1,77 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Squidex.Domain.Apps.Entities.TestHelpers; +using Squidex.Infrastructure; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +namespace Squidex.Domain.Apps.Entities.Contents.Counter +{ + public class CounterServiceTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState state; + private readonly DomainId appId = DomainId.NewGuid(); + private readonly CounterService sut; + + public CounterServiceTests() + { + ct = cts.Token; + + state = new TestState(appId); + + sut = new CounterService(state.PersistenceFactory); + } + + [Fact] + public void Should_run_delete_with_default_order() + { + var order = ((IDeleter)sut).Order; + + Assert.Equal(0, order); + } + + [Fact] + public async Task Should_delete_state_when_app_deleted() + { + await ((IDeleter)sut).DeleteAppAsync(Mocks.App(NamedId.Of(appId, "my-app")), ct); + + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_increment_counters() + { + Assert.Equal(1, await sut.IncrementAsync(appId, "Counter1", ct)); + Assert.Equal(2, await sut.IncrementAsync(appId, "Counter1", ct)); + + Assert.Equal(1, await sut.IncrementAsync(appId, "Counter2", ct)); + Assert.Equal(2, await sut.IncrementAsync(appId, "Counter2", ct)); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, ct)) + .MustHaveHappened(4, Times.Exactly); + } + + [Fact] + public async Task Should_reset_counter() + { + Assert.Equal(1, await sut.IncrementAsync(appId, "Counter1", ct)); + Assert.Equal(2, await sut.IncrementAsync(appId, "Counter1", ct)); + + Assert.Equal(1, await sut.ResetAsync(appId, "Counter1", 1, ct)); + + Assert.Equal(2, await sut.IncrementAsync(appId, "Counter1", ct)); + + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, ct)) + .MustHaveHappened(4, Times.Exactly); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs index 731712d92..539448a2c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentCommandMiddlewareTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Entities.Contents.Commands; using Squidex.Domain.Apps.Entities.Contents.Queries; using Squidex.Domain.Apps.Entities.TestHelpers; @@ -18,7 +17,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { public sealed class ContentCommandMiddlewareTests : HandlerTestBase { - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IDomainObjectCache domainObjectCache = A.Fake(); + private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); private readonly IContentEnricher contentEnricher = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly DomainId contentId = DomainId.NewGuid(); @@ -41,7 +41,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject A.CallTo(() => contextProvider.Context) .Returns(requestContext); - sut = new ContentCommandMiddleware(grainFactory, contentEnricher, contextProvider); + sut = new ContentCommandMiddleware( + domainObjectFactory, + domainObjectCache, + contentEnricher, + contextProvider); } [Fact] @@ -91,13 +95,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject CreateCommand(command); - var grain = A.Fake(); + var domainObject = A.Fake(); - A.CallTo(() => grain.ExecuteAsync(A._)) + A.CallTo(() => domainObject.ExecuteAsync(A._, A._)) .Returns(new CommandResult(command.AggregateId, 1, 0, result)); - A.CallTo(() => grainFactory.GetGrain(command.AggregateId.ToString(), null)) - .Returns(grain); + A.CallTo(() => domainObjectFactory.Create(command.AggregateId)) + .Returns(domainObject); return HandleAsync(sut, command); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectGrainTests.cs deleted file mode 100644 index fea0b7fed..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectGrainTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Core; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.Orleans; -using Xunit; - -#pragma warning disable CA1806 // Do not ignore method results - -namespace Squidex.Domain.Apps.Entities.Contents.DomainObject -{ - public class ContentDomainObjectGrainTests - { - private readonly IActivationLimit limit = A.Fake(); - - [Fact] - public void Should_set_limit() - { - var id = DomainId.NewGuid(); - - var identity = A.Fake(); - - A.CallTo(() => identity.PrimaryKeyString) - .Returns(id.ToString()); - - var factory = A.Fake(); - - A.CallTo(() => factory.Create(id)) - .Returns(A.Dummy()); - - new ContentDomainObjectGrain(identity, factory, limit); - - A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5))) - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs index 597d30d01..7f3d26940 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentDomainObjectTests.cs @@ -1028,7 +1028,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject private async Task PublishAsync(ContentCommand command) { - var result = await sut.ExecuteAsync(CreateContentCommand(command)); + var result = await sut.ExecuteAsync(CreateContentCommand(command), default); return result.Payload; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs index b94227abc..e4fbfa73c 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DomainObject/ContentsBulkUpdateCommandMiddlewareTests.cs @@ -24,6 +24,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { public class ContentsBulkUpdateCommandMiddlewareTests { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; private readonly IContentQueryService contentQuery = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly ICommandBus commandBus = A.Dummy(); @@ -34,6 +36,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject public ContentsBulkUpdateCommandMiddlewareTests() { + ct = cts.Token; + var log = A.Fake>(); sut = new ContentsBulkUpdateCommandMiddleware(contentQuery, contextProvider, log); @@ -73,7 +77,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainObjectNotFoundException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -89,7 +93,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject x.ShouldSkipCleanup() && x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), - schemaId.Name, A.That.Matches(x => x.JsonQuery == query), A._)) + schemaId.Name, A.That.Matches(x => x.JsonQuery == query), ct)) .Returns(ResultList.CreateFrom(2, CreateContent(id), CreateContent(id))); var command = BulkCommand(BulkUpdateContentType.ChangeStatus, query); @@ -99,7 +103,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == null && x.Exception is DomainException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -115,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject x.ShouldSkipCleanup() && x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), - schemaId.Name, A.That.Matches(x => x.JsonQuery == query), A._)) + schemaId.Name, A.That.Matches(x => x.JsonQuery == query), ct)) .Returns(ResultList.CreateFrom(1, CreateContent(id))); var command = BulkCommand(BulkUpdateContentType.Upsert, query: query, data: data); @@ -126,7 +130,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.Data == data && x.ContentId == id))) + A.That.Matches(x => x.Data == data && x.ContentId == id), ct)) .MustHaveHappenedOnceExactly(); } @@ -145,7 +149,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject x.ShouldSkipCleanup() && x.ShouldSkipContentEnrichment() && x.ShouldSkipTotal()), - schemaId.Name, A.That.Matches(x => x.JsonQuery == query), A._)) + schemaId.Name, A.That.Matches(x => x.JsonQuery == query), ct)) .Returns(ResultList.CreateFrom(2, CreateContent(id1), CreateContent(id2))); @@ -161,11 +165,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id == id2 && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.Data == data && x.ContentId == id1))) + A.That.Matches(x => x.Data == data && x.ContentId == id1), ct)) .MustHaveHappenedOnceExactly(); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.Data == data && x.ContentId == id2))) + A.That.Matches(x => x.Data == data && x.ContentId == id2), ct)) .MustHaveHappenedOnceExactly(); } @@ -184,7 +188,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.Data == data && x.ContentId != default))) + A.That.Matches(x => x.Data == data && x.ContentId != default), ct)) .MustHaveHappenedOnceExactly(); } @@ -203,7 +207,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.Data == data && x.ContentId != default))) + A.That.Matches(x => x.Data == data && x.ContentId != default), ct)) .MustHaveHappenedOnceExactly(); } @@ -222,7 +226,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.Data == data && x.ContentId == id))) + A.That.Matches(x => x.Data == data && x.ContentId == id), ct)) .MustHaveHappenedOnceExactly(); } @@ -241,7 +245,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id != default && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.Data == data && x.ContentId == id))) + A.That.Matches(x => x.Data == data && x.ContentId == id), ct)) .MustHaveHappenedOnceExactly(); } @@ -260,7 +264,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.ContentId == id && x.Data == data))) + A.That.Matches(x => x.ContentId == id && x.Data == data), ct)) .MustHaveHappened(); } @@ -278,7 +282,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -297,7 +301,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.ContentId == id && x.Data == data))) + A.That.Matches(x => x.ContentId == id && x.Data == data), ct)) .MustHaveHappened(); } @@ -315,7 +319,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -334,7 +338,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.ContentId == id && x.Data == data))) + A.That.Matches(x => x.ContentId == id && x.Data == data), ct)) .MustHaveHappened(); } @@ -352,7 +356,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -370,7 +374,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.ContentId == id && x.DueTime == null))) + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => x.ContentId == id && x.DueTime == null), ct)) .MustHaveHappened(); } @@ -388,7 +393,8 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.ContentId == id && x.DueTime == time))) + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => x.ContentId == id && x.DueTime == time), ct)) .MustHaveHappened(); } @@ -406,7 +412,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -425,7 +431,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.ContentId == id))) + A.That.Matches(x => x.ContentId == id), ct)) .MustHaveHappened(); } @@ -443,7 +449,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -462,7 +468,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception == null); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => x.ContentId == id))) + A.That.Matches(x => x.ContentId == id), ct)) .MustHaveHappened(); } @@ -480,7 +486,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject Assert.Single(result); Assert.Single(result, x => x.JobIndex == 0 && x.Id == id && x.Exception is DomainForbiddenException); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -488,7 +494,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.DomainObject { var context = new CommandContext(command, commandBus); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); return (context.PlainResult as BulkUpdateResult)!; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs index 54db2e6b3..3454951ca 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLMutationTests.cs @@ -30,7 +30,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL { content = TestContent.Create(contentId, TestSchemas.Ref1.Id, TestSchemas.Ref2.Id, null); - A.CallTo(() => commandBus.PublishAsync(A.Ignored)) + A.CallTo(() => commandBus.PublishAsync(A.Ignored, A._)) .Returns(commandContext); } @@ -72,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -101,11 +101,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ExpectedVersion == EtagVersion.Any && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)))) + A.That.Matches(x => + x.ExpectedVersion == EtagVersion.Any && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A._)) .MustHaveHappened(); } @@ -134,12 +135,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ExpectedVersion == EtagVersion.Any && - x.ContentId == DomainId.Create("123") && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)))) + A.That.Matches(x => + x.ExpectedVersion == EtagVersion.Any && + x.ContentId == DomainId.Create("123") && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A._)) .MustHaveHappened(); } @@ -168,11 +170,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ExpectedVersion == EtagVersion.Any && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)))) + A.That.Matches(x => + x.ExpectedVersion == EtagVersion.Any && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A._)) .MustHaveHappened(); } @@ -214,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -243,11 +246,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Data.Equals(content.Data)))) + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Data.Equals(content.Data)), + A._)) .MustHaveHappened(); } @@ -276,11 +280,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Data.Equals(content.Data)))) + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Data.Equals(content.Data)), + A._)) .MustHaveHappened(); } @@ -322,7 +327,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -351,12 +356,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)))) + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A._)) .MustHaveHappened(); } @@ -385,12 +391,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published && - x.Data.Equals(content.Data)))) + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published && + x.Data.Equals(content.Data)), + A._)) .MustHaveHappened(); } @@ -432,7 +439,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -461,11 +468,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Data.Equals(content.Data)))) + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Data.Equals(content.Data)), + A._)) .MustHaveHappened(); } @@ -494,11 +502,12 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == content.Id && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Data.Equals(content.Data)))) + A.That.Matches(x => + x.ContentId == content.Id && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Data.Equals(content.Data)), + A._)) .MustHaveHappened(); } @@ -540,7 +549,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -571,12 +580,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == contentId && - x.DueTime == dueTime && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published))) + A.That.Matches(x => + x.ContentId == contentId && + x.DueTime == dueTime && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published), + A._)) .MustHaveHappened(); } @@ -605,12 +615,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == contentId && - x.DueTime == null && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published))) + A.That.Matches(x => + x.ContentId == contentId && + x.DueTime == null && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published), + A._)) .MustHaveHappened(); } @@ -639,12 +650,13 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == contentId && - x.DueTime == null && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId) && - x.Status == Status.Published))) + A.That.Matches(x => + x.ContentId == contentId && + x.DueTime == null && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId) && + x.Status == Status.Published), + A._)) .MustHaveHappened(); } @@ -686,7 +698,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -718,10 +730,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL AssertResult(expected, result); A.CallTo(() => commandBus.PublishAsync( - A.That.Matches(x => - x.ContentId == contentId && - x.ExpectedVersion == 10 && - x.SchemaId.Equals(TestSchemas.DefaultId)))) + A.That.Matches(x => + x.ContentId == contentId && + x.ExpectedVersion == 10 && + x.SchemaId.Equals(TestSchemas.DefaultId)), + A._)) .MustHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs index ebbef9c98..1cb95c85d 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs @@ -152,9 +152,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb private static void SetupJson() { - var jsonSerializer = JsonSerializer.Create(TestUtils.DefaultSerializerSettings); + var serializer = JsonSerializer.Create(TestUtils.DefaultSerializerSettings); - BsonJsonConvention.Register(jsonSerializer); + BsonJsonConvention.Register(serializer); } public DomainId RandomAppId() diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculateTokensTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculateTokensTests.cs index c8a76467f..633556399 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculateTokensTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/CalculateTokensTests.cs @@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries public class CalculateTokensTests { private readonly ISchemaEntity schema; - private readonly IJsonSerializer jsonSerializer = A.Fake(); + private readonly IJsonSerializer serializer = A.Fake(); private readonly IUrlGenerator urlGenerator = A.Fake(); private readonly Context requestContext; private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); @@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries schema = Mocks.Schema(appId, schemaId); schemaProvider = x => Task.FromResult((schema, ResolvedComponents.Empty)); - sut = new CalculateTokens(urlGenerator, jsonSerializer); + sut = new CalculateTokens(urlGenerator, serializer); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs index 1bf86d3f5..4c066f6e5 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentEnricherTests.cs @@ -68,10 +68,10 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => step2.EnrichAsync(requestContext, ct)) .MustHaveHappened(); - A.CallTo(() => step1.EnrichAsync(requestContext, A>._, A._, ct)) + A.CallTo(() => step1.EnrichAsync(requestContext, A>._, A._, A._)) .MustNotHaveHappened(); - A.CallTo(() => step2.EnrichAsync(requestContext, A>._, A._, ct)) + A.CallTo(() => step2.EnrichAsync(requestContext, A>._, A._, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs index cffe3e07d..ef5bfc870 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentLoaderTests.cs @@ -6,84 +6,113 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Entities.Contents.DomainObject; using Squidex.Infrastructure; +using Squidex.Infrastructure.Commands; using Xunit; namespace Squidex.Domain.Apps.Entities.Contents.Queries { public class ContentLoaderTests { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IContentGrain grain = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); + private readonly IDomainObjectCache domainObjectCache = A.Fake(); + private readonly ContentDomainObject domainObject = A.Fake(); private readonly DomainId appId = DomainId.NewGuid(); private readonly DomainId id = DomainId.NewGuid(); + private readonly DomainId unqiueId; private readonly ContentLoader sut; public ContentLoaderTests() { - var key = DomainId.Combine(appId, id).ToString(); + ct = cts.Token; - A.CallTo(() => grainFactory.GetGrain(key, null)) - .Returns(grain); + unqiueId = DomainId.Combine(appId, id); - sut = new ContentLoader(grainFactory); + A.CallTo(() => domainObjectCache.GetAsync(A._, A._, ct)) + .Returns(Task.FromResult(null!)); + + A.CallTo(() => domainObjectFactory.Create(unqiueId)) + .Returns(domainObject); + + sut = new ContentLoader(domainObjectFactory, domainObjectCache); } [Fact] public async Task Should_return_null_if_no_state_returned() { - A.CallTo(() => grain.GetStateAsync(10)) - .Returns(Task.FromResult(null!)); + var content = (ContentDomainObject.State)null!; + + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(content); - Assert.Null(await sut.GetAsync(appId, id, 10)); + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); } [Fact] public async Task Should_return_null_if_state_empty() { - var content = new ContentEntity { Version = EtagVersion.Empty }; + var content = new ContentDomainObject.State { Version = EtagVersion.Empty }; - A.CallTo(() => grain.GetStateAsync(10)) + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) .Returns(content); - Assert.Null(await sut.GetAsync(appId, id, 10)); + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); } [Fact] public async Task Should_return_null_if_state_has_other_version() { - var content = new ContentEntity { Version = 5 }; + var content = new ContentDomainObject.State { Version = 5 }; - A.CallTo(() => grain.GetStateAsync(10)) + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) .Returns(content); - Assert.Null(await sut.GetAsync(appId, id, 10)); + Assert.Null(await sut.GetAsync(appId, id, 10, ct)); } [Fact] public async Task Should_not_return_null_if_state_has_other_version_than_any() { - var content = new ContentEntity { Version = 5 }; + var content = new ContentDomainObject.State { Version = 5 }; - A.CallTo(() => grain.GetStateAsync(EtagVersion.Any)) + A.CallTo(() => domainObject.GetSnapshotAsync(EtagVersion.Any, ct)) .Returns(content); - await sut.GetAsync(appId, id, EtagVersion.Any); + var result = await sut.GetAsync(appId, id, EtagVersion.Any, ct); + + Assert.Same(content, result); } [Fact] public async Task Should_return_content_from_state() { - var content = new ContentEntity { Version = 10 }; + var content = new ContentDomainObject.State { Version = 10 }; + + A.CallTo(() => domainObject.GetSnapshotAsync(10, ct)) + .Returns(content); + + var result = await sut.GetAsync(appId, id, 10, ct); + + Assert.Same(content, result); + } + + [Fact] + public async Task Should_return_content_from_cache() + { + var content = new ContentDomainObject.State { Version = 10 }; - A.CallTo(() => grain.GetStateAsync(10)) + A.CallTo(() => domainObjectCache.GetAsync(DomainId.Combine(appId, id), 10, ct)) .Returns(content); - var result = await sut.GetAsync(appId, id, 10); + var result = await sut.GetAsync(appId, id, 10, ct); Assert.Same(content, result); + + A.CallTo(() => domainObjectFactory.Create(unqiueId)) + .MustNotHaveHappened(); } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index 82a9a3c19..2beb151ce 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -180,7 +180,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries var content = CreateContent(DomainId.NewGuid()); - A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, content.Id, 13)) + A.CallTo(() => contentVersionLoader.GetAsync(appId.Id, content.Id, 13, A._)) .Returns(content); var result = await sut.FindAsync(requestContext, schemaId.Name, content.Id, 13, ct); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs index 85bbf701f..5c2ea1234 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/SingletonCommandMiddlewareTests.cs @@ -29,9 +29,10 @@ namespace Squidex.Domain.Apps.Entities.Contents new CommandContext(command, commandBus) .Complete(); - await sut.HandleAsync(context); + await sut.HandleAsync(context, default); - A.CallTo(() => commandBus.PublishAsync(A.That.Matches(x => x.Status == Status.Published))) + A.CallTo(() => commandBus.PublishAsync( + A.That.Matches(x => x.Status == Status.Published), default)) .MustHaveHappened(); } @@ -44,9 +45,9 @@ namespace Squidex.Domain.Apps.Entities.Contents new CommandContext(command, commandBus) .Complete(); - await sut.HandleAsync(context); + await sut.HandleAsync(context, default); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } @@ -58,9 +59,9 @@ namespace Squidex.Domain.Apps.Entities.Contents var context = new CommandContext(command, commandBus); - await sut.HandleAsync(context); + await sut.HandleAsync(context, default); - A.CallTo(() => commandBus.PublishAsync(A._)) + A.CallTo(() => commandBus.PublishAsync(A._, A._)) .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs index d0819147a..9f3382d47 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Text/CachingTextIndexerStateTests.cs @@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Text A.CallTo(() => inner.SetAsync(A>.That.Matches(x => x.Count == 1 && x[0].IsDeleted), ct)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => inner.GetAsync(A>._, ct)) + A.CallTo(() => inner.GetAsync(A>._, A._)) .MustNotHaveHappened(); } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs index 3d67510af..fca723207 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleCommandMiddlewareTests.cs @@ -6,7 +6,6 @@ // ========================================================================== using FakeItEasy; -using Orleans; using Squidex.Domain.Apps.Entities.Rules.Commands; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; @@ -17,7 +16,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject { public sealed class RuleCommandMiddlewareTests : HandlerTestBase { - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly IDomainObjectFactory domainObjectFactory = A.Fake(); private readonly IRuleEnricher ruleEnricher = A.Fake(); private readonly IContextProvider contextProvider = A.Fake(); private readonly DomainId ruleId = DomainId.NewGuid(); @@ -40,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject A.CallTo(() => contextProvider.Context) .Returns(requestContext); - sut = new RuleCommandMiddleware(grainFactory, ruleEnricher, contextProvider); + sut = new RuleCommandMiddleware(domainObjectFactory, ruleEnricher, contextProvider); } [Fact] @@ -90,13 +89,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject CreateCommand(command); - var grain = A.Fake(); + var domainObject = A.Fake(); - A.CallTo(() => grain.ExecuteAsync(A._)) + A.CallTo(() => domainObject.ExecuteAsync(A._, A._)) .Returns(new CommandResult(command.AggregateId, 1, 0, result)); - A.CallTo(() => grainFactory.GetGrain(command.AggregateId.ToString(), null)) - .Returns(grain); + A.CallTo(() => domainObjectFactory.Create(command.AggregateId)) + .Returns(domainObject); return HandleAsync(sut, command); } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs index 7f8803a7e..265938765 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/DomainObject/RuleDomainObjectTests.cs @@ -290,7 +290,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.DomainObject private async Task PublishAsync(RuleCommand command) { - var result = await sut.ExecuteAsync(CreateRuleCommand(command)); + var result = await sut.ExecuteAsync(CreateRuleCommand(command), default); return result.Payload; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesCacheGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesCacheGrainTests.cs deleted file mode 100644 index 1f654d0e1..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesCacheGrainTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Core; -using Squidex.Domain.Apps.Entities.Rules.Repositories; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Rules.Indexes -{ - public class RulesCacheGrainTests - { - private readonly IGrainIdentity identity = A.Fake(); - private readonly IRuleRepository ruleRepository = A.Fake(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly RulesCacheGrain sut; - - public RulesCacheGrainTests() - { - A.CallTo(() => identity.PrimaryKeyString) - .Returns(appId.ToString()); - - sut = new RulesCacheGrain(identity, ruleRepository); - } - - [Fact] - public async Task Should_provide_rule_ids_from_repository_once() - { - var ids = new List - { - DomainId.NewGuid(), - DomainId.NewGuid() - }; - - A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default)) - .Returns(ids); - - var result1 = await sut.GetRuleIdsAsync(); - var result2 = await sut.GetRuleIdsAsync(); - - Assert.Equal(ids, result1); - Assert.Equal(ids, result2); - - A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_add_id_to_loaded_result() - { - var ids = new List - { - DomainId.NewGuid(), - DomainId.NewGuid() - }; - - var newId = DomainId.NewGuid(); - - A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default)) - .Returns(ids); - - await sut.GetRuleIdsAsync(); - await sut.AddAsync(newId); - - var result = await sut.GetRuleIdsAsync(); - - Assert.Equal(ids.Union(Enumerable.Repeat(newId, 1)), result); - - A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_remove_id_from_loaded_result() - { - var ids = new List - { - DomainId.NewGuid(), - DomainId.NewGuid() - }; - - var newId = DomainId.NewGuid(); - - A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default)) - .Returns(ids); - - await sut.GetRuleIdsAsync(); - await sut.RemoveAsync(ids[1]); - - var result = await sut.GetRuleIdsAsync(); - - Assert.Equal(ids.Take(1), result); - - A.CallTo(() => ruleRepository.QueryIdsAsync(appId, default)) - .MustHaveHappenedOnceExactly(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs index 026c3d008..c4205d975 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesIndexTests.cs @@ -6,29 +6,25 @@ // ========================================================================== using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Entities.Rules.Commands; -using Squidex.Domain.Apps.Entities.Rules.DomainObject; +using Squidex.Domain.Apps.Entities.Rules.Repositories; using Squidex.Infrastructure; -using Squidex.Infrastructure.Commands; using Xunit; namespace Squidex.Domain.Apps.Entities.Rules.Indexes { public class RulesIndexTests { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ICommandBus commandBus = A.Fake(); - private readonly IRulesCacheGrain cache = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IRuleRepository ruleRepository = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly RulesIndex sut; public RulesIndexTests() { - A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) - .Returns(cache); + ct = cts.Token; - sut = new RulesIndex(grainFactory); + sut = new RulesIndex(ruleRepository); } [Fact] @@ -36,10 +32,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { var rule = SetupRule(0); - A.CallTo(() => cache.GetRuleIdsAsync()) - .Returns(new List { rule.Id }); + A.CallTo(() => ruleRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List { rule }); - var actual = await sut.GetRulesAsync(appId.Id); + var actual = await sut.GetRulesAsync(appId.Id, ct); Assert.Same(actual[0], rule); } @@ -49,10 +45,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { var rule = SetupRule(-1); - A.CallTo(() => cache.GetRuleIdsAsync()) - .Returns(new List { rule.Id }); + A.CallTo(() => ruleRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List { rule }); - var actual = await sut.GetRulesAsync(appId.Id); + var actual = await sut.GetRulesAsync(appId.Id, ct); Assert.Empty(actual); } @@ -62,64 +58,17 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes { var rule = SetupRule(0, true); - A.CallTo(() => cache.GetRuleIdsAsync()) - .Returns(new List { rule.Id }); + A.CallTo(() => ruleRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List { rule }); - var actual = await sut.GetRulesAsync(appId.Id); + var actual = await sut.GetRulesAsync(appId.Id, ct); Assert.Empty(actual); } - [Fact] - public async Task Should_add_rule_to_index_on_create() - { - var ruleId = DomainId.NewGuid(); - - var command = new CreateRule { RuleId = ruleId, AppId = appId }; - - var context = - new CommandContext(command, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => cache.AddAsync(ruleId)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_remove_rule_from_index_on_delete() - { - var rule = SetupRule(0); - - var command = new DeleteRule { RuleId = rule.Id, AppId = appId }; - - var context = - new CommandContext(command, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => cache.RemoveAsync(rule.Id)) - .MustHaveHappened(); - } - private IRuleEntity SetupRule(long version, bool isDeleted = false) { - var ruleId = DomainId.NewGuid(); - - var ruleEntity = new RuleEntity { Id = ruleId, AppId = appId, Version = version, IsDeleted = isDeleted }; - var ruleGrain = A.Fake(); - - A.CallTo(() => ruleGrain.GetStateAsync()) - .Returns(ruleEntity); - - var key = DomainId.Combine(appId, ruleId).ToString(); - - A.CallTo(() => grainFactory.GetGrain(key, null)) - .Returns(ruleGrain); - - return ruleEntity; + return new RuleEntity { AppId = appId, Version = version, IsDeleted = isDeleted }; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs similarity index 94% rename from backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerGrainTests.cs rename to backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs index ea3db12c4..d804dc336 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerGrainTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleDequeuerWorkerTests.cs @@ -16,20 +16,23 @@ using Xunit; namespace Squidex.Domain.Apps.Entities.Rules { - public class RuleDequeuerGrainTests + public class RuleDequeuerWorkerTests { private readonly IClock clock = A.Fake(); - private readonly ILogger log = A.Dummy>(); private readonly IRuleEventRepository ruleEventRepository = A.Fake(); private readonly IRuleService ruleService = A.Fake(); - private readonly RuleDequeuerGrain sut; + private readonly ILogger log = A.Dummy>(); + private readonly RuleDequeuerWorker sut; - public RuleDequeuerGrainTests() + public RuleDequeuerWorkerTests() { A.CallTo(() => clock.GetCurrentInstant()) .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - sut = new RuleDequeuerGrain(ruleService, ruleEventRepository, log, clock); + sut = new RuleDequeuerWorker(ruleService, ruleEventRepository, log) + { + Clock = clock + }; } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs index e7fca338c..b5a525b0f 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/DomainObject/SchemaDomainObjectTests.cs @@ -785,7 +785,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.DomainObject private async Task PublishAsync(T command) where T : SquidexCommand, IAggregateCommand { - var result = await sut.ExecuteAsync(CreateCommand(command)); + var result = await sut.ExecuteAsync(CreateCommand(command), default); return result.Payload; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasCacheGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasCacheGrainTests.cs deleted file mode 100644 index 443c22a69..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasCacheGrainTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Core; -using Squidex.Domain.Apps.Entities.Schemas.Repositories; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Schemas.Indexes -{ - public class SchemasCacheGrainTests - { - private readonly IGrainIdentity identity = A.Fake(); - private readonly ISchemaRepository schemaRepository = A.Fake(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly SchemasCacheGrain sut; - - public SchemasCacheGrainTests() - { - A.CallTo(() => identity.PrimaryKeyString) - .Returns(appId.ToString()); - - sut = new SchemasCacheGrain(identity, schemaRepository); - } - - [Fact] - public async Task Should_not_reserve_name_if_already_used() - { - var ids = new Dictionary - { - ["name1"] = DomainId.NewGuid() - }; - - A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default)) - .Returns(ids); - - var token1 = await sut.ReserveAsync(DomainId.NewGuid(), "name1"); - var token2 = await sut.ReserveAsync(DomainId.NewGuid(), "name2"); - - Assert.Null(token1); - Assert.NotNull(token2); - } - - [Fact] - public async Task Should_provide_schema_ids_from_repository_once() - { - var ids = new Dictionary - { - ["name1"] = DomainId.NewGuid(), - ["name2"] = DomainId.NewGuid() - }; - - A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default)) - .Returns(ids); - - var result1 = await sut.GetSchemaIdsAsync(); - var result2 = await sut.GetSchemaIdsAsync(); - - Assert.Equal(ids.Values, result1); - Assert.Equal(ids.Values, result2); - - A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_add_id_to_loaded_result() - { - var ids = new Dictionary - { - ["name1"] = DomainId.NewGuid(), - ["name2"] = DomainId.NewGuid() - }; - - var newId = DomainId.NewGuid(); - - A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default)) - .Returns(ids); - - await sut.GetSchemaIdsAsync(); - await sut.AddAsync(newId, "new-name"); - - var result = await sut.GetSchemaIdsAsync(); - - Assert.Equal(ids.Values.Union(Enumerable.Repeat(newId, 1)), result); - - A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default)) - .MustHaveHappenedOnceExactly(); - } - - [Fact] - public async Task Should_remove_id_from_loaded_result() - { - var ids = new Dictionary - { - ["name1"] = DomainId.NewGuid(), - ["name2"] = DomainId.NewGuid() - }; - - var newId = DomainId.NewGuid(); - - A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default)) - .Returns(ids); - - await sut.GetSchemaIdsAsync(); - await sut.RemoveAsync(ids.ElementAt(0).Value); - - var result = await sut.GetSchemaIdsAsync(); - - Assert.Equal(ids.Values.Take(1), result); - - A.CallTo(() => schemaRepository.QueryIdsAsync(appId, default)) - .MustHaveHappenedOnceExactly(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs index f9eff1c84..82d97fcf3 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs @@ -9,13 +9,14 @@ using FakeItEasy; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Orleans; using Squidex.Caching; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Schemas.Commands; -using Squidex.Domain.Apps.Entities.Schemas.DomainObject; +using Squidex.Domain.Apps.Entities.Schemas.Repositories; using Squidex.Infrastructure; using Squidex.Infrastructure.Commands; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; using Squidex.Infrastructure.Validation; using Xunit; @@ -23,129 +24,126 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes { public class SchemasIndexTests { - private readonly IGrainFactory grainFactory = A.Fake(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState state; + private readonly ISchemaRepository schemaRepository = A.Fake(); private readonly ICommandBus commandBus = A.Fake(); - private readonly ISchemasCacheGrain cache = A.Fake(); private readonly NamedId appId = NamedId.Of(DomainId.NewGuid(), "my-app"); private readonly NamedId schemaId = NamedId.Of(DomainId.NewGuid(), "my-schema"); private readonly SchemasIndex sut; public SchemasIndexTests() { - A.CallTo(() => grainFactory.GetGrain(appId.Id.ToString(), null)) - .Returns(this.cache); + state = new TestState($"{appId.Id}_Schemas"); - var localCache = + ct = cts.Token; + + var replicatedCache = new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(A.Fake>()), Options.Create(new ReplicatedCacheOptions { Enable = true })); - sut = new SchemasIndex(grainFactory, localCache); + sut = new SchemasIndex(schemaRepository, replicatedCache, state.PersistenceFactory); } [Fact] public async Task Should_resolve_schema_by_name() { - var (expected, _) = SetupSchema(); + var expected = SetupSchema(); - A.CallTo(() => cache.GetSchemaIdAsync(schemaId.Name)) - .Returns(schemaId.Id); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(expected); - var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false); - var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false); + var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false, ct); + var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, false, ct); Assert.Same(expected, actual1); Assert.Same(expected, actual2); - A.CallTo(() => grainFactory.GetGrain(A._, null)) - .MustHaveHappenedTwiceExactly(); - - A.CallTo(() => cache.GetSchemaIdAsync(A._)) + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) .MustHaveHappenedTwiceExactly(); } [Fact] public async Task Should_resolve_schema_by_name_and_id_if_cached_before() { - var (expected, _) = SetupSchema(); + var expected = SetupSchema(); - A.CallTo(() => cache.GetSchemaIdAsync(schemaId.Name)) - .Returns(schemaId.Id); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(expected); - var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true); - var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true); - var actual3 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true); + var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true, ct); + var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true, ct); + var actual3 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true, ct); Assert.Same(expected, actual1); Assert.Same(expected, actual2); Assert.Same(expected, actual3); - A.CallTo(() => grainFactory.GetGrain(A._, null)) - .MustHaveHappenedOnceExactly(); - - A.CallTo(() => cache.GetSchemaIdAsync(A._)) + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) .MustHaveHappenedOnceExactly(); } [Fact] public async Task Should_resolve_schema_by_id() { - var (expected, _) = SetupSchema(); + var expected = SetupSchema(); + + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) + .Returns(expected); - var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, false); - var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, false); + var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, false, ct); + var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, false, ct); Assert.Same(expected, actual1); Assert.Same(expected, actual2); - A.CallTo(() => grainFactory.GetGrain(A._, null)) + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) .MustHaveHappenedTwiceExactly(); - - A.CallTo(() => cache.GetSchemaIdAsync(A._)) - .MustNotHaveHappened(); } [Fact] public async Task Should_resolve_schema_by_id_and_name_if_cached_before() { - var (expected, _) = SetupSchema(); + var expected = SetupSchema(); + + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) + .Returns(expected); - var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true); - var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true); - var actual3 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true); + var actual1 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true, ct); + var actual2 = await sut.GetSchemaAsync(appId.Id, schemaId.Id, true, ct); + var actual3 = await sut.GetSchemaAsync(appId.Id, schemaId.Name, true, ct); Assert.Same(expected, actual1); Assert.Same(expected, actual2); Assert.Same(expected, actual3); - A.CallTo(() => grainFactory.GetGrain(A._, null)) + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Id, ct)) .MustHaveHappenedOnceExactly(); - - A.CallTo(() => cache.GetSchemaIdAsync(A._)) - .MustNotHaveHappened(); } [Fact] - public async Task Should_resolve_schemas_by_id() + public async Task Should_resolve_schemas() { - var (schema, _) = SetupSchema(); + var expected = SetupSchema(); - A.CallTo(() => cache.GetSchemaIdsAsync()) - .Returns(new List { schema.Id }); + A.CallTo(() => schemaRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List { expected }); - var actual = await sut.GetSchemasAsync(appId.Id); + var actual = await sut.GetSchemasAsync(appId.Id, ct); - Assert.Same(actual[0], schema); + Assert.Same(actual[0], expected); } [Fact] public async Task Should_return_empty_schemas_if_schema_not_created() { - var (schema, _) = SetupSchema(EtagVersion.Empty); + var expected = SetupSchema(EtagVersion.Empty); - A.CallTo(() => cache.GetSchemaIdsAsync()) - .Returns(new List { schema.Id }); + A.CallTo(() => schemaRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List { expected }); - var actual = await sut.GetSchemasAsync(appId.Id); + var actual = await sut.GetSchemasAsync(appId.Id, ct); Assert.Empty(actual); } @@ -153,23 +151,21 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes [Fact] public async Task Should_return_empty_schemas_if_schema_deleted() { - var (schema, _) = SetupSchema(0, true); + var expected = SetupSchema(0, true); - A.CallTo(() => cache.GetSchemaIdsAsync()) - .Returns(new List { schema.Id }); + A.CallTo(() => schemaRepository.QueryAllAsync(appId.Id, ct)) + .Returns(new List { expected }); - var actual = await sut.GetSchemasAsync(appId.Id); + var actual = await sut.GetSchemasAsync(appId.Id, ct); Assert.Empty(actual); } [Fact] - public async Task Should_add_schema_to_index_if_creating() + public async Task Should_take_and_remove_reservation_if_created() { - var token = RandomHash.Simple(); - - A.CallTo(() => cache.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(token); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(Task.FromResult(null)); var command = Create(schemaId.Name); @@ -177,42 +173,55 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes new CommandContext(command, commandBus) .Complete(); - await sut.HandleAsync(context); + NameReservation? madeReservation = null; + + await sut.HandleAsync(context, (c, ct) => + { + madeReservation = state.Snapshot.Reservations.FirstOrDefault(); + + return Task.CompletedTask; + }, ct); - A.CallTo(() => cache.AddAsync(schemaId.Id, schemaId.Name)) - .MustHaveHappened(); + Assert.Empty(state.Snapshot.Reservations); - A.CallTo(() => cache.RemoveReservationAsync(A._)) - .MustHaveHappened(); + Assert.Equal(schemaId.Id, madeReservation?.Id); + Assert.Equal(schemaId.Name, madeReservation?.Name); } [Fact] public async Task Should_clear_reservation_if_schema_creation_failed() { - var token = RandomHash.Simple(); - - A.CallTo(() => cache.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(token); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(Task.FromResult(null)); var command = Create(schemaId.Name); var context = - new CommandContext(command, commandBus); + new CommandContext(command, commandBus) + .Complete(); - await sut.HandleAsync(context); + NameReservation? madeReservation = null; - A.CallTo(() => cache.AddAsync(A._, A._)) - .MustNotHaveHappened(); + await Assert.ThrowsAnyAsync(() => sut.HandleAsync(context, (c, ct) => + { + madeReservation = state.Snapshot.Reservations.FirstOrDefault(); + + throw new InvalidOperationException(); + }, ct)); + + Assert.Empty(state.Snapshot.Reservations); - A.CallTo(() => cache.RemoveReservationAsync(token)) - .MustHaveHappened(); + Assert.Equal(schemaId.Id, madeReservation?.Id); + Assert.Equal(schemaId.Name, madeReservation?.Name); } [Fact] - public async Task Should_not_add_to_indexes_if_name_is_reserved() + public async Task Should_not_create_schema_if_name_is_reserved() { - A.CallTo(() => cache.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(Task.FromResult(null)); + state.Snapshot.Reservations.Add(new NameReservation(RandomHash.Simple(), schemaId.Name, DomainId.NewGuid())); + + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(Task.FromResult(null)); var command = Create(schemaId.Name); @@ -220,25 +229,14 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes new CommandContext(command, commandBus) .Complete(); - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); - - A.CallTo(() => cache.AddAsync(A._, A._)) - .MustNotHaveHappened(); - - A.CallTo(() => cache.RemoveReservationAsync(A._)) - .MustNotHaveHappened(); + await Assert.ThrowsAsync(() => sut.HandleAsync(context, ct)); } [Fact] - public async Task Should_not_add_to_indexes_if_name_is_taken() + public async Task Should_not_create_schema_if_name_is_taken() { - var token = RandomHash.Simple(); - - A.CallTo(() => cache.ReserveAsync(schemaId.Id, schemaId.Name)) - .Returns(Task.FromResult(token)); - - A.CallTo(() => cache.GetSchemaIdAsync(schemaId.Name)) - .Returns(schemaId.Id); + A.CallTo(() => schemaRepository.FindAsync(appId.Id, schemaId.Name, ct)) + .Returns(SetupSchema()); var command = Create(schemaId.Name); @@ -246,19 +244,16 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes new CommandContext(command, commandBus) .Complete(); - await Assert.ThrowsAsync(() => sut.HandleAsync(context)); + await Assert.ThrowsAsync(() => sut.HandleAsync(context, ct)); - A.CallTo(() => cache.AddAsync(A._, A._)) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustNotHaveHappened(); - - A.CallTo(() => cache.RemoveReservationAsync(token)) - .MustHaveHappened(); } [Fact] - public async Task Should_update_index_with_result_if_schema_is_updated() + public async Task Should_not_make_an_update_for_other_command() { - var (schema, schemaGrain) = SetupSchema(); + var schema = SetupSchema(); var command = new UpdateSchema { SchemaId = schemaId, AppId = appId }; @@ -266,60 +261,28 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes new CommandContext(command, commandBus) .Complete(schema); - await sut.HandleAsync(context); + await sut.HandleAsync(context, ct); - A.CallTo(() => schemaGrain.GetStateAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustNotHaveHappened(); } - [Fact] - public async Task Should_remove_schema_from_index_if_deleted_and_exists() - { - var (schema, _) = SetupSchema(isDeleted: true); - - var command = new DeleteSchema { SchemaId = schemaId, AppId = appId }; - - var context = - new CommandContext(command, commandBus) - .Complete(); - - await sut.HandleAsync(context); - - A.CallTo(() => cache.RemoveAsync(schema.Id)) - .MustHaveHappened(); - } - private CreateSchema Create(string name) { return new CreateSchema { SchemaId = schemaId.Id, Name = name, AppId = appId }; } - private (ISchemaEntity, ISchemaGrain) SetupSchema(long version = 0, bool isDeleted = false) + private ISchemaEntity SetupSchema(long version = 0, bool isDeleted = false) { - var schemaEntity = A.Fake(); - - A.CallTo(() => schemaEntity.SchemaDef) - .Returns(new Schema(schemaId.Name)); - A.CallTo(() => schemaEntity.Id) - .Returns(schemaId.Id); - A.CallTo(() => schemaEntity.AppId) - .Returns(appId); - A.CallTo(() => schemaEntity.Version) - .Returns(version); - A.CallTo(() => schemaEntity.IsDeleted) - .Returns(isDeleted); - - var schemaGrain = A.Fake(); - - A.CallTo(() => schemaGrain.GetStateAsync()) - .Returns(schemaEntity); - - var key = DomainId.Combine(appId, schemaId.Id).ToString(); + var schema = A.Fake(); - A.CallTo(() => grainFactory.GetGrain(key, null)) - .Returns(schemaGrain); + A.CallTo(() => schema.SchemaDef).Returns(new Schema(schemaId.Name)); + A.CallTo(() => schema.Id).Returns(schemaId.Id); + A.CallTo(() => schema.AppId).Returns(appId); + A.CallTo(() => schema.Version).Returns(version); + A.CallTo(() => schema.IsDeleted).Returns(isDeleted); - return (schemaEntity, schemaGrain); + return schema; } } } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs index 6800665d8..a61c988cd 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs @@ -80,10 +80,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb { var app = A.Fake(); - A.CallTo(() => app.Id) - .Returns(id); - A.CallTo(() => app.Version) - .Returns(version); + A.CallTo(() => app.Id).Returns(id); + A.CallTo(() => app.Version).Returns(version); return app; } @@ -92,10 +90,8 @@ namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb { var schema = A.Fake(); - A.CallTo(() => schema.Id) - .Returns(id); - A.CallTo(() => schema.Version) - .Returns(version); + A.CallTo(() => schema.Id).Returns(id); + A.CallTo(() => schema.Version).Returns(version); return schema; } diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj index a8d4ee96b..2bdaa7354 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj @@ -18,6 +18,7 @@ + diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs deleted file mode 100644 index 95b3fba2e..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/GrainTagServiceTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Xunit; - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public class GrainTagServiceTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly ITagGrain grain = A.Fake(); - private readonly DomainId appId = DomainId.NewGuid(); - private readonly GrainTagService sut; - - public GrainTagServiceTests() - { - A.CallTo(() => grainFactory.GetGrain($"{appId}_Assets", null)) - .Returns(grain); - - sut = new GrainTagService(grainFactory); - } - - [Fact] - public async Task Should_call_grain_if_clearing() - { - await sut.ClearAsync(appId, TagGroups.Assets); - - A.CallTo(() => grain.ClearAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_call_grain_if_renaming() - { - await sut.RenameTagAsync(appId, TagGroups.Assets, "name", "newName"); - - A.CallTo(() => grain.RenameTagAsync("name", "newName")) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_call_grain_if_rebuilding() - { - var tags = new TagsExport(); - - await sut.RebuildTagsAsync(appId, TagGroups.Assets, tags); - - A.CallTo(() => grain.RebuildAsync(tags)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_call_grain_if_retrieving_raw_tags() - { - await sut.GetExportableTagsAsync(appId, TagGroups.Assets); - - A.CallTo(() => grain.GetExportableTagsAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_call_grain_if_retrieving_tags() - { - await sut.GetTagsAsync(appId, TagGroups.Assets); - - A.CallTo(() => grain.GetTagsAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_call_grain_if_resolving_tag_ids() - { - var tagNames = new HashSet(); - - await sut.GetTagIdsAsync(appId, TagGroups.Assets, tagNames); - - A.CallTo(() => grain.GetTagIdsAsync(tagNames)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_call_grain_if_denormalizing_tags() - { - var tagIds = new HashSet(); - - await sut.DenormalizeTagsAsync(appId, TagGroups.Assets, tagIds); - - A.CallTo(() => grain.DenormalizeTagsAsync(tagIds)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_call_grain_if_normalizing_tags() - { - var tagIds = new HashSet(); - var tagNames = new HashSet(); - - await sut.NormalizeTagsAsync(appId, TagGroups.Assets, tagNames, tagIds); - - A.CallTo(() => grain.NormalizeTagsAsync(tagNames, tagIds)) - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs deleted file mode 100644 index c10d5d897..000000000 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs +++ /dev/null @@ -1,207 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using FluentAssertions; -using Orleans.Core; -using Squidex.Domain.Apps.Core.Tags; -using Squidex.Infrastructure; -using Squidex.Infrastructure.Orleans; -using Xunit; - -#pragma warning disable IDE0059 // Unnecessary assignment of a value - -namespace Squidex.Domain.Apps.Entities.Tags -{ - public class TagGrainTests - { - private readonly IGrainIdentity identity = A.Fake(); - private readonly IGrainState state = A.Fake>(); - private readonly string id = DomainId.NewGuid().ToString(); - private readonly TagGrain sut; - - public TagGrainTests() - { - A.CallTo(() => identity.PrimaryKeyString) - .Returns(id); - - A.CallTo(() => state.ClearAsync()) - .Invokes(() => state.Value = new TagGrain.State()); - - sut = new TagGrain(identity, state); - } - - [Fact] - public async Task Should_delete_and_reset_state_if_cleaning() - { - await sut.NormalizeTagsAsync(HashSet.Of("name1", "name2"), null); - await sut.NormalizeTagsAsync(HashSet.Of("name2", "name3"), null); - await sut.ClearAsync(); - - var allTags = await sut.GetTagsAsync(); - - Assert.Empty(allTags); - - A.CallTo(() => state.ClearAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_rename_tag() - { - await sut.NormalizeTagsAsync(HashSet.Of("tag1"), null); - await sut.NormalizeTagsAsync(HashSet.Of("tag1"), null); - - await sut.RenameTagAsync("tag1", "tag1_new"); - - // Forward the old name to the new name. - await sut.NormalizeTagsAsync(HashSet.Of("tag1"), null); - await sut.NormalizeTagsAsync(HashSet.Of("tag1_new"), null); - - var allTags = await sut.GetTagsAsync(); - - Assert.Equal(new Dictionary - { - ["tag1_new"] = 4 - }, allTags); - } - - [Fact] - public async Task Should_rename_tag_twice() - { - await sut.NormalizeTagsAsync(HashSet.Of("tag1"), null); - await sut.NormalizeTagsAsync(HashSet.Of("tag1"), null); - - await sut.RenameTagAsync("tag1", "tag1_new1"); - - // Rename again. - await sut.RenameTagAsync("tag1_new1", "tag1_new2"); - - // Forward the old name to the new name. - await sut.NormalizeTagsAsync(HashSet.Of("tag1"), null); - await sut.NormalizeTagsAsync(HashSet.Of("tag1_new1"), null); - await sut.NormalizeTagsAsync(HashSet.Of("tag1_new2"), null); - - var allTags = await sut.GetTagsAsync(); - - Assert.Equal(new Dictionary - { - ["tag1_new2"] = 5 - }, allTags); - } - - [Fact] - public async Task Should_rename_tag_back() - { - await sut.NormalizeTagsAsync(HashSet.Of("tag1"), null); - await sut.NormalizeTagsAsync(HashSet.Of("tag1"), null); - - await sut.RenameTagAsync("tag1", "tag1_new1"); - - // Rename back. - await sut.RenameTagAsync("tag1_new1", "tag1"); - - // Forward the old name to the new name. - await sut.NormalizeTagsAsync(HashSet.Of("tag1"), null); - - var allTags = await sut.GetTagsAsync(); - - Assert.Equal(new Dictionary - { - ["tag1"] = 3 - }, allTags); - } - - [Fact] - public async Task Should_rebuild_tags() - { - var tags = new TagsExport - { - Tags = new Dictionary - { - ["id1"] = new Tag { Name = "name1", Count = 1 }, - ["id2"] = new Tag { Name = "name2", Count = 2 }, - ["id3"] = new Tag { Name = "name3", Count = 6 } - } - }; - - await sut.RebuildAsync(tags); - - var allTags = await sut.GetTagsAsync(); - - Assert.Equal(new Dictionary - { - ["name1"] = 1, - ["name2"] = 2, - ["name3"] = 6 - }, allTags); - - var export = await sut.GetExportableTagsAsync(); - - export.Should().BeEquivalentTo(tags); - } - - [Fact] - public async Task Should_add_tags_to_grain() - { - await sut.NormalizeTagsAsync(HashSet.Of("name1", "name2"), null); - await sut.NormalizeTagsAsync(HashSet.Of("name2", "name3"), null); - - var allTags = await sut.GetTagsAsync(); - - Assert.Equal(new Dictionary - { - ["name1"] = 1, - ["name2"] = 2, - ["name3"] = 1 - }, allTags); - } - - [Fact] - public async Task Should_not_add_tags_if_already_added() - { - var result1 = await sut.NormalizeTagsAsync(HashSet.Of("name1", "name2"), null); - var result2 = await sut.NormalizeTagsAsync(HashSet.Of("name1", "name2", "name3"), new HashSet(result1.Values)); - - var allTags = await sut.GetTagsAsync(); - - Assert.Equal(new Dictionary - { - ["name1"] = 1, - ["name2"] = 1, - ["name3"] = 1 - }, allTags); - } - - [Fact] - public async Task Should_remove_tags_from_grain() - { - var result1 = await sut.NormalizeTagsAsync(HashSet.Of("name1", "name2"), null); - var result2 = await sut.NormalizeTagsAsync(HashSet.Of("name2", "name3"), null); - - await sut.NormalizeTagsAsync(null, new HashSet(result1.Values)); - - var allTags = await sut.GetTagsAsync(); - - Assert.Equal(new Dictionary - { - ["name2"] = 1, - ["name3"] = 1 - }, allTags); - } - - [Fact] - public async Task Should_resolve_tag_names() - { - var tagIds = await sut.NormalizeTagsAsync(HashSet.Of("name1", "name2"), null); - - var tagNames = await sut.GetTagIdsAsync(HashSet.Of("name1", "name2", "invalid1")); - - Assert.Equal(tagIds, tagNames); - } - } -} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagServiceTests.cs new file mode 100644 index 000000000..6c81b6fb6 --- /dev/null +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagServiceTests.cs @@ -0,0 +1,211 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using FluentAssertions; +using Squidex.Domain.Apps.Core.Tags; +using Squidex.Infrastructure; +using Squidex.Infrastructure.TestHelpers; +using Xunit; + +#pragma warning disable IDE0059 // Unnecessary assignment of a value + +namespace Squidex.Domain.Apps.Entities.Tags +{ + public class TagServiceTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState state; + private readonly DomainId appId = DomainId.NewGuid(); + private readonly string group = DomainId.NewGuid().ToString(); + private readonly string stateId; + private readonly TagService sut; + + public TagServiceTests() + { + ct = cts.Token; + + stateId = $"{appId}_{group}"; + state = new TestState(stateId); + + sut = new TagService(state.PersistenceFactory); + } + + [Fact] + public async Task Should_delete_and_reset_state_if_cleaning() + { + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("name1", "name2"), null, ct); + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("name2", "name3"), null, ct); + await sut.ClearAsync(appId, group, ct); + + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Empty(allTags); + + A.CallTo(() => state.Persistence.DeleteAsync(ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_rename_tag() + { + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1"), null, ct); + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1"), null, ct); + + await sut.RenameTagAsync(appId, group, "tag1", "tag1_new", ct); + + // Forward the old name to the new name. + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1"), null, ct); + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1_new"), null, ct); + + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Equal(new Dictionary + { + ["tag1_new"] = 4 + }, allTags); + } + + [Fact] + public async Task Should_rename_tag_twice() + { + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1"), null, ct); + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1"), null, ct); + + await sut.RenameTagAsync(appId, group, "tag1", "tag1_new1", ct); + + // Rename again. + await sut.RenameTagAsync(appId, group, "tag1_new1", "tag1_new2", ct); + + // Forward the old name to the new name. + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1"), null, ct); + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1_new1"), null, ct); + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1_new2"), null, ct); + + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Equal(new Dictionary + { + ["tag1_new2"] = 5 + }, allTags); + } + + [Fact] + public async Task Should_rename_tag_back() + { + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1"), null, ct); + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1"), null, ct); + + await sut.RenameTagAsync(appId, group, "tag1", "tag1_new1", ct); + + // Rename back. + await sut.RenameTagAsync(appId, group, "tag1_new1", "tag1", ct); + + // Forward the old name to the new name. + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("tag1"), null, ct); + + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Equal(new Dictionary + { + ["tag1"] = 3 + }, allTags); + } + + [Fact] + public async Task Should_rebuild_tags() + { + var tags = new TagsExport + { + Tags = new Dictionary + { + ["id1"] = new Tag { Name = "name1", Count = 1 }, + ["id2"] = new Tag { Name = "name2", Count = 2 }, + ["id3"] = new Tag { Name = "name3", Count = 6 } + } + }; + + await sut.RebuildTagsAsync(appId, group, tags, ct); + + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Equal(new Dictionary + { + ["name1"] = 1, + ["name2"] = 2, + ["name3"] = 6 + }, allTags); + + var export = await sut.GetExportableTagsAsync(appId, group, ct); + + export.Should().BeEquivalentTo(tags); + } + + [Fact] + public async Task Should_add_tags() + { + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("name1", "name2"), null, ct); + await sut.NormalizeTagsAsync(appId, group, HashSet.Of("name2", "name3"), null, ct); + + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Equal(new Dictionary + { + ["name1"] = 1, + ["name2"] = 2, + ["name3"] = 1 + }, allTags); + } + + [Fact] + public async Task Should_not_add_tags_if_already_added() + { + var result1 = await sut.NormalizeTagsAsync(appId, group, HashSet.Of("name1", "name2"), null, ct); + var result2 = await sut.NormalizeTagsAsync(appId, group, HashSet.Of("name1", "name2", "name3"), new HashSet(result1.Values), ct); + + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Equal(new Dictionary + { + ["name1"] = 1, + ["name2"] = 1, + ["name3"] = 1 + }, allTags); + } + + [Fact] + public async Task Should_remove_tags() + { + var result1 = await sut.NormalizeTagsAsync(appId, group, HashSet.Of("name1", "name2"), null, ct); + var result2 = await sut.NormalizeTagsAsync(appId, group, HashSet.Of("name2", "name3"), null, ct); + + // Tags from the first normalization are decreased and removed if count reaches zero. + await sut.NormalizeTagsAsync(appId, group, null, new HashSet(result1.Values), ct); + + var allTags = await sut.GetTagsAsync(appId, group, ct); + + Assert.Equal(new Dictionary + { + ["name2"] = 1, + ["name3"] = 1 + }, allTags); + } + + [Fact] + public async Task Should_resolve_tag_names() + { + // Get IDs from names. + var tagIds = await sut.NormalizeTagsAsync(appId, group, HashSet.Of("name1", "name2"), null, ct); + + // Get names from IDs (reverse operation). + var tagNames = await sut.GetTagIdsAsync(appId, group, HashSet.Of("name1", "name2", "invalid1"), ct); + + Assert.Equal(tagIds, tagNames); + } + } +} diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs index 3a7385158..f64c341ff 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs @@ -76,23 +76,25 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers return new CommandContext(CreateCommand(command), A.Dummy()); } - protected async Task HandleAsync(ICommandMiddleware middleware, TCommand command) where TCommand : SquidexCommand + protected async Task HandleAsync(ICommandMiddleware middleware, TCommand command, + CancellationToken ct = default) where TCommand : SquidexCommand { var context = new CommandContext(CreateCommand(command), A.Dummy()); - await middleware.HandleAsync(context); + await middleware.HandleAsync(context, ct); return context; } - protected async Task PublishIdempotentAsync(DomainObject domainObject, IAggregateCommand command) where T : class, IDomainState, new() + protected async Task PublishIdempotentAsync(DomainObject domainObject, IAggregateCommand command, + CancellationToken ct = default) where T : class, IDomainState, new() { - var result = await domainObject.ExecuteAsync(command); + var result = await domainObject.ExecuteAsync(command, default); var previousSnapshot = domainObject.Snapshot; var previousVersion = domainObject.Snapshot.Version; - await domainObject.ExecuteAsync(command); + await domainObject.ExecuteAsync(command, ct); Assert.Same(previousSnapshot, domainObject.Snapshot); Assert.Equal(previousVersion, domainObject.Snapshot.Version); diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs index 9380ef888..8f1f071c7 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/Mocks.cs @@ -31,17 +31,10 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers var app = A.Fake(); - A.CallTo(() => app.Id) - .Returns(appId.Id); - - A.CallTo(() => app.Name) - .Returns(appId.Name); - - A.CallTo(() => app.Languages) - .Returns(config); - - A.CallTo(() => app.UniqueId) - .Returns(appId.Id); + A.CallTo(() => app.Id).Returns(appId.Id); + A.CallTo(() => app.Name).Returns(appId.Name); + A.CallTo(() => app.Languages).Returns(config); + A.CallTo(() => app.UniqueId).Returns(appId.Id); return app; } @@ -50,17 +43,12 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers { var schema = A.Fake(); - A.CallTo(() => schema.Id) - .Returns(schemaId.Id); - - A.CallTo(() => schema.AppId) - .Returns(appId); - - A.CallTo(() => schema.SchemaDef) - .Returns(schemaDef ?? new Schema(schemaId.Name)); + schemaDef ??= new Schema(schemaId.Name); - A.CallTo(() => schema.UniqueId) - .Returns(DomainId.Combine(appId, schemaId.Id)); + A.CallTo(() => schema.Id).Returns(schemaId.Id); + A.CallTo(() => schema.AppId).Returns(appId); + A.CallTo(() => schema.SchemaDef).Returns(schemaDef); + A.CallTo(() => schema.UniqueId).Returns(DomainId.Combine(appId, schemaId.Id)); return schema; } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandRequestTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandRequestTests.cs deleted file mode 100644 index 382dc4c30..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/CommandRequestTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Globalization; -using FluentAssertions; -using Orleans; -using Orleans.TestingHost; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -#pragma warning disable SA1133 // Do not combine attributes - -namespace Squidex.Infrastructure.Commands -{ - public class CommandRequestTests - { - public interface IContextGrain : IGrainWithStringKey - { - Task GetCultureUIAsync(CommandRequest request); - - Task GetCultureAsync(CommandRequest request); - } - - public class ContextGrain : Grain, IContextGrain - { - public Task GetCultureAsync(CommandRequest request) - { - request.ApplyContext(); - - return Task.FromResult(CultureInfo.CurrentCulture.Name); - } - - public Task GetCultureUIAsync(CommandRequest request) - { - request.ApplyContext(); - - return Task.FromResult(CultureInfo.CurrentUICulture.Name); - } - } - - [Fact] - public void Should_capture_culture() - { - var culture = CultureInfo.GetCultureInfo("de"); - var cultureUI = CultureInfo.GetCultureInfo("it"); - - CultureInfo.CurrentCulture = culture; - CultureInfo.CurrentUICulture = cultureUI; - - var sut = CommandRequest.Create(null!); - - Assert.Equal(culture.Name, sut.Culture); - Assert.Equal(cultureUI.Name, sut.CultureUI); - } - - [Fact] - public void Should_serialize_and_deserialize() - { - var sut = CommandRequest.Create(null!); - - var serialized = sut.SerializeAndDeserialize(); - - serialized.Should().BeEquivalentTo(sut); - } - - [Fact, Trait("Category", "Dependencies")] - public async Task Should_communicate_with_orleans() - { - var cluster = - new TestClusterBuilder(1) - .Build(); - - await cluster.DeployAsync(); - - try - { - var culture = CultureInfo.GetCultureInfo("de"); - var cultureUI = CultureInfo.GetCultureInfo("it"); - - CultureInfo.CurrentCulture = culture; - CultureInfo.CurrentUICulture = cultureUI; - - var grain = cluster.Client.GetGrain("Default"); - - var request = CommandRequest.Create(null!); - - var cultureFromGrain = await grain.GetCultureAsync(request); - var cultureUIFromGrain = await grain.GetCultureUIAsync(request); - - Assert.Equal(culture.Name, cultureFromGrain); - Assert.Equal(cultureUI.Name, cultureUIFromGrain); - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.DisposeAsync().AsTask()); - } - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs index 5c272c87e..1fe01e5e6 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/CustomCommandMiddlewareRunnerTests.cs @@ -28,14 +28,15 @@ namespace Squidex.Infrastructure.Commands this.value = value; } - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { if (context.Command is Command command) { command.Values.Add(value); } - return next(context); + return next(context, ct); } } @@ -54,14 +55,14 @@ namespace Squidex.Infrastructure.Commands var isNextCalled = false; - await sut.HandleAsync(context, c => + await sut.HandleAsync(context, (c, _) => { isNextCalled = true; Assert.Equal(new[] { 10, 12, 14 }, command.Values); return Task.CompletedTask; - }); + }, default); Assert.True(isNextCalled); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs new file mode 100644 index 000000000..d21edff6a --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectCacheTests.cs @@ -0,0 +1,94 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Caching.Memory; +using Squidex.Infrastructure.Json; +using Xunit; + +namespace Squidex.Infrastructure.Commands +{ + public class DefaultDomainObjectCacheTests + { + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly IJsonSerializer serializer = A.Fake(); + private readonly IMemoryCache cache = A.Fake(); + private readonly IDistributedCache distributedCache = A.Fake(); + private readonly DomainId id = DomainId.NewGuid(); + private readonly DefaultDomainObjectCache sut; + + public DefaultDomainObjectCacheTests() + { + ct = cts.Token; + + sut = new DefaultDomainObjectCache(cache, serializer, distributedCache); + } + + [Fact] + public async Task Should_add_to_cache_and_memory_cache_on_set() + { + await sut.SetAsync(id, 10, 20, ct); + + A.CallTo(() => cache.CreateEntry($"{id}_10")) + .MustHaveHappened(); + + A.CallTo(() => serializer.Serialize(20, A._, true)) + .MustHaveHappened(); + + A.CallTo(() => distributedCache.SetAsync($"{id}_10", A._, A._, ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_ignore_exception_on_set() + { + A.CallTo(() => distributedCache.SetAsync(A._, A._, A._, ct)) + .Throws(new InvalidOperationException()); + + await sut.SetAsync(id, 10, 20, ct); + } + + [Fact] + public async Task Should_provide_from_cache_if_found() + { + object returned; + + A.CallTo(() => cache.TryGetValue($"{id}_10", out returned)) + .Returns(true) + .AssignsOutAndRefParameters(20); + + var result = await sut.GetAsync(id, 10, ct); + + Assert.Equal(20, result); + } + + [Fact] + public async Task Should_provide_from_distributed_cache_if_not_found_in_cache() + { + A.CallTo(() => serializer.Deserialize(A._, null, false)) + .Returns(20); + + var result = await sut.GetAsync(id, 10, ct); + + Assert.Equal(20, result); + + A.CallTo(() => distributedCache.GetAsync($"{id}_10", ct)) + .MustHaveHappened(); + } + + [Fact] + public async Task Should_ignore_exception_on_gett() + { + A.CallTo(() => distributedCache.GetAsync(A._, ct)) + .Throws(new InvalidOperationException()); + + await sut.SetAsync(id, 10, 20, ct); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectFactoryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectFactoryTests.cs new file mode 100644 index 000000000..6db7093d1 --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DefaultDomainObjectFactoryTests.cs @@ -0,0 +1,60 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Microsoft.Extensions.DependencyInjection; +using Squidex.Infrastructure.States; +using Xunit; + +#pragma warning disable SA1313 // Parameter names should begin with lower-case letter + +namespace Squidex.Infrastructure.Commands +{ + public class DefaultDomainObjectFactoryTests + { + private record TestObject(DomainId Id, IPersistenceFactory PersistenceFactory); + + [Fact] + public void Should_create_with_service_locator() + { + var id = DomainId.NewGuid(); + + var persistenceFactory = A.Fake>(); + + var serviceProvider = + new ServiceCollection() + .AddSingleton(persistenceFactory) + .BuildServiceProvider(); + + var sut = new DefaultDomainObjectFactory(serviceProvider); + + var created = sut.Create(id); + + Assert.Equal(id, created.Id); + Assert.Same(persistenceFactory, created.PersistenceFactory); + } + + [Fact] + public void Should_create_with_service_locator_and_custom_persistence_factory() + { + var id = DomainId.NewGuid(); + + var persistenceFactory = A.Fake>(); + + var serviceProvider = + new ServiceCollection() + .BuildServiceProvider(); + + var sut = new DefaultDomainObjectFactory(serviceProvider); + + var created = sut.Create(id, persistenceFactory); + + Assert.Equal(id, created.Id); + Assert.Same(persistenceFactory, created.PersistenceFactory); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs deleted file mode 100644 index 040bf73e1..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectGrainFormatterTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Reflection; -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Commands -{ - public class DomainObjectGrainFormatterTests - { - private readonly IIncomingGrainCallContext context = A.Fake(); - - [Fact] - public void Should_return_fallback_if_no_method_is_defined() - { - A.CallTo(() => context.InterfaceMethod) - .Returns(null!); - - var result = DomainObjectGrainFormatter.Format(context); - - Assert.Equal("Unknown", result); - } - - [Fact] - public void Should_return_method_name_if_not_domain_object_method() - { - var methodInfo = A.Fake(); - - A.CallTo(() => methodInfo.Name) - .Returns("Calculate"); - - A.CallTo(() => context.InterfaceMethod) - .Returns(methodInfo); - - var result = DomainObjectGrainFormatter.Format(context); - - Assert.Equal("Calculate", result); - } - - [Fact] - public void Should_return_nice_method_name_if_domain_object_execute() - { - var methodInfo = A.Fake(); - - A.CallTo(() => methodInfo.Name) - .Returns(nameof(IDomainObjectGrain.ExecuteAsync)); - - A.CallTo(() => context.Arguments) - .Returns(new object[] { new MyCommand() }); - - A.CallTo(() => context.InterfaceMethod) - .Returns(methodInfo); - - var result = DomainObjectGrainFormatter.Format(context); - - Assert.Equal("ExecuteAsync(MyCommand)", result); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs index b2c404cd0..65c5bf7f5 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/DomainObjectTests.cs @@ -15,30 +15,19 @@ namespace Squidex.Infrastructure.Commands { public class DomainObjectTests { - private readonly IPersistenceFactory persistenceFactory = A.Fake>(); - private readonly IPersistence persistence = A.Fake>(); - private readonly IPersistence persistenceEvents = A.Fake>(); + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly CancellationToken ct; + private readonly TestState state; private readonly DomainId id = DomainId.NewGuid(); private readonly MyDomainObject sut; - private HandleEvent? handleEvent; public DomainObjectTests() { - A.CallTo(() => persistenceFactory.WithEventSourcing(typeof(MyDomainObject), id, A._)) - .Invokes(args => - { - handleEvent = args.GetArgument(2)!; - }) - .Returns(persistenceEvents); + ct = cts.Token; - A.CallTo(() => persistenceFactory.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>._, A._)) - .Invokes(args => - { - handleEvent = args.GetArgument(3)!; - }) - .Returns(persistence); + state = new TestState(id); - sut = new MyDomainObject(id, persistenceFactory); + sut = new MyDomainObject(id, state.PersistenceFactory); } [Fact] @@ -51,43 +40,43 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_repair_if_stale() { - A.CallTo(() => persistence.IsSnapshotStale) + A.CallTo(() => state.Persistence.IsSnapshotStale) .Returns(true); SetupCreated(1); - await sut.EnsureLoadedAsync(); + await sut.EnsureLoadedAsync(ct); - A.CallTo(() => persistence.WriteSnapshotAsync(A._, default)) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, default)) .MustHaveHappened(); } [Fact] public async Task Should_not_repair_if_not_stale() { - A.CallTo(() => persistence.IsSnapshotStale) + A.CallTo(() => state.Persistence.IsSnapshotStale) .Returns(false); SetupCreated(1); - await sut.EnsureLoadedAsync(); + await sut.EnsureLoadedAsync(ct); - A.CallTo(() => persistence.WriteSnapshotAsync(A._, default)) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, default)) .MustNotHaveHappened(); } [Fact] public async Task Should_write_state_and_events_if_created() { - SetupEmpty(); + var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); - var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 }); - - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4), default)) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4), default)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), default)) + + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), ct)) .MustHaveHappened(); - A.CallTo(() => persistence.ReadAsync(A._, default)) + + A.CallTo(() => state.Persistence.ReadAsync(A._, A._)) .MustNotHaveHappened(); Assert.Equal(CommandResult.Empty(id, 0, EtagVersion.Empty), result); @@ -103,7 +92,7 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(new ValueChanged { Value = 10 }, new MultipleByTwiceEvent()); - await sut.EnsureLoadedAsync(); + await sut.EnsureLoadedAsync(ct); Assert.Equal(1, sut.Version); Assert.Equal(1, sut.Snapshot.Version); @@ -121,7 +110,7 @@ namespace Squidex.Infrastructure.Commands new Deleted(), new ValueChanged { Value = 4 }); - await sut.EnsureLoadedAsync(); + await sut.EnsureLoadedAsync(ct); Assert.Equal(3, sut.Version); Assert.Equal(3, sut.Snapshot.Version); @@ -138,7 +127,7 @@ namespace Squidex.Infrastructure.Commands new Deleted(), new ValueChanged { Value = 4 }); - await sut.EnsureLoadedAsync(); + await sut.EnsureLoadedAsync(ct); Assert.Equal(2, sut.Version); Assert.Equal(2, sut.Snapshot.Version); @@ -146,6 +135,33 @@ namespace Squidex.Infrastructure.Commands AssertSnapshot(sut.Snapshot, 3, 2, true); } + [Fact] + public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception() + { + sut.Recreate = false; + + SetupCreated(2); + + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) + .Throws(new InconsistentStateException(2, -1)).Once(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto(), ct)); + } + + [Fact] + public async Task Should_throw_exception_if_writing_causes_inconsistent_state_exception_and_deleted() + { + sut.Recreate = false; + + SetupCreated(2); + SetupDeleted(); + + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) + .Throws(new InconsistentStateException(2, -1)).Once(); + + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto(), ct)); + } + [Fact] public async Task Should_recreate_with_create_command_if_deleted_before() { @@ -155,14 +171,15 @@ namespace Squidex.Infrastructure.Commands SetupCreated(2); SetupDeleted(); - A.CallTo(() => persistence.WriteEventsAsync(A>>._, default)) + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) .Throws(new InconsistentStateException(2, -1)).Once(); - var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 }); + var result = await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), default)) + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), ct)) .MustHaveHappenedANumberOfTimesMatching(x => x == 3); - A.CallTo(() => persistence.ReadAsync(A._, default)) + + A.CallTo(() => state.Persistence.ReadAsync(A._, ct)) .MustHaveHappened(); Assert.Equal(CommandResult.Empty(id, 2, 1), result); @@ -174,17 +191,17 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public async Task Should_throw_exception_if_recreation_with_create_command_not_allowed() + public async Task Should_throw_exception_if_recreation_with_create_command_is_not_allowed() { sut.Recreate = false; SetupCreated(2); SetupDeleted(); - A.CallTo(() => persistence.WriteEventsAsync(A>>._, default)) + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) .Throws(new InconsistentStateException(2, -1)).Once(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto { Value = 4 })); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto(), ct)); } [Fact] @@ -196,14 +213,15 @@ namespace Squidex.Infrastructure.Commands SetupCreated(2); SetupDeleted(); - A.CallTo(() => persistence.WriteEventsAsync(A>>._, default)) + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) .Throws(new InconsistentStateException(2, -1)).Once(); - var result = await sut.ExecuteAsync(new Upsert { Value = 4 }); + var result = await sut.ExecuteAsync(new Upsert { Value = 4 }, ct); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), default)) + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), ct)) .MustHaveHappenedANumberOfTimesMatching(x => x == 3); - A.CallTo(() => persistence.ReadAsync(A._, default)) + + A.CallTo(() => state.Persistence.ReadAsync(A._, ct)) .MustHaveHappened(); Assert.Equal(CommandResult.Empty(id, 2, 1), result); @@ -215,33 +233,33 @@ namespace Squidex.Infrastructure.Commands } [Fact] - public async Task Should_throw_exception_if_recreation_with_upsert_command_not_allowed() + public async Task Should_throw_exception_if_recreation_with_upsert_command_is_not_allowed() { sut.Recreate = false; SetupCreated(2); SetupDeleted(); - A.CallTo(() => persistence.WriteEventsAsync(A>>._, default)) + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) .Throws(new InconsistentStateException(2, -1)).Once(); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new Upsert { Value = 4 })); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new Upsert(), ct)); } [Fact] public async Task Should_write_state_and_events_if_updated_after_creation() { - SetupEmpty(); - - await sut.ExecuteAsync(new CreateAuto { Value = 4 }); + await sut.ExecuteAsync(new CreateAuto { Value = 4 }, ct); - var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }); + var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8), default)) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8), default)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), default)) + + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), ct)) .MustHaveHappened(); - A.CallTo(() => persistence.ReadAsync(A._, default)) + + A.CallTo(() => state.Persistence.ReadAsync(A._, A._)) .MustNotHaveHappened(); Assert.Equal(CommandResult.Empty(id, 1, 0), result); @@ -257,13 +275,15 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }); + var result = await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8), default)) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 8), default)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), default)) + + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>.That.Matches(x => x.Count == 1), ct)) .MustHaveHappened(); - A.CallTo(() => persistence.ReadAsync(A._, default)) + + A.CallTo(() => state.Persistence.ReadAsync(A._, ct)) .MustHaveHappenedOnceExactly(); Assert.Equal(CommandResult.Empty(id, 1, 0), result); @@ -277,11 +297,9 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_not_load_on_create() { - SetupEmpty(); + await sut.ExecuteAsync(new CreateAuto(), ct); - await sut.ExecuteAsync(new CreateAuto()); - - A.CallTo(() => persistence.ReadAsync(A._, default)) + A.CallTo(() => state.Persistence.ReadAsync(A._, A._)) .MustNotHaveHappened(); } @@ -290,10 +308,10 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }); - await sut.ExecuteAsync(new UpdateAuto { Value = 9, ExpectedVersion = 1 }); + await sut.ExecuteAsync(new UpdateAuto { Value = 8, ExpectedVersion = 0 }, ct); + await sut.ExecuteAsync(new UpdateAuto { Value = 9, ExpectedVersion = 1 }, ct); - A.CallTo(() => persistence.ReadAsync(A._, default)) + A.CallTo(() => state.Persistence.ReadAsync(A._, ct)) .MustHaveHappenedOnceExactly(); Assert.Empty(sut.GetUncomittedEvents()); @@ -305,55 +323,48 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - await sut.RebuildStateAsync(); + await sut.RebuildStateAsync(ct); - A.CallTo(() => persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4), default)) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A.That.Matches(x => x.Value == 4), ct)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>._, default)) + + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, A._)) .MustNotHaveHappened(); } [Fact] public async Task Should_throw_on_rebuild_if_no_event_found() { - SetupEmpty(); - - await Assert.ThrowsAsync(() => sut.RebuildStateAsync()); + await Assert.ThrowsAsync(() => sut.RebuildStateAsync(ct)); } [Fact] public async Task Should_throw_exception_on_create_command_is_rejected_due_to_version_conflict() { - SetupEmpty(); - - A.CallTo(() => persistence.WriteEventsAsync(A>>._, default)) + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) .Throws(new InconsistentStateException(4, EtagVersion.Empty)); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto(), ct)); } [Fact] public async Task Should_throw_exception_if_create_command_is_invoked_for_loaded_and_created_object() { - await sut.ExecuteAsync(new CreateAuto()); + await sut.ExecuteAsync(new CreateAuto(), ct); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto(), ct)); } [Fact] public async Task Should_throw_exception_if_create_command_not_accepted() { - SetupEmpty(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto { Value = 99 })); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto { Value = 99 }, ct)); } [Fact] public async Task Should_return_custom_result_on_create() { - SetupEmpty(); - - var result = await sut.ExecuteAsync(new CreateCustom()); + var result = await sut.ExecuteAsync(new CreateCustom(), ct); Assert.Equal(new CommandResult(id, 0, EtagVersion.Empty, "CREATED"), result); } @@ -361,9 +372,7 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_throw_exception_if_update_command_invoked_for_empty_object() { - SetupEmpty(); - - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto(), ct)); } [Fact] @@ -371,7 +380,7 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto { Value = 99 })); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto { Value = 99 }, ct)); } [Fact] @@ -379,7 +388,7 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - var result = await sut.ExecuteAsync(new UpdateCustom()); + var result = await sut.ExecuteAsync(new UpdateCustom(), ct); Assert.Equal(new CommandResult(id, 1, 0, "UPDATED"), result); } @@ -389,7 +398,7 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 })); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateCustom { ExpectedVersion = 3 }, ct)); } [Fact] @@ -397,7 +406,7 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - var result = await sut.ExecuteAsync(new UpdateAuto { Value = MyDomainState.Unchanged }); + var result = await sut.ExecuteAsync(new UpdateAuto { Value = MyDomainState.Unchanged }, ct); Assert.Equal(CommandResult.Empty(id, 0, 0), result); Assert.Equal(0, sut.Version); @@ -410,12 +419,10 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_reset_state_if_writing_snapshot_for_create_failed() { - SetupEmpty(); - - A.CallTo(() => persistence.WriteSnapshotAsync(A._, default)) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, default)) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto())); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new CreateAuto(), ct)); Assert.Empty(sut.GetUncomittedEvents()); AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty); @@ -426,10 +433,10 @@ namespace Squidex.Infrastructure.Commands { SetupCreated(4); - A.CallTo(() => persistence.WriteSnapshotAsync(A._, default)) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, default)) .Throws(new InvalidOperationException()); - await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto())); + await Assert.ThrowsAsync(() => sut.ExecuteAsync(new UpdateAuto(), ct)); Assert.Empty(sut.GetUncomittedEvents()); AssertSnapshot(sut.Snapshot, 4, 0); @@ -443,65 +450,34 @@ namespace Squidex.Infrastructure.Commands var deleteStream = A.Fake>(); - A.CallTo(() => persistenceFactory.WithEventSourcing(typeof(MyDomainObject), DomainId.Combine(id, DomainId.Create("deleted")), null)) + A.CallTo(() => state.PersistenceFactory.WithEventSourcing(typeof(MyDomainObject), DomainId.Combine(id, DomainId.Create("deleted")), null)) .Returns(deleteStream); - await sut.ExecuteAsync(new DeletePermanent()); + await sut.ExecuteAsync(new DeletePermanent(), ct); AssertSnapshot(sut.Snapshot, 0, EtagVersion.Empty, false); - A.CallTo(() => persistence.DeleteAsync(default)) + A.CallTo(() => state.Persistence.DeleteAsync(ct)) .MustHaveHappened(); - A.CallTo(() => persistence.WriteEventsAsync(A>>._, default)) + A.CallTo(() => state.Persistence.WriteEventsAsync(A>>._, ct)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => deleteStream.WriteEventsAsync(A>>._, default)) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_get_old_versions_if_cached() - { - sut.VersionsToKeep = int.MaxValue; - - SetupEmpty(); - - await sut.ExecuteAsync(new CreateAuto { Value = 3 }); - await sut.ExecuteAsync(new UpdateAuto { Value = 4 }); - await sut.ExecuteAsync(new UpdateAuto { Value = 5 }); - - var version_Empty = await sut.GetSnapshotAsync(EtagVersion.Empty); - var version_0 = await sut.GetSnapshotAsync(0); - var version_1 = await sut.GetSnapshotAsync(1); - var version_2 = await sut.GetSnapshotAsync(2); - - Assert.Empty(sut.GetUncomittedEvents()); - AssertSnapshot(version_Empty, 0, EtagVersion.Empty); - AssertSnapshot(version_0, 3, 0); - AssertSnapshot(version_1, 4, 1); - AssertSnapshot(version_2, 5, 2); - - A.CallTo(() => persistenceFactory.WithEventSourcing(typeof(MyDomainObject), id, A._)) + A.CallTo(() => deleteStream.WriteEventsAsync(A>>._, A._)) .MustNotHaveHappened(); } [Fact] - public async Task Should_get_old_versions_from_query_if_not_cached() + public async Task Should_get_old_versions_from_query() { - sut.VersionsToKeep = 1; - - SetupEmpty(); - SetupLoaded(); + await sut.ExecuteAsync(new CreateAuto { Value = 3 }, ct); + await sut.ExecuteAsync(new UpdateAuto { Value = 4 }, ct); + await sut.ExecuteAsync(new UpdateAuto { Value = 5 }, ct); - await sut.ExecuteAsync(new CreateAuto { Value = 3 }); - await sut.ExecuteAsync(new UpdateAuto { Value = 4 }); - await sut.ExecuteAsync(new UpdateAuto { Value = 5 }); - - var version_Empty = await sut.GetSnapshotAsync(EtagVersion.Empty); - var version_0 = await sut.GetSnapshotAsync(0); - var version_1 = await sut.GetSnapshotAsync(1); - var version_2 = await sut.GetSnapshotAsync(2); + var version_Empty = await sut.GetSnapshotAsync(EtagVersion.Empty, ct); + var version_0 = await sut.GetSnapshotAsync(0, ct); + var version_1 = await sut.GetSnapshotAsync(1, ct); + var version_2 = await sut.GetSnapshotAsync(2, ct); Assert.Empty(sut.GetUncomittedEvents()); AssertSnapshot(version_Empty, 0, EtagVersion.Empty); @@ -509,8 +485,8 @@ namespace Squidex.Infrastructure.Commands AssertSnapshot(version_1, 4, 1); AssertSnapshot(version_2, 5, 2); - A.CallTo(() => persistenceFactory.WithEventSourcing(typeof(MyDomainObject), id, A._)) - .MustHaveHappenedOnceExactly(); + A.CallTo(() => state.PersistenceFactory.WithEventSourcing(typeof(MyDomainObject), id, A._)) + .MustHaveHappened(3, Times.Exactly); } private static void AssertSnapshot(MyDomainState state, int value, long version, bool isDeleted = false) @@ -520,60 +496,17 @@ namespace Squidex.Infrastructure.Commands private void SetupDeleted() { - sut.ExecuteAsync(new Delete()).Wait(); + sut.ExecuteAsync(new Delete(), ct).Wait(ct); } private void SetupCreated(int value) { - SetupCreated(new ValueChanged { Value = value }); + state.AddEvent(new ValueChanged { Value = value }); } private void SetupCreated(params IEvent[] @events) { - var version = -1; - - A.CallTo(() => persistence.ReadAsync(-2, default)) - .Invokes(() => - { - version++; - - foreach (var @event in events) - { - handleEvent?.Invoke(Envelope.Create(@event)); - } - }); - - A.CallTo(() => persistence.Version) - .ReturnsLazily(() => version); - } - - private void SetupLoaded() - { - var @events = new List>(); - - A.CallTo(() => persistence.WriteEventsAsync(A>>._, default)) - .Invokes(args => - { - @events.AddRange(args.GetArgument>>(0)!); - }); - - A.CallTo(() => persistenceEvents.ReadAsync(EtagVersion.Any, default)) - .Invokes(_ => - { - foreach (var @event in events) - { - handleEvent?.Invoke(@event); - } - }); - } - - private void SetupEmpty() - { - A.CallTo(() => persistenceFactory.WithSnapshotsAndEventSourcing(typeof(MyDomainObject), id, A>._, A._)) - .Returns(persistence); - - A.CallTo(() => persistence.Version) - .Returns(-1); + events.Foreach(@event => state.AddEvent(@event)); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs index c27ba5d07..fbfb4c058 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/EnrichWithTimestampCommandMiddlewareTests.cs @@ -23,7 +23,10 @@ namespace Squidex.Infrastructure.Commands A.CallTo(() => clock.GetCurrentInstant()) .Returns(SystemClock.Instance.GetCurrentInstant().WithoutMs()); - sut = new EnrichWithTimestampCommandMiddleware(clock); + sut = new EnrichWithTimestampCommandMiddleware + { + Clock = clock + }; } [Fact] @@ -31,7 +34,7 @@ namespace Squidex.Infrastructure.Commands { var command = new MyCommand(); - await sut.HandleAsync(new CommandContext(command, commandBus)); + await sut.HandleAsync(new CommandContext(command, commandBus), default); Assert.Equal(clock.GetCurrentInstant(), command.Timestamp); } @@ -39,7 +42,7 @@ namespace Squidex.Infrastructure.Commands [Fact] public async Task Should_do_nothing_for_normal_command() { - await sut.HandleAsync(new CommandContext(A.Dummy(), commandBus)); + await sut.HandleAsync(new CommandContext(A.Dummy(), commandBus), default); A.CallTo(() => clock.GetCurrentInstant()) .MustNotHaveHappened(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs index ee8be8fa2..84c9b6cc5 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/InMemoryCommandBusTests.cs @@ -18,7 +18,8 @@ namespace Squidex.Infrastructure.Commands { public ICommand LastCommand { get; private set; } - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { LastCommand = context.Command; @@ -32,7 +33,8 @@ namespace Squidex.Infrastructure.Commands { public ICommand LastCommand { get; private set; } - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { LastCommand = context.Command; @@ -44,7 +46,8 @@ namespace Squidex.Infrastructure.Commands { public ICommand LastCommand { get; private set; } - public Task HandleAsync(CommandContext context, NextDelegate next) + public Task HandleAsync(CommandContext context, NextDelegate next, + CancellationToken ct) { LastCommand = context.Command; @@ -56,9 +59,10 @@ namespace Squidex.Infrastructure.Commands public async Task Should_not_set_handled_if_no_handler_registered() { var sut = new InMemoryCommandBus(Array.Empty()); - var ctx = await sut.PublishAsync(command); - Assert.False(ctx.IsCompleted); + var context = await sut.PublishAsync(command, default); + + Assert.False(context.IsCompleted); } [Fact] @@ -67,10 +71,11 @@ namespace Squidex.Infrastructure.Commands var handler = new NonHandledHandler(); var sut = new InMemoryCommandBus(new ICommandMiddleware[] { handler }); - var ctx = await sut.PublishAsync(command); + + var context = await sut.PublishAsync(command, default); Assert.Equal(command, handler.LastCommand); - Assert.False(ctx.IsCompleted); + Assert.False(context.IsCompleted); } [Fact] @@ -79,10 +84,11 @@ namespace Squidex.Infrastructure.Commands var handler = new HandledHandler(); var sut = new InMemoryCommandBus(new ICommandMiddleware[] { handler }); - var ctx = await sut.PublishAsync(command); + + var context = await sut.PublishAsync(command, default); Assert.Equal(command, handler.LastCommand); - Assert.True(ctx.IsCompleted); + Assert.True(context.IsCompleted); } [Fact] @@ -92,7 +98,7 @@ namespace Squidex.Infrastructure.Commands var sut = new InMemoryCommandBus(new ICommandMiddleware[] { handler }); - await Assert.ThrowsAsync(async () => await sut.PublishAsync(command)); + await Assert.ThrowsAsync(async () => await sut.PublishAsync(command, default)); Assert.Equal(command, handler.LastCommand); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs index 13aed58e7..222b93d21 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/LogCommandMiddlewareTests.cs @@ -31,12 +31,12 @@ namespace Squidex.Infrastructure.Commands { var context = new CommandContext(command, commandBus); - await sut.HandleAsync(context, c => + await sut.HandleAsync(context, (c, ct) => { context.Complete(true); return Task.CompletedTask; - }); + }, default); A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument(0) == LogLevel.Debug) .MustHaveHappened(); @@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.Commands await Assert.ThrowsAsync(async () => { - await sut.HandleAsync(context, c => throw new InvalidOperationException()); + await sut.HandleAsync(context, (c, ct) => throw new InvalidOperationException(), default); }); A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument(0) == LogLevel.Debug) @@ -70,7 +70,7 @@ namespace Squidex.Infrastructure.Commands { var context = new CommandContext(command, commandBus); - await sut.HandleAsync(context, c => Task.CompletedTask); + await sut.HandleAsync(context, (c, ct) => Task.CompletedTask, default); A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument(0) == LogLevel.Debug) .MustHaveHappened(); diff --git a/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs index ee3c6c82c..7d69bb191 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Commands/ReadonlyCommandMiddlewareTests.cs @@ -49,12 +49,12 @@ namespace Squidex.Infrastructure.Commands private async Task MakeCallAsync(CommandContext context) { - await sut.HandleAsync(context, c => + await sut.HandleAsync(context, (c, ct) => { context.Complete(true); return Task.CompletedTask; - }); + }, default); } } -} \ No newline at end of file +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs new file mode 100644 index 000000000..7cd1f421a --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerManagerTests.cs @@ -0,0 +1,119 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using FluentAssertions; +using Squidex.Infrastructure.States; +using Squidex.Infrastructure.TestHelpers; +using Squidex.Messaging; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing.Consume +{ + public class EventConsumerManagerTests + { + private readonly IPersistenceFactory persistenceFactory = A.Fake>(); + private readonly IMessageBus messaging = A.Fake(); + private readonly string consumerName = Guid.NewGuid().ToString(); + private readonly EventConsumerManager sut; + + public EventConsumerManagerTests() + { + sut = new EventConsumerManager(persistenceFactory, messaging); + } + + [Fact] + public async Task Should_get_states_from_store() + { + var snapshotStore = A.Fake>(); + + A.CallTo(() => persistenceFactory.Snapshots) + .Returns(snapshotStore); + + A.CallTo(() => snapshotStore.ReadAllAsync(default)) + .Returns(new List> + { + new SnapshotResult(DomainId.Create("consumer1"), + new EventConsumerState + { + Position = "1" + }, 1), + new SnapshotResult(DomainId.Create("consumer2"), + new EventConsumerState + { + Position = "2" + }, 2) + }.ToAsyncEnumerable()); + + var result = await sut.GetConsumersAsync(default); + + result.Should().BeEquivalentTo( + new List + { + new EventConsumerInfo { Name = "consumer1", Position = "1" }, + new EventConsumerInfo { Name = "consumer2", Position = "2" } + }); + } + + [Fact] + public async Task Should_publish_event_on_start() + { + var testState = new TestState(DomainId.Create(consumerName), persistenceFactory) + { + Snapshot = new EventConsumerState + { + Position = "42" + } + }; + + var response = await sut.StartAsync(consumerName, default); + + A.CallTo(() => messaging.PublishAsync(new EventConsumerStart(consumerName), null, default)) + .MustHaveHappened(); + + Assert.Equal("42", response.Position); + } + + [Fact] + public async Task Should_publish_event_on_stop() + { + var testState = new TestState(DomainId.Create(consumerName), persistenceFactory) + { + Snapshot = new EventConsumerState + { + Position = "42" + } + }; + + var response = await sut.StopAsync(consumerName, default); + + A.CallTo(() => messaging.PublishAsync(new EventConsumerStop(consumerName), null, default)) + .MustHaveHappened(); + + Assert.Equal("42", response.Position); + } + + [Fact] + public async Task Should_publish_event_on_reset() + { + var testState = new TestState(DomainId.Create(consumerName), persistenceFactory) + { + Snapshot = new EventConsumerState + { + Position = "42" + } + }; + + var response = await sut.ResetAsync(consumerName, default); + + A.CallTo(() => messaging.PublishAsync(new EventConsumerReset(consumerName), null, default)) + .MustHaveHappened(); + + Assert.Equal("42", response.Position); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs similarity index 72% rename from backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs index 5575c259c..d0372a502 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerProcessorTests.cs @@ -8,41 +8,28 @@ using FakeItEasy; using FluentAssertions; using Microsoft.Extensions.Logging; -using Orleans.Core; -using Orleans.Storage; -using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.States; using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing.Grains +namespace Squidex.Infrastructure.EventSourcing.Consume { - public class EventConsumerGrainTests + public class EventConsumerProcessorTests { - public sealed class MyEventConsumerGrain : EventConsumerGrain + public sealed class MyEventConsumerProcessor : EventConsumerProcessor { - private IEventSubscriber? currentSubscriber; + public IEventSubscriber? Subscriber { get; set; } - public MyEventConsumerGrain( - IGrainIdentity identity, - IGrainState state, - IEventConsumerFactory eventConsumerFactory, + public MyEventConsumerProcessor( + IPersistenceFactory persistenceFactory, + IEventConsumer eventConsumer, IEventFormatter eventFormatter, IEventStore eventStore, - ILogger log) - : base(identity, state, eventConsumerFactory, eventFormatter, eventStore, log) + ILogger log) + : base(persistenceFactory, eventConsumer, eventFormatter, eventStore, log) { } - public Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) - { - return currentSubscriber!.OnEventAsync(subscription, storedEvent); - } - - public Task OnErrorAsync(IEventSubscription subscription, Exception exception) - { - return currentSubscriber!.OnErrorAsync(subscription, exception); - } - protected override IEventSubscription CreateRetrySubscription(IEventSubscriber subscriber) { return CreateSubscription(subscriber); @@ -50,50 +37,40 @@ namespace Squidex.Infrastructure.EventSourcing.Grains protected override IEventSubscription CreateSubscription(IEventSubscriber subscriber) { - currentSubscriber = subscriber; + Subscriber = subscriber; return base.CreateSubscription(subscriber); } } - private readonly IGrainIdentity identity = A.Fake(); - private readonly IGrainState state = A.Fake>(); private readonly IEventConsumer eventConsumer = A.Fake(); - private readonly IEventConsumerFactory eventConsumerFactory = A.Fake(); private readonly IEventFormatter eventFormatter = A.Fake(); private readonly IEventStore eventStore = A.Fake(); private readonly IEventSubscription eventSubscription = A.Fake(); + private readonly TestState state; private readonly StoredEvent storedEvent; private readonly EventData eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); private readonly Envelope envelope = new Envelope(new MyEvent()); - private readonly MyEventConsumerGrain sut; - private readonly string consumerName; + private readonly MyEventConsumerProcessor sut; + private readonly string consumerName = Guid.NewGuid().ToString(); private readonly string initialPosition = Guid.NewGuid().ToString(); - public EventConsumerGrainTests() + public EventConsumerProcessorTests() { - state.Value = new EventConsumerState + state = new TestState(DomainId.Create(consumerName)) { - Position = initialPosition + Snapshot = new EventConsumerState + { + Position = initialPosition + } }; - consumerName = eventConsumer.GetType().Name; - - A.CallTo(() => identity.PrimaryKeyString) - .Returns(consumerName); - A.CallTo(() => eventStore.CreateSubscription(A._, A._, A._)) .Returns(eventSubscription); A.CallTo(() => eventConsumer.Name) .Returns(consumerName); - A.CallTo(() => eventSubscription.Sender) - .Returns(eventSubscription); - - A.CallTo(() => eventConsumerFactory.Create(eventConsumer.Name)) - .Returns(eventConsumer); - A.CallTo(() => eventConsumer.Handles(A._)) .Returns(true); @@ -111,22 +88,20 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventFormatter.ParseIfKnown(storedEvent)) .Returns(envelope); - var log = A.Fake>(); - - sut = new MyEventConsumerGrain( - identity, - state, - eventConsumerFactory, + sut = new MyEventConsumerProcessor( + state.PersistenceFactory, + eventConsumer, eventFormatter, eventStore, - log); + A.Fake>()); } [Fact] public async Task Should_not_subscribe_to_event_store_if_stopped_in_db() { - state.Value = state.Value.Stopped(); + state.Snapshot = state.Snapshot.Stopped(); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await sut.CompleteAsync(); @@ -140,6 +115,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_subscribe_to_event_store_if_not_found_in_db() { + await sut.InitializeAsync(default); await sut.ActivateAsync(); await sut.CompleteAsync(); @@ -153,8 +129,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_subscribe_to_event_store_if_failed() { - state.Value = state.Value.Stopped(new InvalidOperationException()); + state.Snapshot = state.Snapshot.Stopped(new InvalidOperationException()); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await sut.CompleteAsync(); @@ -168,6 +145,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_subscribe_to_event_store_if_not_stopped_in_db() { + await sut.InitializeAsync(default); await sut.ActivateAsync(); await sut.CompleteAsync(); @@ -181,7 +159,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_stop_subscription_if_stopped() { + await sut.InitializeAsync(default); await sut.ActivateAsync(); + await sut.StopAsync(); await sut.StopAsync(); @@ -189,17 +169,19 @@ namespace Squidex.Infrastructure.EventSourcing.Grains AssertGrainState(isStopped: true, position: initialPosition); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventSubscription.Unsubscribe()) + A.CallTo(() => eventSubscription.Dispose()) .MustHaveHappenedOnceExactly(); } [Fact] public async Task Should_reset_consumer_if_resetting() { + await sut.InitializeAsync(default); await sut.ActivateAsync(); + await sut.StopAsync(); await sut.ResetAsync(); @@ -207,16 +189,16 @@ namespace Squidex.Infrastructure.EventSourcing.Grains AssertGrainState(isStopped: false, position: null); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappened(2, Times.Exactly); A.CallTo(() => eventConsumer.ClearAsync()) .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventSubscription.Unsubscribe()) + A.CallTo(() => eventSubscription.Dispose()) .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventStore.CreateSubscription(A._, A._, state.Value.Position)) + A.CallTo(() => eventStore.CreateSubscription(A._, A._, state.Snapshot.Position)) .MustHaveHappenedOnceExactly(); A.CallTo(() => eventStore.CreateSubscription(A._, A._, null)) @@ -226,15 +208,15 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_invoke_and_update_position_if_event_received() { + await sut.InitializeAsync(default); await sut.ActivateAsync(); await OnEventAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); AssertGrainState(isStopped: false, position: storedEvent.EventPosition, count: 1); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappenedOnceExactly(); A.CallTo(() => eventConsumer.On(envelope)) @@ -247,6 +229,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.BatchSize) .Returns(1); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await OnEventAsync(eventSubscription, storedEvent); @@ -259,7 +242,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains AssertGrainState(isStopped: false, position: storedEvent.EventPosition, count: 5); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappened(5, Times.Exactly); A.CallTo(() => eventConsumer.On(A>>._)) @@ -272,6 +255,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.BatchSize) .Returns(100); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await OnEventAsync(eventSubscription, storedEvent); @@ -284,7 +268,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains AssertGrainState(isStopped: false, position: storedEvent.EventPosition, count: 5); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappenedOnceExactly(); A.CallTo(() => eventConsumer.On(A>>._)) @@ -297,15 +281,15 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.Handles(storedEvent)) .Returns(false); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await OnEventAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); AssertGrainState(isStopped: false, position: storedEvent.EventPosition); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappenedOnceExactly(); A.CallTo(() => eventConsumer.On(envelope)) @@ -318,15 +302,15 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventFormatter.ParseIfKnown(A.That.Matches(x => x.Data == eventData))) .Returns(null); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await OnEventAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); AssertGrainState(isStopped: false, position: storedEvent.EventPosition); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappenedOnceExactly(); A.CallTo(() => eventConsumer.On(envelope)) @@ -336,10 +320,10 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_not_invoke_and_update_position_if_event_is_from_another_subscription() { + await sut.InitializeAsync(default); await sut.ActivateAsync(); - await OnEventAsync(A.Fake(), storedEvent); - + await sut.OnEventsAsync(A.Fake(), new[] { envelope }.ToList(), storedEvent.EventPosition); await sut.CompleteAsync(); AssertGrainState(isStopped: false, position: initialPosition); @@ -351,20 +335,20 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_stop_if_consumer_failed() { - await sut.ActivateAsync(); - var ex = new InvalidOperationException(); - await OnErrorAsync(eventSubscription, ex); + await sut.InitializeAsync(default); + await sut.ActivateAsync(); + await OnErrorAsync(eventSubscription, ex); await sut.CompleteAsync(); - AssertGrainState(isStopped: true, position: initialPosition, error: ex.ToString()); + AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventSubscription.Unsubscribe()) + A.CallTo(() => eventSubscription.Dispose()) .MustHaveHappenedOnceExactly(); } @@ -373,22 +357,24 @@ namespace Squidex.Infrastructure.EventSourcing.Grains { var ex = new InvalidOperationException(); + await sut.InitializeAsync(default); await sut.ActivateAsync(); - await OnErrorAsync(A.Fake(), ex); - + await sut.OnErrorAsync(A.Fake(), ex); await sut.CompleteAsync(); AssertGrainState(isStopped: false, position: initialPosition); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustNotHaveHappened(); } [Fact] public async Task Should_wakeup_if_already_subscribed() { + await sut.InitializeAsync(default); await sut.ActivateAsync(); + await sut.ActivateAsync(); await sut.CompleteAsync(); @@ -404,16 +390,18 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.ClearAsync()) .Throws(ex); + await sut.InitializeAsync(default); await sut.ActivateAsync(); + await sut.ResetAsync(); await sut.CompleteAsync(); - AssertGrainState(isStopped: true, position: initialPosition, error: ex.ToString()); + AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventSubscription.Unsubscribe()) + A.CallTo(() => eventSubscription.Dispose()) .MustHaveHappenedOnceExactly(); } @@ -425,21 +413,21 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .Throws(ex); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await OnEventAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); - AssertGrainState(isStopped: true, position: initialPosition, error: ex.ToString()); + AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); A.CallTo(() => eventConsumer.On(envelope)) .MustHaveHappened(); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventSubscription.Unsubscribe()) + A.CallTo(() => eventSubscription.Dispose()) .MustHaveHappenedOnceExactly(); } @@ -451,21 +439,21 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventFormatter.ParseIfKnown(A.That.Matches(x => x.Data == eventData))) .Throws(ex); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await OnEventAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); - AssertGrainState(isStopped: true, position: initialPosition, error: ex.ToString()); + AssertGrainState(isStopped: true, position: initialPosition, error: ex.Message); A.CallTo(() => eventConsumer.On(envelope)) .MustNotHaveHappened(); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappenedOnceExactly(); - A.CallTo(() => eventSubscription.Unsubscribe()) + A.CallTo(() => eventSubscription.Dispose()) .MustHaveHappenedOnceExactly(); } @@ -477,10 +465,10 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .Throws(ex); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await OnEventAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); await sut.StopAsync(); @@ -492,10 +480,10 @@ namespace Squidex.Infrastructure.EventSourcing.Grains A.CallTo(() => eventConsumer.On(envelope)) .MustHaveHappened(); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .MustHaveHappened(2, Times.Exactly); - A.CallTo(() => eventSubscription.Unsubscribe()) + A.CallTo(() => eventSubscription.Dispose()) .MustHaveHappenedOnceExactly(); A.CallTo(() => eventStore.CreateSubscription(A._, A._, A._)) @@ -505,35 +493,34 @@ namespace Squidex.Infrastructure.EventSourcing.Grains [Fact] public async Task Should_fail_if_writing_failed() { - var ex = new InconsistentStateException(); + var ex = new InconsistentStateException(0, 1); - A.CallTo(() => state.WriteAsync()) + A.CallTo(() => state.Persistence.WriteSnapshotAsync(A._, A._)) .Throws(ex); + await sut.InitializeAsync(default); await sut.ActivateAsync(); await OnEventAsync(eventSubscription, storedEvent); - await sut.CompleteAsync(); - AssertGrainState(isStopped: true, position: storedEvent.EventPosition, error: ex.ToString(), 1); + AssertGrainState(isStopped: true, position: storedEvent.EventPosition, error: ex.Message, 1); } - private Task OnErrorAsync(IEventSubscription subscription, Exception exception) + private ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception) { - return sut.OnErrorAsync(subscription, exception); + return sut.Subscriber?.OnErrorAsync(subscription, exception) ?? default; } - private Task OnEventAsync(IEventSubscription subscription, StoredEvent ev) + private ValueTask OnEventAsync(IEventSubscription subscription, StoredEvent @event) { - return sut.OnEventAsync(subscription, ev); + return sut.Subscriber?.OnEventAsync(subscription, @event) ?? default; } private void AssertGrainState(bool isStopped = false, string? position = null, string? error = null, int count = 0) { - var expected = new EventConsumerState { IsStopped = isStopped, Position = position, Error = error, Count = count }; - - state.Value.Should().BeEquivalentTo(expected); + sut.State.Should().BeEquivalentTo( + new EventConsumerState { IsStopped = isStopped, Position = position, Error = error, Count = count }); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerStateTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerStateTests.cs similarity index 94% rename from backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerStateTests.cs rename to backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerStateTests.cs index a2db439af..803c65d89 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerStateTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerStateTests.cs @@ -9,7 +9,7 @@ using FluentAssertions; using Squidex.Infrastructure.TestHelpers; using Xunit; -namespace Squidex.Infrastructure.EventSourcing.Grains +namespace Squidex.Infrastructure.EventSourcing.Consume { public class EventConsumerStateTests { diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs new file mode 100644 index 000000000..22d5b46bc --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs @@ -0,0 +1,100 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Xunit; + +namespace Squidex.Infrastructure.EventSourcing.Consume +{ + public class EventConsumerWorkerTests + { + private readonly IEventConsumer consumer1 = A.Fake(); + private readonly IEventConsumer consumer2 = A.Fake(); + private readonly EventConsumerProcessor processor1 = A.Fake(); + private readonly EventConsumerProcessor processor2 = A.Fake(); + private readonly EventConsumerWorker sut; + + public EventConsumerWorkerTests() + { + var factory = new Func(consumer => + { + return consumer == consumer1 ? processor1 : processor2; + }); + + A.CallTo(() => consumer1.Name).Returns("1"); + A.CallTo(() => consumer2.Name).Returns("2"); + + sut = new EventConsumerWorker(new[] { consumer1, consumer2 }, factory); + } + + [Fact] + public async Task Should_release_without_initialize() + { + await sut.ReleaseAsync(default); + } + + [Fact] + public async Task Should_initialize_all_processors_on_initialize() + { + await sut.InitializeAsync(default); + + A.CallTo(() => processor1.InitializeAsync(default)) + .MustHaveHappenedOnceExactly(); + + A.CallTo(() => processor2.InitializeAsync(default)) + .MustHaveHappenedOnceExactly(); + } + + [Fact] + public async Task Should_activate_all_processors_on_initialize() + { + await sut.InitializeAsync(default); + + A.CallTo(() => processor1.ActivateAsync()) + .MustHaveHappened(); + + A.CallTo(() => processor2.ActivateAsync()) + .MustHaveHappened(); + } + + [Fact] + public async Task Shoud_start_correct_processor() + { + await sut.HandleAsync(new EventConsumerStart(consumer1.Name), default); + + A.CallTo(() => processor1.StartAsync()) + .MustHaveHappened(); + + A.CallTo(() => processor2.StartAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Shoud_stop_correct_processor() + { + await sut.HandleAsync(new EventConsumerStart(consumer1.Name), default); + + A.CallTo(() => processor1.StartAsync()) + .MustHaveHappened(); + + A.CallTo(() => processor2.StartAsync()) + .MustNotHaveHappened(); + } + + [Fact] + public async Task Shoud_reset_correct_processor() + { + await sut.HandleAsync(new EventConsumerReset(consumer1.Name), default); + + A.CallTo(() => processor1.ResetAsync()) + .MustHaveHappened(); + + A.CallTo(() => processor2.ResetAsync()) + .MustNotHaveHappened(); + } + } +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventConsumerFactoryTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventConsumerFactoryTests.cs deleted file mode 100644 index a06042ec6..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/DefaultEventConsumerFactoryTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing -{ - public class DefaultEventConsumerFactoryTests - { - private readonly IEventConsumer consumer1 = A.Fake(); - private readonly IEventConsumer consumer2 = A.Fake(); - private readonly DefaultEventConsumerFactory sut; - - public DefaultEventConsumerFactoryTests() - { - A.CallTo(() => consumer1.Name) - .Returns("consumer1"); - - A.CallTo(() => consumer2.Name) - .Returns("consumer2"); - - sut = new DefaultEventConsumerFactory(new[] { consumer1, consumer2 }); - } - - [Fact] - public void Should_return_consumer_by_name() - { - var returned1 = sut.Create(consumer1.Name); - var returned2 = sut.Create(consumer2.Name); - - Assert.Same(consumer1, returned1); - Assert.Same(returned2, returned2); - } - - [Fact] - public void Should_throw_exception_for_invalid_name() - { - Assert.Throws(() => sut.Create("invalid")); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventConsumersHealthCheckTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventConsumersHealthCheckTests.cs deleted file mode 100644 index 2a0bbfd66..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventConsumersHealthCheckTests.cs +++ /dev/null @@ -1,119 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Microsoft.Extensions.Diagnostics.HealthChecks; -using Orleans; -using Orleans.Concurrency; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Orleans; -using Xunit; - -namespace Squidex.Infrastructure.EventSourcing -{ - public class EventConsumersHealthCheckTests - { - private readonly IGrainFactory grainFactory = A.Fake(); - private readonly IEventConsumerManagerGrain eventConsumerManager = A.Fake(); - private readonly List consumers = new List(); - private readonly EventConsumersHealthCheck sut; - - public EventConsumersHealthCheckTests() - { - A.CallTo(() => grainFactory.GetGrain(SingleGrain.Id, null)) - .Returns(eventConsumerManager); - - A.CallTo(() => eventConsumerManager.GetConsumersAsync()) - .Returns(consumers.AsImmutable()); - - sut = new EventConsumersHealthCheck(grainFactory); - } - - [Fact] - public async Task Should_return_healthy_if_no_consumer_found() - { - var status = await sut.CheckHealthAsync(null!); - - Assert.Equal(HealthStatus.Healthy, status.Status); - } - - [Fact] - public async Task Should_return_healthy_if_no_consumer_failed() - { - consumers.Add(new EventConsumerInfo - { - Name = "Consumer1" - }); - - consumers.Add(new EventConsumerInfo - { - Name = "Consumer2" - }); - - consumers.Add(new EventConsumerInfo - { - Name = "Consumer2" - }); - - var status = await sut.CheckHealthAsync(null!); - - Assert.Equal(HealthStatus.Healthy, status.Status); - } - - [Fact] - public async Task Should_return_unhealthy_if_all_consumers_failed() - { - consumers.Add(new EventConsumerInfo - { - Name = "Consumer1", - Error = "Failed1" - }); - - consumers.Add(new EventConsumerInfo - { - Name = "Consumer2", - Error = "Failed2" - }); - - consumers.Add(new EventConsumerInfo - { - Name = "Consumer3", - Error = "Failed3" - }); - - var status = await sut.CheckHealthAsync(null!); - - Assert.Equal(HealthStatus.Unhealthy, status.Status); - } - - [Fact] - public async Task Should_return_degrated_if_at_least_one_consumers_failed() - { - consumers.Add(new EventConsumerInfo - { - Name = "Consumer1", - Error = "Failed1" - }); - - consumers.Add(new EventConsumerInfo - { - Name = "Consumer2", - IsStopped = true - }); - - consumers.Add(new EventConsumerInfo - { - Name = "Consumer3", - IsStopped = false - }); - - var status = await sut.CheckHealthAsync(null!); - - Assert.Equal(HealthStatus.Degraded, status.Status); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs index 1853eec57..0611da061 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/EventStoreTests.cs @@ -22,18 +22,22 @@ namespace Squidex.Infrastructure.EventSourcing public string LastPosition { get; set; } - public Task OnErrorAsync(IEventSubscription subscription, Exception exception) + public void Dispose() + { + } + + public ValueTask OnErrorAsync(IEventSubscription subscription, Exception exception) { throw exception; } - public Task OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) + public ValueTask OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) { LastPosition = storedEvent.EventPosition; Events.Add(storedEvent); - return Task.CompletedTask; + return default; } } @@ -483,7 +487,7 @@ namespace Squidex.Infrastructure.EventSourcing } finally { - subscription?.Unsubscribe(); + subscription?.Dispose(); } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs deleted file mode 100644 index dbfd39e15..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerManagerGrainTests.cs +++ /dev/null @@ -1,181 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using FluentAssertions; -using Orleans; -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); - - 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_if_stream_name_defined() - { - await sut.ActivateAsync("a-123"); - - A.CallTo(() => grainA.ActivateAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.ActivateAsync()) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_start_all_grains() - { - await sut.StartAllAsync(); - - A.CallTo(() => grainA.StartAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.StartAsync()) - .MustHaveHappened(); - } - - [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_all_grains() - { - await sut.StopAllAsync(); - - A.CallTo(() => grainA.StopAsync()) - .MustHaveHappened(); - - A.CallTo(() => grainB.StopAsync()) - .MustHaveHappened(); - } - - [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 EventConsumerInfo { Name = "A", Error = "A-Error", IsStopped = false, Position = "123" }); - - A.CallTo(() => grainB.GetStateAsync()) - .Returns(new EventConsumerInfo { Name = "B", Error = "B-Error", IsStopped = false, Position = "456" }); - - var infos = await sut.GetConsumersAsync(); - - infos.Value.Should().BeEquivalentTo( - 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/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs deleted file mode 100644 index 62424ecfc..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/OrleansEventNotifierTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans; -using Squidex.Infrastructure.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(SingleGrain.Id, null)) - .Returns(manager); - - sut = new OrleansEventNotifier(factory); - } - - [Fact] - public void Should_wakeup_manager_with_stream_name() - { - sut.NotifyEventsStored("my-stream"); - - A.CallTo(() => manager.ActivateAsync("my-stream")) - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs index 80d6f2dea..a6ba8d6a1 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/MongoParallelInsertTests.cs @@ -7,12 +7,8 @@ using FakeItEasy; using Microsoft.Extensions.Logging; -using Orleans.Core; -using Orleans.Internal; -using Squidex.Infrastructure.EventSourcing.Grains; -using Squidex.Infrastructure.Orleans; +using Squidex.Infrastructure.EventSourcing.Consume; using Squidex.Infrastructure.Reflection; -using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.TestHelpers; using Xunit; @@ -23,147 +19,11 @@ namespace Squidex.Infrastructure.EventSourcing [Trait("Category", "Dependencies")] public sealed class MongoParallelInsertTests : IClassFixture { - private readonly IGrainState state = A.Fake>(); - private readonly ILogger log = A.Fake>(); + private readonly TestState state = new TestState(DomainId.Empty); private readonly IEventFormatter eventFormatter; public MongoEventStoreFixture _ { get; } - public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler - { - [ThreadStatic] - private static bool currentThreadIsProcessingItems; - - private readonly LinkedList tasks = new LinkedList(); - private readonly int maxDegreeOfParallelism; - private int delegatesQueuedOrRunning; - - public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) - { - this.maxDegreeOfParallelism = maxDegreeOfParallelism; - } - - protected sealed override void QueueTask(Task task) - { - lock (tasks) - { - tasks.AddLast(task); - - if (delegatesQueuedOrRunning < maxDegreeOfParallelism) - { - ++delegatesQueuedOrRunning; - - NotifyThreadPoolOfPendingWork(); - } - } - } - - private void NotifyThreadPoolOfPendingWork() - { - ThreadPool.UnsafeQueueUserWorkItem(_ => - { - currentThreadIsProcessingItems = true; - try - { - while (true) - { - Task item; - lock (tasks) - { - if (tasks.Count == 0) - { - --delegatesQueuedOrRunning; - break; - } - - item = tasks.First!.Value; - - tasks.RemoveFirst(); - } - - TryExecuteTask(item); - } - } - finally - { - currentThreadIsProcessingItems = false; - } - }, null); - } - - protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) - { - if (!currentThreadIsProcessingItems) - { - return false; - } - - if (taskWasPreviouslyQueued) - { - TryDequeue(task); - } - - return TryExecuteTask(task); - } - - protected sealed override bool TryDequeue(Task task) - { - lock (tasks) - { - return tasks.Remove(task); - } - } - - public sealed override int MaximumConcurrencyLevel - { - get => maxDegreeOfParallelism; - } - - protected sealed override IEnumerable GetScheduledTasks() - { - var lockTaken = false; - try - { - Monitor.TryEnter(tasks, ref lockTaken); - - if (lockTaken) - { - return tasks.ToArray(); - } - else - { - ThrowHelper.NotSupportedException(); - return default!; - } - } - finally - { - if (lockTaken) - { - Monitor.Exit(tasks); - } - } - } - } - - public sealed class MyEventConsumerGrain : EventConsumerGrain - { - private readonly TaskScheduler scheduler = new LimitedConcurrencyLevelTaskScheduler(1); - - public TaskScheduler Scheduler => scheduler; - - public MyEventConsumerGrain( - IGrainIdentity identity, - IGrainState state, - IEventConsumerFactory eventConsumerFactory, - IEventFormatter eventFormatter, - IEventStore eventStore, - ILogger log) - : base(identity, state, eventConsumerFactory, eventFormatter, eventStore, log) - { - } - } - public class MyEvent : IEvent { } @@ -176,11 +36,11 @@ namespace Squidex.Infrastructure.EventSourcing public Func EventReceived { get; set; } - public string Name => "Test"; + public int Received { get; set; } - public string EventsFilter => ".*"; + public string Name { get; } = RandomHash.Simple(); - public int Received { get; set; } + public string EventsFilter => $"^{Name}"; public Task Completed => tcs.Task; @@ -220,24 +80,15 @@ namespace Squidex.Infrastructure.EventSourcing [Fact] public async Task Should_insert_and_retrieve_parallel() { - var expectedEvents = 10 * 1000; + const int expectedEvents = 2_000; var eventConsumer = new MyEventConsumer(expectedEvents); - var eventConsumerGrain = BuildGrain(eventConsumer); + var eventProcessor = BuildProcessor(eventConsumer); - await eventConsumerGrain.ActivateAsync(); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (_, _) => - { - for (var i = 0; i < 500; i++) - { - var commitId = Guid.NewGuid(); - - var data = eventFormatter.ToEventData(Envelope.Create(new MyEvent()), commitId); - - await _.EventStore.AppendAsync(commitId, commitId.ToString(), new[] { data }); - } - }); + await InsertAsync(eventConsumer, expectedEvents, parallelism: 20); await AssertConsumerAsync(expectedEvents, eventConsumer); } @@ -245,25 +96,15 @@ namespace Squidex.Infrastructure.EventSourcing [Fact] public async Task Should_insert_and_retrieve_parallel_with_multiple_events_per_commit() { - var expectedEvents = 10 * 1000; + const int expectedEvents = 2_000; var eventConsumer = new MyEventConsumer(expectedEvents); - var eventConsumerGrain = BuildGrain(eventConsumer); + var eventProcessor = BuildProcessor(eventConsumer); - await eventConsumerGrain.ActivateAsync(); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await Parallel.ForEachAsync(Enumerable.Range(0, 10), async (_, _) => - { - for (var i = 0; i < 500; i++) - { - var commitId = Guid.NewGuid(); - - var data1 = eventFormatter.ToEventData(Envelope.Create(new MyEvent()), commitId); - var data2 = eventFormatter.ToEventData(Envelope.Create(new MyEvent()), commitId); - - await _.EventStore.AppendAsync(commitId, commitId.ToString(), new[] { data1, data2 }); - } - }); + await InsertAsync(eventConsumer, expectedEvents, messagesPerCommit: 2); await AssertConsumerAsync(expectedEvents, eventConsumer); } @@ -271,24 +112,15 @@ namespace Squidex.Infrastructure.EventSourcing [Fact] public async Task Should_insert_and_retrieve_afterwards() { - var expectedEvents = 10 * 1000; + const int expectedEvents = 2_000; var eventConsumer = new MyEventConsumer(expectedEvents); - var eventConsumerGrain = BuildGrain(eventConsumer); + var eventProcessor = BuildProcessor(eventConsumer); - await Parallel.ForEachAsync(Enumerable.Range(0, 10), async (_, _) => - { - for (var i = 0; i < 1000; i++) - { - var commitId = Guid.NewGuid(); + await InsertAsync(eventConsumer, expectedEvents); - var data = eventFormatter.ToEventData(Envelope.Create(new MyEvent()), commitId); - - await _.EventStore.AppendAsync(commitId, commitId.ToString(), new[] { data }); - } - }); - - await eventConsumerGrain.ActivateAsync(); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); await AssertConsumerAsync(expectedEvents, eventConsumer); } @@ -296,36 +128,17 @@ namespace Squidex.Infrastructure.EventSourcing [Fact] public async Task Should_insert_and_retrieve_partially_afterwards() { - var expectedEvents = 10 * 1000; + const int expectedEvents = 2_000; var eventConsumer = new MyEventConsumer(expectedEvents); - var eventConsumerGrain = BuildGrain(eventConsumer); - - await Parallel.ForEachAsync(Enumerable.Range(0, 10), async (_, _) => - { - for (var i = 0; i < 500; i++) - { - var commitId = Guid.NewGuid(); - - var data = eventFormatter.ToEventData(Envelope.Create(new MyEvent()), commitId); - - await _.EventStore.AppendAsync(commitId, commitId.ToString(), new[] { data }); - } - }); - - await eventConsumerGrain.ActivateAsync(); + var eventProcessor = BuildProcessor(eventConsumer); - await Parallel.ForEachAsync(Enumerable.Range(0, 10), async (_, _) => - { - for (var i = 0; i < 500; i++) - { - var commitId = Guid.NewGuid(); + await InsertAsync(eventConsumer, expectedEvents / 2); - var data = eventFormatter.ToEventData(Envelope.Create(new MyEvent()), commitId); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await _.EventStore.AppendAsync(commitId, commitId.ToString(), new[] { data }); - } - }); + await InsertAsync(eventConsumer, expectedEvents / 2); await AssertConsumerAsync(expectedEvents, eventConsumer); } @@ -333,29 +146,15 @@ namespace Squidex.Infrastructure.EventSourcing [Fact] public async Task Should_insert_and_retrieve_parallel_with_waits() { - var expectedEvents = 10 * 1000; + const int expectedEvents = 2_000; var eventConsumer = new MyEventConsumer(expectedEvents); - var eventConsumerGrain = BuildGrain(eventConsumer); - - await eventConsumerGrain.ActivateAsync(); - - await Parallel.ForEachAsync(Enumerable.Range(0, 10), async (_, _) => - { - for (var j = 0; j < 10; j++) - { - for (var i = 0; i < 100; i++) - { - var commitId = Guid.NewGuid(); + var eventProcessor = BuildProcessor(eventConsumer); - var data = eventFormatter.ToEventData(Envelope.Create(new MyEvent()), commitId); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await _.EventStore.AppendAsync(commitId, commitId.ToString(), new[] { data }); - } - - await Task.Delay(1000); - } - }); + await InsertAsync(eventConsumer, expectedEvents, iterations: 10); await AssertConsumerAsync(expectedEvents, eventConsumer); } @@ -363,63 +162,72 @@ namespace Squidex.Infrastructure.EventSourcing [Fact] public async Task Should_insert_and_retrieve_parallel_with_stops_and_starts() { - var expectedEvents = 10 * 1000; + const int expectedEvents = 2_000; var eventConsumer = new MyEventConsumer(expectedEvents); - var eventConsumerGrain = BuildGrain(eventConsumer); - - var scheduler = eventConsumerGrain.Scheduler; + var eventProcessor = BuildProcessor(eventConsumer); - eventConsumer.EventReceived = count => + eventConsumer.EventReceived = async count => { if (count % 1000 == 0) { - Task.Factory.StartNew(async () => - { - await eventConsumerGrain.StopAsync(); - await eventConsumerGrain.StartAsync(); - }, default, default, scheduler).Forget(); + await eventProcessor.StopAsync(); + await eventProcessor.StartAsync(); } - - return Task.CompletedTask; }; - await eventConsumerGrain.ActivateAsync(); - - await Parallel.ForEachAsync(Enumerable.Range(0, 10), async (_, _) => - { - for (var i = 0; i < 1000; i++) - { - var commitId = Guid.NewGuid(); - - var data = eventFormatter.ToEventData(Envelope.Create(new MyEvent()), commitId); + await eventProcessor.InitializeAsync(default); + await eventProcessor.ActivateAsync(); - await _.EventStore.AppendAsync(commitId, commitId.ToString(), new[] { data }); - } - }); + await InsertAsync(eventConsumer, expectedEvents); await AssertConsumerAsync(expectedEvents, eventConsumer); } - private MyEventConsumerGrain BuildGrain(IEventConsumer eventConsumer) + private EventConsumerProcessor BuildProcessor(IEventConsumer eventConsumer) { - var identity = A.Fake(); + return new EventConsumerProcessor( + state.PersistenceFactory, + eventConsumer, + eventFormatter, + _.EventStore, + A.Fake>()); + } + + private Task InsertAsync(IEventConsumer consumer, int numItems, int parallelism = 5, int messagesPerCommit = 1, int iterations = 1) + { + var perTask = numItems / (parallelism * messagesPerCommit * iterations); + + return Parallel.ForEachAsync(Enumerable.Range(0, parallelism), async (_, _) => + { + for (var i = 0; i < iterations; i++) + { + for (var j = 0; j < perTask; j++) + { + var streamName = $"{consumer.Name}-{Guid.NewGuid()}"; - A.CallTo(() => identity.PrimaryKeyString) - .Returns(eventConsumer.Name); + var commitId = Guid.NewGuid(); + var commitList = new List(); - var eventConsumerFactory = A.Fake(); + for (var k = 0; k < messagesPerCommit; k++) + { + commitList.Add(eventFormatter.ToEventData(Envelope.Create(new MyEvent()), commitId)); + } - A.CallTo(() => eventConsumerFactory.Create(eventConsumer.Name)) - .Returns(eventConsumer); + await _.EventStore.AppendAsync(commitId, streamName, commitList); + } - return new MyEventConsumerGrain(identity, state, eventConsumerFactory, eventFormatter, _.EventStore, log); + if (i < iterations - 1) + { + await Task.Delay(1000); + } + } + }); } private static async Task AssertConsumerAsync(int expectedEvents, MyEventConsumer eventConsumer) { - await eventConsumer.Completed.WithTimeout(TimeSpan.FromSeconds(100)); - + await Task.WhenAny(eventConsumer.Completed, Task.Delay(TimeSpan.FromSeconds(20))); await Task.Delay(2000); Assert.Equal(expectedEvents, eventConsumer.Received); diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs index 0c9e0d954..df0682b60 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/PollingSubscriptionTests.cs @@ -92,7 +92,7 @@ namespace Squidex.Infrastructure.EventSourcing { await Task.Delay(200); - sut.Unsubscribe(); + sut.Dispose(); } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs index e507647b6..3f9f0b065 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/EventSourcing/RetrySubscriptionTests.cs @@ -23,26 +23,15 @@ namespace Squidex.Infrastructure.EventSourcing A.CallTo(() => eventStore.CreateSubscription(A._, A._, A._)) .Returns(eventSubscription); - A.CallTo(() => eventSubscription.Sender) - .Returns(eventSubscription); - sut = new RetrySubscription(eventSubscriber, s => eventStore.CreateSubscription(s)) { ReconnectWaitMs = 50 }; sutSubscriber = sut; } - [Fact] - public void Should_return_original_subscription_as_sender() - { - var sender = sut.Sender; - - Assert.Same(eventSubscription, sender); - } - [Fact] public void Should_subscribe_after_constructor() { - sut.Unsubscribe(); + sut.Dispose(); A.CallTo(() => eventStore.CreateSubscription(sut, A._, A._)) .MustHaveHappened(); @@ -55,9 +44,9 @@ namespace Squidex.Infrastructure.EventSourcing await Task.Delay(1000); - sut.Unsubscribe(); + sut.Dispose(); - A.CallTo(() => eventSubscription.Unsubscribe()) + A.CallTo(() => eventSubscription.Dispose()) .MustHaveHappened(2, Times.Exactly); A.CallTo(() => eventStore.CreateSubscription(A._, A._, A._)) @@ -79,32 +68,12 @@ namespace Squidex.Infrastructure.EventSourcing await OnErrorAsync(eventSubscription, ex); await OnErrorAsync(eventSubscription, ex); - sut.Unsubscribe(); + sut.Dispose(); - A.CallTo(() => eventSubscriber.OnErrorAsync(eventSubscription, ex)) + A.CallTo(() => eventSubscriber.OnErrorAsync(sut, ex)) .MustHaveHappened(); } - [Fact] - public async Task Should_not_unsubscribe_after_last_error_to_keep_sender() - { - var ex = new InvalidOperationException(); - - await OnErrorAsync(eventSubscription, ex); - await OnErrorAsync(eventSubscription, ex); - await OnErrorAsync(eventSubscription, ex); - await OnErrorAsync(eventSubscription, ex); - await OnErrorAsync(eventSubscription, ex); - await OnErrorAsync(eventSubscription, ex); - - A.CallTo(() => eventSubscriber.OnErrorAsync(eventSubscription, ex)) - .MustHaveHappened(); - - Assert.NotNull(sut.Sender); - - sut.Unsubscribe(); - } - [Fact] public async Task Should_not_forward_error_if_exception_is_raised_after_unsubscribe() { @@ -112,7 +81,7 @@ namespace Squidex.Infrastructure.EventSourcing await OnErrorAsync(eventSubscription, ex); - sut.Unsubscribe(); + sut.Dispose(); A.CallTo(() => eventSubscriber.OnErrorAsync(eventSubscription, A._)) .MustNotHaveHappened(); @@ -121,37 +90,37 @@ namespace Squidex.Infrastructure.EventSourcing [Fact] public async Task Should_forward_event_from_inner_subscription() { - var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); + var @event = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); - await OnEventAsync(eventSubscription, ev); + await OnEventAsync(eventSubscription, @event); - sut.Unsubscribe(); + sut.Dispose(); - A.CallTo(() => eventSubscriber.OnEventAsync(eventSubscription, ev)) + A.CallTo(() => eventSubscriber.OnEventAsync(sut, @event)) .MustHaveHappened(); } [Fact] - public async Task Should_forward_event_if_message_is_from_another_subscription() + public async Task Should_not_forward_event_if_message_is_from_another_subscription() { - var ev = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); + var @event = new StoredEvent("Stream", "1", 2, new EventData("Type", new EnvelopeHeaders(), "Payload")); - await OnEventAsync(A.Fake(), ev); + await OnEventAsync(A.Fake(), @event); - sut.Unsubscribe(); + sut.Dispose(); A.CallTo(() => eventSubscriber.OnEventAsync(A._, A._)) - .MustHaveHappened(); + .MustNotHaveHappened(); } - private Task OnErrorAsync(IEventSubscription subscriber, Exception ex) + private ValueTask OnErrorAsync(IEventSubscription subscriber, Exception ex) { return sutSubscriber.OnErrorAsync(subscriber, ex); } - private Task OnEventAsync(IEventSubscription subscriber, StoredEvent ev) + private ValueTask OnEventAsync(IEventSubscription subscriber, StoredEvent ev) { return sutSubscriber.OnEventAsync(subscriber, ev); } } -} \ No newline at end of file +} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs index 8633c0b17..b93c45179 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Log/BackgroundRequestLogStoreTests.cs @@ -104,7 +104,7 @@ namespace Squidex.Infrastructure.Log Assert.Empty(results); - A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, ct)) + A.CallTo(() => requestLogRepository.QueryAllAsync(key, dateFrom, dateTo, A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs index bee62a38c..fd70844fe 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/Migrations/MigratorTests.cs @@ -119,10 +119,10 @@ namespace Squidex.Infrastructure.Migrations A.CallTo(() => migrator_2_3.UpdateAsync(ct)) .MustHaveHappened(); - A.CallTo(() => status.CompleteAsync(1, ct)) + A.CallTo(() => status.CompleteAsync(1, A._)) .MustNotHaveHappened(); - A.CallTo(() => status.CompleteAsync(2, ct)) + A.CallTo(() => status.CompleteAsync(2, A._)) .MustNotHaveHappened(); A.CallTo(() => status.CompleteAsync(3, ct)) @@ -219,7 +219,7 @@ namespace Squidex.Infrastructure.Migrations A.CallTo(log).Where(x => x.Method.Name == "Log" && x.GetArgument(0) == LogLevel.Critical && x.GetArgument(3) == ex) .MustHaveHappened(); - A.CallTo(() => migrator_1_2.UpdateAsync(ct)) + A.CallTo(() => migrator_1_2.UpdateAsync(A._)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs deleted file mode 100644 index 4748cc1b6..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs +++ /dev/null @@ -1,80 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans; -using Orleans.Core; -using Orleans.Runtime; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - public class ActivationLimiterFilterTests - { - private readonly IIncomingGrainCallContext context = A.Fake(); - private readonly ActivationLimiterFilter sut; - - public ActivationLimiterFilterTests() - { - sut = new ActivationLimiterFilter(); - } - - public sealed class MyGrain : GrainBase - { - public MyGrain(IGrainIdentity identity, IActivationLimit limit) - : base(identity, CreateRuntime(limit)) - { - } - - private static IGrainRuntime CreateRuntime(IActivationLimit limit) - { - var serviceProvider = A.Fake(); - - var grainRuntime = A.Fake(); - - A.CallTo(() => grainRuntime.ServiceProvider) - .Returns(serviceProvider); - - A.CallTo(() => serviceProvider.GetService(typeof(IActivationLimit))) - .Returns(limit); - - return grainRuntime; - } - } - - [Fact] - public async Task Should_update_iam_alive_for_grain_base() - { - var limit = A.Fake(); - - A.CallTo(() => context.Grain) - .Returns(new MyGrain(A.Fake(), limit)); - - await sut.Invoke(context); - - A.CallTo(() => limit.ReportIAmAlive()) - .MustHaveHappened(); - - A.CallTo(() => context.Invoke()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_also_handle_other_grains() - { - var grain = A.Fake(); - - A.CallTo(() => context.Grain) - .Returns(grain); - - await sut.Invoke(context); - - A.CallTo(() => context.Invoke()) - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs deleted file mode 100644 index 1042166d8..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans; -using Orleans.Core; -using Orleans.Runtime; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - public class ActivationLimiterTests - { - private readonly IGrainIdentity grainIdentity = A.Fake(); - private readonly IGrainRuntime grainRuntime = A.Fake(); - private readonly ActivationLimiter sut; - - private sealed class MyGrain : GrainBase - { - public MyGrain(IGrainIdentity identity, IGrainRuntime runtime, IActivationLimit limit) - : base(identity, runtime) - { - limit.SetLimit(3, TimeSpan.FromMinutes(3)); - } - } - - public ActivationLimiterTests() - { - sut = new ActivationLimiter(); - } - - [Fact] - public void Should_deactivate_last_grain() - { - var grain1 = CreateGuidGrain(); - - CreateGuidGrain(); - CreateGuidGrain(); - CreateGuidGrain(); - - A.CallTo(() => grainRuntime.DeactivateOnIdle(grain1)) - .MustHaveHappened(); - } - - [Fact] - public void Should_not_deactivate_last_grain_if_other_died() - { - CreateGuidGrain(); - CreateGuidGrain().ReportIAmDead(); - CreateGuidGrain(); - CreateGuidGrain(); - - A.CallTo(() => grainRuntime.DeactivateOnIdle(A._)) - .MustNotHaveHappened(); - } - - private MyGrain CreateGuidGrain() - { - var context = A.Fake(); - - var limit = new ActivationLimit(context, sut); - - var serviceProvider = A.Fake(); - - A.CallTo(() => grainRuntime.ServiceProvider) - .Returns(serviceProvider); - - A.CallTo(() => context.ActivationServices) - .Returns(serviceProvider); - - A.CallTo(() => serviceProvider.GetService(typeof(IActivationLimit))) - .Returns(limit); - - A.CallTo(() => serviceProvider.GetService(typeof(IGrainRuntime))) - .Returns(grainRuntime); - - var grain = new MyGrain(grainIdentity, grainRuntime, limit); - - A.CallTo(() => context.GrainInstance) - .Returns(grain); - - A.CallTo(() => context.GrainType) - .Returns(typeof(MyGrain)); - - grain.ReportIAmAlive(); - - return grain; - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivityPropagationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivityPropagationTests.cs deleted file mode 100644 index aa3acfb86..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ActivityPropagationTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Diagnostics; -using Microsoft.Extensions.Configuration; -using Orleans; -using Orleans.Hosting; -using Orleans.TestingHost; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - [Trait("Category", "Dependencies")] - public class ActivityPropagationTests - { - public interface IActivityGrain : IGrainWithStringKey - { - public Task GetActivityId(); - } - - public class ActivityGrain : Grain, IActivityGrain - { - public Task GetActivityId() - { - return Task.FromResult(Activity.Current?.TraceId.ToHexString() ?? string.Empty); - } - } - - public sealed class Configurator : ISiloConfigurator, IClientBuilderConfigurator - { - public void Configure(ISiloBuilder siloBuilder) - { - siloBuilder.AddIncomingGrainCallFilter(); - siloBuilder.AddOutgoingGrainCallFilter(); - } - - public void Configure(IConfiguration configuration, IClientBuilder clientBuilder) - { - clientBuilder.AddOutgoingGrainCallFilter(); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_forward_activity(bool listen) - { - var cluster = - new TestClusterBuilder(1) - .AddSiloBuilderConfigurator() - .AddClientBuilderConfigurator() - .Build(); - - await cluster.DeployAsync(); - try - { - using var listener = new ActivityListener - { - ShouldListenTo = s => true, - Sample = (ref ActivityCreationOptions activityOptions) => - { - return ActivitySamplingResult.AllData; - }, - SampleUsingParentId = (ref ActivityCreationOptions activityOptions) => - { - return ActivitySamplingResult.AllData; - } - }; - - if (listen) - { - ActivitySource.AddActivityListener(listener); - } - - using (var activity = Telemetry.Activities.StartActivity("Test", ActivityKind.Server)) - { - var grain = cluster.GrainFactory.GetGrain(SingleGrain.Id); - - var activityId = await grain.GetActivityId(); - - if (listen) - { - Assert.Equal(activity?.TraceId.ToHexString(), activityId); - } - else - { - Assert.Empty(activityId); - } - } - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.DisposeAsync().AsTask()); - } - } - - [Fact] - public async Task Should_create_new_activity() - { - var cluster = - new TestClusterBuilder(1) - .AddSiloBuilderConfigurator() - .AddClientBuilderConfigurator() - .Build(); - - await cluster.DeployAsync(); - try - { - using var listener = new ActivityListener - { - ShouldListenTo = s => true, - Sample = (ref ActivityCreationOptions activityOptions) => - { - return ActivitySamplingResult.AllData; - }, - SampleUsingParentId = (ref ActivityCreationOptions activityOptions) => - { - return ActivitySamplingResult.AllData; - } - }; - - ActivitySource.AddActivityListener(listener); - - var grain = cluster.GrainFactory.GetGrain(SingleGrain.Id); - - var activityId = await grain.GetActivityId(); - - Assert.NotEmpty(activityId); - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.DisposeAsync().AsTask()); - } - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs deleted file mode 100644 index 6abe8dd8b..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/AsyncLocalTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using Orleans; -using Orleans.TestingHost; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - [Trait("Category", "Dependencies")] - public class AsyncLocalTests - { - public interface IAsyncLocalGrain : IGrainWithStringKey - { - public Task GetValueAsync(); - } - - public class AsyncLocalGrain : Grain, IAsyncLocalGrain - { - private readonly AsyncLocal temp = new AsyncLocal(); - - public Task GetValueAsync() - { - temp.Value++; - - return Task.FromResult(temp.Value); - } - } - - [Fact] - public async Task Should_use_async_local() - { - var cluster = - new TestClusterBuilder(1) - .Build(); - - await cluster.DeployAsync(); - - try - { - var grain = cluster.GrainFactory.GetGrain(SingleGrain.Id); - - var result1 = await grain.GetValueAsync(); - var result2 = await grain.GetValueAsync(); - - await cluster.KillSiloAsync(cluster.Silos[0]); - await cluster.StartAdditionalSiloAsync(); - - var result3 = await grain.GetValueAsync(); - - Assert.Equal(1, result1); - Assert.Equal(1, result2); - Assert.Equal(1, result3); - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.DisposeAsync().AsTask()); - } - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs deleted file mode 100644 index 47c3b5970..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/BootstrapTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans; -using Orleans.Runtime; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - public class BootstrapTests - { - private readonly IBackgroundGrain grain = A.Fake(); - private readonly GrainBootstrap sut; - - public BootstrapTests() - { - var factory = A.Fake(); - - sut = new GrainBootstrap(factory); - - A.CallTo(() => factory.GetGrain("Default", null)) - .Returns(grain); - } - - [Fact] - public async Task Should_activate_grain_on_run() - { - await sut.StartAsync(default); - - A.CallTo(() => grain.ActivateAsync()) - .MustHaveHappened(); - } - - [Fact] - public async Task Should_fail_on_non_rejection_exception() - { - A.CallTo(() => grain.ActivateAsync()) - .Throws(new InvalidOperationException()); - - await Assert.ThrowsAsync(() => sut.StartAsync(default)); - } - - [Fact] - public async Task Should_retry_after_rejection_exception() - { - A.CallTo(() => grain.ActivateAsync()) - .Throws(new OrleansException()).Once(); - - await sut.StartAsync(default); - - A.CallTo(() => grain.ActivateAsync()) - .MustHaveHappened(2, Times.Exactly); - } - - [Fact] - public async Task Should_fail_after_10_rejection_exception() - { - A.CallTo(() => grain.ActivateAsync()) - .Throws(new OrleansException()); - - await Assert.ThrowsAsync(() => sut.StartAsync(default)); - - A.CallTo(() => grain.ActivateAsync()) - .MustHaveHappened(10, Times.Exactly); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/CultureFilterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/CultureFilterTests.cs deleted file mode 100644 index eeff370e5..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/CultureFilterTests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Globalization; -using Microsoft.Extensions.Configuration; -using Orleans; -using Orleans.Hosting; -using Orleans.TestingHost; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - [Trait("Category", "Dependencies")] - public class CultureFilterTests - { - public interface ICultureGrain : IGrainWithStringKey - { - public Task GetCultureAsync(bool chaining); - - public Task GetUICultureAsync(bool chaining); - } - - public class CultureGrain : Grain, ICultureGrain - { - public Task GetCultureAsync(bool chaining) - { - if (chaining) - { - return GrainFactory.GetGrain("1").GetCultureAsync(false); - } - - return Task.FromResult(CultureInfo.CurrentCulture.ToString()); - } - - public Task GetUICultureAsync(bool chaining) - { - if (chaining) - { - return GrainFactory.GetGrain("1").GetUICultureAsync(false); - } - - return Task.FromResult(CultureInfo.CurrentUICulture.ToString()); - } - } - - public sealed class Configurator : ISiloConfigurator, IClientBuilderConfigurator - { - public void Configure(ISiloBuilder siloBuilder) - { - siloBuilder.AddIncomingGrainCallFilter(); - siloBuilder.AddOutgoingGrainCallFilter(); - } - - public void Configure(IConfiguration configuration, IClientBuilder clientBuilder) - { - clientBuilder.AddOutgoingGrainCallFilter(); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task Should_forward_culture(bool chaining) - { - await using var cluster = - new TestClusterBuilder(1) - .AddSiloBuilderConfigurator() - .AddClientBuilderConfigurator() - .Build(); - - await cluster.DeployAsync(); - - var previousCulture = CultureInfo.CurrentCulture; - var previousUICulture = CultureInfo.CurrentUICulture; - try - { - var culture = CultureInfo.GetCultureInfo("de"); - var cultureUI = CultureInfo.GetCultureInfo("it"); - - CultureInfo.CurrentCulture = culture; - CultureInfo.CurrentUICulture = cultureUI; - - var grain = cluster.GrainFactory.GetGrain(SingleGrain.Id); - - var cultureFromGrain = await grain.GetCultureAsync(chaining); - var cultureUIFromGrain = await grain.GetUICultureAsync(chaining); - - Assert.Equal(culture.Name, cultureFromGrain); - Assert.Equal(cultureUI.Name, cultureUIFromGrain); - } - finally - { - CultureInfo.CurrentCulture = previousCulture; - CultureInfo.CurrentUICulture = previousUICulture; - } - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ExceptionWrapperFilterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/ExceptionWrapperFilterTests.cs deleted file mode 100644 index 0a9aac59e..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/ExceptionWrapperFilterTests.cs +++ /dev/null @@ -1,157 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using System.Net; -using FakeItEasy; -using MongoDB.Driver; -using MongoDB.Driver.Core.Clusters; -using MongoDB.Driver.Core.Connections; -using MongoDB.Driver.Core.Servers; -using Orleans; -using Orleans.Hosting; -using Orleans.TestingHost; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -#pragma warning disable SA1133 // Do not combine attributes - -namespace Squidex.Infrastructure.Orleans -{ - public class ExceptionWrapperFilterTests - { - private readonly IIncomingGrainCallContext context = A.Fake(); - private readonly ExceptionWrapperFilter sut; - - public interface IExceptionGrain : IGrainWithStringKey - { - Task ThrowCustomAsync(); - - Task ThrowMongoAsync(); - } - - public sealed class ExceptionGrain : Grain, IExceptionGrain - { - public Task ThrowCustomAsync() - { - throw new InvalidException("My Message"); - } - - public Task ThrowMongoAsync() - { - var connection = new ConnectionId(new ServerId(new ClusterId(), new IPEndPoint(IPAddress.Loopback, 21017)), 1); - - throw new MongoWriteException(connection, null, null, null); - } - } - - private sealed class InvalidException : Exception - { - public InvalidException(string message) - : base(message) - { - } - } - - public sealed class Configurator : ISiloConfigurator - { - public void Configure(ISiloBuilder siloBuilder) - { - siloBuilder.AddIncomingGrainCallFilter(); - } - } - - public ExceptionWrapperFilterTests() - { - sut = new ExceptionWrapperFilter(); - } - - [Fact] - public async Task Should_just_forward_serializable_exception() - { - var original = new InvalidOperationException(); - - A.CallTo(() => context.Invoke()) - .Throws(original); - - var ex = await Assert.ThrowsAnyAsync(() => sut.Invoke(context)); - - Assert.Same(ex, original); - } - - [Fact] - public async Task Should_wrap_non_serializable_exception() - { - var original = new InvalidException("My Message"); - - A.CallTo(() => context.Invoke()) - .Throws(original); - - var ex = await Assert.ThrowsAnyAsync(() => sut.Invoke(context)); - - Assert.Equal(original.GetType(), ex.ExceptionType); - Assert.Contains(original.Message, ex.Message, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public void Should_serialize_and_deserialize() - { - var original = new InvalidException("My Message"); - - var source = new OrleansWrapperException(original, original.GetType()); - var serialized = source.SerializeAndDeserializeBinary(); - - Assert.Equal(serialized.ExceptionType, source.ExceptionType); - Assert.Equal(serialized.Message, source.Message); - } - - [Fact, Trait("Category", "Dependencies")] - public async Task Simple_grain_tests() - { - var (cluster, grain) = await GetGrainAsync(); - - try - { - var ex = await Assert.ThrowsAsync(() => grain.ThrowCustomAsync()); - - Assert.Equal(typeof(InvalidException), ex.ExceptionType); - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.DisposeAsync().AsTask()); - } - } - - [Fact, Trait("Category", "Dependencies")] - public async Task Simple_grain_tests_with_mongo_exception() - { - var (cluster, grain) = await GetGrainAsync(); - - try - { - var ex = await Assert.ThrowsAsync(() => grain.ThrowMongoAsync()); - - Assert.Equal(typeof(MongoWriteException), ex.ExceptionType); - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.DisposeAsync().AsTask()); - } - } - - private static async Task<(TestCluster, IExceptionGrain)> GetGrainAsync() - { - var cluster = - new TestClusterBuilder(1) - .AddSiloBuilderConfigurator() - .Build(); - - await cluster.DeployAsync(); - - return (cluster, cluster.GrainFactory.GetGrain(SingleGrain.Id)); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameGrainTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameGrainTests.cs deleted file mode 100644 index c751f4b7c..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/Indexes/UniqueNameGrainTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Core; -using Xunit; - -namespace Squidex.Infrastructure.Orleans.Indexes -{ - public class UniqueNameGrainTests - { - private readonly UniqueNameGrain sut; - - public UniqueNameGrainTests() - { - sut = new UniqueNameGrain(A.Fake()); - } - - [Fact] - public async Task Should_acquire_token_if_not_reserved() - { - var token = await sut.ReserveAsync("1", "name1"); - - Assert.NotNull(token); - } - - [Fact] - public async Task Should_reserve_again_if_reservation_removed() - { - var token1 = await sut.ReserveAsync("1", "name1"); - - await sut.RemoveReservationAsync(token1); - - var token = await sut.ReserveAsync("2", "name1"); - - Assert.NotNull(token); - } - - [Fact] - public async Task Should_not_make_reservation_if_name_already_reserved() - { - await sut.ReserveAsync("1", "name1"); - - var token = await sut.ReserveAsync("2", "name1"); - - Assert.Null(token); - } - - [Fact] - public async Task Should_acquire_token_again_if_reserved_with_same_id() - { - var token1 = await sut.ReserveAsync("1", "name1"); - var token2 = await sut.ReserveAsync("1", "name1"); - - Assert.NotNull(token1); - Assert.NotNull(token2); - Assert.Equal(token2, token1); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializationTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializationTests.cs deleted file mode 100644 index b0d969609..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializationTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -// ========================================================================== -// 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 Orleans; -using Orleans.Configuration; -using Orleans.Hosting; -using Orleans.TestingHost; -using Squidex.Infrastructure.Commands; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - [Trait("Category", "Dependencies")] - public class JsonExternalSerializationTests - { - public interface ICommandGrain : IGrainWithStringKey - { - public Task ExecuteAsync(IAggregateCommand command); - } - - public class CommandGrain : Grain, ICommandGrain - { - public Task ExecuteAsync(IAggregateCommand command) - { - var result = new CommandResult(command.AggregateId, 0, 0, ((TestCommand)command).Value); - - return Task.FromResult(result); - } - } - - public class TestCommand : IAggregateCommand - { - public DomainId AggregateId { get; set; } - - public long ExpectedVersion { get; set; } - - public string Value { get; set; } - } - - public sealed class Configurator : ISiloConfigurator, IClientBuilderConfigurator - { - public void Configure(ISiloBuilder siloBuilder) - { - siloBuilder.ConfigureServices(services => - { - services.AddSingleton(TestUtils.DefaultSerializer); - }); - - siloBuilder.Configure(options => - { - options.SerializationProviders.Add(typeof(JsonSerializer)); - }); - } - - public void Configure(IConfiguration configuration, IClientBuilder clientBuilder) - { - clientBuilder.ConfigureServices(services => - { - services.AddSingleton(TestUtils.DefaultSerializer); - }); - - clientBuilder.Configure(options => - { - options.SerializationProviders.Add(typeof(JsonSerializer)); - }); - } - } - - [Fact] - public async Task Should_make_request_with_json_serializer() - { - var cluster = - new TestClusterBuilder(1) - .AddSiloBuilderConfigurator() - .AddClientBuilderConfigurator() - .Build(); - - await cluster.DeployAsync(); - - try - { - for (var i = 0; i < 100; i++) - { - var id = DomainId.NewGuid().ToString(); - - var commandGrain = cluster.GrainFactory.GetGrain(id); - var commandTest = new TestCommand { Value = id }; - - var result = await commandGrain.ExecuteAsync(commandTest); - - Assert.Equal(id, result.Payload); - } - } - finally - { - await Task.WhenAny(Task.Delay(2000), cluster.StopAllSilosAsync()); - } - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs deleted file mode 100644 index 2bf9130f1..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/JsonExternalSerializerTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Orleans.Serialization; -using Squidex.Infrastructure.TestHelpers; -using Xunit; - -#pragma warning disable MA0060 // The value returned by Stream.Read/Stream.ReadAsync is not used - -namespace Squidex.Infrastructure.Orleans -{ - public class JsonExternalSerializerTests - { - private readonly IExternalSerializer serializer = new JsonSerializer(TestUtils.DefaultSerializer); - - [Fact] - public void Should_not_copy_null() - { - var source = (string?)null; - - var clone = serializer.DeepCopy(source, null); - - Assert.Null(clone); - } - - [Fact] - public void Should_not_copy_values() - { - var source = new List { 1, 2, 3 }; - - var copy = serializer.DeepCopy(source, null)!; - - Assert.Same(source, copy); - } - - [Fact] - public void Should_serialize_and_deserialize_value() - { - SerializeAndDeserialize(ArrayOfLength(100), Assert.Equal); - } - - [Fact] - public void Should_serialize_and_deserialize_large_value() - { - SerializeAndDeserialize(ArrayOfLength(8000), Assert.Equal); - } - - private void SerializeAndDeserialize(T value, Action assertEquals) where T : class - { - using (var buffer = new MemoryStream()) - { - var jsonWriter = CreateWriter(buffer); - var jsonReader = CreateReader(buffer); - - serializer.Serialize(value, jsonWriter, typeof(T)); - - buffer.Position = 0; - - var deserialized = (T)serializer.Deserialize(typeof(T), jsonReader)!; - - assertEquals(deserialized, value); - Assert.NotSame(value, deserialized); - } - } - - private static IDeserializationContext CreateReader(MemoryStream buffer) - { - var reader = A.Fake(); - - A.CallTo(() => reader.ReadByteArray(A._, A._, A._)) - .Invokes(new Action((array, offset, length) => buffer.Read(array, offset, length))); - - A.CallTo(() => reader.CurrentPosition) - .ReturnsLazily(x => (int)buffer.Position); - - A.CallTo(() => reader.Length) - .ReturnsLazily(x => (int)buffer.Length); - - var context = A.Fake(); - - A.CallTo(() => context.StreamReader) - .Returns(reader); - - return context; - } - - private static ISerializationContext CreateWriter(MemoryStream buffer) - { - var writer = A.Fake(); - - A.CallTo(() => writer.Write(A._, A._, A._)) - .Invokes(new Action(buffer.Write)); - - A.CallTo(() => writer.CurrentOffset) - .ReturnsLazily(x => (int)buffer.Position); - - var context = A.Fake(); - - A.CallTo(() => context.StreamWriter) - .Returns(writer); - - return context; - } - - private static List ArrayOfLength(int length) - { - var result = new List(); - - for (var i = 0; i < length; i++) - { - result.Add(i); - } - - return result; - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs b/backend/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs deleted file mode 100644 index 2291cb207..000000000 --- a/backend/tests/Squidex.Infrastructure.Tests/Orleans/LoggingFilterTests.cs +++ /dev/null @@ -1,71 +0,0 @@ -// ========================================================================== -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex UG (haftungsbeschraenkt) -// All rights reserved. Licensed under the MIT license. -// ========================================================================== - -using FakeItEasy; -using Microsoft.Extensions.Logging; -using Orleans; -using Orleans.Runtime; -using Squidex.Infrastructure.Validation; -using Xunit; - -namespace Squidex.Infrastructure.Orleans -{ - public class LoggingFilterTests - { - private readonly ILoggerFactory logFactory = A.Fake(); - private readonly IIncomingGrainCallContext context = A.Fake(); - private readonly LoggingFilter sut; - - public LoggingFilterTests() - { - sut = new LoggingFilter(logFactory); - } - - [Fact] - public async Task Should_not_log_if_no_exception_happened() - { - await sut.Invoke(context); - - A.CallTo(() => logFactory.CreateLogger(A._)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_not_log_domain_exceptions() - { - A.CallTo(() => context.Invoke()) - .Throws(new ValidationException("Failed")); - - await Assert.ThrowsAsync(() => sut.Invoke(context)); - - A.CallTo(() => logFactory.CreateLogger(A._)) - .MustNotHaveHappened(); - } - - [Fact] - public async Task Should_log_exception_and_forward_it() - { - var log = A.Fake(); - - var grain = A.Fake(); - - A.CallTo(() => context.Invoke()) - .Throws(new InvalidOperationException()); - - A.CallTo(() => context.Grain) - .Returns(grain); - - A.CallTo(() => logFactory.CreateLogger(A._)) - .Returns(log); - - await Assert.ThrowsAsync(() => sut.Invoke(context)); - - A.CallTo(log).Where(x => x.Method.Name == "Log") - .MustHaveHappened(); - } - } -} diff --git a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj index 4f7965ade..5bdf32958 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj +++ b/backend/tests/Squidex.Infrastructure.Tests/Squidex.Infrastructure.Tests.csproj @@ -23,11 +23,6 @@ - - - all - runtime; build; native; contentfiles; analyzers - diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs index 077d6a163..35d2a0daf 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/BinaryFormatterHelper.cs @@ -17,13 +17,14 @@ namespace Squidex.Infrastructure.TestHelpers public static T SerializeAndDeserializeBinary(this T source) { - var stream = new MemoryStream(); + using (var stream = new MemoryStream()) + { + Formatter.Serialize(stream, source!); - Formatter.Serialize(stream, source!); + stream.Position = 0; - stream.Position = 0; - - return (T)Formatter.Deserialize(stream); + return (T)Formatter.Deserialize(stream); + } } } } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs index 96333803e..b89bc579c 100644 --- a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs @@ -21,12 +21,6 @@ namespace Squidex.Infrastructure.TestHelpers public bool RecreateEvent { get; set; } - public int VersionsToKeep - { - get => Capacity; - set => Capacity = value; - } - public MyDomainObject(DomainId id, IPersistenceFactory factory) : base(id, factory, A.Dummy()) { @@ -67,7 +61,8 @@ namespace Squidex.Infrastructure.TestHelpers return snapshot.IsDeleted; } - public override Task ExecuteAsync(IAggregateCommand command) + public override Task ExecuteAsync(IAggregateCommand command, + CancellationToken ct) { switch (command) { @@ -75,13 +70,13 @@ namespace Squidex.Infrastructure.TestHelpers return Upsert(c, createAuto => { RaiseEvent(new ValueChanged { Value = createAuto.Value }); - }); + }, ct); case CreateAuto c: return Create(c, createAuto => { RaiseEvent(new ValueChanged { Value = createAuto.Value }); - }); + }, ct); case CreateCustom c: return CreateReturn(c, createCustom => @@ -89,13 +84,13 @@ namespace Squidex.Infrastructure.TestHelpers RaiseEvent(new ValueChanged { Value = createCustom.Value }); return "CREATED"; - }); + }, ct); case UpdateAuto c: return Update(c, updateAuto => { RaiseEvent(new ValueChanged { Value = updateAuto.Value }); - }); + }, ct); case UpdateCustom c: return UpdateReturn(c, updateCustom => @@ -103,17 +98,20 @@ namespace Squidex.Infrastructure.TestHelpers RaiseEvent(new ValueChanged { Value = updateCustom.Value }); return "UPDATED"; - }); + }, ct); + case Delete c: return Update(c, delete => { RaiseEvent(new Deleted()); - }); + }, ct); + case DeletePermanent c: return DeletePermanent(c, delete => { RaiseEvent(new Deleted()); - }); + }, ct); + default: throw new NotSupportedException(); } diff --git a/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestState.cs b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestState.cs new file mode 100644 index 000000000..a15f5a6dc --- /dev/null +++ b/backend/tests/Squidex.Infrastructure.Tests/TestHelpers/TestState.cs @@ -0,0 +1,102 @@ +// ========================================================================== +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex UG (haftungsbeschraenkt) +// All rights reserved. Licensed under the MIT license. +// ========================================================================== + +using FakeItEasy; +using Squidex.Infrastructure.EventSourcing; +using Squidex.Infrastructure.States; + +namespace Squidex.Infrastructure.TestHelpers +{ + public sealed class TestState where T : class, new() + { + private readonly List> events = new List>(); + private HandleSnapshot? handleSnapshot; + private HandleEvent? handleEvent; + + public IPersistenceFactory PersistenceFactory { get; private set; } + + public IPersistence Persistence { get; } = A.Fake>(); + + public long Version { get; set; } = EtagVersion.Empty; + + public T Snapshot { get; set; } = new T(); + + public TestState(string id, IPersistenceFactory? persistenceFactory = null) + : this(DomainId.Create(id), persistenceFactory) + { + } + + public void AddEvent(Envelope @event) + { + events.Add(@event); + } + + public void AddEvent(IEvent @event) + { + events.Add(Envelope.Create(@event)); + } + + public TestState(DomainId id, IPersistenceFactory? persistenceFactory = null) + { + PersistenceFactory = persistenceFactory ?? A.Fake>(); + + A.CallTo(() => PersistenceFactory.WithEventSourcing(A._, id, A._)) + .Invokes(x => + { + handleEvent = x.GetArgument(2); + }) + .Returns(Persistence); + + A.CallTo(() => PersistenceFactory.WithSnapshots(A._, id, A>._)) + .Invokes(x => + { + handleSnapshot = x.GetArgument>(2); + }) + .Returns(Persistence); + + A.CallTo(() => PersistenceFactory.WithSnapshotsAndEventSourcing(A._, id, A>._, A._)) + .Invokes(x => + { + handleSnapshot = x.GetArgument>(2); + handleEvent = x.GetArgument(3); + }) + .Returns(Persistence); + + A.CallTo(() => Persistence.WriteEventsAsync(A>>._, A._)) + .Invokes(x => + { + events.AddRange(x.GetArgument>>(0)!); + }); + + A.CallTo(() => Persistence.WriteSnapshotAsync(A._, A._)) + .Invokes(x => + { + Snapshot = x.GetArgument(0)!; + }); + + A.CallTo(() => Persistence.ReadAsync(A._, A._)) + .Invokes(x => + { + handleSnapshot?.Invoke(Snapshot, Version); + + if (handleEvent != null) + { + foreach (var @event in events) + { + handleEvent(@event); + } + } + }); + + A.CallTo(() => Persistence.DeleteAsync(A._)) + .Invokes(x => + { + Snapshot = new T(); + }); + } + } +} diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs index 77e44a30a..f830bdd72 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/ETagCommandMiddlewareTests.cs @@ -94,7 +94,7 @@ namespace Squidex.Web.CommandMiddlewares { var commandContext = new CommandContext(command, A.Fake()).Complete(result); - await sut.HandleAsync(commandContext); + await sut.HandleAsync(commandContext, default); return commandContext; } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs index 1132eda3d..3019f8cc0 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithActorCommandMiddlewareTests.cs @@ -86,7 +86,7 @@ namespace Squidex.Web.CommandMiddlewares { var commandContext = new CommandContext(command, A.Fake()); - await sut.HandleAsync(commandContext); + await sut.HandleAsync(commandContext, default); return commandContext; } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs index a482ac020..25e265401 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithAppIdCommandMiddlewareTests.cs @@ -63,7 +63,7 @@ namespace Squidex.Web.CommandMiddlewares { var commandContext = new CommandContext(command, A.Fake()); - await sut.HandleAsync(commandContext); + await sut.HandleAsync(commandContext, default); return commandContext; } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs index d34d911f0..60cb0fdaa 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithContentIdCommandMiddlewareTests.cs @@ -70,7 +70,7 @@ namespace Squidex.Web.CommandMiddlewares var commandContext = new CommandContext(command, A.Fake()); - await sut.HandleAsync(commandContext); + await sut.HandleAsync(commandContext, default); return commandContext; } diff --git a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs index 54a59d6fb..5bed9d096 100644 --- a/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs @@ -92,7 +92,7 @@ namespace Squidex.Web.CommandMiddlewares var commandContext = new CommandContext(command, A.Fake()); - await sut.HandleAsync(commandContext); + await sut.HandleAsync(commandContext, default); return commandContext; } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs index d5766b350..399908bf5 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/ApiCostsFilterTests.cs @@ -53,7 +53,7 @@ namespace Squidex.Web.Pipeline SetupApp(); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today)) + A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) .Returns(true); await sut.OnActionExecutionAsync(actionContext, next); @@ -69,7 +69,7 @@ namespace Squidex.Web.Pipeline SetupApp(); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today)) + A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) .Returns(false); await sut.OnActionExecutionAsync(actionContext, next); @@ -88,7 +88,7 @@ namespace Squidex.Web.Pipeline Assert.True(isNextCalled); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today)) + A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) .MustNotHaveHappened(); } @@ -101,7 +101,7 @@ namespace Squidex.Web.Pipeline Assert.True(isNextCalled); - A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today)) + A.CallTo(() => usageGate.IsBlockedAsync(appEntity, A._, DateTime.Today, default)) .MustNotHaveHappened(); } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs index 150749121..38667f73d 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/AppResolverTests.cs @@ -298,7 +298,7 @@ namespace Squidex.Web.Pipeline private static IAppEntity CreateApp(string name, string? appUser = null, string? appClient = null, long? apiCallsLimit = null, bool? allowAnonymous = null) { - var appEntity = A.Fake(); + var app = A.Fake(); var contributors = AppContributors.Empty; @@ -314,19 +314,12 @@ namespace Squidex.Web.Pipeline clients = clients.Add(appClient, "secret").Update(appClient, apiCallsLimit: apiCallsLimit, allowAnonymous: allowAnonymous); } - A.CallTo(() => appEntity.Contributors) - .Returns(contributors); + A.CallTo(() => app.Contributors).Returns(contributors); + A.CallTo(() => app.Clients).Returns(clients); + A.CallTo(() => app.Name).Returns(name); + A.CallTo(() => app.Roles).Returns(Roles.Empty); - A.CallTo(() => appEntity.Clients) - .Returns(clients); - - A.CallTo(() => appEntity.Name) - .Returns(name); - - A.CallTo(() => appEntity.Roles) - .Returns(Roles.Empty); - - return appEntity; + return app; } } } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs index c11e3ce6b..3a4621be3 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cs @@ -232,11 +232,8 @@ namespace Squidex.Web.Pipeline var schemaEntity = A.Fake(); - A.CallTo(() => schemaEntity.SchemaDef) - .Returns(schema); - - A.CallTo(() => schemaEntity.Id) - .Returns(schemaId.Id); + A.CallTo(() => schemaEntity.Id).Returns(schemaId.Id); + A.CallTo(() => schemaEntity.SchemaDef).Returns(schema); return schemaEntity; } diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs index 579a8615e..125de77ee 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/UsageMiddlewareTests.cs @@ -41,7 +41,10 @@ namespace Squidex.Web.Pipeline return Task.CompletedTask; }; - sut = new UsageMiddleware(appLogStore, usageTracker, clock); + sut = new UsageMiddleware(appLogStore, usageTracker) + { + Clock = clock + }; } [Fact] diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs index 37573bdaa..9bfaf80e1 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/AssetTests.cs @@ -5,6 +5,7 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System.Net; using Squidex.Assets; using Squidex.ClientLibrary.Management; using Xunit; @@ -362,7 +363,7 @@ namespace TestSuite.ApiTests }); // Should return 403 when not authenticated. - Assert.Contains("403", ex.Message, StringComparison.Ordinal); + Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode); } @@ -375,7 +376,7 @@ namespace TestSuite.ApiTests }); // Should return 403 when not authenticated. - Assert.Contains("403", ex.Message, StringComparison.Ordinal); + Assert.Equal(HttpStatusCode.Forbidden, ex.StatusCode); } } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs index bff059f07..e67013c08 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentQueryTests.cs @@ -43,10 +43,9 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_query_by_ids() { - var items = await _.Contents.GetAsync(new ContentQuery - { - OrderBy = "data/number/iv asc" - }); + var q = new ContentQuery { OrderBy = "data/number/iv asc" }; + + var items = await _.Contents.GetAsync(q); var itemsById = await _.Contents.GetAsync(new HashSet(items.Items.Take(3).Select(x => x.Id))); @@ -63,10 +62,9 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_all_with_odata() { - var items = await _.Contents.GetAsync(new ContentQuery - { - OrderBy = "data/number/iv asc" - }); + var q = new ContentQuery { OrderBy = "data/number/iv asc" }; + + var items = await _.Contents.GetAsync(q); AssertItems(items, 10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); } @@ -74,7 +72,7 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_all_with_json() { - var items = await _.Contents.GetAsync(new ContentQuery + var q = new ContentQuery { JsonQuery = new { @@ -86,7 +84,9 @@ namespace TestSuite.ApiTests } } } - }); + }; + + var items = await _.Contents.GetAsync(q); AssertItems(items, 10, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); } @@ -94,10 +94,9 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_skip_with_odata() { - var items = await _.Contents.GetAsync(new ContentQuery - { - Skip = 5, OrderBy = "data/number/iv asc" - }); + var q = new ContentQuery { OrderBy = "data/number/iv asc", Skip = 5 }; + + var items = await _.Contents.GetAsync(q); AssertItems(items, 10, new[] { 6, 7, 8, 9, 10 }); } @@ -105,7 +104,7 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_skip_with_json() { - var items = await _.Contents.GetAsync(new ContentQuery + var q = new ContentQuery { JsonQuery = new { @@ -118,7 +117,9 @@ namespace TestSuite.ApiTests }, skip = 5 } - }); + }; + + var items = await _.Contents.GetAsync(q); AssertItems(items, 10, new[] { 6, 7, 8, 9, 10 }); } @@ -126,10 +127,9 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_skip_and_top_with_odata() { - var items = await _.Contents.GetAsync(new ContentQuery - { - Skip = 2, Top = 5, OrderBy = "data/number/iv asc" - }); + var q = new ContentQuery { Skip = 2, Top = 5, OrderBy = "data/number/iv asc" }; + + var items = await _.Contents.GetAsync(q); AssertItems(items, 10, new[] { 3, 4, 5, 6, 7 }); } @@ -137,10 +137,11 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_skip_and_top_with_json() { - var items = await _.Contents.GetAsync(new ContentQuery + var q = new ContentQuery { JsonQuery = new { + skip = 2, sort = new[] { new @@ -148,9 +149,11 @@ namespace TestSuite.ApiTests path = "data.number.iv", order = "ascending" } }, - skip = 2, top = 5 + top = 5 } - }); + }; + + var items = await _.Contents.GetAsync(q); AssertItems(items, 10, new[] { 3, 4, 5, 6, 7 }); } @@ -158,10 +161,9 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_filter_with_odata() { - var items = await _.Contents.GetAsync(new ContentQuery - { - Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" - }); + var q = new ContentQuery { Filter = "data/number/iv gt 3 and data/number/iv lt 7", OrderBy = "data/number/iv asc" }; + + var items = await _.Contents.GetAsync(q); AssertItems(items, 3, new[] { 4, 5, 6 }); } @@ -169,7 +171,7 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_filter_with_json() { - var items = await _.Contents.GetAsync(new ContentQuery + var q = new ContentQuery { JsonQuery = new { @@ -199,7 +201,9 @@ namespace TestSuite.ApiTests } } } - }); + }; + + var items = await _.Contents.GetAsync(q); AssertItems(items, 3, new[] { 4, 5, 6 }); } @@ -207,7 +211,7 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_json_filter_with_json() { - var items = await _.Contents.GetAsync(new ContentQuery + var q = new ContentQuery { JsonQuery = new { @@ -237,7 +241,9 @@ namespace TestSuite.ApiTests } } } - }); + }; + + var items = await _.Contents.GetAsync(q); AssertItems(items, 3, new[] { 4, 5, 6 }); } @@ -245,15 +251,14 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_full_text_with_odata() { + var q = new ContentQuery { Search = "1" }; + // Query multiple times to wait for async text indexer. for (var i = 0; i < 10; i++) { await Task.Delay(500); - var items = await _.Contents.GetAsync(new ContentQuery - { - Search = "1" - }); + var items = await _.Contents.GetAsync(q); if (items.Items.Any()) { @@ -268,18 +273,20 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_full_text_with_json() { + var q = new ContentQuery + { + JsonQuery = new + { + fullText = "2" + } + }; + // Query multiple times to wait for async text indexer. for (var i = 0; i < 10; i++) { await Task.Delay(500); - var items = await _.Contents.GetAsync(new ContentQuery - { - JsonQuery = new - { - fullText = "2" - } - }); + var items = await _.Contents.GetAsync(q); if (items.Items.Any()) { @@ -294,15 +301,14 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_near_location_with_odata() { + var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(3 3)') lt 1000" }; + // Query multiple times to wait for async text indexer. - for (var i = 0; i < 10; i++) + for (var i = 0; i < 20; i++) { await Task.Delay(500); - var items = await _.Contents.GetAsync(new ContentQuery - { - Filter = "geo.distance(data/geo/iv, geography'POINT(3 3)') lt 1000" - }); + var items = await _.Contents.GetAsync(q); if (items.Items.Any()) { @@ -317,28 +323,30 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_near_location_with_json() { - // Query multiple times to wait for async text indexer. - for (var i = 0; i < 10; i++) + var q = new ContentQuery { - await Task.Delay(500); - - var items = await _.Contents.GetAsync(new ContentQuery + JsonQuery = new { - JsonQuery = new + filter = new { - filter = new + path = "data.geo.iv", + op = "lt", + value = new { - path = "data.geo.iv", - op = "lt", - value = new - { - longitude = 3, - latitude = 3, - distance = 1000 - } + longitude = 3, + latitude = 3, + distance = 1000 } } - }); + } + }; + + // Query multiple times to wait for async text indexer. + for (var i = 0; i < 20; i++) + { + await Task.Delay(500); + + var items = await _.Contents.GetAsync(q); if (items.Items.Any()) { @@ -353,15 +361,14 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_near_geoson_location_with_odata() { + var q = new ContentQuery { Filter = "geo.distance(data/geo/iv, geography'POINT(4 4)') lt 1000" }; + // Query multiple times to wait for async text indexer. for (var i = 0; i < 10; i++) { await Task.Delay(500); - var items = await _.Contents.GetAsync(new ContentQuery - { - Filter = "geo.distance(data/geo/iv, geography'POINT(4 4)') lt 1000" - }); + var items = await _.Contents.GetAsync(q); if (items.Items.Any()) { @@ -376,28 +383,30 @@ namespace TestSuite.ApiTests [Fact] public async Task Should_return_items_by_near_geoson_location_with_json() { - // Query multiple times to wait for async text indexer. - for (var i = 0; i < 10; i++) + var q = new ContentQuery { - await Task.Delay(500); - - var items = await _.Contents.GetAsync(new ContentQuery + JsonQuery = new { - JsonQuery = new + filter = new { - filter = new + path = "data.geo.iv", + op = "lt", + value = new { - path = "data.geo.iv", - op = "lt", - value = new - { - longitude = 4, - latitude = 4, - distance = 1000 - } + longitude = 4, + latitude = 4, + distance = 1000 } } - }); + } + }; + + // Query multiple times to wait for async text indexer. + for (var i = 0; i < 10; i++) + { + await Task.Delay(500); + + var items = await _.Contents.GetAsync(q); if (items.Items.Any()) { diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs index 7803f6c28..66c9f0de8 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs +++ b/backend/tools/TestSuite/TestSuite.ApiTests/ContentUpdateTests.cs @@ -299,7 +299,7 @@ namespace TestSuite.ApiTests return _.Contents.CreateAsync(new TestEntityData { Number = 1 }, options); }); - Assert.Contains("\"statusCode\":409", ex.Message, StringComparison.Ordinal); + Assert.Equal(409, ex.StatusCode); } finally { @@ -370,6 +370,106 @@ namespace TestSuite.ApiTests } } + [Fact] + public async Task Should_update_content_in_parallel() + { + TestEntity content = null; + try + { + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, ContentCreateOptions.AsPublish); + + + var numErrors = 0; + var numSuccess = 0; + + // STEP 3: Make parallel updates. + await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) => + { + try + { + await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number = i }); + + Interlocked.Increment(ref numErrors); + } + catch (SquidexException ex) when (ex.StatusCode == 412) + { + Interlocked.Increment(ref numSuccess); + return; + } + }); + + // At least some errors and success should have happened. + Assert.True(numErrors > 0); + Assert.True(numSuccess > 0); + + + // STEP 3: Make an normal update to ensure nothing is corrupt. + await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number = 2 }); + + var updated = await _.Contents.GetAsync(content.Id); + + Assert.Equal(2, content.Data.Number); + } + finally + { + if (content != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + + [Fact] + public async Task Should_upsert_content_in_parallel() + { + TestEntity content = null; + try + { + // STEP 1: Create a new item. + content = await _.Contents.CreateAsync(new TestEntityData { Number = 2 }, ContentCreateOptions.AsPublish); + + + var numErrors = 0; + var numSuccess = 0; + + // STEP 3: Make parallel upserts. + await Parallel.ForEachAsync(Enumerable.Range(0, 20), async (i, ct) => + { + try + { + await _.Contents.UpsertAsync(content.Id, new TestEntityData { Number = i }); + + Interlocked.Increment(ref numErrors); + } + catch (SquidexException ex) when (ex.StatusCode == 409) + { + Interlocked.Increment(ref numSuccess); + return; + } + }); + + // At least some errors and success should have happened. + Assert.True(numErrors > 0); + Assert.True(numSuccess > 0); + + + // STEP 3: Make an normal update to ensure nothing is corrupt. + await _.Contents.UpdateAsync(content.Id, new TestEntityData { Number = 2 }); + + var updated = await _.Contents.GetAsync(content.Id); + + Assert.Equal(2, content.Data.Number); + } + finally + { + if (content != null) + { + await _.Contents.DeleteAsync(content.Id); + } + } + } + [Fact] public async Task Should_update_content_to_null() { @@ -648,10 +748,9 @@ namespace TestSuite.ApiTests // STEP 4: Retrieve all deleted items and check if found. - var deleted = await _.Contents.GetAsync(new ContentQuery - { - Filter = "isDeleted eq true" - }, QueryContext.Default.Unpublished(true)); + var q = new ContentQuery { Filter = "isDeleted eq true" }; + + var deleted = await _.Contents.GetAsync(q, QueryContext.Default.Unpublished(true)); Assert.Equal(!permanent, deleted.Items.Any(x => x.Id == content_1.Id)); } @@ -680,7 +779,9 @@ namespace TestSuite.ApiTests // STEP 4: Check if we can find it again with a query. - var contents_4 = await _.Contents.GetAsync(new ContentQuery { Filter = $"id eq '{content_1.Id}'" }); + var q = new ContentQuery { Filter = $"id eq '{content_1.Id}'" }; + + var contents_4 = await _.Contents.GetAsync(q); Assert.NotNull(contents_4.Items.Find(x => x.Id == content_1.Id)); } @@ -707,7 +808,9 @@ namespace TestSuite.ApiTests // STEP 4: Check if we can find it again with a query. - var contents_4 = await _.Contents.GetAsync(new ContentQuery { Filter = $"id eq '{content_1.Id}'" }); + var q = new ContentQuery { Filter = $"id eq '{content_1.Id}'" }; + + var contents_4 = await _.Contents.GetAsync(q); Assert.NotNull(contents_4.Items.Find(x => x.Id == content_1.Id)); } diff --git a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj index 92d0429ed..0985440b8 100644 --- a/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj +++ b/backend/tools/TestSuite/TestSuite.ApiTests/TestSuite.ApiTests.csproj @@ -14,18 +14,18 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + - + all runtime; build; native; contentfiles; analyzers diff --git a/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj index be74f562b..8aa61b528 100644 --- a/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj +++ b/backend/tools/TestSuite/TestSuite.LoadTests/TestSuite.LoadTests.csproj @@ -6,14 +6,14 @@ enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + all runtime; build; native; contentfiles; analyzers diff --git a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj index 9be865dad..fee661026 100644 --- a/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj +++ b/backend/tools/TestSuite/TestSuite.Shared/TestSuite.Shared.csproj @@ -6,7 +6,7 @@ enable - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -16,7 +16,7 @@ - + diff --git a/frontend/src/app/features/administration/administration-area.component.html b/frontend/src/app/features/administration/administration-area.component.html index 295d1ae8d..c32e8fd9d 100644 --- a/frontend/src/app/features/administration/administration-area.component.html +++ b/frontend/src/app/features/administration/administration-area.component.html @@ -17,11 +17,6 @@ - diff --git a/frontend/src/app/features/administration/declarations.ts b/frontend/src/app/features/administration/declarations.ts index 9ab76df1c..992413f28 100644 --- a/frontend/src/app/features/administration/declarations.ts +++ b/frontend/src/app/features/administration/declarations.ts @@ -8,7 +8,6 @@ export * from './administration-area.component'; export * from './guards/user-must-exist.guard'; export * from './internal'; -export * from './pages/cluster/cluster-page.component'; export * from './pages/event-consumers/event-consumer.component'; export * from './pages/event-consumers/event-consumers-page.component'; export * from './pages/restore/restore-page.component'; diff --git a/frontend/src/app/features/administration/module.ts b/frontend/src/app/features/administration/module.ts index bdf0e8617..e6d9b2d55 100644 --- a/frontend/src/app/features/administration/module.ts +++ b/frontend/src/app/features/administration/module.ts @@ -8,7 +8,7 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { SqxFrameworkModule, SqxSharedModule } from '@app/shared'; -import { AdministrationAreaComponent, ClusterPageComponent, EventConsumerComponent, EventConsumersPageComponent, EventConsumersService, EventConsumersState, RestorePageComponent, UserComponent, UserMustExistGuard, UserPageComponent, UsersPageComponent, UsersService, UsersState } from './declarations'; +import { AdministrationAreaComponent, EventConsumerComponent, EventConsumersPageComponent, EventConsumersService, EventConsumersState, RestorePageComponent, UserComponent, UserMustExistGuard, UserPageComponent, UsersPageComponent, UsersService, UsersState } from './declarations'; const routes: Routes = [ { @@ -22,10 +22,6 @@ const routes: Routes = [ path: 'event-consumers', component: EventConsumersPageComponent, }, - { - path: 'cluster', - component: ClusterPageComponent, - }, { path: 'restore', component: RestorePageComponent, @@ -59,7 +55,6 @@ const routes: Routes = [ ], declarations: [ AdministrationAreaComponent, - ClusterPageComponent, EventConsumerComponent, EventConsumersPageComponent, RestorePageComponent, diff --git a/frontend/src/app/features/administration/pages/cluster/cluster-page.component.html b/frontend/src/app/features/administration/pages/cluster/cluster-page.component.html deleted file mode 100644 index 965db67d5..000000000 --- a/frontend/src/app/features/administration/pages/cluster/cluster-page.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - -
- -
\ No newline at end of file diff --git a/frontend/src/app/features/administration/pages/cluster/cluster-page.component.scss b/frontend/src/app/features/administration/pages/cluster/cluster-page.component.scss deleted file mode 100644 index ea7717011..000000000 --- a/frontend/src/app/features/administration/pages/cluster/cluster-page.component.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import 'mixins'; -@import 'vars'; - -iframe { - @include absolute(0, 0, 0, 0); - @include force-height(100%); - @include force-width(100%); - border: 0; -} \ No newline at end of file diff --git a/frontend/src/app/features/administration/pages/cluster/cluster-page.component.ts b/frontend/src/app/features/administration/pages/cluster/cluster-page.component.ts deleted file mode 100644 index f6e5bac2d..000000000 --- a/frontend/src/app/features/administration/pages/cluster/cluster-page.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Squidex Headless CMS - * - * @license - * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. - */ - -import { Component } from '@angular/core'; - -@Component({ - selector: 'sqx-cluster-area', - styleUrls: ['./cluster-page.component.scss'], - templateUrl: './cluster-page.component.html', -}) -export class ClusterPageComponent { -} diff --git a/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index 5ecdb6517..62cc0dc0f 100644 --- a/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/frontend/src/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -29,7 +29,7 @@ export class EventConsumersPageComponent extends ResourceOwner implements OnInit public ngOnInit() { this.eventConsumersState.load(); - this.own(timer(5000, 5000).pipe(switchMap(() => this.eventConsumersState.load(false, true)))); + this.own(timer(1000, 1000).pipe(switchMap(() => this.eventConsumersState.load(false, true)))); } public reload() { diff --git a/frontend/src/app/features/administration/state/event-consumers.state.ts b/frontend/src/app/features/administration/state/event-consumers.state.ts index e572360b8..30285ff22 100644 --- a/frontend/src/app/features/administration/state/event-consumers.state.ts +++ b/frontend/src/app/features/administration/state/event-consumers.state.ts @@ -76,6 +76,8 @@ export class EventConsumersState extends State { public start(eventConsumer: EventConsumerDto): Observable { return this.eventConsumersService.putStart(eventConsumer).pipe( tap(updated => { + this.dialogs.notifyInfo('i18n:eventConsumers.started'); + this.replaceEventConsumer(updated); }), shareSubscribed(this.dialogs)); @@ -84,6 +86,8 @@ export class EventConsumersState extends State { public stop(eventConsumer: EventConsumerDto): Observable { return this.eventConsumersService.putStop(eventConsumer).pipe( tap(updated => { + this.dialogs.notifyInfo('i18n:eventConsumers.stopped'); + this.replaceEventConsumer(updated); }), shareSubscribed(this.dialogs)); @@ -92,6 +96,8 @@ export class EventConsumersState extends State { public reset(eventConsumer: EventConsumerDto): Observable { return this.eventConsumersService.putReset(eventConsumer).pipe( tap(updated => { + this.dialogs.notifyInfo('i18n:eventConsumers.reset'); + this.replaceEventConsumer(updated); }), shareSubscribed(this.dialogs)); diff --git a/frontend/src/app/features/rules/pages/rules/rules-page.component.html b/frontend/src/app/features/rules/pages/rules/rules-page.component.html index 43ed1ac9e..e3b787868 100644 --- a/frontend/src/app/features/rules/pages/rules/rules-page.component.html +++ b/frontend/src/app/features/rules/pages/rules/rules-page.component.html @@ -15,7 +15,7 @@
{{ 'rules.runningRule' | sqxTranslate: { name: runningRule.name || 'Unnamed Rule' } }} diff --git a/frontend/src/app/shared/state/ui.state.ts b/frontend/src/app/shared/state/ui.state.ts index 2282e0823..e89600931 100644 --- a/frontend/src/app/shared/state/ui.state.ts +++ b/frontend/src/app/shared/state/ui.state.ts @@ -36,9 +36,6 @@ interface Snapshot { // Indicates if the user can restore backups. canRestore?: boolean; - // Indicates if the user can see the orleans dashboard. - canUseOrleans?: boolean; - // Indicates if the user can use at least one admin resource. canUserAdminResource?: boolean; } @@ -63,11 +60,8 @@ export class UIState extends State { public canRestore = this.project(x => x.canRestore === true); - public canUseOrleans = - this.project(x => x.canUseOrleans === true); - public canUserAdminResource = - this.project(x => x.canRestore === true || x.canReadUsers === true || x.canReadEvents === true || x.canUseOrleans); + this.project(x => x.canRestore === true || x.canReadUsers === true || x.canReadEvents === true); public get(path: string, defaultValue: T) { return this.settings.pipe(map(x => getValue(x, path, defaultValue)), @@ -144,7 +138,6 @@ export class UIState extends State { canReadEvents: hasAnyLink(payload, 'admin/events'), canReadUsers: hasAnyLink(payload, 'admin/users'), canRestore: hasAnyLink(payload, 'admin/restore'), - canUseOrleans: hasAnyLink(payload, 'admin/orleans'), }, 'Loading Resources Done'); }); }